"""Tests for the tamper-evident audit log."""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from certisigma_census.audit_log import (
    AuditEntry,
    ChainVerification,
    _HASH_GENESIS,
    append_entry,
    iter_entries,
    verify_chain,
)


class TestAuditEntry:
    def test_compute_hash_deterministic(self):
        e = AuditEntry(
            timestamp="2026-03-18T10:00:00+0000",
            action="scan",
            params={"dir": "/tmp"},
            result={"total": 10},
            prev_hash=_HASH_GENESIS,
        )
        h1 = e.compute_hash()
        h2 = e.compute_hash()
        assert h1 == h2
        assert len(h1) == 64

    def test_different_content_different_hash(self):
        e1 = AuditEntry(timestamp="t1", action="scan", prev_hash=_HASH_GENESIS)
        e2 = AuditEntry(timestamp="t2", action="scan", prev_hash=_HASH_GENESIS)
        assert e1.compute_hash() != e2.compute_hash()

    def test_hash_excludes_entry_hash(self):
        e = AuditEntry(timestamp="t1", action="scan", prev_hash=_HASH_GENESIS)
        e.entry_hash = "will_be_ignored"
        h = e.compute_hash()
        e.entry_hash = "different_value"
        assert e.compute_hash() == h


class TestAppendEntry:
    def test_creates_file_and_appends(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        e1 = append_entry("scan", {"dir": "/data"}, {"total": 5}, log_path=log)
        assert log.exists()
        assert e1.entry_hash
        assert e1.prev_hash == _HASH_GENESIS

        e2 = append_entry("compare", {"dir": "/suspect"}, {"matched": 2}, log_path=log)
        assert e2.prev_hash == e1.entry_hash

    def test_entries_are_valid_json(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        append_entry("test", log_path=log)
        line = log.read_text().strip()
        data = json.loads(line)
        assert data["action"] == "test"
        assert "entry_hash" in data
        assert "prev_hash" in data

    def test_creates_parent_dirs(self, tmp_path: Path):
        log = tmp_path / "deep" / "nested" / "audit.jsonl"
        append_entry("test", log_path=log)
        assert log.exists()


class TestIterEntries:
    def test_empty_log(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        assert list(iter_entries(log)) == []

    def test_nonexistent_log(self, tmp_path: Path):
        log = tmp_path / "nonexistent.jsonl"
        assert list(iter_entries(log)) == []

    def test_round_trip(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        append_entry("scan", {"x": 1}, log_path=log)
        append_entry("compare", {"y": 2}, log_path=log)
        entries = list(iter_entries(log))
        assert len(entries) == 2
        assert entries[0].action == "scan"
        assert entries[1].action == "compare"


class TestVerifyChain:
    def test_empty_log_is_intact(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        result = verify_chain(log)
        assert result.intact
        assert result.total == 0

    def test_valid_chain(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        for i in range(5):
            append_entry(f"action_{i}", log_path=log)
        result = verify_chain(log)
        assert result.intact
        assert result.total == 5
        assert result.valid == 5

    def test_detects_tampered_entry(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        append_entry("first", log_path=log)
        append_entry("second", log_path=log)
        append_entry("third", log_path=log)

        lines = log.read_text().strip().split("\n")
        tampered = json.loads(lines[1])
        tampered["params"]["injected"] = True
        lines[1] = json.dumps(tampered, sort_keys=True, separators=(",", ":"))
        log.write_text("\n".join(lines) + "\n")

        result = verify_chain(log)
        assert not result.intact
        assert result.broken_at == 2
        assert "Hash mismatch" in result.error

    def test_detects_broken_chain_link(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        append_entry("first", log_path=log)
        append_entry("second", log_path=log)

        lines = log.read_text().strip().split("\n")
        entry = json.loads(lines[1])
        entry["prev_hash"] = "a" * 64
        entry["entry_hash"] = AuditEntry(**{
            k: entry[k] for k in ["timestamp", "action", "params", "result", "prev_hash"]
        }).compute_hash()
        lines[1] = json.dumps(entry, sort_keys=True, separators=(",", ":"))
        log.write_text("\n".join(lines) + "\n")

        result = verify_chain(log)
        assert not result.intact
        assert result.broken_at == 2
        assert "Chain broken" in result.error

    def test_nonexistent_log(self, tmp_path: Path):
        result = verify_chain(tmp_path / "gone.jsonl")
        assert result.intact
        assert result.total == 0

    def test_detects_corrupt_line(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        append_entry("first", log_path=log)
        append_entry("second", log_path=log)

        lines = log.read_text().strip().split("\n")
        lines[1] = "THIS IS NOT JSON"
        log.write_text("\n".join(lines) + "\n")

        result = verify_chain(log)
        assert not result.intact
        assert "Corrupt entry" in result.error


class TestAuditLogTypeSafety:
    """Non-string entry_hash in tampered audit log must not poison chain."""

    def test_non_string_entry_hash_ignored(self, tmp_path: Path):
        log = tmp_path / "audit.jsonl"
        append_entry("first", log_path=log)

        lines = log.read_text().strip().split("\n")
        entry = json.loads(lines[0])
        entry["entry_hash"] = 12345
        lines[0] = json.dumps(entry, sort_keys=True, separators=(",", ":"))
        log.write_text("\n".join(lines) + "\n")

        append_entry("second", log_path=log)
        second_lines = log.read_text().strip().split("\n")
        second_entry = json.loads(second_lines[-1])
        assert second_entry["prev_hash"] == _HASH_GENESIS

    def test_non_dict_json_entry_falls_back_to_genesis(self, tmp_path: Path):
        """Last entry being a JSON list/string must not crash _last_hash."""
        log = tmp_path / "audit.jsonl"
        log.write_text('"just a string"\n')
        e = append_entry("next", log_path=log)
        assert e.prev_hash == _HASH_GENESIS

    def test_non_dict_json_array_falls_back_to_genesis(self, tmp_path: Path):
        """Last entry being a JSON array must not crash _last_hash."""
        log = tmp_path / "audit.jsonl"
        log.write_text("[1, 2, 3]\n")
        e = append_entry("next", log_path=log)
        assert e.prev_hash == _HASH_GENESIS
