"""Unit tests for the observed disks/nics/devices_attached mapping.

Covers the adapter boundary (``mgf.vm.libvirt._xml.read.xml_to_domaininfo``)
feeding canned domain XML + best-effort enrichment callables, asserting the
``DiskStatus`` / ``NicStatus`` / ``AttachedDevice`` mapping — including the
degrade-on-``blockInfo``-failure path and the off-domain
``interfaceAddresses`` path. The backend wiring
(:class:`LibvirtHypervisorBackend.get_domain` / ``list_domains``) is also
exercised end-to-end through a fake ``virDomain`` exposing
``blockInfo``/``interfaceAddresses``.

Tier-1: no real libvirt connection. All parsing happens in the adapter; the
canned XML stands in for ``virDomain.XMLDesc(0)`` (untrusted, defused).
"""

from __future__ import annotations

from typing import Any

import pytest
from mgf.vm.core.contracts import AttachedDevice, DiskStatus, NicStatus
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._xml.read import xml_to_domaininfo

_RUNNING = 1
_SHUTOFF = 5


# ---------------------------------------------------------------------------
# Canned domain XML
# ---------------------------------------------------------------------------


def _domain_xml(devices: str = "", *, name: str = "vm1", uuid: str = "u-1") -> str:
    return (
        f'<domain type="kvm"><name>{name}</name><uuid>{uuid}</uuid>'
        '<memory unit="bytes">1048576</memory>'
        '<currentMemory unit="bytes">1048576</currentMemory>'
        "<vcpu>2</vcpu><os><type>hvm</type></os>"
        f"<devices>{devices}</devices></domain>"
    )


_DISK_FILE = (
    '<disk type="file" device="disk">'
    '<source file="/var/lib/libvirt/images/vm1.qcow2"/>'
    '<target dev="vda" bus="virtio"/>'
    "</disk>"
)
_DISK_BLOCK_RO = (
    '<disk type="block" device="cdrom">'
    '<source dev="/dev/sr0"/>'
    '<target dev="sda" bus="sata"/>'
    "<readonly/>"
    "</disk>"
)
_DISK_NO_TARGET = '<disk type="file" device="disk"><source file="/x.img"/></disk>'
_DISK_EMPTY_CDROM = '<disk type="file" device="cdrom"><target dev="hdc"/></disk>'

_NIC_VIRTIO = (
    '<interface type="network">'
    '<mac address="52:54:00:AA:BB:CC"/>'
    '<source network="default"/>'
    '<model type="virtio"/>'
    "</interface>"
)
_NIC_BRIDGE_E1000 = (
    '<interface type="bridge">'
    '<mac address="52:54:00:11:22:33"/>'
    '<source bridge="br0"/>'
    '<model type="e1000"/>'
    "</interface>"
)
_NIC_NO_MODEL = (
    '<interface type="network">'
    '<mac address="52:54:00:99:99:99"/>'
    '<source network="other"/>'
    "</interface>"
)

_HOSTDEV_PCI = (
    '<hostdev mode="subsystem" type="pci" managed="yes"><source>'
    '<address domain="0x0000" bus="0x01" slot="0x00" function="0x0"/>'
    "</source></hostdev>"
)
_HOSTDEV_USB = (
    '<hostdev mode="subsystem" type="usb"><source>'
    '<vendor id="0x1234"/><product id="0x5678"/>'
    '<address bus="2" device="3"/>'
    "</source></hostdev>"
)


# ---------------------------------------------------------------------------
# disks
# ---------------------------------------------------------------------------


def test_disks_parsed_from_xml_without_enrichment() -> None:
    info = xml_to_domaininfo(_domain_xml(_DISK_FILE + _DISK_BLOCK_RO))
    assert info.disks == (
        DiskStatus(
            target="vda",
            source="/var/lib/libvirt/images/vm1.qcow2",
            size_bytes=0,
            readonly=False,
            allocation_bytes=None,
        ),
        DiskStatus(
            target="sda",
            source="/dev/sr0",
            size_bytes=0,
            readonly=True,
            allocation_bytes=None,
        ),
    )


def test_disks_enriched_with_block_info() -> None:
    sizes = {"vda": (10 * 1024**3, 4 * 1024**3)}

    def block_info(target: str) -> tuple[int, int] | None:
        return sizes.get(target)

    info = xml_to_domaininfo(_domain_xml(_DISK_FILE), block_info=block_info)
    disk = info.disks[0]
    assert disk.target == "vda"
    assert disk.size_bytes == 10 * 1024**3
    assert disk.allocation_bytes == 4 * 1024**3


def test_disks_degrade_when_block_info_raises_on_one_disk() -> None:
    # blockInfo blows up for sda (e.g. domain off / backing file gone) but
    # succeeds for vda — only sda's sizes degrade, the read still returns
    # both disks.
    def block_info(target: str) -> tuple[int, int] | None:
        if target == "sda":
            raise RuntimeError("blockInfo failed: device not found")
        return (5 * 1024**3, 1 * 1024**3)

    info = xml_to_domaininfo(_domain_xml(_DISK_FILE + _DISK_BLOCK_RO), block_info=block_info)
    by_target = {d.target: d for d in info.disks}
    assert by_target["vda"].size_bytes == 5 * 1024**3
    assert by_target["vda"].allocation_bytes == 1 * 1024**3
    # sda degraded to defaults, not dropped, not raised.
    assert by_target["sda"].size_bytes == 0
    assert by_target["sda"].allocation_bytes is None
    assert by_target["sda"].readonly is True


def test_disk_missing_target_is_skipped() -> None:
    info = xml_to_domaininfo(_domain_xml(_DISK_NO_TARGET + _DISK_FILE))
    assert [d.target for d in info.disks] == ["vda"]


def test_empty_cdrom_disk_has_no_source() -> None:
    info = xml_to_domaininfo(_domain_xml(_DISK_EMPTY_CDROM))
    assert info.disks[0].target == "hdc"
    assert info.disks[0].source is None


def test_no_disks_yields_empty_tuple() -> None:
    info = xml_to_domaininfo(_domain_xml(""))
    assert info.disks == ()


def test_block_info_returning_none_keeps_defaults() -> None:
    info = xml_to_domaininfo(_domain_xml(_DISK_FILE), block_info=lambda _t: None)
    assert info.disks[0].size_bytes == 0
    assert info.disks[0].allocation_bytes is None


# ---------------------------------------------------------------------------
# nics
# ---------------------------------------------------------------------------


def test_nics_parsed_from_xml() -> None:
    info = xml_to_domaininfo(_domain_xml(_NIC_VIRTIO + _NIC_BRIDGE_E1000))
    assert info.nics == (
        NicStatus(model="virtio", mac="52:54:00:AA:BB:CC", network="default", ip=None),
        NicStatus(model="e1000", mac="52:54:00:11:22:33", network="br0", ip=None),
    )


def test_nic_defaults_to_virtio_model() -> None:
    info = xml_to_domaininfo(_domain_xml(_NIC_NO_MODEL))
    assert info.nics[0].model == "virtio"


def test_nics_enriched_with_lease_ip_by_mac() -> None:
    # interfaceAddresses returns the libvirt shape keyed by iface name.
    leases: dict[str, Any] = {
        "vnet0": {
            "hwaddr": "52:54:00:aa:bb:cc",
            "addrs": [{"addr": "192.168.122.50", "prefix": 24, "type": 0}],
        }
    }
    info = xml_to_domaininfo(
        _domain_xml(_NIC_VIRTIO + _NIC_BRIDGE_E1000),
        interface_addresses=lambda: leases,
    )
    by_mac = {n.mac: n for n in info.nics}
    # Case-insensitive MAC match against the canned upper-case XML mac.
    assert by_mac["52:54:00:AA:BB:CC"].ip == "192.168.122.50"
    # The bridge NIC has no lease entry → ip stays None.
    assert by_mac["52:54:00:11:22:33"].ip is None


def test_nics_ip_none_when_interface_addresses_raises() -> None:
    # Off domain / no agent / unsupported → interfaceAddresses raises; every
    # NIC's ip stays None, no exception escapes.
    def raising() -> dict[str, Any] | None:
        raise RuntimeError("domain is not running")

    info = xml_to_domaininfo(_domain_xml(_NIC_VIRTIO), interface_addresses=raising)
    assert info.nics[0].ip is None


def test_nics_ip_none_when_no_callable() -> None:
    info = xml_to_domaininfo(_domain_xml(_NIC_VIRTIO))
    assert info.nics[0].ip is None


def test_nics_tolerate_garbage_interface_addresses_shape() -> None:
    # A non-dict / unusable return must not raise — just no enrichment.
    info = xml_to_domaininfo(
        _domain_xml(_NIC_VIRTIO),
        interface_addresses=lambda: ["nope"],  # type: ignore[return-value,arg-type]
    )
    assert info.nics[0].ip is None


def test_no_nics_yields_empty_tuple() -> None:
    info = xml_to_domaininfo(_domain_xml(""))
    assert info.nics == ()


# ---------------------------------------------------------------------------
# devices_attached
# ---------------------------------------------------------------------------


def test_pci_hostdev_mapped_to_attached_device() -> None:
    info = xml_to_domaininfo(_domain_xml(_HOSTDEV_PCI))
    assert info.devices_attached == (
        AttachedDevice(
            kind="pci",
            selector={
                "domain": "0x0000",
                "bus": "0x01",
                "slot": "0x00",
                "function": "0x0",
            },
            lease=None,
            address="0000:01:00.0",
        ),
    )


def test_usb_hostdev_mapped_to_attached_device() -> None:
    info = xml_to_domaininfo(_domain_xml(_HOSTDEV_USB))
    dev = info.devices_attached[0]
    assert dev.kind == "usb"
    assert dev.selector == {
        "vendor": "0x1234",
        "product": "0x5678",
        "bus": "2",
        "device": "3",
    }
    assert dev.address == "2:3"
    assert dev.lease is None


def test_hostdev_lease_is_always_none() -> None:
    # libvirt does not know broker leases — every AttachedDevice.lease=None.
    info = xml_to_domaininfo(_domain_xml(_HOSTDEV_PCI + _HOSTDEV_USB))
    assert all(d.lease is None for d in info.devices_attached)


def test_no_hostdev_yields_empty_tuple() -> None:
    info = xml_to_domaininfo(_domain_xml(_DISK_FILE + _NIC_VIRTIO))
    assert info.devices_attached == ()


# ---------------------------------------------------------------------------
# Backend wiring: get_domain / list_domains return populated DomainInfo,
# with a fake virDomain exposing blockInfo / interfaceAddresses.
# ---------------------------------------------------------------------------


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


class FakeObserveDomain:
    """Fake ``virDomain`` carrying canned XML + block/iface enrichment."""

    def __init__(
        self,
        name: str,
        uuid: str,
        *,
        state: int,
        devices: str,
        block_sizes: dict[str, tuple[int, int]] | None = None,
        block_raises: set[str] | None = None,
        leases: dict[str, Any] | None = None,
        iface_raises: bool = False,
    ) -> None:
        self._name = name
        self._uuid = uuid
        self._state = state
        self._xml = _domain_xml(devices, name=name, uuid=uuid)
        self._block_sizes = block_sizes or {}
        self._block_raises = block_raises or set()
        self._leases = leases
        self._iface_raises = iface_raises

    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 self._xml

    def blockInfo(self, target: str, _flags: int = 0) -> list[int]:  # noqa: N802
        if target in self._block_raises:
            raise FakeLibvirtError(9, f"blockInfo failed for {target}")
        cap, alloc = self._block_sizes[target]
        return [cap, alloc, cap]  # capacity, allocation, physical

    def interfaceAddresses(self, _source: int, _flags: int = 0) -> dict[str, Any]:  # noqa: N802
        if self._iface_raises or self._leases is None:
            raise FakeLibvirtError(84, "interfaceAddresses unsupported / domain off")
        return self._leases


class FakeObserveConn:
    def __init__(self) -> None:
        self.domains: dict[str, FakeObserveDomain] = {}
        self.closed = 0

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

    def getType(self) -> str:  # noqa: N802
        return "TEST"

    def listAllDomains(self, _flags: int = 0) -> list[FakeObserveDomain]:  # noqa: N802
        return list(self.domains.values())

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

    def close(self) -> int:
        self.closed += 1
        return 0


@pytest.fixture
def obs_conn() -> FakeObserveConn:
    return FakeObserveConn()


@pytest.fixture
def obs_backend(obs_conn: FakeObserveConn) -> Any:
    wrapped = LibvirtConnection.adopt(obs_conn, uri="test:///observe")
    be = LibvirtHypervisorBackend(connection=wrapped)
    yield be
    be.close()


def test_get_domain_populates_full_observed_state(
    obs_backend: Any, obs_conn: FakeObserveConn
) -> None:
    obs_conn._add(
        FakeObserveDomain(
            "rich",
            "u-rich",
            state=_RUNNING,
            devices=_DISK_FILE + _NIC_VIRTIO + _HOSTDEV_PCI,
            block_sizes={"vda": (20 * 1024**3, 8 * 1024**3)},
            leases={
                "vnet0": {
                    "hwaddr": "52:54:00:aa:bb:cc",
                    "addrs": [{"addr": "10.0.0.5", "prefix": 24, "type": 0}],
                }
            },
        )
    )
    info = obs_backend.get_domain(DomainId("u-rich"))
    assert info.is_running is True
    # disk enriched
    assert info.disks[0].target == "vda"
    assert info.disks[0].size_bytes == 20 * 1024**3
    assert info.disks[0].allocation_bytes == 8 * 1024**3
    # nic enriched with lease ip
    assert info.nics[0].mac == "52:54:00:AA:BB:CC"
    assert info.nics[0].ip == "10.0.0.5"
    # passthrough device
    assert info.devices_attached[0].kind == "pci"
    assert info.devices_attached[0].address == "0000:01:00.0"


def test_get_domain_off_degrades_block_and_iface(
    obs_backend: Any, obs_conn: FakeObserveConn
) -> None:
    # A stopped domain: blockInfo raises for the disk, interfaceAddresses
    # raises — sizes default, ip None, but disks/nics still parsed from XML.
    obs_conn._add(
        FakeObserveDomain(
            "off",
            "u-off",
            state=_SHUTOFF,
            devices=_DISK_FILE + _NIC_VIRTIO,
            block_raises={"vda"},
            iface_raises=True,
        )
    )
    info = obs_backend.get_domain(DomainId("u-off"))
    assert info.is_running is False
    assert info.disks[0].target == "vda"
    assert info.disks[0].size_bytes == 0
    assert info.disks[0].allocation_bytes is None
    assert info.nics[0].mac == "52:54:00:AA:BB:CC"
    assert info.nics[0].ip is None


def test_list_domains_populates_observed_state(obs_backend: Any, obs_conn: FakeObserveConn) -> None:
    obs_conn._add(
        FakeObserveDomain(
            "a",
            "u-a",
            state=_RUNNING,
            devices=_DISK_BLOCK_RO,
            block_sizes={"sda": (1024, 512)},
        )
    )
    infos = obs_backend.list_domains()
    disk = infos[0].disks[0]
    assert disk.target == "sda"
    assert disk.readonly is True
    assert disk.size_bytes == 1024
    assert disk.allocation_bytes == 512
