"""F2 unit tests — the exception-translation seam (>=95% target).

Each libvirt error code maps to the expected core type; the
``@translate_at_seam`` decorator chains ``__cause__``; the conservative
fallback handles unknown / code-less errors.
"""

from __future__ import annotations

import pytest
from mgf.vm.core.exceptions import (
    AppError,
    AuthError,
    DeviceAttachFailed,
    DomainNotFound,
    DomainStateInvalid,
    GuestUnresponsive,
    HypervisorConnectionError,
    HypervisorError,
    HypervisorTransientError,
    MigrationFailed,
    OperationCancelled,
    PoolExists,
    PoolNotFound,
    ResourceUnavailable,
    SnapshotExists,
    SnapshotNotFound,
    SpecInvalid,
    StorageError,
    VolumeExists,
    VolumeNotFound,
)
from tests.conftest import FakeLibvirtError

from mgf.vm.libvirt import _errors as E
from mgf.vm.libvirt._errors import LibvirtErrorTranslator, translate_at_seam

# (libvirt code, expected core type) — the load-bearing matrix.
CODE_CASES: list[tuple[int, type[AppError]]] = [
    # connectivity
    (E.VIR_ERR_NO_CONNECT, HypervisorConnectionError),
    (E.VIR_ERR_INVALID_CONN, HypervisorConnectionError),
    (E.VIR_ERR_NO_SERVER, HypervisorConnectionError),
    (E.VIR_ERR_NO_CLIENT, HypervisorConnectionError),
    (E.VIR_ERR_RPC, HypervisorConnectionError),
    (E.VIR_ERR_GNUTLS_ERROR, HypervisorConnectionError),
    (E.VIR_ERR_SSH, HypervisorConnectionError),
    (E.VIR_ERR_LIBSSH, HypervisorConnectionError),
    (E.VIR_ERR_DBUS_SERVICE, HypervisorConnectionError),
    (E.VIR_ERR_UNKNOWN_HOST, HypervisorConnectionError),
    (E.VIR_ERR_NO_HOSTNAME, HypervisorConnectionError),
    # transient
    (E.VIR_ERR_OPERATION_TIMEOUT, HypervisorTransientError),
    (E.VIR_ERR_RESOURCE_BUSY, HypervisorTransientError),
    (E.VIR_ERR_SYSTEM_ERROR, HypervisorTransientError),
    (E.VIR_ERR_AGENT_UNSYNCED, HypervisorTransientError),
    # not-found
    (E.VIR_ERR_NO_DOMAIN, DomainNotFound),
    (E.VIR_ERR_INVALID_DOMAIN, DomainNotFound),
    (E.VIR_ERR_NO_DOMAIN_SNAPSHOT, SnapshotNotFound),
    (E.VIR_ERR_NO_STORAGE_POOL, PoolNotFound),
    (E.VIR_ERR_INVALID_STORAGE_POOL, PoolNotFound),
    (E.VIR_ERR_NO_STORAGE_VOL, VolumeNotFound),
    (E.VIR_ERR_INVALID_STORAGE_VOL, VolumeNotFound),
    (E.VIR_ERR_DEVICE_MISSING, DomainStateInvalid),
    # exists
    (E.VIR_ERR_STORAGE_VOL_EXIST, VolumeExists),
    (E.VIR_ERR_STORAGE_POOL_BUILT, PoolExists),
    # state-invalid
    (E.VIR_ERR_OPERATION_INVALID, DomainStateInvalid),
    (E.VIR_ERR_OPERATION_DENIED, DomainStateInvalid),
    (E.VIR_ERR_BLOCK_COPY_ACTIVE, DomainStateInvalid),
    (E.VIR_ERR_MULTIPLE_DOMAINS, DomainStateInvalid),
    # spec / xml / config invalid
    (E.VIR_ERR_XML_ERROR, SpecInvalid),
    (E.VIR_ERR_XML_DETAIL, SpecInvalid),
    (E.VIR_ERR_XML_INVALID_SCHEMA, SpecInvalid),
    (E.VIR_ERR_INVALID_ARG, SpecInvalid),
    (E.VIR_ERR_INVALID_MAC, SpecInvalid),
    (E.VIR_ERR_CONFIG_UNSUPPORTED, SpecInvalid),
    (E.VIR_ERR_ARGUMENT_UNSUPPORTED, SpecInvalid),
    (E.VIR_ERR_OPERATION_UNSUPPORTED, SpecInvalid),
    (E.VIR_ERR_NO_SUPPORT, SpecInvalid),
    (E.VIR_ERR_OVERFLOW, SpecInvalid),
    # capacity
    (E.VIR_ERR_NO_MEMORY, ResourceUnavailable),
    # guest agent
    (E.VIR_ERR_AGENT_UNRESPONSIVE, GuestUnresponsive),
    # snapshot
    (E.VIR_ERR_SNAPSHOT_REVERT_RISKY, SnapshotExists),
    # migration
    (E.VIR_ERR_MIGRATE_PERSIST_FAILED, MigrationFailed),
    (E.VIR_ERR_MIGRATE_UNSAFE, MigrationFailed),
    (E.VIR_ERR_CPU_INCOMPATIBLE, MigrationFailed),
    # storage I/O
    (E.VIR_ERR_STORAGE_PROBE_FAILED, StorageError),
    (E.VIR_ERR_OPEN_FAILED, StorageError),
    (E.VIR_ERR_READ_FAILED, StorageError),
    (E.VIR_ERR_WRITE_FAILED, StorageError),
    # cancellation
    (E.VIR_ERR_OPERATION_ABORTED, OperationCancelled),
    (E.VIR_ERR_AUTH_CANCELLED, OperationCancelled),
    # auth
    (E.VIR_ERR_AUTH_FAILED, AuthError),
    (E.VIR_ERR_AUTH_UNAVAILABLE, AuthError),
    (E.VIR_ERR_ACCESS_DENIED, AuthError),
    # device-attach hint
    (E.VIR_ERR_HOOK_SCRIPT_FAILED, DeviceAttachFailed),
]


@pytest.fixture
def translator() -> LibvirtErrorTranslator:
    return LibvirtErrorTranslator()


@pytest.mark.parametrize(("code", "expected"), CODE_CASES)
def test_code_maps_to_expected_type(
    translator: LibvirtErrorTranslator,
    code: int,
    expected: type[AppError],
) -> None:
    err = FakeLibvirtError(code, message=f"code {code}")
    translated = translator.translate_exception(err)
    assert type(translated) is expected
    assert str(code) in str(translated) or "code" in str(translated)


def test_matrix_covers_at_least_thirty_codes() -> None:
    assert len(CODE_CASES) >= 30


def test_unknown_code_falls_back_to_generic_parent(
    translator: LibvirtErrorTranslator,
) -> None:
    # 9999 is not a real libvirt code → conservative generic parent.
    translated = translator.translate_exception(FakeLibvirtError(9999))
    assert type(translated) is HypervisorError


def test_operation_failed_is_intentionally_unmapped(
    translator: LibvirtErrorTranslator,
) -> None:
    """OPERATION_FAILED is ambiguous → generic parent, never a wrong subclass."""
    translated = translator.translate_exception(FakeLibvirtError(E.VIR_ERR_OPERATION_FAILED))
    assert type(translated) is HypervisorError


def test_no_code_falls_back_to_generic(translator: LibvirtErrorTranslator) -> None:
    translated = translator.translate_exception(FakeLibvirtError(None))
    assert type(translated) is HypervisorError


def test_non_libvirt_exception_falls_back(translator: LibvirtErrorTranslator) -> None:
    translated = translator.translate_exception(ValueError("plain"))
    assert type(translated) is HypervisorError
    assert "plain" in str(translated)


def test_broken_get_error_code_is_tolerated(
    translator: LibvirtErrorTranslator,
) -> None:
    class Broken(Exception):
        def get_error_code(self) -> int:
            raise RuntimeError("getter exploded")

    translated = translator.translate_exception(Broken("x"))
    assert type(translated) is HypervisorError


def test_apperror_passes_through_unchanged(
    translator: LibvirtErrorTranslator,
) -> None:
    original = DomainNotFound("vm-x")
    assert translator.translate_exception(original) is original


# --- the @translate_at_seam decorator on a backend method -------------------


class _Backend(LibvirtErrorTranslator):
    @translate_at_seam
    def boom(self, code: int) -> None:
        raise FakeLibvirtError(code, message="seam test")

    @translate_at_seam
    def fine(self) -> str:
        return "ok"

    @translate_at_seam
    def reraise_typed(self) -> None:
        raise DomainNotFound("vm-y")


def test_seam_translates_and_chains_cause() -> None:
    backend = _Backend()
    with pytest.raises(DomainNotFound) as exc_info:
        backend.boom(E.VIR_ERR_NO_DOMAIN)
    # __cause__ is the original libvirt error (EH-03 raise-from chain).
    cause = exc_info.value.__cause__
    assert isinstance(cause, FakeLibvirtError)
    assert cause.get_error_code() == E.VIR_ERR_NO_DOMAIN


def test_seam_passes_success_through() -> None:
    assert _Backend().fine() == "ok"


def test_seam_lets_typed_apperror_pass_without_double_wrap() -> None:
    backend = _Backend()
    with pytest.raises(DomainNotFound) as exc_info:
        backend.reraise_typed()
    # Not wrapped in a second AppError; no spurious __cause__ added.
    assert exc_info.value.__cause__ is None
