"""Tests for demo manifest models and evidence classification."""

from __future__ import annotations

from mcp_vm_blackbox.demo_manifest import (
    EvidenceItem,
    EvidenceKind,
    GsdContextLink,
    MissingReason,
    ReplayBundle,
    build_evidence_for_job,
    load_manifest,
    save_manifest,
)


class TestEvidenceKind:
    """Tests for EvidenceKind enum."""

    def test_source_kind_value(self):
        assert EvidenceKind.SOURCE.value == "source"

    def test_overlay_kind_value(self):
        assert EvidenceKind.OVERLAY.value == "overlay"

    def test_derived_kind_value(self):
        assert EvidenceKind.DERIVED.value == "derived"


class TestMissingReason:
    """Tests for MissingReason enum."""

    def test_not_captured_value(self):
        assert MissingReason.NOT_CAPTURED.value == "not_captured"

    def test_path_absent_value(self):
        assert MissingReason.PATH_ABSENT.value == "path_absent"

    def test_optional_value(self):
        assert MissingReason.OPTIONAL.value == "optional"


class TestEvidenceItem:
    """Tests for EvidenceItem model."""

    def test_present_source_evidence(self):
        """Source evidence that is present has all required fields."""
        item = EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="screenshot",
            path="scratch/screenshots/vm1.png",
            present=True,
            missing_reason=None,
            timestamp="2026-03-14T18:00:00",
            metadata={"vm_name": "vm1"},
        )

        assert item.present is True
        assert item.missing_reason is None
        assert item.kind == EvidenceKind.SOURCE

    def test_missing_source_requires_reason(self):
        """Missing source evidence must specify a reason."""
        item = EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="recording",
            path=None,
            present=False,
            missing_reason=MissingReason.OPTIONAL,
            metadata={},
        )

        assert item.present is False
        assert item.missing_reason == MissingReason.OPTIONAL

    def test_derived_asset_tracks_sources(self):
        """Derived assets should track what they were generated from."""
        item = EvidenceItem(
            kind=EvidenceKind.DERIVED,
            source_kind="video",
            path="output/demo.webm",
            present=True,
            missing_reason=None,
            derived_from=[
                "scratch/screenshots/vm1-20260314-180000-seed0001.png",
                "scratch/screenshots/vm1-20260314-180100-seed0001.png",
            ],
            metadata={},
        )

        assert item.kind == EvidenceKind.DERIVED
        assert len(item.derived_from) == 2

    def test_overlay_dashboard_export(self):
        """Dashboard exports are classified as overlays."""
        item = EvidenceItem(
            kind=EvidenceKind.OVERLAY,
            source_kind="dashboard",
            path="dashboards/vm1-status.png",
            present=True,
            missing_reason=None,
            metadata={"dashboard_name": "vm-status"},
        )

        assert item.kind == EvidenceKind.OVERLAY
        assert item.source_kind == "dashboard"


class TestGsdContextLink:
    """Tests for optional .gsd context linkage."""

    def test_task_link(self):
        """Link to a task artifact."""
        link = GsdContextLink(
            artifact_type="task",
            artifact_id="T01",
            path=".gsd/milestones/M006/slices/S01/tasks/T01-PLAN.md",
            narrative_fields={"title": "Add demo manifest models"},
            linked_to_job=True,
        )

        assert link.artifact_type == "task"
        assert link.linked_to_job is True

    def test_slice_link(self):
        """Link to a slice artifact."""
        link = GsdContextLink(
            artifact_type="slice",
            artifact_id="S01",
            path=".gsd/milestones/M006/slices/S01/S01-PLAN.md",
            narrative_fields={},
            linked_to_job=False,
        )

        assert link.artifact_type == "slice"
        assert link.linked_to_job is False


class TestReplayBundle:
    """Tests for the ReplayBundle root model."""

    def test_minimal_bundle(self):
        """A minimal bundle with just required fields."""
        bundle = ReplayBundle(
            bundle_id="abc123",
            created_at="2026-03-14T18:00:00",
            description="Test bundle",
            primary_job_id="abc123",
            primary_vm_name="test-vm",
            primary_job_path=".mcp-vm-blackbox/jobs/test-vm/abc123.json",
            evidence=[],
            gsd_context=[],
            has_continuous_motion=False,
            missing_evidence_summary={},
            source_evidence_count=0,
            overlay_evidence_count=0,
        )

        assert bundle.primary_job_id == "abc123"
        assert bundle.primary_vm_name == "test-vm"
        assert bundle.has_continuous_motion is False

    def test_full_bundle_with_evidence(self):
        """A bundle with evidence items."""
        job_evidence = EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="job_record",
            path=".mcp-vm-blackbox/jobs/test-vm/abc123.json",
            present=True,
            missing_reason=None,
            metadata={"vm_name": "test-vm"},
        )

        screenshot = EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="screenshot",
            path="scratch/screenshots/test-vm-20260314-180000-seed0001.png",
            present=True,
            missing_reason=None,
            timestamp="2026-03-14T18:00:00",
            metadata={},
        )

        missing_recording = EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="recording",
            path=None,
            present=False,
            missing_reason=MissingReason.OPTIONAL,
            metadata={"note": "recordings not available"},
        )

        bundle = ReplayBundle(
            bundle_id="abc123",
            created_at="2026-03-14T18:00:00",
            description="Test bundle with evidence",
            primary_job_id="abc123",
            primary_vm_name="test-vm",
            primary_job_path=".mcp-vm-blackbox/jobs/test-vm/abc123.json",
            evidence=[job_evidence, screenshot, missing_recording],
            gsd_context=[],
            has_continuous_motion=False,
            missing_evidence_summary={"optional": 1},
            source_evidence_count=2,
            overlay_evidence_count=0,
        )

        assert len(bundle.evidence) == 3
        assert bundle.source_evidence_count == 2
        assert bundle.missing_evidence_summary["optional"] == 1


class TestBuildManifest:
    """Tests for manifest builder functions."""

    def test_build_evidence_with_job(self, tmp_path):
        """Build evidence list from a job file."""
        job_dir = tmp_path / "jobs" / "test-vm"
        job_dir.mkdir(parents=True)
        job_file = job_dir / "abc123.json"
        job_file.write_text(
            """{
            "job_id": "abc123",
            "vm_name": "test-vm",
            "scenario_name": "test-scenario",
            "created_at": "2026-03-14T18:00:00",
            "status": "completed"
        }"""
        )

        screenshots_dir = tmp_path / "screenshots"
        screenshots_dir.mkdir()
        screenshot = screenshots_dir / "test-vm-20260314-180000-seed0001.png"
        screenshot.write_bytes(b"fake png")

        evidence = build_evidence_for_job(job_path=job_file, screenshots_dir=screenshots_dir, recordings_dir=None)

        assert len(evidence) == 3

        job_item = next(e for e in evidence if e.source_kind == "job_record")
        assert job_item.present is True

        screenshot_item = next(e for e in evidence if e.source_kind == "screenshot")
        assert screenshot_item.present is True
        assert screenshot_item.timestamp is not None

        recording_item = next(e for e in evidence if e.source_kind == "recording")
        assert recording_item.present is False
        assert recording_item.missing_reason == MissingReason.OPTIONAL

    def test_build_evidence_no_screenshots(self, tmp_path):
        """Build evidence when no screenshots exist."""
        job_dir = tmp_path / "jobs" / "test-vm"
        job_dir.mkdir(parents=True)
        job_file = job_dir / "abc123.json"
        job_file.write_text(
            """{
            "job_id": "abc123",
            "vm_name": "test-vm",
            "created_at": "2026-03-14T18:00:00",
            "status": "active"
        }"""
        )

        screenshots_dir = tmp_path / "screenshots"

        evidence = build_evidence_for_job(job_path=job_file, screenshots_dir=screenshots_dir, recordings_dir=None)

        screenshot_item = next(e for e in evidence if e.source_kind == "screenshot")
        assert screenshot_item.present is False
        assert screenshot_item.missing_reason == MissingReason.PATH_ABSENT


class TestManifestPersistence:
    """Tests for manifest save/load."""

    def test_save_and_load(self, tmp_path):
        """Save a manifest and load it back."""
        bundle = ReplayBundle(
            bundle_id="test123",
            created_at="2026-03-14T18:00:00",
            description="Test manifest",
            primary_job_id="test123",
            primary_vm_name="test-vm",
            primary_job_path=".mcp-vm-blackbox/jobs/test-vm/test123.json",
            evidence=[],
            gsd_context=[],
            has_continuous_motion=False,
            missing_evidence_summary={},
            source_evidence_count=0,
            overlay_evidence_count=0,
        )

        manifest_path = tmp_path / "manifest.json"
        save_manifest(bundle, manifest_path)
        assert manifest_path.exists()

        loaded = load_manifest(manifest_path)
        assert loaded.bundle_id == "test123"
        assert loaded.primary_vm_name == "test-vm"

    def test_json_roundtrip(self, tmp_path):
        """Verify JSON serialization is stable."""
        bundle = ReplayBundle(
            bundle_id="test456",
            created_at="2026-03-14T18:00:00",
            description="JSON roundtrip test",
            primary_job_id="test456",
            primary_vm_name="test-vm",
            primary_job_path=".mcp-vm-blackbox/jobs/test-vm/test456.json",
            evidence=[
                EvidenceItem(
                    kind=EvidenceKind.SOURCE,
                    source_kind="screenshot",
                    path="test.png",
                    present=True,
                    missing_reason=None,
                    metadata={},
                )
            ],
            gsd_context=[],
            has_continuous_motion=False,
            missing_evidence_summary={},
            source_evidence_count=1,
            overlay_evidence_count=0,
        )

        manifest_path = tmp_path / "manifest.json"
        save_manifest(bundle, manifest_path)
        parsed = load_manifest(manifest_path)
        assert parsed.evidence[0].kind == EvidenceKind.SOURCE


class TestContractInvariants:
    """Tests that prove the contract invariants."""

    def test_vm_job_is_primary_not_gsd(self):
        """VM job is the primary replay unit - .gsd is optional linkage."""
        bundle = ReplayBundle(
            bundle_id="test",
            created_at="2026-03-14T18:00:00",
            description="Test",
            primary_job_id="test",
            primary_vm_name="test-vm",
            primary_job_path=".mcp-vm-blackbox/jobs/test-vm/test.json",
            evidence=[],
            gsd_context=[
                GsdContextLink(
                    artifact_type="task",
                    artifact_id="T01",
                    path=".gsd/.../T01-PLAN.md",
                    narrative_fields={"title": "Test task"},
                    linked_to_job=True,
                )
            ],
            has_continuous_motion=False,
            missing_evidence_summary={},
            source_evidence_count=0,
            overlay_evidence_count=0,
        )

        assert bundle.primary_job_id is not None
        assert len(bundle.gsd_context) == 1
        assert bundle.gsd_context[0].linked_to_job is True

    def test_missing_recording_not_implied_as_continuous(self):
        """Missing recordings should not imply continuous motion."""
        recording_missing = EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="recording",
            path=None,
            present=False,
            missing_reason=MissingReason.OPTIONAL,
            metadata={"note": "not captured"},
        )

        bundle = ReplayBundle(
            bundle_id="test",
            created_at="2026-03-14T18:00:00",
            description="Test",
            primary_job_id="test",
            primary_vm_name="test-vm",
            primary_job_path=".mcp-vm-blackbox/jobs/test-vm/test.json",
            evidence=[recording_missing],
            gsd_context=[],
            has_continuous_motion=False,
            missing_evidence_summary={"optional": 1},
            source_evidence_count=0,
            overlay_evidence_count=0,
        )

        assert bundle.has_continuous_motion is False

    def test_source_vs_overlay_distinction(self):
        """Source evidence and overlays are distinctly classified."""
        source = EvidenceItem(
            kind=EvidenceKind.SOURCE,
            source_kind="screenshot",
            path="test.png",
            present=True,
            missing_reason=None,
            metadata={},
        )

        overlay = EvidenceItem(
            kind=EvidenceKind.OVERLAY,
            source_kind="dashboard",
            path="dashboard.png",
            present=True,
            missing_reason=None,
            metadata={},
        )

        assert source.kind == EvidenceKind.SOURCE
        assert overlay.kind == EvidenceKind.OVERLAY
