"""B6 unit tests — streams + the remaining sync surface.

Covers the sync ``VmManagerClient``:

* ``watch_vm`` yields :class:`VmEvent`s and surfaces an :class:`EventGap`
  (drives an event-emitting :class:`FakeHypervisorBackend`);
* the watch iterator closes cleanly — the engine subscription is released
  (DP-13), no leaked iterator threads;
* ``get_operation`` / ``list_operations`` read the op registry through the
  transport;
* ``migrate_vm`` happy path + failure (``MigrationIncompatible``) vs the fake.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import TYPE_CHECKING

import pytest
from mgf.vm.core.contracts import (
    CompatibilityResult,
    EventGap,
    MigrationPlan,
    MigrationTarget,
    OperationKind,
    OperationStatus,
    VmEvent,
)
from mgf.vm.core.exceptions import (
    DomainStateInvalid,
    MigrationIncompatible,
    TransportError,
)
from mgf.vm.core.fakes.hypervisor import FakeHypervisorBackend
from mgf.vm.core.ids import DomainId, HostId, OperationId, VmId

from mgf.vm.sdk.client import VmManagerClient
from mgf.vm.sdk.transport.inprocess import InProcessTransport

if TYPE_CHECKING:
    from collections.abc import Iterator

    from mgf.vm.core.contracts import VmSpec


def _spec(name: str = "vm-a") -> VmSpec:
    from mgf.vm.core.contracts import VmSpec

    return VmSpec(name=name, cpu=2, memory_bytes=2 << 30)


class _EventingHypervisor(FakeHypervisorBackend):
    """A fake whose ``watch_events`` replays a scripted event stream.

    The script is a list of :class:`VmEvent` / :class:`EventGap` items; the
    iterator tracks closure so a test can assert the subscription was released
    (DP-13).
    """

    def __init__(self, script: list[VmEvent | EventGap]) -> None:
        super().__init__()
        self._script = script
        self.closed_streams = 0

    def watch_events(self, d: DomainId | None = None) -> Iterator[VmEvent | EventGap]:
        return _ClosableStream(self._script, self)


class _ClosableStream:
    """Replays a scripted list; records ``close()`` on its owner (DP-13)."""

    def __init__(self, script: list[VmEvent | EventGap], owner: _EventingHypervisor) -> None:
        self._items = iter(script)
        self._owner = owner
        self._closed = False

    def __iter__(self) -> _ClosableStream:
        return self

    def __next__(self) -> VmEvent | EventGap:
        if self._closed:
            raise StopIteration
        return next(self._items)

    def close(self) -> None:
        if not self._closed:
            self._closed = True
            self._owner.closed_streams += 1


def _event(seq: int) -> VmEvent:
    return VmEvent(
        domain_id=DomainId("d-1"),
        sequence=seq,
        timestamp=datetime.now(UTC),
        payload={"state": "running"},
    )


def _gap(after: int) -> EventGap:
    return EventGap(
        domain_id=DomainId("d-1"),
        after_sequence=after,
        detected_at=datetime.now(UTC),
    )


# -- watch_vm: events + gap pass-through ------------------------------------


def test_watch_vm_yields_events_and_surfaces_gap() -> None:
    script: list[VmEvent | EventGap] = [_event(1), _event(2), _gap(2), _event(8)]
    backend = _EventingHypervisor(script)
    transport = InProcessTransport.from_adapters(backend)
    with VmManagerClient(transport) as client:
        out = list(client.watch_vm(VmId("d-1")))

    assert [type(x).__name__ for x in out] == ["VmEvent", "VmEvent", "EventGap", "VmEvent"]
    # The gap is surfaced (not swallowed) so the caller can reconcile.
    gap = out[2]
    assert isinstance(gap, EventGap)
    assert gap.after_sequence == 2
    # Sequences resume past the gap.
    assert isinstance(out[3], VmEvent)
    assert out[3].sequence == 8


def test_watch_vm_none_subscribes_to_all_domains() -> None:
    backend = _EventingHypervisor([_event(1)])
    transport = InProcessTransport.from_adapters(backend)
    client = VmManagerClient(transport)
    out = list(client.watch_vm())
    assert len(out) == 1


def test_watch_vm_iterator_closes_cleanly() -> None:
    """Closing the iterator releases the engine subscription (DP-13)."""
    backend = _EventingHypervisor([_event(1), _event(2), _event(3)])
    transport = InProcessTransport.from_adapters(backend)
    client = VmManagerClient(transport)

    stream = client.watch_vm(VmId("d-1"))
    assert next(stream).sequence == 1  # type: ignore[union-attr]
    # Early close — even mid-stream — must release the subscription.
    stream.close()  # type: ignore[attr-defined]
    assert backend.closed_streams == 1


def test_watch_vm_with_explicit_close_after_break() -> None:
    """The idiomatic ``try/finally close()`` pattern releases the subscription."""
    backend = _EventingHypervisor([_event(1), _event(2), _event(3)])
    transport = InProcessTransport.from_adapters(backend)
    client = VmManagerClient(transport)

    stream = client.watch_vm(VmId("d-1"))
    try:
        for ev in stream:
            assert isinstance(ev, VmEvent)
            break  # caller decides to stop early
    finally:
        stream.close()  # type: ignore[attr-defined]
    assert backend.closed_streams == 1


# -- get_operation / list_operations ---------------------------------------


def test_get_and_list_operations() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    client = VmManagerClient(transport)

    info = client.create_vm(_spec("ops"))
    vm_id = info.vm_id
    client.start_vm(vm_id)

    ops = client.list_operations()
    assert len(ops) == 2  # VM_CREATE + VM_START
    kinds = {o.kind for o in ops}
    assert kinds == {OperationKind.VM_CREATE, OperationKind.VM_START}
    assert all(o.status is OperationStatus.SUCCEEDED for o in ops)

    # get_operation round-trips one handle.
    one = client.get_operation(ops[0].op_id)
    assert one.op_id == ops[0].op_id
    assert one.status is OperationStatus.SUCCEEDED


def test_get_operation_unknown_raises() -> None:
    transport = InProcessTransport.from_adapters(FakeHypervisorBackend())
    client = VmManagerClient(transport)
    with pytest.raises(TransportError):
        client.get_operation(OperationId("op-nope"))


# -- migrate_vm ------------------------------------------------------------


def _plan(*, compatible: bool) -> MigrationPlan:
    reasons = () if compatible else ("cpu features missing on dest",)
    return MigrationPlan(
        compatibility=CompatibilityResult(compatible=compatible, reasons=reasons),
        storage_strategy="shared",
    )


def test_migrate_vm_happy_path() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    client = VmManagerClient(transport)
    info = client.create_vm(_spec("mig"))
    vm_id = info.vm_id
    client.start_vm(vm_id)

    dest = MigrationTarget(host=HostId("host-2"))
    client.migrate_vm(vm_id, dest, plan=_plan(compatible=True))
    # The fake marks the source stopped (it "migrated away").
    assert not backend.get_domain(DomainId(vm_id.value)).is_running


def test_migrate_vm_incompatible_reraises() -> None:
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    client = VmManagerClient(transport)
    info = client.create_vm(_spec("migbad"))
    vm_id = info.vm_id
    client.start_vm(vm_id)

    dest = MigrationTarget(host=HostId("host-2"))
    with pytest.raises(MigrationIncompatible):
        client.migrate_vm(vm_id, dest, plan=_plan(compatible=False))
    # Atomic: on failure the source is left running.
    assert backend.get_domain(DomainId(vm_id.value)).is_running


def test_migrate_stopped_vm_fails_fast() -> None:
    # MIGRATING is reachable only from RUNNING — a stopped VM fails the
    # intermediate transition before the backend is touched.
    backend = FakeHypervisorBackend()
    transport = InProcessTransport.from_adapters(backend)
    client = VmManagerClient(transport)
    info = client.create_vm(_spec("migstop"))
    vm_id = info.vm_id
    dest = MigrationTarget(host=HostId("host-2"))
    with pytest.raises(DomainStateInvalid):
        client.migrate_vm(vm_id, dest, plan=_plan(compatible=True))
