"""Tests for :mod:`mgf.vm.core.security.isolation.analyzers`.

Mirrors the umbrella's ``test_static_analyzer`` /
``test_runtime_analyzer`` suites, adapted to the pure
inject-Element / inject-facts API: the umbrella parsed untrusted XML
and shelled out to collect host facts; here the Element arrives
pre-parsed and the facts arrive pre-collected in a frozen
:class:`HostFacts`. We assert the produced findings match the
umbrella analyzer's output on the same logical inputs (same fixtures,
same expected failing-id sets, same total/static counts).

Pure, no I/O: every input is a stdlib ``ET.fromstring`` element and a
constructed :class:`HostFacts`.
"""

from __future__ import annotations

import xml.etree.ElementTree as ET
from datetime import UTC, datetime

from _isolation_xml import domain_with

from mgf.vm.core.security.isolation import (
    HostFacts,
    IsolationLevel,
    IsolationReport,
    analyze_runtime,
    analyze_static,
    runtime_only_checks,
    static_checks,
)

# ---------------------------------------------------------------------------
# Catalogue counts (pinned against the v1 check catalogue; same numbers
# the umbrella's test_finding_count_pin pins).
# ---------------------------------------------------------------------------

EXPECTED_STATIC_COUNT = 29
EXPECTED_RUNTIME_ONLY_COUNT = 5
EXPECTED_TOTAL_COUNT = 34


def _el(xml_text: str) -> ET.Element:
    return ET.fromstring(xml_text)


# A minimal quarantine-clean domain: isolated network, no disks, balloon
# off. Mirrors the umbrella runtime test's ``_QUARANTINE_XML``.
_QUARANTINE_XML = (
    "<domain type='kvm'>"
    "<name>vm-1</name>"
    "<memory unit='MiB'>2048</memory>"
    "<vcpu>2</vcpu>"
    "<devices>"
    "<interface type='network'>"
    "<source network='vmanager-quarantine'/>"
    "<model type='virtio'/>"
    "</interface>"
    "<memballoon model='none'/>"
    "</devices>"
    "</domain>"
)


def _clean_facts() -> HostFacts:
    return HostFacts(
        libvirt_uri="qemu:///system",
        qemu_pid=1234,
        apparmor_profile_state="enforce",
        apparmor_profile_name="libvirt-qemu-vm-1",
        qemu_open_paths=(),
        ksm_run="0",
        network_active_state=(("vmanager-quarantine", True),),
    )


# ---------------------------------------------------------------------------
# analyze_static
# ---------------------------------------------------------------------------


class TestAnalyzeStatic:
    def test_returns_report_static_audit_false(self) -> None:
        report = analyze_static(_el(_QUARANTINE_XML))
        assert isinstance(report, IsolationReport)
        assert report.runtime_audit is False
        assert report.vm_name == "vm-1"
        assert report.declared_level is IsolationLevel.STRICT

    def test_name_inferred_from_element(self) -> None:
        report = analyze_static(_el(domain_with(name="raw-1")))
        assert report.vm_name == "raw-1"

    def test_explicit_vm_name_overrides_inferred(self) -> None:
        report = analyze_static(_el(domain_with(name="orig")), vm_name="custom")
        assert report.vm_name == "custom"

    def test_unnamed_element_falls_back(self) -> None:
        report = analyze_static(_el("<domain type='kvm'><devices/></domain>"))
        assert report.vm_name == "<unnamed>"

    def test_only_static_checks_run(self) -> None:
        report = analyze_static(_el(_QUARANTINE_XML))
        assert len(report.findings) == EXPECTED_STATIC_COUNT
        assert len(report.findings) == len(static_checks())

    def test_explicit_now_pinning(self) -> None:
        fixed = datetime(2026, 4, 25, 12, 0, 0, tzinfo=UTC)
        report = analyze_static(_el(_QUARANTINE_XML), now=fixed)
        assert report.generated_at == fixed

    def test_declared_level_propagates(self) -> None:
        report = analyze_static(_el(_QUARANTINE_XML), declared_level=IsolationLevel.AIRGAP)
        assert report.declared_level is IsolationLevel.AIRGAP

    def test_default_network_disk_have_failing_findings(self) -> None:
        # Mirrors the umbrella's "default spec fails several checks":
        # a domain on the 'default' network with a /var/lib disk should
        # fail the quarantine-network + external-route + pool checks.
        xml = (
            "<domain type='kvm'>"
            "<name>suspect-1</name>"
            "<memory unit='MiB'>2048</memory>"
            "<vcpu>2</vcpu>"
            "<devices>"
            "<interface type='network'>"
            "<source network='default'/>"
            "<model type='virtio'/>"
            "</interface>"
            "<disk type='file' device='disk'>"
            "<source file='/var/lib/libvirt/images/suspect-1.qcow2'/>"
            "<target dev='vda' bus='virtio'/>"
            "</disk>"
            "</devices>"
            "</domain>"
        )
        report = analyze_static(_el(xml))
        assert report.summary.blocking_failing > 0
        ids = {f.id for f in report.findings if not f.passing}
        assert "net.quarantine_network_used" in ids
        assert "net.no_external_route" in ids
        assert "disk.in_quarantine_pool" in ids

    def test_findings_order_is_stable(self) -> None:
        first = [f.id for f in analyze_static(_el(_QUARANTINE_XML)).findings]
        second = [f.id for f in analyze_static(_el(_QUARANTINE_XML)).findings]
        assert first == second


# ---------------------------------------------------------------------------
# analyze_runtime
# ---------------------------------------------------------------------------


class TestAnalyzeRuntime:
    def test_returns_report_runtime_audit_true(self) -> None:
        report = analyze_runtime(_el(_QUARANTINE_XML), _clean_facts())
        assert report.runtime_audit is True
        assert report.vm_name == "vm-1"

    def test_total_findings_match_full_catalogue(self) -> None:
        report = analyze_runtime(_el(_QUARANTINE_XML), HostFacts())
        assert len(report.findings) == EXPECTED_TOTAL_COUNT
        assert len(static_checks()) + len(runtime_only_checks()) == EXPECTED_TOTAL_COUNT

    def test_clean_fixture_passes_every_blocking_check(self) -> None:
        report = analyze_runtime(_el(_QUARANTINE_XML), _clean_facts())
        blocking_failing = [
            f for f in report.findings if not f.passing and f.category.value == "blocking"
        ]
        assert blocking_failing == [], [f.id for f in blocking_failing]
        assert report.summary.is_clean()

    def test_adverse_facts_fire_every_runtime_only_check(self) -> None:
        # Mirrors the umbrella's adversarial runtime case: a session URI,
        # complain-mode AppArmor, an unsanctioned open path, KSM enabled,
        # and a non-isolated attached network all surface as failures.
        facts = HostFacts(
            libvirt_uri="qemu:///session",
            apparmor_profile_state="complain",
            qemu_open_paths=("/etc/shadow",),
            ksm_run="1",
            network_active_state=(("default", False),),
        )
        report = analyze_runtime(_el(_QUARANTINE_XML), facts)
        ids = {f.id for f in report.findings if not f.passing}
        assert "runtime.libvirt_uri_system" in ids
        assert "runtime.apparmor_enforcing" in ids
        assert "runtime.no_open_host_paths" in ids
        assert "runtime.network_active_isolated" in ids
        assert "host.ksm_disabled" in ids

    def test_empty_facts_default_dataclass(self) -> None:
        # A default-constructed HostFacts (all empty) must not raise and
        # must fail the runtime-only blocking checks (we can't confirm
        # system URI / enforce mode from nothing).
        report = analyze_runtime(_el(_QUARANTINE_XML), HostFacts())
        ids = {f.id for f in report.findings if not f.passing}
        assert "runtime.libvirt_uri_system" in ids
        assert "runtime.apparmor_enforcing" in ids

    def test_explicit_now_pinning(self) -> None:
        fixed = datetime(2026, 4, 25, 14, 0, 0, tzinfo=UTC)
        report = analyze_runtime(_el(_QUARANTINE_XML), HostFacts(), now=fixed)
        assert report.generated_at == fixed

    def test_declared_level_propagates(self) -> None:
        report = analyze_runtime(
            _el(_QUARANTINE_XML),
            HostFacts(),
            declared_level=IsolationLevel.AIRGAP,
        )
        assert report.declared_level is IsolationLevel.AIRGAP

    def test_name_inference_and_override(self) -> None:
        report = analyze_runtime(_el(domain_with(name="live-1")), HostFacts(), vm_name="pinned")
        assert report.vm_name == "pinned"


# ---------------------------------------------------------------------------
# HostFacts — the collection contract
# ---------------------------------------------------------------------------


class TestHostFacts:
    def test_defaults_are_empty(self) -> None:
        facts = HostFacts()
        assert facts.libvirt_uri == ""
        assert facts.qemu_pid == 0
        assert facts.apparmor_profile_state == ""
        assert facts.apparmor_profile_name == ""
        assert facts.qemu_open_paths == ()
        assert facts.ksm_run == ""
        assert facts.network_active_state == ()

    def test_as_check_facts_projects_dotted_keys(self) -> None:
        facts = HostFacts(
            libvirt_uri="qemu:///system",
            qemu_pid=42,
            apparmor_profile_state="enforce",
            apparmor_profile_name="libvirt-qemu",
            qemu_open_paths=("/etc/shadow", "/tmp/x"),
            ksm_run="0",
            network_active_state=(("vmanager-quarantine", True), ("default", False)),
        )
        cf = facts.as_check_facts()
        assert cf["libvirt.uri"] == "qemu:///system"
        assert cf["qemu.pid"] == 42
        assert cf["apparmor.profile_state"] == "enforce"
        assert cf["apparmor.profile_name"] == "libvirt-qemu"
        # Lists at the boundary (checks isinstance-narrow on list).
        assert cf["qemu.open_paths"] == ["/etc/shadow", "/tmp/x"]
        assert isinstance(cf["qemu.open_paths"], list)
        assert cf["host.ksm_run"] == "0"
        assert cf["network.active_state"] == [
            ("vmanager-quarantine", True),
            ("default", False),
        ]
        assert isinstance(cf["network.active_state"], list)

    def test_is_frozen(self) -> None:
        import dataclasses

        facts = HostFacts()
        try:
            facts.libvirt_uri = "x"  # type: ignore[misc]
        except dataclasses.FrozenInstanceError:
            return
        msg = "HostFacts must be frozen"
        raise AssertionError(msg)
