"""Tests for timeline merger + Markdown report renderer (FN-A5, FN-A7)."""

from __future__ import annotations

import json
from datetime import UTC, datetime
from pathlib import Path
from uuid import uuid4

from mgf.vm.forensics import (
    Artifact,
    ArtifactKind,
    Case,
    CaseStatus,
)
from mgf.vm.forensics.analysis import build_timeline
from mgf.vm.forensics.analysis.filesystem import DiffSummary
from mgf.vm.forensics.analysis.ioc import FileHashes
from mgf.vm.forensics.analysis.network import (
    DnsQuery,
    NetworkAnalysis,
    ProtocolBreakdown,
    TalkerPair,
)
from mgf.vm.forensics.reports import ReportInputs, render_markdown_report


def _case(*, name: str = "vm-1", artifacts: tuple[Artifact, ...] = ()) -> Case:
    return Case(
        name=name,
        case_id=uuid4(),
        vm_name="suspect-1",
        sample_path=Path("/tmp/sample.exe"),
        sample_sha256="deadbeef" * 8,
        description="A test case.",
        status=CaseStatus.COMPLETED,
        created_at=datetime(2026, 4, 26, 10, tzinfo=UTC),
        completed_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
        vmanager_version="0.1.0dev0",
        qemu_version="9.0.0",
        host_kernel="6.5.0",
        artifacts=artifacts,
    )


def _network_artifact(
    *,
    captured_at: datetime,
    started_at: datetime | None = None,
) -> Artifact:
    metadata: dict[str, object] = {
        "capture_id": "cap-1",
        "bridge": "virbr-vman-q",
        "bpf": "",
    }
    if started_at is not None:
        metadata["started_at"] = started_at.isoformat()
    return Artifact(
        kind=ArtifactKind.NETWORK_PCAP,
        path=Path("/tmp/cap.pcap"),
        sha256="aa" * 32,
        size_bytes=4096,
        captured_at=captured_at,
        captured_by="tcpdump 4.99.1",
        metadata=metadata,
    )


def _fs_diff_artifact(
    tmp_path: Path,
    *,
    captured_at: datetime,
    added: int,
    modified: int,
    deleted: int,
) -> Artifact:
    diff_path = tmp_path / "diff.json"
    diff_path.write_text(
        json.dumps(
            {
                "schema_version": 1,
                "captured_at": captured_at.isoformat(),
                "baseline_captured_at": "2026-04-26T10:00:00+00:00",
                "vm_state": "stopped",
                "disk_view": "consistent",
                "added": [
                    {
                        "path": f"/added{i}",
                        "size_bytes": 100,
                        "mtime": 1700000000.0,
                        "sha256": "abc",
                    }
                    for i in range(added)
                ],
                "modified": [
                    {
                        "path": f"/modified{i}",
                        "size_bytes": 100,
                        "mtime": 1700000000.0,
                        "sha256": "def",
                    }
                    for i in range(modified)
                ],
                "deleted": [f"/deleted{i}" for i in range(deleted)],
            },
        ),
        encoding="utf-8",
    )
    return Artifact(
        kind=ArtifactKind.FS_DIFF,
        path=diff_path,
        sha256="bb" * 32,
        size_bytes=diff_path.stat().st_size,
        captured_at=captured_at,
        metadata={
            "vm_state": "stopped",
            "disk_view": "consistent",
            "added_count": added,
            "modified_count": modified,
            "deleted_count": deleted,
        },
    )


# ---------------------------------------------------------------------------
# Timeline
# ---------------------------------------------------------------------------


class TestTimeline:
    def test_empty_case_yields_empty_timeline(self) -> None:
        events = build_timeline(_case())
        assert events == ()

    def test_network_artifact_yields_start_and_stop(self) -> None:
        a = _network_artifact(
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
            started_at=datetime(2026, 4, 26, 11, tzinfo=UTC),
        )
        events = build_timeline(_case(artifacts=(a,)))
        sources = [e.source for e in events]
        assert "capture.network.start" in sources
        assert "capture.network.stop" in sources

    def test_events_sorted_chronologically(self) -> None:
        a = _network_artifact(
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
            started_at=datetime(2026, 4, 26, 11, tzinfo=UTC),
        )
        events = build_timeline(_case(artifacts=(a,)))
        timestamps = [e.occurred_at for e in events]
        assert timestamps == sorted(timestamps)

    def test_fs_diff_summary_event(self, tmp_path: Path) -> None:
        a = _fs_diff_artifact(
            tmp_path,
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
            added=2,
            modified=1,
            deleted=1,
        )
        events = build_timeline(_case(artifacts=(a,)))
        # At least the summary event + per-file events.
        assert any(e.source == "capture.fs.diff" for e in events)
        assert any(e.source == "capture.fs.diff.added" for e in events)
        assert any(e.source == "capture.fs.diff.deleted" for e in events)

    def test_fs_diff_huge_skips_per_file_events(self, tmp_path: Path) -> None:
        a = _fs_diff_artifact(
            tmp_path,
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
            added=200,
            modified=0,
            deleted=0,
        )
        events = build_timeline(_case(artifacts=(a,)), fs_diff_event_cap=10)
        # Only the summary event survives — no per-file enumeration.
        per_file = [e for e in events if e.source != "capture.fs.diff"]
        assert per_file == []

    def test_memory_artifact_event(self, tmp_path: Path) -> None:
        a = Artifact(
            kind=ArtifactKind.MEMORY_DUMP,
            path=Path("/tmp/m.dump"),
            sha256="cc" * 32,
            size_bytes=1024 * 1024 * 1024,
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
        )
        events = build_timeline(_case(artifacts=(a,)))
        assert any(e.source == "capture.memory" for e in events)


# ---------------------------------------------------------------------------
# Markdown report
# ---------------------------------------------------------------------------


def _network_analysis() -> NetworkAnalysis:
    return NetworkAnalysis(
        pcap_path="/tmp/cap.pcap",
        packet_count=42,
        bytes_total=4096,
        duration_s=10.0,
        started_at=datetime(2026, 4, 26, 11, tzinfo=UTC),
        ended_at=datetime(2026, 4, 26, 11, 0, 10, tzinfo=UTC),
        protocols=ProtocolBreakdown(tcp=10, udp=5, icmp=1, ipv4=15, ipv6=0, arp=0),
        top_talkers=(TalkerPair(src="10.0.0.1", dst="8.8.8.8", bytes=2000, packets=10),),
        dns_queries=(
            DnsQuery(
                name="example.com",
                qtype="A",
                first_seen_at=datetime(2026, 4, 26, 11, tzinfo=UTC),
            ),
        ),
        tls_snis=(),
        http_requests=(),
    )


class TestRenderMarkdownReport:
    def test_minimal_renders_overview(self) -> None:
        case = _case()
        report = render_markdown_report(ReportInputs(case))
        assert "# Forensics Report: vm-1" in report
        assert "## 1. Case Overview" in report
        assert "vm_name" not in report  # raw field name shouldn't leak
        assert "suspect-1" in report

    def test_includes_network_section_when_provided(self) -> None:
        case = _case()
        report = render_markdown_report(
            ReportInputs(case, network=_network_analysis()),
        )
        assert "## 3. Network Capture" in report
        assert "10.0.0.1" in report
        assert "example.com" in report

    def test_no_network_says_so(self) -> None:
        report = render_markdown_report(ReportInputs(_case()))
        assert "no network capture analyzed" in report

    def test_filesystem_section_with_summary(self) -> None:
        s = DiffSummary(
            added=2,
            modified=1,
            deleted=3,
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
            baseline_captured_at=datetime(2026, 4, 26, 10, tzinfo=UTC),
            vm_state="stopped",
            disk_view="consistent",
        )
        report = render_markdown_report(
            ReportInputs(_case(), fs_summary=s),
        )
        assert "## 4. Filesystem Changes" in report
        assert "**Added:** 2" in report
        assert "**Deleted:** 3" in report

    def test_no_filesystem_section_says_so(self) -> None:
        report = render_markdown_report(ReportInputs(_case()))
        assert "no filesystem diff captured" in report

    def test_ioc_hashes_section(self) -> None:
        hashes = [
            FileHashes(
                file="/tmp/sample.exe",
                size_bytes=1024,
                md5="d41d8cd98f00b204e9800998ecf8427e",
                sha1="da39a3ee5e6b4b0d3255bfef95601890afd80709",
                sha256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            ),
        ]
        report = render_markdown_report(
            ReportInputs(_case(), ioc_hashes=hashes),
        )
        assert "## 7. IOC Analysis" in report
        assert "/tmp/sample.exe" in report
        assert "da39a3" in report  # part of the sha1

    def test_memory_section_lists_artifacts(self) -> None:
        a = Artifact(
            kind=ArtifactKind.MEMORY_DUMP,
            path=Path("/tmp/case/memory/dump.bin"),
            sha256="aa" * 32,
            size_bytes=2048,
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
        )
        report = render_markdown_report(ReportInputs(_case(artifacts=(a,))))
        assert "## 5. Memory Dump" in report
        assert "dump.bin" in report

    def test_screenshots_section_links_images(self) -> None:
        a = Artifact(
            kind=ArtifactKind.SCREENSHOT,
            path=Path("/tmp/case/screenshots/shot.png"),
            sha256="bb" * 32,
            size_bytes=2048,
            captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
        )
        report = render_markdown_report(ReportInputs(_case(artifacts=(a,))))
        assert "## 6. Screenshots" in report
        # Markdown image syntax with relative path.
        assert "../screenshots/shot.png" in report

    def test_footer_present(self) -> None:
        report = render_markdown_report(ReportInputs(_case()))
        assert "case show" in report
        assert "verify" in report

    def test_no_isolation_snapshot_message(self) -> None:
        report = render_markdown_report(ReportInputs(_case()))
        assert "no IsolationReport" in report
