"""Tests for :mod:`mgf.vm.core.security.isolation.types` (MA-A1, Wave 1).

Covers the frozen-dataclass shapes (`IsolationFinding`,
`IsolationSummary`, `IsolationReport`), the StrEnum classifications,
the decorator registries (`@isolation_check`, `@isolation_fix`), and
the per-check exception-isolation contract in `run_static_checks`.

The original ``VMSpec.isolation`` field + ``assert_spec_consistent_with_isolation``
suites are NOT ported — those functions were DEFERRED (the new core
``VmSpec`` carries no ``isolation`` field; see the module docstring).
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

import pytest
import yaml

from mgf.vm.core.security.isolation.types import (
    _CHECKS,
    _FIXES,
    ISOLATION_REPORT_SCHEMA_VERSION,
    FixKind,
    IsolationCategory,
    IsolationFinding,
    IsolationLevel,
    IsolationReport,
    IsolationSummary,
    all_checks,
    all_fixes,
    get_fix,
    isolation_check,
    isolation_fix,
    run_static_checks,
)


def _finding(
    fid: str,
    *,
    category: IsolationCategory = IsolationCategory.BLOCKING,
    fix_kind: FixKind = FixKind.AUTO_FIX,
    passing: bool = False,
) -> IsolationFinding:
    return IsolationFinding(
        id=fid,
        title=f"{fid} title",
        summary=f"{fid} summary",
        category=category,
        fix_kind=fix_kind,
        passing=passing,
    )


class TestEnums:
    def test_isolation_level_values(self) -> None:
        assert IsolationLevel.OFF.value == "off"
        assert IsolationLevel.STRICT.value == "strict"
        assert IsolationLevel.AIRGAP.value == "airgap"

    def test_isolation_level_yaml_round_trip(self) -> None:
        text = yaml.safe_dump({"level": IsolationLevel.STRICT.value})
        loaded = yaml.safe_load(text)
        assert loaded == {"level": "strict"}
        assert IsolationLevel(loaded["level"]) is IsolationLevel.STRICT

    def test_category_values(self) -> None:
        assert IsolationCategory.BLOCKING.value == "blocking"
        assert IsolationCategory.ADVISORY.value == "advisory"
        assert IsolationCategory.INFO.value == "info"

    def test_fixkind_values(self) -> None:
        assert FixKind.AUTO_FIX.value == "auto_fix"
        assert FixKind.MANUAL.value == "manual"
        assert FixKind.HOST_LEVEL.value == "host_level"


class TestIsolationFinding:
    def test_basic_construction(self) -> None:
        f = _finding("net.no_external_route")
        assert f.id == "net.no_external_route"
        assert f.passing is False
        assert f.details == {}
        assert f.fix_executor == ""

    def test_frozen(self) -> None:
        f = _finding("x")
        with pytest.raises(AttributeError):
            f.passing = True  # type: ignore[misc]

    def test_details_round_trip(self) -> None:
        details = {"sources": ["/host/share-a", "/host/share-b"]}
        f = IsolationFinding(
            id="share.no_filesystem",
            title="No filesystem shares",
            summary="found 2 shares",
            category=IsolationCategory.BLOCKING,
            fix_kind=FixKind.AUTO_FIX,
            passing=False,
            details=details,
            fix_executor="strip.filesystem",
        )
        assert f.details["sources"] == ["/host/share-a", "/host/share-b"]
        assert f.fix_executor == "strip.filesystem"


class TestIsolationSummary:
    def test_empty(self) -> None:
        s = IsolationSummary.from_findings([])
        assert s.total_findings == 0
        assert s.is_clean() is True

    def test_passing_blocking_is_clean(self) -> None:
        s = IsolationSummary.from_findings(
            [
                _finding("a", category=IsolationCategory.BLOCKING, passing=True),
                _finding("b", category=IsolationCategory.BLOCKING, passing=True),
            ]
        )
        assert s.is_clean() is True
        assert s.blocking_total == 2
        assert s.blocking_failing == 0

    def test_failing_blocking_is_unclean(self) -> None:
        s = IsolationSummary.from_findings(
            [
                _finding("a", category=IsolationCategory.BLOCKING, passing=False),
            ]
        )
        assert s.is_clean() is False
        assert s.blocking_failing == 1

    def test_failing_advisory_does_not_taint_clean(self) -> None:
        s = IsolationSummary.from_findings(
            [
                _finding("a", category=IsolationCategory.BLOCKING, passing=True),
                _finding(
                    "b",
                    category=IsolationCategory.ADVISORY,
                    fix_kind=FixKind.AUTO_FIX,
                    passing=False,
                ),
            ]
        )
        assert s.is_clean() is True
        assert s.advisory_failing == 1

    def test_auto_fixable_count(self) -> None:
        s = IsolationSummary.from_findings(
            [
                _finding("a", fix_kind=FixKind.AUTO_FIX, passing=False),
                _finding("b", fix_kind=FixKind.AUTO_FIX, passing=False),
                _finding("c", fix_kind=FixKind.MANUAL, passing=False),
                _finding("d", fix_kind=FixKind.AUTO_FIX, passing=True),
            ]
        )
        assert s.auto_fixable == 2

    def test_total_failing(self) -> None:
        s = IsolationSummary.from_findings(
            [
                _finding("a", category=IsolationCategory.BLOCKING, passing=False),
                _finding("b", category=IsolationCategory.ADVISORY, passing=False),
                _finding("c", category=IsolationCategory.INFO, passing=False),
                _finding("d", category=IsolationCategory.BLOCKING, passing=True),
            ]
        )
        assert s.total_failing == 3
        assert s.total_findings == 4


class TestIsolationReport:
    def test_default_schema_version(self) -> None:
        r = IsolationReport(
            generated_at=datetime(2026, 4, 25, tzinfo=UTC),
            vm_name="suspect-1",
            declared_level=IsolationLevel.STRICT,
            findings=(_finding("a", passing=True),),
            summary=IsolationSummary.from_findings([_finding("a", passing=True)]),
        )
        assert r.schema_version == ISOLATION_REPORT_SCHEMA_VERSION

    def test_frozen(self) -> None:
        r = IsolationReport(
            generated_at=datetime(2026, 4, 25, tzinfo=UTC),
            vm_name="suspect-1",
            declared_level=IsolationLevel.STRICT,
            findings=(),
            summary=IsolationSummary(),
        )
        with pytest.raises(AttributeError):
            r.vm_name = "x"  # type: ignore[misc]

    def test_runtime_audit_flag(self) -> None:
        r = IsolationReport(
            generated_at=datetime(2026, 4, 25, tzinfo=UTC),
            vm_name="x",
            declared_level=IsolationLevel.STRICT,
            findings=(),
            summary=IsolationSummary(),
            runtime_audit=True,
        )
        assert r.runtime_audit is True


@pytest.fixture(autouse=True)
def _isolated_registries() -> Any:
    """Per-test snapshot/restore of the global registries."""
    saved_checks = list(_CHECKS)
    saved_fixes = dict(_FIXES)
    yield
    _CHECKS.clear()
    _CHECKS.extend(saved_checks)
    _FIXES.clear()
    _FIXES.update(saved_fixes)


class TestCheckRegistry:
    def test_decorator_registers(self) -> None:
        @isolation_check
        def _my_check(_ctx: object) -> IsolationFinding:
            return _finding("test.demo", passing=True)

        assert _my_check in all_checks()

    def test_run_static_checks_collects_findings(self) -> None:
        @isolation_check
        def _a(_ctx: object) -> IsolationFinding:
            return _finding("a", passing=True)

        @isolation_check
        def _b(_ctx: object) -> IsolationFinding:
            return _finding("b", passing=False)

        results = run_static_checks(object())
        ids = [r.id for r in results]
        assert "a" in ids
        assert "b" in ids

    def test_run_static_checks_isolates_exceptions(self) -> None:
        @isolation_check
        def _good(_ctx: object) -> IsolationFinding:
            return _finding("good", passing=True)

        @isolation_check
        def _broken(_ctx: object) -> IsolationFinding:
            msg = "intentional test failure"
            raise RuntimeError(msg)

        @isolation_check
        def _also_good(_ctx: object) -> IsolationFinding:
            return _finding("also_good", passing=True)

        results = run_static_checks(object())
        ids = [r.id for r in results]
        assert "good" in ids
        assert "also_good" in ids
        broken = next(
            (r for r in results if r.id == "runtime.check_failed._broken"),
            None,
        )
        assert broken is not None
        assert broken.category == IsolationCategory.INFO
        assert broken.fix_kind == FixKind.HOST_LEVEL
        assert broken.passing is False
        assert "intentional test failure" in broken.summary

    def test_all_checks_returns_snapshot(self) -> None:
        @isolation_check
        def _x(_ctx: object) -> IsolationFinding:
            return _finding("x", passing=True)

        snap = all_checks()
        assert isinstance(snap, tuple)


class TestFixRegistry:
    def test_register_and_lookup(self) -> None:
        @isolation_fix("test.noop")
        def _noop(*, target: Any, reporter: Any, dry_run: bool = False, **kw: Any) -> None:
            del target, reporter, dry_run, kw

        resolved = get_fix("test.noop")
        assert resolved is _noop

    def test_unknown_fix_returns_none(self) -> None:
        assert get_fix("test.does_not_exist") is None

    def test_double_register_raises(self) -> None:
        @isolation_fix("test.dup")
        def _first(*, target: Any, reporter: Any, dry_run: bool = False, **kw: Any) -> None:
            del target, reporter, dry_run, kw

        with pytest.raises(ValueError, match="already registered"):

            @isolation_fix("test.dup")
            def _second(*, target: Any, reporter: Any, dry_run: bool = False, **kw: Any) -> None:
                del target, reporter, dry_run, kw

    def test_all_fixes_returns_copy(self) -> None:
        @isolation_fix("test.snap")
        def _x(*, target: Any, reporter: Any, dry_run: bool = False, **kw: Any) -> None:
            del target, reporter, dry_run, kw

        snap = all_fixes()
        assert "test.snap" in snap
        snap.pop("test.snap")  # type: ignore[attr-defined]
        assert "test.snap" in all_fixes()
