"""Tests for the structured audit trail."""

import json
import os
import tempfile
import pytest

from crewai_mcps.audit import AuditTrail, AuditEvent, AuditEventType


class TestAuditEvent:
    """Test individual audit events."""

    def test_create_event(self):
        event = AuditEvent(
            event_type=AuditEventType.TOOL_CALL,
            tool_name="search",
            timestamp=1000.0,
            nonce="abc123",
            details={"query": "test"},
        )
        assert event.event_type == "tool_call"
        assert event.tool_name == "search"
        assert event.nonce == "abc123"

    def test_to_dict(self):
        event = AuditEvent(
            event_type=AuditEventType.TOOL_CALL,
            tool_name="search",
            timestamp=1000.0,
        )
        d = event.to_dict()
        assert isinstance(d, dict)
        assert d["event_type"] == "tool_call"
        assert d["tool_name"] == "search"
        assert d["timestamp"] == 1000.0

    def test_to_json(self):
        event = AuditEvent(
            event_type=AuditEventType.TOOL_RESPONSE,
            tool_name="write",
            timestamp=2000.0,
            details={"length": 42},
        )
        j = event.to_json()
        parsed = json.loads(j)
        assert parsed["event_type"] == "tool_response"
        assert parsed["details"]["length"] == 42


class TestAuditTrail:
    """Test the full audit trail system."""

    def test_log_call(self):
        audit = AuditTrail()
        event = audit.log_call(
            "search",
            {"query": "hello"},
            {"nonce": "n1", "signature": "sig123456789012345"},
        )
        assert event.event_type == AuditEventType.TOOL_CALL
        assert event.tool_name == "search"
        assert event.sequence == 1

    def test_log_response(self):
        audit = AuditTrail()
        event = audit.log_response(
            "search",
            "search results here",
            {"nonce": "n1", "verified": True},
        )
        assert event.event_type == AuditEventType.TOOL_RESPONSE
        assert event.details["verified"] is True

    def test_log_error(self):
        audit = AuditTrail()
        event = audit.log_error(
            "search",
            "Connection timeout",
            {"nonce": "n1"},
        )
        assert event.event_type == AuditEventType.TOOL_ERROR
        assert "timeout" in event.details["error"].lower()

    def test_sequence_numbers(self):
        audit = AuditTrail()
        e1 = audit.log_call("t1", {}, {"nonce": "n1", "signature": "s" * 20})
        e2 = audit.log_call("t2", {}, {"nonce": "n2", "signature": "s" * 20})
        e3 = audit.log_call("t3", {}, {"nonce": "n3", "signature": "s" * 20})
        assert e1.sequence == 1
        assert e2.sequence == 2
        assert e3.sequence == 3

    def test_event_count(self):
        audit = AuditTrail()
        assert audit.event_count == 0
        audit.log_call("t", {}, {"nonce": "n", "signature": "s" * 20})
        assert audit.event_count == 1
        audit.log_response("t", "r", {"nonce": "n"})
        assert audit.event_count == 2

    def test_events_returns_copy(self):
        audit = AuditTrail()
        audit.log_call("t", {}, {"nonce": "n", "signature": "s" * 20})
        events = audit.events
        events.clear()  # Modify the copy
        assert audit.event_count == 1  # Original unaffected

    def test_get_events_for_tool(self):
        audit = AuditTrail()
        audit.log_call("search", {}, {"nonce": "n1", "signature": "s" * 20})
        audit.log_call("write", {}, {"nonce": "n2", "signature": "s" * 20})
        audit.log_call("search", {}, {"nonce": "n3", "signature": "s" * 20})
        search_events = audit.get_events_for_tool("search")
        assert len(search_events) == 2

    def test_get_events_by_type(self):
        audit = AuditTrail()
        audit.log_call("t", {}, {"nonce": "n", "signature": "s" * 20})
        audit.log_error("t", "err")
        audit.log_call("t", {}, {"nonce": "n2", "signature": "s" * 20})
        calls = audit.get_events_by_type(AuditEventType.TOOL_CALL)
        assert len(calls) == 2
        errors = audit.get_errors()
        assert len(errors) == 1

    def test_security_alerts(self):
        audit = AuditTrail()
        audit.log_pin_mismatch("tool", "hash1", "hash2")
        audit.log_replay_detected("tool", "nonce")
        audit.log_call("tool", {}, {"nonce": "n", "signature": "s" * 20})
        alerts = audit.get_security_alerts()
        assert len(alerts) == 2

    def test_log_pin_created(self):
        audit = AuditTrail()
        event = audit.log_pin_created("search", "abcdef123456")
        assert event.event_type == AuditEventType.PIN_CREATED
        assert event.details["definition_hash"] == "abcdef123456"

    def test_summary(self):
        audit = AuditTrail()
        audit.log_call("search", {}, {"nonce": "n1", "signature": "s" * 20})
        audit.log_response("search", "ok", {"nonce": "n1"})
        audit.log_call("write", {}, {"nonce": "n2", "signature": "s" * 20})
        audit.log_error("write", "fail")
        audit.log_pin_mismatch("write", "h1", "h2")

        summary = audit.summary()
        assert summary["total_events"] == 5
        assert summary["total_calls"] == 2
        assert summary["total_errors"] == 1
        assert summary["security_alerts"] == 1
        assert summary["unique_tools"] == 2
        assert set(summary["tools"]) == {"search", "write"}

    def test_export_json(self):
        audit = AuditTrail()
        audit.log_call("tool", {"k": "v"}, {"nonce": "n", "signature": "s" * 20})

        with tempfile.NamedTemporaryFile(
            mode="w", suffix=".json", delete=False
        ) as f:
            filepath = f.name

        try:
            audit.export_json(filepath)
            with open(filepath) as f:
                data = json.load(f)
            assert "audit_trail" in data
            assert "summary" in data
            assert "exported_at" in data
            assert len(data["audit_trail"]) == 1
        finally:
            os.unlink(filepath)

    def test_clear(self):
        audit = AuditTrail()
        audit.log_call("t", {}, {"nonce": "n", "signature": "s" * 20})
        assert audit.event_count == 1
        audit.clear()
        assert audit.event_count == 0

    def test_max_events_trimming(self):
        audit = AuditTrail(max_events=5)
        for i in range(10):
            audit.log_call(
                f"tool_{i}", {}, {"nonce": f"n{i}", "signature": "s" * 20}
            )
        assert audit.event_count == 5
        # Should keep the most recent events
        events = audit.events
        assert events[-1].tool_name == "tool_9"

    def test_on_event_callback(self):
        received = []
        audit = AuditTrail(on_event=lambda e: received.append(e))
        audit.log_call("t", {}, {"nonce": "n", "signature": "s" * 20})
        assert len(received) == 1
        assert received[0].tool_name == "t"

    def test_callback_error_does_not_break_audit(self):
        def bad_callback(event):
            raise ValueError("boom")

        audit = AuditTrail(on_event=bad_callback)
        # Should not raise
        audit.log_call("t", {}, {"nonce": "n", "signature": "s" * 20})
        assert audit.event_count == 1

    def test_response_preview_truncation(self):
        audit = AuditTrail()
        long_response = "x" * 200
        event = audit.log_response("t", long_response, {"nonce": "n"})
        assert event.details["response_preview"].endswith("...")
        assert len(event.details["response_preview"]) == 103  # 100 + "..."

    def test_empty_summary(self):
        audit = AuditTrail()
        summary = audit.summary()
        assert summary["total_events"] == 0
        assert summary["first_event"] is None
        assert summary["last_event"] is None
