"""Tests for :mod:`mgf.vm.forensics.analysis.filesystem` (Phase 17, FN-A3.2)."""

from __future__ import annotations

import json
from datetime import UTC, datetime
from typing import TYPE_CHECKING

import pytest
from mgf.vm.core.exceptions import OperationError

from mgf.vm.forensics.analysis.filesystem import (
    DiffRenderRow,
    DiffSummary,
    load_diff,
    render_diff_table,
    summarize_diff,
)
from mgf.vm.forensics.capture.filesystem import (
    DIFF_SCHEMA_VERSION,
    FileEntry,
    FilesystemDiff,
)

if TYPE_CHECKING:
    from pathlib import Path


def _entry(path: str, size: int = 100, sha: str = "abc") -> FileEntry:
    return FileEntry(path=path, size_bytes=size, mtime=1700000000.0, sha256=sha)


def _diff(
    *,
    added: tuple[FileEntry, ...] = (),
    modified: tuple[FileEntry, ...] = (),
    deleted: tuple[str, ...] = (),
) -> FilesystemDiff:
    return FilesystemDiff(
        captured_at=datetime(2026, 4, 26, 14, tzinfo=UTC),
        baseline_captured_at=datetime(2026, 4, 26, 12, tzinfo=UTC),
        vm_state="stopped",
        disk_view="consistent",
        added=added,
        modified=modified,
        deleted=deleted,
    )


class TestSummarizeDiff:
    def test_counts_each_bucket(self) -> None:
        d = _diff(
            added=(_entry("/a"),),
            modified=(_entry("/b"), _entry("/c")),
            deleted=("/d", "/e", "/f"),
        )
        s = summarize_diff(d)
        assert s.added == 1
        assert s.modified == 2
        assert s.deleted == 3
        assert s.total_changes == 6

    def test_carries_metadata(self) -> None:
        d = _diff()
        s = summarize_diff(d)
        assert isinstance(s, DiffSummary)
        assert s.vm_state == "stopped"
        assert s.disk_view == "consistent"


class TestRenderDiffTable:
    def test_grouped_by_change_then_path(self) -> None:
        d = _diff(
            added=(_entry("/a"),),
            modified=(_entry("/b"),),
            deleted=("/c",),
        )
        rows = render_diff_table(d)
        assert [r.change for r in rows] == ["added", "modified", "deleted"]
        assert [r.path for r in rows] == ["/a", "/b", "/c"]

    def test_deleted_rows_have_zero_size(self) -> None:
        d = _diff(deleted=("/x",))
        rows = render_diff_table(d)
        assert rows[0].size_bytes == 0
        assert rows[0].sha256 == ""

    def test_returns_dataclass_instances(self) -> None:
        d = _diff(added=(_entry("/a"),))
        rows = render_diff_table(d)
        assert all(isinstance(r, DiffRenderRow) for r in rows)


class TestLoadDiff:
    def test_round_trip(self, tmp_path: Path) -> None:
        path = tmp_path / "diff.json"
        payload = {
            "schema_version": DIFF_SCHEMA_VERSION,
            "captured_at": "2026-04-26T14:00:00+00:00",
            "baseline_captured_at": "2026-04-26T12:00:00+00:00",
            "vm_state": "running",
            "disk_view": "inconsistent",
            "added": [
                {"path": "/x", "size_bytes": 5, "mtime": 1700000000.0, "sha256": "abc"},
            ],
            "modified": [],
            "deleted": ["/y"],
        }
        path.write_text(json.dumps(payload), encoding="utf-8")
        d = load_diff(path)
        assert d.vm_state == "running"
        assert d.disk_view == "inconsistent"
        assert len(d.added) == 1
        assert d.added[0].path == "/x"
        assert d.deleted == ("/y",)

    def test_unsupported_schema_raises(self, tmp_path: Path) -> None:
        path = tmp_path / "bad.json"
        path.write_text(
            json.dumps(
                {
                    "schema_version": 99,
                    "captured_at": "2026-04-26T00:00:00+00:00",
                    "baseline_captured_at": "2026-04-26T00:00:00+00:00",
                }
            ),
            encoding="utf-8",
        )
        with pytest.raises(OperationError, match="schema_version"):
            load_diff(path)

    def test_corrupt_json_raises(self, tmp_path: Path) -> None:
        path = tmp_path / "bad.json"
        path.write_text("not json", encoding="utf-8")
        with pytest.raises(OperationError, match="unreadable"):
            load_diff(path)
