"""Tests for the expanded exception hierarchy per ``contracts.md`` §7.

Verifies inheritance shape (every typed error is under
``VmManagerError``) and the retryability tags (``RETRYABLE: bool``).
"""

from __future__ import annotations

import pytest

from mgf.vm.core.exceptions import (
    AuthenticationFailed,
    AuthError,
    AuthorizationDenied,
    BrokerConnectionError,
    BrokerTransientError,
    CapabilityExpired,
    ConstraintUnsatisfiable,
    DestUnreachable,
    DeviceAttachFailed,
    DeviceContended,
    DeviceDetachFailed,
    DeviceError,
    DeviceNotFound,
    DeviceNotPermitted,
    DomainNotFound,
    DomainNotFoundError,
    DomainStateInvalid,
    FormatUnsupported,
    GuestUnresponsive,
    HardwareFault,
    HypervisorConnectionError,
    HypervisorError,
    HypervisorTransientError,
    ImageChecksumMismatch,
    ImageInUse,
    ImageNotFound,
    InsufficientCapacity,
    LeaseExpired,
    LeaseNotFound,
    MigrationCancelled,
    MigrationFailed,
    MigrationIncompatible,
    NoHostAvailable,
    OperationCancelled,
    PolicyViolation,
    PoolExists,
    PoolNotEmpty,
    PoolNotFound,
    RegistryError,
    ResizeUnsupported,
    ResourceUnavailable,
    SnapshotExists,
    SnapshotHasChildren,
    SnapshotInUse,
    SnapshotNotFound,
    SpecConflict,
    SpecInvalid,
    StorageError,
    StorageTransientError,
    TransportCancelled,
    TransportConnectionError,
    TransportError,
    TransportProtocolError,
    TransportTimeout,
    VManagerError,
    VmManagerError,
    VolumeExists,
    VolumeHasSnapshots,
    VolumeInUse,
    VolumeNotFound,
)

# ---------------------------------------------------------------------------
# Root aliasing
# ---------------------------------------------------------------------------


def test_vm_manager_error_aliases_legacy() -> None:
    """``VmManagerError`` (contracts-canonical) is the same class as
    ``VManagerError`` (legacy)."""
    assert VmManagerError is VManagerError


def test_domain_not_found_aliases_legacy() -> None:
    """``DomainNotFound`` (contracts-canonical) is the same as
    ``DomainNotFoundError`` (legacy)."""
    assert DomainNotFound is DomainNotFoundError


# ---------------------------------------------------------------------------
# Every typed error subclasses VmManagerError (EH-01 spirit)
# ---------------------------------------------------------------------------

ALL_TYPED_ERRORS = [
    # HypervisorError subtree
    HypervisorError,
    HypervisorConnectionError,
    HypervisorTransientError,
    DomainStateInvalid,
    SpecInvalid,
    SpecConflict,
    ResourceUnavailable,
    GuestUnresponsive,
    DeviceAttachFailed,
    DeviceDetachFailed,
    MigrationIncompatible,
    MigrationFailed,
    MigrationCancelled,
    SnapshotExists,
    SnapshotNotFound,
    SnapshotInUse,
    SnapshotHasChildren,
    # StorageError subtree
    StorageError,
    StorageTransientError,
    PoolNotFound,
    PoolExists,
    PoolNotEmpty,
    VolumeNotFound,
    VolumeExists,
    VolumeInUse,
    VolumeHasSnapshots,
    InsufficientCapacity,
    ResizeUnsupported,
    FormatUnsupported,
    # DeviceError subtree
    DeviceError,
    BrokerConnectionError,
    BrokerTransientError,
    DeviceNotFound,
    DeviceContended,
    DeviceNotPermitted,
    HardwareFault,
    LeaseNotFound,
    LeaseExpired,
    DestUnreachable,
    # TransportError subtree
    TransportError,
    TransportConnectionError,
    TransportTimeout,
    TransportCancelled,
    TransportProtocolError,
    # AuthError subtree
    AuthError,
    AuthenticationFailed,
    AuthorizationDenied,
    CapabilityExpired,
    # SchedulerError additions
    NoHostAvailable,
    ConstraintUnsatisfiable,
    PolicyViolation,
    # RegistryError subtree
    RegistryError,
    ImageNotFound,
    ImageChecksumMismatch,
    ImageInUse,
    # Cancellation
    OperationCancelled,
]


@pytest.mark.parametrize("exc_class", ALL_TYPED_ERRORS, ids=lambda c: c.__name__)
def test_every_typed_error_subclasses_vmmanager(exc_class: type[Exception]) -> None:
    """EH-01 spirit: every typed error in our taxonomy is catchable
    via ``except VmManagerError``."""
    assert issubclass(exc_class, VmManagerError)


# ---------------------------------------------------------------------------
# Subtree shape — each subtree root catches all its members
# ---------------------------------------------------------------------------


def test_hypervisor_subtree() -> None:
    for sub in (
        HypervisorConnectionError,
        HypervisorTransientError,
        DomainStateInvalid,
        SpecInvalid,
        SpecConflict,
        ResourceUnavailable,
        GuestUnresponsive,
        DeviceAttachFailed,
        DeviceDetachFailed,
        MigrationIncompatible,
        MigrationFailed,
        MigrationCancelled,
        SnapshotExists,
        SnapshotNotFound,
        SnapshotInUse,
        SnapshotHasChildren,
    ):
        assert issubclass(sub, HypervisorError), f"{sub.__name__} not in HypervisorError"


def test_storage_subtree() -> None:
    for sub in (
        StorageTransientError,
        PoolNotFound,
        PoolExists,
        PoolNotEmpty,
        VolumeNotFound,
        VolumeExists,
        VolumeInUse,
        VolumeHasSnapshots,
        InsufficientCapacity,
        ResizeUnsupported,
        FormatUnsupported,
    ):
        assert issubclass(sub, StorageError), f"{sub.__name__} not in StorageError"


def test_device_subtree() -> None:
    for sub in (
        BrokerConnectionError,
        BrokerTransientError,
        DeviceNotFound,
        DeviceContended,
        DeviceNotPermitted,
        HardwareFault,
        LeaseNotFound,
        LeaseExpired,
        DestUnreachable,
    ):
        assert issubclass(sub, DeviceError), f"{sub.__name__} not in DeviceError"


def test_transport_subtree() -> None:
    for sub in (
        TransportConnectionError,
        TransportTimeout,
        TransportCancelled,
        TransportProtocolError,
    ):
        assert issubclass(sub, TransportError), f"{sub.__name__} not in TransportError"


def test_auth_subtree() -> None:
    for sub in (
        AuthenticationFailed,
        AuthorizationDenied,
        CapabilityExpired,
    ):
        assert issubclass(sub, AuthError), f"{sub.__name__} not in AuthError"


def test_registry_subtree() -> None:
    for sub in (ImageNotFound, ImageChecksumMismatch, ImageInUse):
        assert issubclass(sub, RegistryError), f"{sub.__name__} not in RegistryError"


# ---------------------------------------------------------------------------
# Retryability tags
# ---------------------------------------------------------------------------

EXPECTED_RETRYABLE = {
    # Transient classes — retryable
    HypervisorTransientError: True,
    StorageTransientError: True,
    BrokerTransientError: True,
    HypervisorConnectionError: True,
    BrokerConnectionError: True,
    TransportConnectionError: True,
    TransportTimeout: True,
    DeviceContended: True,
    DestUnreachable: True,
    InsufficientCapacity: True,
    NoHostAvailable: True,
    ResourceUnavailable: True,
    GuestUnresponsive: True,  # caller may retry with force=True
    # Permanent — not retryable
    DomainNotFound: False,
    SpecInvalid: False,
    SpecConflict: False,
    DomainStateInvalid: False,
    MigrationIncompatible: False,
    MigrationFailed: False,
    MigrationCancelled: False,
    SnapshotInUse: False,
    SnapshotHasChildren: False,
    SnapshotExists: False,
    SnapshotNotFound: False,
    PoolNotFound: False,
    VolumeNotFound: False,
    VolumeExists: False,
    PoolExists: False,
    VolumeInUse: False,
    VolumeHasSnapshots: False,
    ResizeUnsupported: False,
    FormatUnsupported: False,
    LeaseNotFound: False,
    LeaseExpired: False,
    DeviceNotFound: False,
    DeviceNotPermitted: False,
    HardwareFault: False,
    DeviceAttachFailed: False,
    DeviceDetachFailed: False,
    TransportCancelled: False,
    TransportProtocolError: False,
    AuthenticationFailed: False,
    AuthorizationDenied: False,
    CapabilityExpired: False,
    ConstraintUnsatisfiable: False,
    PolicyViolation: False,
    ImageNotFound: False,
    ImageChecksumMismatch: False,
    ImageInUse: False,
    OperationCancelled: False,
}


@pytest.mark.parametrize(
    ("exc_class", "expected"),
    list(EXPECTED_RETRYABLE.items()),
    ids=lambda v: v.__name__ if isinstance(v, type) else str(v),
)
def test_retryability_tag(exc_class: type[Exception], expected: bool) -> None:
    """Every contracts-aligned exception class carries ``RETRYABLE``
    matching the table in ``contracts.md`` §7.4."""
    actual = exc_class.RETRYABLE  # type: ignore[attr-defined]
    assert actual is expected, f"{exc_class.__name__}.RETRYABLE = {actual}, expected {expected}"


# ---------------------------------------------------------------------------
# Exceptions can be raised + chained (EH-03)
# ---------------------------------------------------------------------------


def test_typed_error_chained_from_engine_error() -> None:
    """A typed error can be raised ``from`` an engine-native one."""
    original = OSError("libvirt: failed to connect")
    try:
        try:
            raise original
        except OSError as e:
            raise HypervisorConnectionError("could not reach libvirtd") from e
    except HypervisorConnectionError as e:
        assert e.__cause__ is original
        return
    msg = "expected HypervisorConnectionError"
    raise AssertionError(msg)
