"""D-G12 unit tests — ``create_vm`` / ``get_vm`` / ``list_vms`` return ``Vm``.

Covers the canonical :class:`~mgf.vm.core.contracts.Vm` aggregate the SDK now
returns (replacing the Phase-0 ``DomainInfo`` stand-in):

* ``create_vm`` returns a ``Vm`` carrying the *submitted* spec + the engine-
  observed :class:`~mgf.vm.core.contracts.VmStatus`;
* ``get_vm`` / ``list_vms`` return ``Vm``\\(s) with the live observed status and
  the remembered submitted spec;
* the **best-effort** spec path: a VM the transport never saw defined (out-of-
  band) yields a minimal reconstructed spec from ``DomainInfo``;
* timestamps are injectable + UTC-aware (DP-12 / D-G6);
* ``redefine_vm`` returns a ``Vm`` with the new desired-state spec;
* the async twins return ``Vm``.
"""

from __future__ import annotations

from datetime import UTC, datetime

from mgf.vm.core.contracts import (
    AttachedDevice,
    DiskSpec,
    DiskStatus,
    DomainInfo,
    NicStatus,
    Vm,
    VmSpec,
    VmState,
    VmStatus,
)
from mgf.vm.core.fakes.hypervisor import FakeHypervisorBackend
from mgf.vm.core.ids import DomainId, VmId

from mgf.vm.sdk.client import AsyncVmManagerClient, VmManagerClient
from mgf.vm.sdk.transport.async_inprocess import AsyncInProcessTransport
from mgf.vm.sdk.transport.inprocess import InProcessTransport


def _rich_spec(name: str = "vm-rich") -> VmSpec:
    """A spec with more than the three required fields (disks), to prove the
    *submitted* spec — not a lossy reconstruction — is what create/get return."""
    return VmSpec(
        name=name,
        cpu=4,
        memory_bytes=4 << 30,
        disks=(DiskSpec(size_bytes=10 << 30, format="qcow2"),),
    )


_FIXED = datetime(2026, 5, 30, 12, 0, 0, tzinfo=UTC)


def _fixed_clock() -> datetime:
    return _FIXED


# -- create_vm assembles the Vm from submitted spec + observed status --------


def test_create_vm_returns_vm_with_submitted_spec_and_status() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend, clock=_fixed_clock)
    with VmManagerClient(transport) as client:
        spec = _rich_spec("c1")
        vm = client.create_vm(spec)

    assert isinstance(vm, Vm)
    # The *exact* submitted spec round-trips (disks preserved — not lossy).
    assert vm.spec == spec
    assert vm.spec.disks == spec.disks
    # Freshly defined (not started) → observed STOPPED, carrying the VM's
    # identity and empty observed lists (the fake surfaces no disks/nics/devices).
    assert vm.status == VmStatus(state=VmState.STOPPED, vm_id=vm.vm_id)
    assert vm.status.disks == ()
    assert vm.status.nics == ()
    assert vm.status.devices_attached == ()
    assert vm.status.host_id == ""
    assert vm.status.error is None
    assert vm.status.last_transition_at is None
    # Identity + UTC-aware injected timestamps.
    assert isinstance(vm.vm_id, VmId)
    assert vm.created_at == _FIXED
    assert vm.last_transition_at == _FIXED
    assert vm.created_at.tzinfo is UTC


def test_get_vm_carries_remembered_spec_and_live_status() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    with VmManagerClient(transport) as client:
        spec = _rich_spec("g1")
        created = client.create_vm(spec)
        client.start_vm(created.vm_id)

        got = client.get_vm(created.vm_id)
        assert isinstance(got, Vm)
        assert got.spec == spec  # remembered submitted spec
        assert got.status.state is VmState.RUNNING  # live engine truth


def test_list_vms_returns_vms() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    with VmManagerClient(transport) as client:
        client.create_vm(_rich_spec("l1"))
        client.create_vm(_rich_spec("l2"))
        vms = client.list_vms()

    assert {type(v) for v in vms} == {Vm}
    assert sorted(v.spec.name for v in vms) == ["l1", "l2"]


def test_redefine_returns_vm_with_new_spec() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    with VmManagerClient(transport) as client:
        created = client.create_vm(VmSpec(name="rd", cpu=2, memory_bytes=2 << 30))
        new = client.redefine_vm(created.vm_id, VmSpec(name="rd", cpu=8, memory_bytes=4 << 30))
    assert isinstance(new, Vm)
    assert new.spec.cpu == 8
    assert new.spec.memory_bytes == 4 << 30


# -- best-effort spec: out-of-band VM unknown to the transport --------------


def test_get_vm_reconstructs_minimal_spec_when_unknown() -> None:
    """A VM defined directly on the backend (the transport never saw the spec)
    yields a minimal reconstruction from the observed DomainInfo (best-effort —
    disks/nics/etc. are lost; the docstring documents the limitation)."""
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    # Define out-of-band: the transport's submitted-spec map never learns it.
    domain_id = backend.define(_rich_spec("oob"))
    with VmManagerClient(transport) as client:
        got = client.get_vm(VmId(domain_id.value))

    assert isinstance(got, Vm)
    # Minimal neutral reconstruction: name/cpu/memory survive, disks do not.
    assert got.spec.name == "oob"
    assert got.spec.cpu == 4
    assert got.spec.memory_bytes == 4 << 30
    assert got.spec.disks == ()  # lossy — the submitted disk is gone
    assert got.status.state is VmState.STOPPED


def test_create_then_delete_drops_remembered_spec() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    with VmManagerClient(transport) as client:
        vm = client.create_vm(_rich_spec("d1"))
        client.delete_vm(vm.vm_id)
    # The transport forgot the spec on undefine (no leak in the map).
    assert vm.vm_id not in transport._submitted_specs


# -- async twins -------------------------------------------------------------


async def test_async_create_and_get_return_vm() -> None:
    backend = FakeHypervisorBackend()
    transport = AsyncInProcessTransport.from_adapters(backend)
    async with AsyncVmManagerClient(transport) as client:
        spec = _rich_spec("a1")
        created = await client.create_vm(spec)
        assert isinstance(created, Vm)
        assert created.spec == spec

        await client.start_vm(created.vm_id)
        got = await client.get_vm(created.vm_id)
        assert isinstance(got, Vm)
        assert got.status.state is VmState.RUNNING
        assert got.spec == spec

        vms = await client.list_vms()
        assert all(isinstance(v, Vm) for v in vms)


def test_build_vm_helper_pairs_status_and_spec_directly() -> None:
    """Unit-level check of the transport's ``_build_vm`` for the known-spec path
    (the read + create handlers funnel through it)."""
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend, clock=_fixed_clock)
    spec = _rich_spec("b1")
    domain_id = backend.define(spec)
    vm_id = VmId(domain_id.value)
    transport._remember_spec(vm_id, spec)

    info = backend.get_domain(DomainId(vm_id.value))
    vm = transport._build_vm(vm_id, info)
    assert vm.vm_id == vm_id
    assert vm.spec == spec
    assert vm.status.state is VmState.STOPPED
    assert vm.created_at == _FIXED


# -- observed disks/nics/devices propagate from DomainInfo into Vm.status -----


_OBSERVED_DISKS = (DiskStatus(target="vda", source="/pool/obs.qcow2", size_bytes=10 << 30),)
_OBSERVED_NICS = (
    NicStatus(model="virtio", mac="52:54:00:ab:cd:ef", network="default", ip="10.0.0.5"),
)
_OBSERVED_DEVICES = (
    AttachedDevice(kind="pci", selector={"vendor": "10de"}, address="0000:01:00.0"),
)


class _ObservingFakeHypervisor(FakeHypervisorBackend):
    """Fake whose ``get_domain`` surfaces non-empty observed disks/nics/devices.

    The stock core fake reports empty observed tuples; this overlays the richer
    observed shape an adapter would fill from its native reads, so we can assert
    the SDK carries them through into ``Vm.status`` (contracts §6.3).
    """

    def get_domain(self, d: DomainId) -> DomainInfo:
        base = super().get_domain(d)
        return DomainInfo(
            domain_id=base.domain_id,
            name=base.name,
            cpu=base.cpu,
            memory_bytes=base.memory_bytes,
            is_running=base.is_running,
            disks=_OBSERVED_DISKS,
            nics=_OBSERVED_NICS,
            devices_attached=_OBSERVED_DEVICES,
        )


def test_get_vm_propagates_observed_disks_nics_devices_into_status() -> None:
    """A backend ``get_domain`` carrying observed disks/nics/devices must flow
    through the Vm-assembly into ``client.get_vm(...).status`` (contracts §6.3)."""
    backend = _ObservingFakeHypervisor()
    transport = InProcessTransport.from_adapters(backend)
    with VmManagerClient(transport) as client:
        created = client.create_vm(_rich_spec("obs1"))
        got = client.get_vm(created.vm_id)

    assert got.status.disks == _OBSERVED_DISKS
    assert got.status.nics == _OBSERVED_NICS
    assert got.status.devices_attached == _OBSERVED_DEVICES
    # Identity carried; placement/error/timestamp stay at the contract defaults.
    assert got.status.vm_id == got.vm_id
    assert got.status.host_id == ""
    assert got.status.error is None
    assert got.status.last_transition_at is None
    # create_vm's returned Vm flows through the same helper — same observed shape.
    assert created.status.disks == _OBSERVED_DISKS
    assert created.status.nics == _OBSERVED_NICS
    assert created.status.devices_attached == _OBSERVED_DEVICES
