"""Hypervisor conformance scenarios (CONF-H-* in ``contracts.md`` §10).

Each scenario is a callable taking a ``HypervisorBackend`` instance
and either passing (return) or failing (raise). Scenarios are
deliberately small and standalone — easy to debug when one fails.
"""

from __future__ import annotations

import contextlib
from typing import TYPE_CHECKING

from mgf.vm.core.contracts import NetworkProfile, VmSpec
from mgf.vm.core.exceptions import DomainStateInvalid, SpecConflict

if TYPE_CHECKING:
    from mgf.vm.core.contracts import HypervisorBackend


def _spec(name: str = "conf-vm", cpu: int = 1, memory_bytes: int = 1024 * 1024) -> VmSpec:
    return VmSpec(name=name, cpu=cpu, memory_bytes=memory_bytes)


def define_start_stop_undefine_round_trip(adapter: HypervisorBackend) -> None:
    """CONF-H-01 — the basic happy path.

    Define a VM, start it, stop it, undefine it. State observable
    via ``get_domain`` at every step.
    """
    d = adapter.define(_spec("conf-h-01"))
    info = adapter.get_domain(d)
    assert info.is_running is False, "freshly defined VM should not be running"
    adapter.start(d)
    assert adapter.get_domain(d).is_running is True, "after start, should be running"
    adapter.stop(d)
    assert adapter.get_domain(d).is_running is False, "after stop, should not be running"
    adapter.undefine(d)
    # Domain should no longer be enumerable.
    listed = [info.domain_id for info in adapter.list_domains()]
    assert d not in listed, "undefined VM should not appear in list_domains"


def define_idempotency(adapter: HypervisorBackend) -> None:
    """CONF-H-02 — same spec → same DomainId; different spec → SpecConflict."""
    spec = _spec("conf-h-02")
    d1 = adapter.define(spec)
    d2 = adapter.define(spec)
    assert d1 == d2, "define() must be idempotent on same spec"
    # Different spec on the same name must raise SpecConflict.
    bigger = VmSpec(name="conf-h-02", cpu=4, memory_bytes=4 * 1024 * 1024)
    raised = False
    try:
        adapter.define(bigger)
    except SpecConflict:
        raised = True
    assert raised, "define() on same name with different spec must raise SpecConflict"
    # Cleanup.
    adapter.undefine(d1)


def start_idempotency(adapter: HypervisorBackend) -> None:
    """CONF-H-03 — start() is idempotent (already-running is success)."""
    d = adapter.define(_spec("conf-h-03"))
    try:
        adapter.start(d)
        # Second start must not raise.
        adapter.start(d)
        assert adapter.get_domain(d).is_running is True
    finally:
        # Best-effort cleanup.
        try:
            adapter.stop(d, force=True)
            adapter.undefine(d)
        except Exception:
            pass


def network_observation(adapter: HypervisorBackend) -> None:
    """CONF-H-11 — host-network observation on ``HypervisorBackend`` (D-G14).

    A read-only observation: list the host virtual networks, read the DHCP
    leases of each listed network by name, and resolve a (possibly absent)
    domain bridge. Asserts the neutral shapes and the idempotent-read
    semantics — listing twice returns the same networks, an unknown network
    has no observed leases, and ``bridge_for_domain`` returns ``None`` or a
    non-empty bridge name.
    """
    networks = adapter.list_networks()
    assert isinstance(networks, tuple), "list_networks must return a tuple"
    assert tuple(adapter.list_networks()) == networks, "list_networks must be idempotent"
    for net in networks:
        assert isinstance(net, NetworkProfile)
        assert net.name, "a NetworkProfile must carry a non-empty name"
        leases = adapter.network_leases(net.name)
        assert isinstance(leases, tuple), "network_leases must return a tuple"
        for lease in leases:
            assert lease.mac, "an observed NetworkLease must carry a MAC"

    # An unknown network has no observed leases (idempotent read).
    assert adapter.network_leases("conf-h-11-no-such-network") == ()

    # bridge_for_domain on a defined domain: None or a non-empty bridge name.
    d = adapter.define(_spec("conf-h-11"))
    try:
        bridge = adapter.bridge_for_domain(d)
        assert bridge is None or bridge, "bridge_for_domain returns None or a bridge name"
    finally:
        with contextlib.suppress(Exception):
            adapter.undefine(d)


def rename_round_trip(adapter: HypervisorBackend) -> None:
    """CONF-H-12 — rename a stopped domain, preserving identity (D-G26).

    Define a VM, rename it while stopped, and confirm the ``DomainId`` is
    stable while ``get_domain`` reports the new name. A rename of a *running*
    domain must raise ``DomainStateInvalid``.
    """
    d = adapter.define(_spec("conf-h-12"))
    try:
        adapter.rename(d, "conf-h-12-renamed")
        info = adapter.get_domain(d)
        assert info.domain_id == d, "rename must preserve the DomainId identity"
        assert info.name == "conf-h-12-renamed", "get_domain must report the new name"

        # A running domain refuses rename.
        adapter.start(d)
        raised = False
        try:
            adapter.rename(d, "conf-h-12-while-running")
        except DomainStateInvalid:
            raised = True
        assert raised, "renaming a running domain must raise DomainStateInvalid"
    finally:
        with contextlib.suppress(Exception):
            adapter.stop(d, force=True)
            adapter.undefine(d)


__all__ = [
    "define_idempotency",
    "define_start_stop_undefine_round_trip",
    "network_observation",
    "rename_round_trip",
    "start_idempotency",
]
