"""Tests for manifest diffing — diff_manifests(), DiffResult, HTML report."""

from __future__ import annotations

from pathlib import Path

import pytest

from certisigma_census.diff import (
    EXIT_ADDED,
    EXIT_MODIFIED,
    EXIT_REMOVED,
    DiffEntry,
    DiffResult,
    diff_manifests,
    generate_diff_html,
)
from certisigma_census.manifest import Manifest, ManifestEntry


def _manifest_with_entries(root: str, entries: list[tuple[str, str, int]]) -> Manifest:
    """Helper: create manifest with (hash, path, size) tuples."""
    m = Manifest(root_dir=root)
    for hash_hex, path, size in entries:
        m.add(ManifestEntry(hash_hex=hash_hex, path=path, size=size, mtime=1.0))
    return m


class TestDiffManifests:
    def test_identical_manifests(self):
        entries = [("aa" * 32, "a.txt", 100), ("bb" * 32, "b.txt", 200)]
        base = _manifest_with_entries("/data", entries)
        target = _manifest_with_entries("/data", entries)
        try:
            result = diff_manifests(base, target)
            assert not result.has_changes
            assert result.unchanged_count == 2
            assert result.exit_code == 0
        finally:
            base.close()
            target.close()

    def test_added_files(self):
        base = _manifest_with_entries("/data", [("aa" * 32, "a.txt", 100)])
        target = _manifest_with_entries("/data", [
            ("aa" * 32, "a.txt", 100),
            ("bb" * 32, "b.txt", 200),
        ])
        try:
            result = diff_manifests(base, target)
            assert len(result.added) == 1
            assert result.added[0].path == "b.txt"
            assert result.added[0].new_hash == "bb" * 32
            assert result.exit_code == EXIT_ADDED
        finally:
            base.close()
            target.close()

    def test_removed_files(self):
        base = _manifest_with_entries("/data", [
            ("aa" * 32, "a.txt", 100),
            ("bb" * 32, "b.txt", 200),
        ])
        target = _manifest_with_entries("/data", [("aa" * 32, "a.txt", 100)])
        try:
            result = diff_manifests(base, target)
            assert len(result.removed) == 1
            assert result.removed[0].path == "b.txt"
            assert result.exit_code == EXIT_REMOVED
        finally:
            base.close()
            target.close()

    def test_modified_files(self):
        base = _manifest_with_entries("/data", [("aa" * 32, "a.txt", 100)])
        target = _manifest_with_entries("/data", [("bb" * 32, "a.txt", 150)])
        try:
            result = diff_manifests(base, target)
            assert len(result.modified) == 1
            assert result.modified[0].old_hash == "aa" * 32
            assert result.modified[0].new_hash == "bb" * 32
            assert result.exit_code == EXIT_MODIFIED
        finally:
            base.close()
            target.close()

    def test_combined_changes(self):
        base = _manifest_with_entries("/data", [
            ("aa" * 32, "a.txt", 100),
            ("bb" * 32, "b.txt", 200),
            ("cc" * 32, "c.txt", 300),
        ])
        target = _manifest_with_entries("/data", [
            ("aa" * 32, "a.txt", 100),
            ("dd" * 32, "b.txt", 250),
            ("ee" * 32, "d.txt", 400),
        ])
        try:
            result = diff_manifests(base, target)
            assert result.unchanged_count == 1
            assert len(result.modified) == 1
            assert len(result.removed) == 1
            assert len(result.added) == 1
            assert result.exit_code == EXIT_ADDED | EXIT_REMOVED | EXIT_MODIFIED
        finally:
            base.close()
            target.close()

    def test_empty_manifests(self):
        base = _manifest_with_entries("/data", [])
        target = _manifest_with_entries("/data", [])
        try:
            result = diff_manifests(base, target)
            assert not result.has_changes
            assert result.exit_code == 0
        finally:
            base.close()
            target.close()

    def test_paths_are_sorted(self):
        base = _manifest_with_entries("/data", [])
        target = _manifest_with_entries("/data", [
            ("bb" * 32, "z.txt", 100),
            ("aa" * 32, "a.txt", 100),
        ])
        try:
            result = diff_manifests(base, target)
            assert [e.path for e in result.added] == ["a.txt", "z.txt"]
        finally:
            base.close()
            target.close()

    def test_different_roots(self):
        base = _manifest_with_entries("/old", [("aa" * 32, "a.txt", 100)])
        target = _manifest_with_entries("/new", [("aa" * 32, "a.txt", 100)])
        try:
            result = diff_manifests(base, target)
            assert result.base_root == "/old"
            assert result.target_root == "/new"
            assert result.unchanged_count == 1
        finally:
            base.close()
            target.close()


class TestDiffExitCode:
    def test_no_changes_exit_0(self):
        r = DiffResult(unchanged_count=10)
        assert r.exit_code == 0

    def test_only_added_exit_1(self):
        r = DiffResult(added=[DiffEntry(path="x", status="added")])
        assert r.exit_code == 1

    def test_only_removed_exit_2(self):
        r = DiffResult(removed=[DiffEntry(path="x", status="removed")])
        assert r.exit_code == 2

    def test_only_modified_exit_4(self):
        r = DiffResult(modified=[DiffEntry(path="x", status="modified")])
        assert r.exit_code == 4

    def test_all_changes_exit_7(self):
        r = DiffResult(
            added=[DiffEntry(path="a", status="added")],
            removed=[DiffEntry(path="b", status="removed")],
            modified=[DiffEntry(path="c", status="modified")],
        )
        assert r.exit_code == 7


class TestDiffHTML:
    def test_generates_valid_html(self):
        base = _manifest_with_entries("/data", [("aa" * 32, "old.txt", 100)])
        target = _manifest_with_entries("/data", [
            ("bb" * 32, "old.txt", 150),
            ("cc" * 32, "new.txt", 200),
        ])
        try:
            result = diff_manifests(base, target)
            html = generate_diff_html(result)
            assert "<!DOCTYPE html>" in html
            assert "CertiSigma Census" in html
            assert "old.txt" in html
            assert "new.txt" in html
        finally:
            base.close()
            target.close()

    def test_xss_prevention_in_paths(self):
        base = _manifest_with_entries("/data", [])
        target = _manifest_with_entries("/data", [
            ("aa" * 32, '<script>alert("xss")</script>', 100),
        ])
        try:
            result = diff_manifests(base, target)
            html = generate_diff_html(result)
            assert "<script>" not in html
            assert "&lt;script&gt;" in html
        finally:
            base.close()
            target.close()

    def test_empty_diff_html(self):
        result = DiffResult(base_root="/a", target_root="/b", unchanged_count=5)
        html = generate_diff_html(result)
        assert "<!DOCTYPE html>" in html
        assert "0" in html  # added/removed/modified all 0

    def test_shows_summary_counts(self):
        result = DiffResult(
            base_root="/a", target_root="/b",
            added=[DiffEntry(path="x", status="added", new_hash="aa" * 32, new_size=100)],
            removed=[DiffEntry(path="y", status="removed", old_hash="bb" * 32, old_size=200)],
            unchanged_count=10,
        )
        html = generate_diff_html(result)
        assert "Added" in html
        assert "Removed" in html
        assert "Unchanged" in html
