"""Unit tests for the D-G17 :class:`LibvirtEventPump` (mock libvirt).

Tier-1: no real libvirt event loop. The loop thread is stubbed out
(``_ensure_loop_thread`` → no-op) and a fake ``virConnect`` records the
single ``domainEventRegisterAny`` registration. We then invoke the
captured callback directly to prove the fan-out + per-subscriber filter
+ shared-registration + deterministic teardown that D-G17 specifies.

The live end-to-end delivery (real ``virEventRunDefaultImpl`` on
``test:///default``) is covered by the conformance tier.
"""

from __future__ import annotations

import sys
import types
from typing import Any

import pytest
from mgf.vm.core.ids import DomainId

from mgf.vm.libvirt import _event_pump
from mgf.vm.libvirt._connection import LibvirtConnection
from mgf.vm.libvirt._event_pump import LibvirtEventPump


class _FakeDom:
    def __init__(self, uuid: str) -> None:
        self._uuid = uuid

    def UUIDString(self) -> str:  # noqa: N802 - mirrors libvirt binding
        return self._uuid


class _FakeRawConn:
    """Fake ``virConnect`` recording lifecycle (de)registration."""

    def __init__(self) -> None:
        self.register_calls = 0
        self.dereg_calls = 0
        self.callback: Any = None
        self.last_dereg_id: int | None = None

    def domainEventRegisterAny(  # noqa: N802 - mirrors libvirt binding
        self, dom: Any, event_id: int, cb: Any, opaque: Any
    ) -> int:
        self.register_calls += 1
        self.callback = cb
        return 99

    def domainEventDeregisterAny(self, callback_id: int) -> None:  # noqa: N802
        self.dereg_calls += 1
        self.last_dereg_id = callback_id

    def close(self) -> int:
        return 0


@pytest.fixture
def pump(monkeypatch: pytest.MonkeyPatch) -> LibvirtEventPump:
    p = LibvirtEventPump()
    # Never start the real default-impl loop thread in a unit test.
    monkeypatch.setattr(p, "_ensure_loop_thread", lambda: None)
    return p


@pytest.fixture
def make_conn() -> Any:
    """Yield a factory for wrapped fake connections; close them on teardown.

    Closing avoids the ``ResourceWarning`` the connection emits on
    ``__del__`` when it is GC'd without :meth:`close` (DP-13).
    """
    created: list[tuple[_FakeRawConn, LibvirtConnection]] = []

    def _make() -> tuple[_FakeRawConn, LibvirtConnection]:
        raw = _FakeRawConn()
        conn = LibvirtConnection.adopt(raw, uri="test:///pump")
        created.append((raw, conn))
        return raw, conn

    yield _make
    for _raw, conn in created:
        conn.close()


def test_fan_out_respects_per_subscriber_filter(pump: LibvirtEventPump, make_conn: Any) -> None:
    raw, conn = make_conn()
    seen_all: list[DomainId] = []
    seen_x: list[DomainId] = []

    pump.subscribe(conn, None, lambda d, _p: seen_all.append(d))
    pump.subscribe(conn, DomainId("u-x"), lambda d, _p: seen_x.append(d))

    # Two subscribers on one connection share ONE libvirt registration.
    assert raw.register_calls == 1

    cb = raw.callback
    cb(raw, _FakeDom("u-x"), 1, 0, None)  # STARTED for u-x
    cb(raw, _FakeDom("u-y"), 5, 0, None)  # STOPPED for u-y

    assert seen_all == [DomainId("u-x"), DomainId("u-y")]  # host-wide gets both
    assert seen_x == [DomainId("u-x")]  # filtered: only its domain


def test_payload_carries_lifecycle_event_and_detail(pump: LibvirtEventPump, make_conn: Any) -> None:
    raw, conn = make_conn()
    payloads: list[dict[str, Any]] = []
    pump.subscribe(conn, None, lambda _d, p: payloads.append(p))

    raw.callback(raw, _FakeDom("u-1"), 2, 7, None)

    assert payloads == [{"event": "lifecycle", "lifecycle_event": 2, "detail": 7}]


def test_unsubscribe_deregisters_only_when_last(pump: LibvirtEventPump, make_conn: Any) -> None:
    raw, conn = make_conn()
    id_a = pump.subscribe(conn, None, lambda _d, _p: None)
    id_b = pump.subscribe(conn, None, lambda _d, _p: None)

    pump.unsubscribe(id_a)
    assert raw.dereg_calls == 0  # b still holds the connection

    pump.unsubscribe(id_b)
    assert raw.dereg_calls == 1  # last subscriber → deregister
    assert raw.last_dereg_id == 99


def test_dispatch_swallows_subscriber_errors(pump: LibvirtEventPump, make_conn: Any) -> None:
    # The callback runs inside libvirt's C loop; a raising subscriber must
    # never propagate (it would kill the event loop).
    raw, conn = make_conn()

    def _boom(_d: DomainId, _p: dict[str, Any]) -> None:
        raise RuntimeError("subscriber blew up")

    pump.subscribe(conn, None, _boom)
    raw.callback(raw, _FakeDom("u-1"), 1, 0, None)  # must not raise


def test_dispatch_after_unsubscribe_is_noop(pump: LibvirtEventPump, make_conn: Any) -> None:
    raw, conn = make_conn()
    seen: list[DomainId] = []
    sub_id = pump.subscribe(conn, None, lambda d, _p: seen.append(d))
    cb = raw.callback
    pump.unsubscribe(sub_id)
    cb(raw, _FakeDom("u-1"), 1, 0, None)  # connection dropped from registry
    assert seen == []


def test_instance_is_process_singleton() -> None:
    assert LibvirtEventPump.instance() is LibvirtEventPump.instance()


def test_ensure_event_impl_registered_is_once(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(_event_pump, "_impl_registered", False)
    calls = {"n": 0}

    def _reg() -> None:
        calls["n"] += 1

    fake_libvirt = types.SimpleNamespace(virEventRegisterDefaultImpl=_reg)
    monkeypatch.setitem(sys.modules, "libvirt", fake_libvirt)

    _event_pump.ensure_event_impl_registered()
    _event_pump.ensure_event_impl_registered()

    assert calls["n"] == 1  # guarded: registered exactly once


def test_run_loop_survives_a_raising_iteration(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    # _run_loop must swallow an error from virEventRunDefaultImpl and back
    # off rather than die. We let it run one failing iteration, then break
    # the loop from the back-off sleep with a BaseException.
    def _raise() -> None:
        raise RuntimeError("loop hiccup")

    fake_libvirt = types.SimpleNamespace(virEventRunDefaultImpl=_raise)
    monkeypatch.setitem(sys.modules, "libvirt", fake_libvirt)

    def _stop(_secs: float) -> None:
        raise KeyboardInterrupt  # BaseException → escapes the while True

    monkeypatch.setattr(_event_pump.time, "sleep", _stop)

    with pytest.raises(KeyboardInterrupt):
        _event_pump._run_loop()
