"""Unit tests for the Wave-B observation surfaces (mock libvirt).

Tier-1: no real libvirt connection. Self-contained ``virConnect`` /
``virDomain`` / ``virNetwork`` / ``virDomainSnapshot`` doubles drive the
new observation surfaces end-to-end through the foundation seam:

* richer snapshots — ``SnapshotInfo.description`` (from the snapshot XML
  ``<description>``) + ``parent_name`` (from ``<parent><name>`` else
  ``getParent()``; ``None`` at the root);
* the three host-network methods on ``LibvirtHypervisorBackend``
  (``list_networks`` / ``network_leases`` / ``bridge_for_domain``) incl.
  the degrade-on-failure paths;
* ``LibvirtMetricsBackend.sample_metrics`` — cpu/memory/per-disk/per-nic
  mapping + per-device degrade.

The fakes raise ``FakeLibvirtError`` (carrying ``get_error_code()``) so the
translation seam is exercised; nothing here imports the real binding.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

import pytest
from mgf.vm.core.contracts import (
    DiskIo,
    MetricsSample,
    NetworkLease,
    NetworkProfile,
    NicIo,
)
from mgf.vm.core.exceptions import DomainNotFound
from mgf.vm.core.ids import DomainId

from mgf.vm.libvirt._connection import LibvirtConnection
from mgf.vm.libvirt._hypervisor import LibvirtHypervisorBackend
from mgf.vm.libvirt._metrics import LibvirtMetricsBackend

_RUNNING = 1
_SHUTOFF = 5


class FakeLibvirtError(Exception):
    def __init__(self, code: int | None, message: str = "libvirt failure") -> None:
        super().__init__(message)
        self._code = code

    def get_error_code(self) -> int | None:
        return self._code


# ---------------------------------------------------------------------------
# Snapshot double (richer XML + parent linkage)
# ---------------------------------------------------------------------------


class FakeSnapshot:
    def __init__(
        self,
        name: str,
        *,
        description: str = "",
        parent: FakeSnapshot | None = None,
        parent_in_xml: bool = False,
        getparent_raises: bool = False,
        state: str = "shutoff",
    ) -> None:
        self._name = name
        self._description = description
        self._parent = parent
        self._parent_in_xml = parent_in_xml
        self._getparent_raises = getparent_raises
        self._state = state

    def getName(self) -> str:  # noqa: N802
        return self._name

    def numChildren(self, _flags: int = 0) -> int:  # noqa: N802
        return 0

    def getXMLDesc(self, _flags: int = 0) -> str:  # noqa: N802
        parent_xml = ""
        if self._parent_in_xml and self._parent is not None:
            parent_xml = f"<parent><name>{self._parent._name}</name></parent>"
        return (
            "<domainsnapshot>"
            f"<name>{self._name}</name>"
            f"<description>{self._description}</description>"
            f"<state>{self._state}</state>"
            "<memory snapshot='no'/>"
            "<creationTime>1700000000</creationTime>"
            f"{parent_xml}"
            "</domainsnapshot>"
        )

    def getParent(self) -> FakeSnapshot:  # noqa: N802
        if self._getparent_raises or self._parent is None:
            # libvirt raises VIR_ERR_NO_DOMAIN_SNAPSHOT at the tree root.
            raise FakeLibvirtError(72, "no parent")
        return self._parent


# ---------------------------------------------------------------------------
# Network double
# ---------------------------------------------------------------------------


class FakeNetwork:
    def __init__(
        self,
        name: str,
        *,
        bridge: str | None = "virbr0",
        active: bool = True,
        forward_mode: str | None = "nat",
        leases: list[dict[str, Any]] | None = None,
        bridge_raises: bool = False,
        leases_raise: bool = False,
        xml_raises: bool = False,
    ) -> None:
        self._name = name
        self._bridge = bridge
        self._active = active
        self._forward = forward_mode
        self._leases = leases or []
        self._bridge_raises = bridge_raises
        self._leases_raise = leases_raise
        self._xml_raises = xml_raises

    def name(self) -> str:
        return self._name

    def bridgeName(self) -> str:  # noqa: N802
        if self._bridge_raises or self._bridge is None:
            raise FakeLibvirtError(1, "no bridge")
        return self._bridge

    def isActive(self) -> int:  # noqa: N802
        return 1 if self._active else 0

    def XMLDesc(self, _flags: int = 0) -> str:  # noqa: N802
        if self._xml_raises:
            raise FakeLibvirtError(1, "xml failure")
        forward = f"<forward mode='{self._forward}'/>" if self._forward else ""
        return f"<network><name>{self._name}</name>{forward}</network>"

    def DHCPLeases(self) -> list[dict[str, Any]]:  # noqa: N802
        if self._leases_raise:
            raise FakeLibvirtError(1, "leases failure")
        return self._leases


# ---------------------------------------------------------------------------
# Domain double (snapshots + interfaces + metrics)
# ---------------------------------------------------------------------------


class FakeDomain:
    def __init__(
        self,
        name: str,
        uuid: str,
        *,
        state: int = _RUNNING,
        interfaces_xml: str = "",
        disks_xml: str = "",
        block_stats: dict[str, list[int]] | None = None,
        iface_stats: dict[str, list[int]] | None = None,
        block_raises: set[str] | None = None,
        iface_raises: set[str] | None = None,
        memory_stats: dict[str, int] | None = None,
        memory_stats_raises: bool = False,
        info: list[int] | None = None,
    ) -> None:
        self._name = name
        self._uuid = uuid
        self._state = state
        self._interfaces_xml = interfaces_xml
        self._disks_xml = disks_xml
        self._block_stats = block_stats or {}
        self._iface_stats = iface_stats or {}
        self._block_raises = block_raises or set()
        self._iface_raises = iface_raises or set()
        self._memory_stats = memory_stats
        self._memory_stats_raises = memory_stats_raises
        self._info = info or [_RUNNING, 2048, 1024, 2, 5_000_000_000]
        self.snapshots: dict[str, FakeSnapshot] = {}

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

    def name(self) -> str:
        return self._name

    def state(self, _flags: int = 0) -> list[int]:
        return [self._state, 0]

    def XMLDesc(self, _flags: int = 0) -> str:  # noqa: N802
        return (
            f'<domain type="test"><name>{self._name}</name>'
            f"<uuid>{self._uuid}</uuid>"
            f"<devices>{self._disks_xml}{self._interfaces_xml}</devices></domain>"
        )

    def listAllSnapshots(self, _flags: int = 0) -> list[FakeSnapshot]:  # noqa: N802
        return list(self.snapshots.values())

    # metrics
    def info(self) -> list[int]:
        return self._info

    def memoryStats(self) -> dict[str, int]:  # noqa: N802
        if self._memory_stats_raises:
            raise FakeLibvirtError(1, "memoryStats failure")
        if self._memory_stats is None:
            return {}
        return self._memory_stats

    def blockStats(self, target: str) -> list[int]:  # noqa: N802
        if target in self._block_raises:
            raise FakeLibvirtError(1, f"blockStats {target} failure")
        return self._block_stats[target]

    def interfaceStats(self, dev: str) -> list[int]:  # noqa: N802
        if dev in self._iface_raises:
            raise FakeLibvirtError(1, f"interfaceStats {dev} failure")
        return self._iface_stats[dev]


class FakeConn:
    def __init__(self) -> None:
        self.domains: dict[str, FakeDomain] = {}
        self.networks: dict[str, FakeNetwork] = {}
        self._list_networks_raises = False

    def _add_domain(self, dom: FakeDomain) -> None:
        self.domains[dom._uuid] = dom

    def _add_network(self, net: FakeNetwork) -> None:
        self.networks[net._name] = net

    def listAllNetworks(self, _flags: int = 0) -> list[FakeNetwork]:  # noqa: N802
        if self._list_networks_raises:
            raise FakeLibvirtError(1, "listAllNetworks failure")
        return list(self.networks.values())

    def networkLookupByName(self, name: str) -> FakeNetwork:  # noqa: N802
        if name not in self.networks:
            raise FakeLibvirtError(43, f"no network {name}")
        return self.networks[name]

    def lookupByUUIDString(self, uuid: str) -> FakeDomain:  # noqa: N802
        if uuid not in self.domains:
            raise FakeLibvirtError(42, f"no domain {uuid}")
        return self.domains[uuid]

    def close(self) -> int:
        return 0


@pytest.fixture
def conn() -> FakeConn:
    return FakeConn()


@pytest.fixture
def backend(conn: FakeConn) -> Any:
    wrapped = LibvirtConnection.adopt(conn, uri="test:///unit")
    be = LibvirtHypervisorBackend(connection=wrapped)
    yield be
    be.close()


@pytest.fixture
def metrics(conn: FakeConn) -> Any:
    wrapped = LibvirtConnection.adopt(conn, uri="test:///unit")
    fixed = datetime(2026, 5, 31, 12, 0, 0, tzinfo=UTC)
    try:
        yield LibvirtMetricsBackend(wrapped, clock=lambda: fixed)
    finally:
        # The metrics backend never owns the connection's lifecycle, so the
        # test that opened it closes it (DP-13 leak detection).
        wrapped.close()


# ---------------------------------------------------------------------------
# 1. Richer snapshots
# ---------------------------------------------------------------------------


def test_list_snapshots_populates_description_and_parent_from_xml(
    backend: Any, conn: FakeConn
) -> None:
    dom = FakeDomain("vm", "u-1", state=_SHUTOFF)
    root = FakeSnapshot("base", description="initial install")
    child = FakeSnapshot(
        "after-update",
        description="post apt upgrade",
        parent=root,
        parent_in_xml=True,
    )
    dom.snapshots = {"base": root, "after-update": child}
    conn._add_domain(dom)

    snaps = {s.name: s for s in backend.list_snapshots(DomainId("u-1"))}

    assert snaps["base"].description == "initial install"
    assert snaps["base"].parent_name is None  # root
    assert snaps["after-update"].description == "post apt upgrade"
    # parent linkage resolved from the <parent><name> XML element.
    assert snaps["after-update"].parent_name == "base"


def test_list_snapshots_populates_state_from_xml(backend: Any, conn: FakeConn) -> None:
    # D-G21: the snapshot <state> string folds onto the neutral VmState;
    # an unknown/missing state degrades to None.
    from mgf.vm.core.contracts import VmState

    dom = FakeDomain("vm", "u-1", state=_SHUTOFF)
    dom.snapshots = {
        "live": FakeSnapshot("live", state="running"),
        "frozen": FakeSnapshot("frozen", state="paused"),
        "off": FakeSnapshot("off", state="shutoff"),
        "weird": FakeSnapshot("weird", state="disk-snapshot"),
    }
    conn._add_domain(dom)

    snaps = {s.name: s for s in backend.list_snapshots(DomainId("u-1"))}

    assert snaps["live"].state is VmState.RUNNING
    assert snaps["frozen"].state is VmState.PAUSED
    assert snaps["off"].state is VmState.STOPPED
    assert snaps["weird"].state is None


def test_list_snapshots_parent_falls_back_to_getparent(backend: Any, conn: FakeConn) -> None:
    dom = FakeDomain("vm", "u-2", state=_SHUTOFF)
    root = FakeSnapshot("root")
    # No <parent> in XML → must fall back to getParent().getName().
    child = FakeSnapshot("leaf", parent=root, parent_in_xml=False)
    dom.snapshots = {"root": root, "leaf": child}
    conn._add_domain(dom)

    snaps = {s.name: s for s in backend.list_snapshots(DomainId("u-2"))}
    assert snaps["leaf"].parent_name == "root"
    assert snaps["root"].parent_name is None  # getParent raises at root → None


def test_list_snapshots_missing_description_degrades_to_empty(backend: Any, conn: FakeConn) -> None:
    dom = FakeDomain("vm", "u-3", state=_SHUTOFF)
    dom.snapshots = {"s": FakeSnapshot("s", description="")}
    conn._add_domain(dom)
    (snap,) = backend.list_snapshots(DomainId("u-3"))
    assert snap.description == ""
    assert snap.parent_name is None


# ---------------------------------------------------------------------------
# 2a. list_networks
# ---------------------------------------------------------------------------


def test_list_networks_maps_profile_fields(backend: Any, conn: FakeConn) -> None:
    conn._add_network(FakeNetwork("default", bridge="virbr0", active=True, forward_mode="nat"))
    nets = backend.list_networks()
    assert isinstance(nets, tuple)
    assert nets == (
        NetworkProfile(name="default", bridge="virbr0", active=True, forward_mode="nat"),
    )


def test_list_networks_degrades_bad_network_to_partial(backend: Any, conn: FakeConn) -> None:
    # An inactive net whose bridge read raises → bridge degrades to None,
    # the network still surfaces (per-field degrade, not a whole-list abort).
    conn._add_network(
        FakeNetwork("isolated", bridge_raises=True, active=False, forward_mode="isolated")
    )
    conn._add_network(FakeNetwork("ok", bridge="virbr1", forward_mode=None))
    nets = {n.name: n for n in backend.list_networks()}
    assert nets["isolated"].bridge is None
    assert nets["isolated"].active is False
    assert nets["isolated"].forward_mode == "isolated"
    assert nets["ok"].bridge == "virbr1"
    assert nets["ok"].forward_mode is None


def test_list_networks_xml_failure_degrades_forward_mode(backend: Any, conn: FakeConn) -> None:
    conn._add_network(FakeNetwork("n", bridge="br0", xml_raises=True))
    (net,) = backend.list_networks()
    assert net.bridge == "br0"
    assert net.forward_mode is None  # XML parse failed → forward degrades


# ---------------------------------------------------------------------------
# 2b. network_leases
# ---------------------------------------------------------------------------


def test_network_leases_maps_lease_fields(backend: Any, conn: FakeConn) -> None:
    expiry = 1_900_000_000
    conn._add_network(
        FakeNetwork(
            "default",
            leases=[
                {
                    "mac": "52:54:00:aa:bb:cc",
                    "ipaddr": "192.168.122.10",
                    "hostname": "guest1",
                    "expirytime": expiry,
                }
            ],
        )
    )
    leases = backend.network_leases("default")
    assert leases == (
        NetworkLease(
            mac="52:54:00:aa:bb:cc",
            ip="192.168.122.10",
            hostname="guest1",
            expires_at=datetime.fromtimestamp(expiry, tz=UTC),
        ),
    )


def test_network_leases_handles_missing_optional_fields(backend: Any, conn: FakeConn) -> None:
    conn._add_network(FakeNetwork("default", leases=[{"mac": "52:54:00:00:00:01"}]))
    (lease,) = backend.network_leases("default")
    assert lease.mac == "52:54:00:00:00:01"
    assert lease.ip is None
    assert lease.hostname is None
    assert lease.expires_at is None


def test_network_leases_unknown_network_returns_empty(backend: Any, conn: FakeConn) -> None:
    assert backend.network_leases("no-such-net") == ()


def test_network_leases_inactive_network_returns_empty(backend: Any, conn: FakeConn) -> None:
    conn._add_network(FakeNetwork("off", leases_raise=True))
    assert backend.network_leases("off") == ()


# ---------------------------------------------------------------------------
# 2c. bridge_for_domain
# ---------------------------------------------------------------------------


def test_bridge_for_domain_resolves_network_to_bridge(backend: Any, conn: FakeConn) -> None:
    dom = FakeDomain(
        "vm",
        "u-net",
        interfaces_xml="<interface type='network'><source network='default'/></interface>",
    )
    conn._add_domain(dom)
    conn._add_network(FakeNetwork("default", bridge="virbr-resolved"))
    assert backend.bridge_for_domain(DomainId("u-net")) == "virbr-resolved"


def test_bridge_for_domain_direct_bridge(backend: Any, conn: FakeConn) -> None:
    dom = FakeDomain(
        "vm",
        "u-br",
        interfaces_xml="<interface type='bridge'><source bridge='br-direct'/></interface>",
    )
    conn._add_domain(dom)
    assert backend.bridge_for_domain(DomainId("u-br")) == "br-direct"


def test_bridge_for_domain_no_bridged_interface_returns_none(backend: Any, conn: FakeConn) -> None:
    dom = FakeDomain(
        "vm",
        "u-mac",
        interfaces_xml="<interface type='direct'><source dev='eth0' mode='vepa'/></interface>",
    )
    conn._add_domain(dom)
    assert backend.bridge_for_domain(DomainId("u-mac")) is None


def test_bridge_for_domain_unknown_domain_raises(backend: Any, conn: FakeConn) -> None:
    with pytest.raises(DomainNotFound):
        backend.bridge_for_domain(DomainId("ghost"))


# ---------------------------------------------------------------------------
# 3. LibvirtMetricsBackend.sample_metrics
# ---------------------------------------------------------------------------


def test_sample_metrics_maps_all_fields(metrics: Any, conn: FakeConn) -> None:
    dom = FakeDomain(
        "vm",
        "u-m",
        disks_xml="<disk><target dev='vda'/></disk><disk><target dev='vdb'/></disk>",
        interfaces_xml=(
            "<interface type='network'><target dev='vnet0'/>"
            "<mac address='52:54:00:11:22:33'/></interface>"
        ),
        block_stats={
            # [rd_req, rd_bytes, wr_req, wr_bytes, errs]
            "vda": [10, 4096, 5, 2048, 0],
            "vdb": [1, 512, 1, 256, 0],
        },
        iface_stats={
            # [rx_bytes, rx_pkts, rx_err, rx_drop, tx_bytes, tx_pkts, ...]
            "vnet0": [1000, 10, 0, 0, 2000, 20, 0, 0],
        },
        memory_stats={"rss": 524288, "usable": 131072},  # KiB
        info=[_RUNNING, 2048, 1024, 2, 7_000_000_000],
    )
    conn._add_domain(dom)

    sample = metrics.sample_metrics(DomainId("u-m"))
    assert isinstance(sample, MetricsSample)
    assert sample.cpu_time_ns == 7_000_000_000
    assert sample.cpu_percent is None  # single sample cannot compute %
    assert sample.memory_used_bytes == 524288 * 1024
    assert sample.memory_available_bytes == 131072 * 1024
    assert sample.sampled_at.tzinfo is UTC
    assert sample.disks == (
        DiskIo(target="vda", read_bytes=4096, write_bytes=2048, read_reqs=10, write_reqs=5),
        DiskIo(target="vdb", read_bytes=512, write_bytes=256, read_reqs=1, write_reqs=1),
    )
    assert sample.nics == (
        NicIo(
            mac="52:54:00:11:22:33",
            rx_bytes=1000,
            tx_bytes=2000,
            rx_packets=10,
            tx_packets=20,
        ),
    )


def test_sample_metrics_memory_falls_back_to_info(metrics: Any, conn: FakeConn) -> None:
    dom = FakeDomain(
        "vm",
        "u-noms",
        memory_stats_raises=True,
        info=[_RUNNING, 2048, 2048, 2, 1_000_000_000],
    )
    conn._add_domain(dom)
    sample = metrics.sample_metrics(DomainId("u-noms"))
    assert sample.memory_used_bytes == 2048 * 1024  # info()[2] KiB → bytes
    assert sample.memory_available_bytes is None


def test_sample_metrics_per_disk_and_nic_degrade(metrics: Any, conn: FakeConn) -> None:
    dom = FakeDomain(
        "vm",
        "u-deg",
        disks_xml="<disk><target dev='vda'/></disk><disk><target dev='vdb'/></disk>",
        interfaces_xml=(
            "<interface><target dev='vnet0'/></interface>"
            "<interface><target dev='vnet1'/><mac address='aa:bb:cc:dd:ee:ff'/></interface>"
        ),
        block_stats={"vda": [1, 100, 1, 50, 0]},
        block_raises={"vdb"},  # vdb stats raise → dropped
        iface_stats={"vnet1": [5, 1, 0, 0, 7, 1, 0, 0]},
        iface_raises={"vnet0"},  # vnet0 stats raise → dropped
        memory_stats={"rss": 1024},
    )
    conn._add_domain(dom)
    sample = metrics.sample_metrics(DomainId("u-deg"))
    # Only the readable disk / nic survive (per-device degrade).
    assert [d.target for d in sample.disks] == ["vda"]
    assert [n.mac for n in sample.nics] == ["aa:bb:cc:dd:ee:ff"]


def test_sample_metrics_nic_without_mac_keys_by_dev(metrics: Any, conn: FakeConn) -> None:
    dom = FakeDomain(
        "vm",
        "u-nomac",
        interfaces_xml="<interface><target dev='vnet9'/></interface>",
        iface_stats={"vnet9": [1, 1, 0, 0, 2, 1, 0, 0]},
        memory_stats={"rss": 1},
    )
    conn._add_domain(dom)
    (nic,) = metrics.sample_metrics(DomainId("u-nomac")).nics
    assert nic.mac == "vnet9"  # degrades to the device name when no MAC


def test_sample_metrics_unknown_domain_raises(metrics: Any) -> None:
    with pytest.raises(DomainNotFound):
        metrics.sample_metrics(DomainId("ghost"))


def test_metrics_backend_context_manager_does_not_close_connection(
    conn: FakeConn,
) -> None:
    wrapped = LibvirtConnection.adopt(conn, uri="test:///unit")
    with LibvirtMetricsBackend(wrapped) as m:
        assert m is not None
    # The metrics backend never owns the connection's lifecycle.
    assert wrapped.closed is False
    wrapped.close()
