"""Tier-1 unit tests for :class:`LibvirtGuestAgentBackend` (D-G19).

No real libvirt connection: a self-contained ``virDomain`` / ``virConnect``
double drives the qemu-guest-agent calls (``qemuAgentCommand`` +
``interfaceAddresses(SRC_AGENT)``) so the mapping + error translation are
exercised deterministically. The CONF-G-01 conformance suite needs a LIVE
agent-running guest, so it runs only in the integration tier.
"""

from __future__ import annotations

import json
from typing import Any

import pytest
from mgf.vm.core.contracts import GuestNic, GuestOsInfo
from mgf.vm.core.exceptions import DomainNotFound, GuestUnreachableError
from mgf.vm.core.ids import DomainId

from mgf.vm.libvirt._connection import LibvirtConnection
from mgf.vm.libvirt._guest_agent import LibvirtGuestAgentBackend

_AGENT_UNRESPONSIVE = 86  # VIR_ERR_AGENT_UNRESPONSIVE


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 FakeAgentDomain:
    def __init__(
        self,
        uuid: str,
        *,
        agent_up: bool = True,
        osinfo: dict[str, Any] | None = None,
        ifaces: dict[str, Any] | None = None,
    ) -> None:
        self._uuid = uuid
        self._agent_up = agent_up
        self._osinfo = osinfo or {}
        self._ifaces = ifaces or {}

    def qemuAgentCommand(self, command: str, _timeout: int, _flags: int) -> str:  # noqa: N802
        if not self._agent_up:
            raise FakeLibvirtError(_AGENT_UNRESPONSIVE, "Guest agent is not responding")
        if "guest-get-osinfo" in command:
            return json.dumps({"return": self._osinfo})
        return json.dumps({"return": {}})

    def interfaceAddresses(self, _source: int, _flags: int) -> dict[str, Any]:  # noqa: N802
        if not self._agent_up:
            raise FakeLibvirtError(_AGENT_UNRESPONSIVE, "Guest agent is not responding")
        return self._ifaces


class FakeConn:
    def __init__(self) -> None:
        self.domains: dict[str, FakeAgentDomain] = {}

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

    def lookupByUUIDString(self, uuid: str) -> FakeAgentDomain:  # 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


_UUID = "11111111-2222-3333-4444-555555555555"

_OSINFO = {
    "id": "ubuntu",
    "name": "Ubuntu",
    "pretty-name": "Ubuntu 24.04 LTS",
    "version": "24.04 LTS (Noble Numbat)",
    "version-id": "24.04",
}

_IFACES = {
    "lo": {
        "hwaddr": "00:00:00:00:00:00",
        "addrs": [{"addr": "127.0.0.1", "prefix": 8, "type": 0}],
    },
    "enp1s0": {
        "hwaddr": "52:54:00:aa:bb:cc",
        "addrs": [
            {"addr": "10.0.0.5", "prefix": 24, "type": 0},
            {"addr": "fe80::5054:ff:feaa:bbcc", "prefix": 64, "type": 1},
        ],
    },
}


def _seed(conn: FakeConn, **kw: Any) -> DomainId:
    conn._add(FakeAgentDomain(_UUID, **kw))
    return DomainId(_UUID)


@pytest.fixture
def make_backend() -> Any:
    """Factory: adopt a fake conn into a backend; close every conn on teardown.

    The adopted :class:`LibvirtConnection` must be closed (DP-13 leak
    detection) or its ``__del__`` raises an unraisable warning the suite
    treats as a failure.
    """
    wrapped_conns: list[Any] = []

    def _make(conn: FakeConn) -> LibvirtGuestAgentBackend:
        wrapped = LibvirtConnection.adopt(conn, uri="test:///unit")
        wrapped_conns.append(wrapped)
        return LibvirtGuestAgentBackend(wrapped)

    yield _make
    for wrapped in wrapped_conns:
        wrapped.close()


class TestGuestPing:
    def test_reachable_returns_true(self, make_backend: Any) -> None:
        conn = FakeConn()
        d = _seed(conn, agent_up=True)
        assert make_backend(conn).guest_ping(d) is True

    def test_unreachable_returns_false(self, make_backend: Any) -> None:
        conn = FakeConn()
        d = _seed(conn, agent_up=False)
        # Does NOT raise — a guest with no running agent is the common case.
        assert make_backend(conn).guest_ping(d) is False


class TestGuestOsInfo:
    def test_maps_osinfo_fields(self, make_backend: Any) -> None:
        conn = FakeConn()
        d = _seed(conn, osinfo=_OSINFO)
        info = make_backend(conn).guest_os_info(d)
        assert isinstance(info, GuestOsInfo)
        assert info.id == "ubuntu"
        assert info.name == "Ubuntu"
        assert info.pretty_name == "Ubuntu 24.04 LTS"
        # version prefers version-id over the verbose version string.
        assert info.version == "24.04"

    def test_unreachable_raises(self, make_backend: Any) -> None:
        conn = FakeConn()
        d = _seed(conn, agent_up=False)
        with pytest.raises(GuestUnreachableError):
            make_backend(conn).guest_os_info(d)


class TestGuestInterfaces:
    def test_maps_nics_ipv4_and_ipv6(self, make_backend: Any) -> None:
        conn = FakeConn()
        d = _seed(conn, ifaces=_IFACES)
        nics = make_backend(conn).guest_interfaces(d)
        assert all(isinstance(n, GuestNic) for n in nics)
        by_name = {n.name: n for n in nics}
        assert by_name["enp1s0"].mac == "52:54:00:aa:bb:cc"
        assert by_name["enp1s0"].ip_addresses == (
            "10.0.0.5",
            "fe80::5054:ff:feaa:bbcc",
        )
        assert by_name["lo"].ip_addresses == ("127.0.0.1",)

    def test_empty_interfaces(self, make_backend: Any) -> None:
        conn = FakeConn()
        d = _seed(conn, ifaces={})
        assert make_backend(conn).guest_interfaces(d) == ()

    def test_unreachable_raises(self, make_backend: Any) -> None:
        conn = FakeConn()
        d = _seed(conn, agent_up=False)
        with pytest.raises(GuestUnreachableError):
            make_backend(conn).guest_interfaces(d)


class TestUnknownDomain:
    def test_all_methods_raise_domain_not_found(self, make_backend: Any) -> None:
        conn = FakeConn()  # no domains seeded
        backend = make_backend(conn)
        ghost = DomainId("99999999-0000-0000-0000-000000000000")
        with pytest.raises(DomainNotFound):
            backend.guest_ping(ghost)
        with pytest.raises(DomainNotFound):
            backend.guest_os_info(ghost)
        with pytest.raises(DomainNotFound):
            backend.guest_interfaces(ghost)
