"""Tests for kv.audit — structured audit logging with tamper detection."""

import json
import os
import tempfile
import unittest

import kv.audit as audit


class AuditTestCase(unittest.TestCase):
    """Base class that redirects audit log to a temp directory."""

    def setUp(self):
        self._orig_dir = audit.AUDIT_DIR
        self._tmpdir = tempfile.mkdtemp()
        audit.AUDIT_DIR = self._tmpdir

    def tearDown(self):
        audit.AUDIT_DIR = self._orig_dir


class TestEmitAndRead(AuditTestCase):

    def test_emit_creates_file(self):
        audit.emit("agent_start", pid=1234)
        path = os.path.join(self._tmpdir, "audit.jsonl")
        self.assertTrue(os.path.isfile(path))

    def test_emit_writes_jsonl(self):
        audit.emit("api_call", provider="openai", path="/v1/models")
        path = os.path.join(self._tmpdir, "audit.jsonl")
        with open(path) as f:
            line = f.readline().strip()
        entry = json.loads(line)
        self.assertEqual(entry["event"], "api_call")
        self.assertEqual(entry["provider"], "openai")

    def test_emit_includes_timestamp(self):
        audit.emit("status_query")
        events = audit.read_events()
        self.assertIn("ts", events[0])
        # ISO format: YYYY-MM-DDTHH:MM:SSZ
        self.assertRegex(events[0]["ts"], r"\d{4}-\d{2}-\d{2}T")

    def test_emit_includes_hash_and_prev(self):
        audit.emit("agent_start", pid=1)
        events = audit.read_events()
        self.assertIn("hash", events[0])
        self.assertIn("prev", events[0])
        self.assertEqual(events[0]["prev"], "genesis")

    def test_read_events_most_recent_first(self):
        audit.emit("agent_start", pid=1)
        audit.emit("api_call", provider="openai")
        audit.emit("agent_stop", pid=1)
        events = audit.read_events()
        self.assertEqual(events[0]["event"], "agent_stop")
        self.assertEqual(events[-1]["event"], "agent_start")

    def test_read_events_limit(self):
        for i in range(10):
            audit.emit("api_call", provider="openai", call=i)
        events = audit.read_events(limit=3)
        self.assertEqual(len(events), 3)

    def test_read_empty_log(self):
        events = audit.read_events()
        self.assertEqual(events, [])


class TestFilters(AuditTestCase):

    def setUp(self):
        super().setUp()
        audit.emit("agent_start", pid=1)
        audit.emit("api_call", provider="openai", path="/v1/chat")
        audit.emit("api_call", provider="anthropic", path="/v1/messages")
        audit.emit("run_exec", program="python3", exit_code=0)
        audit.emit("run_blocked", program="curl", reason="network tool")
        audit.emit("leak_detected", count=2)

    def test_filter_by_event(self):
        events = audit.read_events(event_filter="api_call")
        self.assertEqual(len(events), 2)
        for e in events:
            self.assertEqual(e["event"], "api_call")

    def test_filter_by_provider(self):
        events = audit.read_events(provider_filter="openai")
        self.assertEqual(len(events), 1)
        self.assertEqual(events[0]["provider"], "openai")

    def test_security_only(self):
        events = audit.read_events(security_only=True)
        self.assertEqual(len(events), 2)
        event_types = {e["event"] for e in events}
        self.assertEqual(event_types, {"run_blocked", "leak_detected"})

    def test_combined_filters(self):
        events = audit.read_events(event_filter="api_call",
                                   provider_filter="anthropic")
        self.assertEqual(len(events), 1)
        self.assertEqual(events[0]["provider"], "anthropic")


class TestChainIntegrity(AuditTestCase):

    def test_chain_intact(self):
        audit.emit("agent_start", pid=1)
        audit.emit("api_call", provider="openai")
        audit.emit("agent_stop", pid=1)
        ok, detail = audit.verify_chain()
        self.assertTrue(ok)
        self.assertIn("3 entries", detail)

    def test_empty_log_valid(self):
        ok, detail = audit.verify_chain()
        self.assertTrue(ok)

    def test_single_entry_valid(self):
        audit.emit("agent_start", pid=1)
        ok, _ = audit.verify_chain()
        self.assertTrue(ok)

    def test_tamper_deletion_detected(self):
        audit.emit("agent_start", pid=1)
        audit.emit("api_call", provider="openai")
        audit.emit("api_call", provider="anthropic")
        audit.emit("agent_stop", pid=1)

        # Delete middle entry
        path = os.path.join(self._tmpdir, "audit.jsonl")
        with open(path) as f:
            lines = f.readlines()
        with open(path, "w") as f:
            f.writelines([lines[0], lines[1], lines[3]])

        ok, detail = audit.verify_chain()
        self.assertFalse(ok)
        self.assertIn("broken", detail)

    def test_tamper_modification_detected(self):
        audit.emit("agent_start", pid=1)
        audit.emit("api_call", provider="openai")

        # Modify an entry
        path = os.path.join(self._tmpdir, "audit.jsonl")
        with open(path) as f:
            lines = f.readlines()
        entry = json.loads(lines[1])
        entry["provider"] = "evil"
        lines[1] = json.dumps(entry) + "\n"
        with open(path, "w") as f:
            f.writelines(lines)

        ok, _ = audit.verify_chain()
        self.assertFalse(ok)

    def test_chain_links_correctly(self):
        audit.emit("agent_start", pid=1)
        audit.emit("agent_stop", pid=1)

        path = os.path.join(self._tmpdir, "audit.jsonl")
        with open(path) as f:
            lines = f.readlines()

        first = json.loads(lines[0])
        second = json.loads(lines[1])

        # Second entry's prev should be first entry's hash
        self.assertEqual(second["prev"], first["hash"])


class TestSummaryStats(AuditTestCase):

    def test_summary_empty(self):
        stats = audit.summary_stats()
        self.assertEqual(stats["total"], 0)

    def test_summary_counts(self):
        audit.emit("api_call", provider="openai", duration_ms=100)
        audit.emit("api_call", provider="openai", duration_ms=200)
        audit.emit("api_call", provider="anthropic", duration_ms=300)
        audit.emit("run_blocked", program="curl", reason="blocked")

        stats = audit.summary_stats()
        self.assertEqual(stats["total"], 4)
        self.assertEqual(stats["by_event"]["api_call"], 3)
        self.assertEqual(stats["by_event"]["run_blocked"], 1)
        self.assertEqual(stats["by_provider"]["openai"], 2)
        self.assertEqual(stats["by_provider"]["anthropic"], 1)
        self.assertEqual(stats["security_events"], 1)
        self.assertEqual(stats["avg_api_ms"], 200)


class TestEventTypes(AuditTestCase):
    """Verify all documented event types emit correctly."""

    def test_agent_start(self):
        audit.emit("agent_start", pid=1, envs=2, secrets=5, policy="blocklist")
        e = audit.read_events()[0]
        self.assertEqual(e["pid"], 1)
        self.assertEqual(e["secrets"], 5)

    def test_agent_stop(self):
        audit.emit("agent_stop", pid=1, uptime_s=3600)
        e = audit.read_events()[0]
        self.assertEqual(e["uptime_s"], 3600)

    def test_api_call(self):
        audit.emit("api_call", provider="openai", path="/v1/chat",
                   method="POST", status=200, duration_ms=500, env="dev")
        e = audit.read_events()[0]
        self.assertEqual(e["provider"], "openai")
        self.assertEqual(e["status"], 200)
        self.assertEqual(e["duration_ms"], 500)

    def test_run_exec(self):
        audit.emit("run_exec", program="python3", exit_code=0,
                   duration_ms=100, env="dev")
        e = audit.read_events()[0]
        self.assertEqual(e["program"], "python3")
        self.assertEqual(e["exit_code"], 0)

    def test_run_blocked(self):
        audit.emit("run_blocked", program="curl", reason="network tool")
        e = audit.read_events()[0]
        self.assertEqual(e["program"], "curl")
        self.assertIn("network", e["reason"])

    def test_leak_detected(self):
        audit.emit("leak_detected", count=3, env="dev")
        e = audit.read_events()[0]
        self.assertEqual(e["count"], 3)

    def test_leak_file(self):
        audit.emit("leak_file", path="/tmp/leaked.txt", secret_name="API_KEY")
        e = audit.read_events()[0]
        self.assertEqual(e["secret_name"], "API_KEY")


if __name__ == "__main__":
    unittest.main()
