"""Tests for the four Protocols + their supporting types.

Verifies that the Python materialization of
``vm-vmanager-docs/component-specs/contracts.md`` is well-formed:
every method named in the spec is present with the right signature;
the contracts are runtime-checkable Protocols; the helper types are
frozen dataclasses (DP-03).
"""

from __future__ import annotations

import inspect
from typing import Protocol

import mgf.vm.core as core
from mgf.vm.core.contracts import (
    AsyncLifecycleObserver,
    ClaimMode,
    DeviceBackend,
    HypervisorBackend,
    LifecycleObserver,
    StorageBackend,
    VmState,
)

# ---------------------------------------------------------------------------
# All four contracts are Protocols
# ---------------------------------------------------------------------------


def test_hypervisor_backend_is_protocol() -> None:
    assert issubclass(HypervisorBackend, Protocol)


def test_storage_backend_is_protocol() -> None:
    assert issubclass(StorageBackend, Protocol)


def test_device_backend_is_protocol() -> None:
    assert issubclass(DeviceBackend, Protocol)


def test_lifecycle_observer_is_protocol() -> None:
    assert issubclass(LifecycleObserver, Protocol)


def test_async_lifecycle_observer_is_protocol() -> None:
    assert issubclass(AsyncLifecycleObserver, Protocol)


# ---------------------------------------------------------------------------
# HypervisorBackend has every method contracts.md §3 names
# ---------------------------------------------------------------------------

HYPERVISOR_METHODS = {
    # connection lifecycle
    "close",
    # read-only
    "list_domains",
    "get_domain",
    "capabilities",
    # definition + lifecycle
    "define",
    "redefine",
    "undefine",
    "rename",
    "start",
    "stop",
    "pause",
    "resume",
    "reboot",
    # snapshots
    "snapshot",
    "list_snapshots",
    "revert_snapshot",
    "delete_snapshot",
    # migration
    "migrate",
    # device attach (hypervisor-side half)
    "attach_device",
    "detach_device",
    # events
    "watch_events",
}


def test_hypervisor_backend_method_names() -> None:
    """Every method contracts.md §3 names is on HypervisorBackend."""
    members = {name for name in dir(HypervisorBackend) if not name.startswith("_")}
    missing = HYPERVISOR_METHODS - members
    assert not missing, f"HypervisorBackend missing methods: {missing}"


STORAGE_METHODS = {
    "close",
    # pools
    "list_pools",
    "get_pool",
    "refresh_pool",
    "create_pool",
    "delete_pool",
    # volumes
    "list_volumes",
    "get_volume",
    "create_volume",
    "resize_volume",
    "delete_volume",
    # snapshots
    "snapshot_volume",
    "list_snapshots",
    "delete_snapshot",
    "revert_volume_to_snapshot",
    # format conversion
    "convert_format",
    # capabilities
    "capabilities",
}


def test_storage_backend_method_names() -> None:
    members = {name for name in dir(StorageBackend) if not name.startswith("_")}
    missing = STORAGE_METHODS - members
    assert not missing, f"StorageBackend missing methods: {missing}"


DEVICE_METHODS = {
    "close",
    "list_assignable",
    "list_leased",
    "get_device",
    "check_compatibility",
    "claim",
    "assignment_for",
    "release",
    "transfer_lease",
    "watch_leases",
    "capabilities",
}


def test_device_backend_method_names() -> None:
    members = {name for name in dir(DeviceBackend) if not name.startswith("_")}
    missing = DEVICE_METHODS - members
    assert not missing, f"DeviceBackend missing methods: {missing}"


def test_lifecycle_observer_method_names() -> None:
    members = {name for name in dir(LifecycleObserver) if not name.startswith("_")}
    assert "on_transition" in members


# ---------------------------------------------------------------------------
# Long-running methods take a CancellationToken (DP-09 / contracts.md §9)
# ---------------------------------------------------------------------------

LONG_RUNNING_METHODS = [
    (HypervisorBackend, "start"),
    (HypervisorBackend, "stop"),
    (HypervisorBackend, "reboot"),
    (HypervisorBackend, "revert_snapshot"),
    (HypervisorBackend, "migrate"),
    (StorageBackend, "revert_volume_to_snapshot"),
    (StorageBackend, "convert_format"),
    (DeviceBackend, "transfer_lease"),
]


def test_long_running_methods_take_cancellation_token() -> None:
    """Every long-running method per contracts.md §9 declares a
    ``token`` parameter."""
    for proto, method_name in LONG_RUNNING_METHODS:
        method = getattr(proto, method_name)
        sig = inspect.signature(method)
        assert "token" in sig.parameters, (
            f"{proto.__name__}.{method_name} missing 'token' parameter"
        )


# ---------------------------------------------------------------------------
# Supporting types are frozen (DP-03)
# ---------------------------------------------------------------------------


def test_vmspec_is_frozen() -> None:
    import dataclasses

    assert dataclasses.is_dataclass(core.VmSpec)
    spec = core.VmSpec(name="t", cpu=1, memory_bytes=1024)
    try:
        spec.cpu = 2  # type: ignore[misc]
    except dataclasses.FrozenInstanceError:
        return
    msg = "VmSpec should be frozen"
    raise AssertionError(msg)


def test_transition_event_is_frozen() -> None:
    import dataclasses

    assert dataclasses.is_dataclass(core.TransitionEvent)


def test_capabilities_are_frozen() -> None:
    import dataclasses

    assert dataclasses.is_dataclass(core.HypervisorCapabilities)
    assert dataclasses.is_dataclass(core.StorageCapabilities)
    assert dataclasses.is_dataclass(core.DeviceCapabilities)


# ---------------------------------------------------------------------------
# IDs are typed and distinct
# ---------------------------------------------------------------------------


def test_id_types_are_distinct() -> None:
    """A VmId and a DomainId with the same string value are not equal."""
    assert core.VmId("x") != core.DomainId("x")
    assert core.HostId("a") != core.VmId("a")
    # Same type + same value == equal
    assert core.VmId("y") == core.VmId("y")


def test_id_types_are_frozen() -> None:
    import dataclasses

    vmid = core.VmId("abc")
    try:
        vmid.value = "def"  # type: ignore[misc]
    except dataclasses.FrozenInstanceError:
        return
    msg = "VmId should be frozen"
    raise AssertionError(msg)


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------


def test_vm_state_enum_has_all_required_states() -> None:
    """contracts.md §2.2 enumerates the canonical states."""
    required = {
        "DEFINED",
        "STARTING",
        "RUNNING",
        "PAUSING",
        "PAUSED",
        "RESUMING",
        "STOPPING",
        "STOPPED",
        "MIGRATING",
        "FAILED",
    }
    present = {m.name for m in VmState}
    missing = required - present
    assert not missing, f"VmState missing: {missing}"


def test_claim_mode_has_three_values() -> None:
    """The three claim modes per contracts.md §5."""
    names = {m.name for m in ClaimMode}
    assert "EXCLUSIVE" in names
    assert "SHARED" in names
    assert "READ_ONLY" in names


# ---------------------------------------------------------------------------
# CancellationToken Protocol + default impl
# ---------------------------------------------------------------------------


def test_cancellation_event_implements_token_protocol() -> None:
    """The default ``CancellationEvent`` satisfies ``CancellationToken``."""
    token = core.CancellationEvent()
    assert isinstance(token, core.CancellationToken)


def test_cancellation_event_one_way() -> None:
    """``cancelled`` is one-way: never un-set after cancel."""
    token = core.CancellationEvent()
    assert token.cancelled is False
    token.cancel()
    assert token.cancelled is True
    # Second cancel is idempotent
    token.cancel()
    assert token.cancelled is True


def test_cancellation_event_raises_when_cancelled() -> None:
    from mgf.vm.core.exceptions import OperationCancelled

    token = core.CancellationEvent()
    token.raise_if_cancelled()  # not raises before cancel
    token.cancel()
    try:
        token.raise_if_cancelled()
    except OperationCancelled:
        return
    msg = "raise_if_cancelled should have raised OperationCancelled"
    raise AssertionError(msg)
