"""Tests for Audit Event Handler."""

from datetime import datetime, UTC

import pytest

from mcp_hangar.application.event_handlers.audit_handler import (
    AuditEventHandler,
    AuditRecord,
    AuditStore,
    InMemoryAuditStore,
)
from mcp_hangar.domain.events import (
    HealthCheckFailed,
    HealthCheckPassed,
    McpServerDegraded,
    McpServerStarted,
    McpServerStateChanged,
    McpServerStopped,
    ToolInvocationCompleted,
    ToolInvocationFailed,
    ToolInvocationRequested,
)


class TestAuditRecord:
    """Test AuditRecord dataclass."""

    def test_audit_record_creation(self):
        """Test creating an audit record."""
        now = datetime.now(UTC)
        record = AuditRecord(
            event_id="evt-123",
            event_type="McpServerStarted",
            occurred_at=now,
            mcp_server_id="test-provider",
            data={"mode": "subprocess"},
        )

        assert record.event_id == "evt-123"
        assert record.event_type == "McpServerStarted"
        assert record.occurred_at == now
        assert record.mcp_server_id == "test-provider"
        assert record.data == {"mode": "subprocess"}
        assert isinstance(record.recorded_at, datetime)

    def test_audit_record_to_dict(self):
        """Test audit record to dictionary conversion."""
        now = datetime.now(UTC)
        record = AuditRecord(
            event_id="evt-456",
            event_type="McpServerStopped",
            occurred_at=now,
            mcp_server_id="test",
            data={"reason": "idle"},
        )

        d = record.to_dict()

        assert d["event_id"] == "evt-456"
        assert d["event_type"] == "McpServerStopped"
        assert "occurred_at" in d
        assert d["mcp_server_id"] == "test"
        assert d["data"] == {"reason": "idle"}
        assert "recorded_at" in d
        assert d["caller_user_id"] is None
        assert d["caller_agent_id"] is None
        assert d["caller_session_id"] is None
        assert d["caller_principal_type"] is None

    def test_audit_record_with_identity(self):
        """Test audit record with caller identity fields."""
        now = datetime.now(UTC)
        record = AuditRecord(
            event_id="evt-id-1",
            event_type="ToolInvocationCompleted",
            occurred_at=now,
            mcp_server_id="math",
            data={"tool_name": "add"},
            caller_user_id="alice",
            caller_agent_id="agent-007",
            caller_session_id="sess-abc",
            caller_principal_type="user",
        )

        assert record.caller_user_id == "alice"
        assert record.caller_agent_id == "agent-007"
        assert record.caller_session_id == "sess-abc"
        assert record.caller_principal_type == "user"

        d = record.to_dict()
        assert d["caller_user_id"] == "alice"
        assert d["caller_agent_id"] == "agent-007"

    def test_audit_record_to_json(self):
        """Test audit record to JSON conversion."""
        now = datetime.now(UTC)
        record = AuditRecord(
            event_id="evt-789",
            event_type="Test",
            occurred_at=now,
            mcp_server_id="p1",
            data={},
        )

        json_str = record.to_json()

        assert "evt-789" in json_str
        assert "Test" in json_str
        assert "p1" in json_str


class TestInMemoryAuditStore:
    """Test InMemoryAuditStore implementation."""

    def test_record_audit_entry(self):
        """Test recording an audit entry."""
        store = InMemoryAuditStore()

        record = AuditRecord(
            event_id="evt-1",
            event_type="McpServerStarted",
            occurred_at=datetime.now(UTC),
            mcp_server_id="p1",
            data={},
        )

        store.record(record)

        assert store.count == 1

    def test_query_all_records(self):
        """Test querying all records."""
        store = InMemoryAuditStore()

        record1 = AuditRecord("evt-1", "McpServerStarted", datetime.now(UTC), "p1", {})
        record2 = AuditRecord("evt-2", "McpServerStopped", datetime.now(UTC), "p1", {})

        store.record(record1)
        store.record(record2)

        records = store.query()

        # Returns most recent first
        assert len(records) == 2

    def test_query_by_provider(self):
        """Test querying records by provider ID."""
        store = InMemoryAuditStore()

        store.record(AuditRecord("e1", "McpServerStarted", datetime.now(UTC), "p1", {}))
        store.record(AuditRecord("e2", "McpServerStarted", datetime.now(UTC), "p2", {}))
        store.record(AuditRecord("e3", "McpServerStopped", datetime.now(UTC), "p1", {}))

        p1_records = store.query(mcp_server_id="p1")
        p2_records = store.query(mcp_server_id="p2")

        assert len(p1_records) == 2
        assert len(p2_records) == 1

    def test_query_by_event_type(self):
        """Test querying records by event type."""
        store = InMemoryAuditStore()

        store.record(AuditRecord("e1", "McpServerStarted", datetime.now(UTC), "p1", {}))
        store.record(AuditRecord("e2", "McpServerStarted", datetime.now(UTC), "p2", {}))
        store.record(AuditRecord("e3", "McpServerStopped", datetime.now(UTC), "p1", {}))

        started_records = store.query(event_type="McpServerStarted")
        stopped_records = store.query(event_type="McpServerStopped")

        assert len(started_records) == 2
        assert len(stopped_records) == 1

    def test_query_with_limit(self):
        """Test querying with limit."""
        store = InMemoryAuditStore()

        for i in range(10):
            store.record(AuditRecord(f"e{i}", "Event", datetime.now(UTC), "p1", {}))

        records = store.query(limit=5)

        assert len(records) == 5

    def test_clear_records(self):
        """Test clearing all records."""
        store = InMemoryAuditStore()

        store.record(AuditRecord("e1", "Event", datetime.now(UTC), "p1", {}))
        store.record(AuditRecord("e2", "Event", datetime.now(UTC), "p1", {}))

        store.clear()

        assert store.count == 0

    def test_max_records_limit(self):
        """Test that store respects max records limit."""
        store = InMemoryAuditStore(max_records=3)

        for i in range(5):
            store.record(AuditRecord(f"e{i}", "Event", datetime.now(UTC), "p1", {}))

        # Should only keep last 3 records
        assert store.count == 3
        records = store.query()
        event_ids = [r.event_id for r in records]
        assert "e2" in event_ids
        assert "e3" in event_ids
        assert "e4" in event_ids

    def test_query_returns_most_recent_first(self):
        """Test query returns records in reverse chronological order."""
        store = InMemoryAuditStore()

        store.record(AuditRecord("e1", "Event", datetime.now(UTC), "p1", {}))
        store.record(AuditRecord("e2", "Event", datetime.now(UTC), "p1", {}))
        store.record(AuditRecord("e3", "Event", datetime.now(UTC), "p1", {}))

        records = store.query()

        assert records[0].event_id == "e3"
        assert records[1].event_id == "e2"
        assert records[2].event_id == "e1"


class TestAuditEventHandler:
    """Test AuditEventHandler."""

    def test_handler_with_default_store(self):
        """Test handler uses InMemoryAuditStore by default."""
        handler = AuditEventHandler()

        assert isinstance(handler._store, InMemoryAuditStore)

    def test_handler_with_custom_store(self):
        """Test handler with custom store."""
        custom_store = InMemoryAuditStore()
        handler = AuditEventHandler(store=custom_store)

        assert handler._store is custom_store

    def test_handle_provider_started_event(self):
        """Test handling McpServerStarted event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = McpServerStarted(
            mcp_server_id="test-provider",
            mode="subprocess",
            tools_count=5,
            startup_duration_ms=150.0,
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].event_type == "McpServerStarted"
        assert records[0].mcp_server_id == "test-provider"

    def test_handle_provider_stopped_event(self):
        """Test handling McpServerStopped event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = McpServerStopped(mcp_server_id="test-provider", reason="idle")

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].event_type == "McpServerStopped"
        assert "reason" in records[0].data

    def test_handle_mcp_server_state_changed_event(self):
        """Test handling McpServerStateChanged event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = McpServerStateChanged(mcp_server_id="test", old_state="cold", new_state="ready")

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].data["old_state"] == "cold"
        assert records[0].data["new_state"] == "ready"

    def test_handle_tool_invocation_requested_event(self):
        """Test handling ToolInvocationRequested event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = ToolInvocationRequested(
            mcp_server_id="test",
            tool_name="add",
            correlation_id="corr-123",
            arguments={"a": 1, "b": 2},
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].data["tool_name"] == "add"

    def test_handle_tool_invocation_completed_event(self):
        """Test handling ToolInvocationCompleted event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = ToolInvocationCompleted(
            mcp_server_id="test",
            tool_name="add",
            correlation_id="corr-123",
            duration_ms=150.0,
            result_size_bytes=42,
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].data["duration_ms"] == 150.0

    def test_handle_tool_invocation_completed_with_identity(self):
        """Test that identity_context is extracted from ToolInvocationCompleted."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = ToolInvocationCompleted(
            mcp_server_id="math",
            tool_name="add",
            correlation_id="corr-456",
            duration_ms=100.0,
            result_size_bytes=10,
            identity_context={
                "user_id": "alice",
                "agent_id": "agent-007",
                "session_id": "sess-abc",
                "principal_type": "user",
            },
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].caller_user_id == "alice"
        assert records[0].caller_agent_id == "agent-007"
        assert records[0].caller_session_id == "sess-abc"
        assert records[0].caller_principal_type == "user"

    def test_handle_tool_invocation_requested_with_identity(self):
        """Test that identity_context is extracted from ToolInvocationRequested."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = ToolInvocationRequested(
            mcp_server_id="test",
            tool_name="add",
            correlation_id="corr-789",
            arguments={"a": 1},
            identity_context={
                "user_id": "bob",
                "agent_id": None,
                "session_id": "sess-xyz",
                "principal_type": "service",
            },
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].caller_user_id == "bob"
        assert records[0].caller_principal_type == "service"

    def test_handle_tool_invocation_failed_with_identity(self):
        """Test that identity_context is extracted from ToolInvocationFailed."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = ToolInvocationFailed(
            mcp_server_id="test",
            tool_name="add",
            correlation_id="corr-fail",
            duration_ms=50.0,
            error_message="timeout",
            error_type="TimeoutError",
            identity_context={
                "user_id": "charlie",
                "agent_id": "agent-x",
                "session_id": None,
                "principal_type": "user",
            },
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].caller_user_id == "charlie"
        assert records[0].caller_agent_id == "agent-x"
        assert records[0].caller_session_id is None

    def test_handle_event_without_identity_context(self):
        """Test that events without identity_context get None for identity fields."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = McpServerStarted(
            mcp_server_id="test",
            mode="subprocess",
            tools_count=3,
            startup_duration_ms=200.0,
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].caller_user_id is None
        assert records[0].caller_agent_id is None
        assert records[0].caller_session_id is None
        assert records[0].caller_principal_type is None

    def test_handle_tool_invocation_failed_event(self):
        """Test handling ToolInvocationFailed event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = ToolInvocationFailed(
            mcp_server_id="test",
            tool_name="add",
            correlation_id="corr-123",
            duration_ms=50.0,
            error_message="timeout",
            error_type="TimeoutError",
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].data["error_message"] == "timeout"

    def test_handle_provider_degraded_event(self):
        """Test handling McpServerDegraded event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = McpServerDegraded(
            mcp_server_id="test",
            consecutive_failures=5,
            total_failures=10,
            reason="timeout",
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].data["consecutive_failures"] == 5

    def test_handle_health_check_passed_event(self):
        """Test handling HealthCheckPassed event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = HealthCheckPassed(mcp_server_id="test", duration_ms=50.0)

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].event_type == "HealthCheckPassed"

    def test_handle_health_check_failed_event(self):
        """Test handling HealthCheckFailed event."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        event = HealthCheckFailed(
            mcp_server_id="test",
            consecutive_failures=3,
            error_message="connection refused",
        )

        handler.handle(event)

        records = store.query()
        assert len(records) == 1
        assert records[0].data["error_message"] == "connection refused"

    def test_records_have_unique_event_ids(self):
        """Test that each record uses the event's ID."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        for i in range(5):
            event = McpServerStarted(
                mcp_server_id=f"p{i}",
                mode="subprocess",
                tools_count=1,
                startup_duration_ms=100.0,
            )
            handler.handle(event)

        records = store.query()
        event_ids = [r.event_id for r in records]

        # All IDs should be unique
        assert len(event_ids) == len(set(event_ids))

    def test_records_have_accurate_timestamps(self):
        """Test that records have accurate timestamps."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        before = datetime.now(UTC)
        event = McpServerStarted(
            mcp_server_id="test",
            mode="subprocess",
            tools_count=1,
            startup_duration_ms=100.0,
        )
        handler.handle(event)
        after = datetime.now(UTC)

        records = store.query()
        assert before <= records[0].recorded_at <= after

    def test_multiple_events_all_recorded(self):
        """Test multiple events are all recorded."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        handler.handle(McpServerStarted("p1", "subprocess", 1, 100.0))
        handler.handle(McpServerStopped("p1", "idle"))
        handler.handle(McpServerStarted("p2", "docker", 2, 200.0))

        records = store.query()
        assert len(records) == 3

    def test_query_method(self):
        """Test handler provides access to records via query."""
        handler = AuditEventHandler()

        handler.handle(McpServerStarted("test", "subprocess", 1, 100.0))

        records = handler.query()
        assert len(records) == 1

    def test_query_by_provider(self):
        """Test querying records filtered by provider."""
        handler = AuditEventHandler()

        handler.handle(McpServerStarted("p1", "subprocess", 1, 100.0))
        handler.handle(McpServerStarted("p2", "subprocess", 1, 100.0))
        handler.handle(McpServerStopped("p1", "idle"))

        p1_records = handler.query(mcp_server_id="p1")
        assert len(p1_records) == 2

    def test_query_by_caller_user_id(self):
        """Test querying records filtered by caller_user_id."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store)

        handler.handle(
            ToolInvocationCompleted(
                mcp_server_id="math",
                tool_name="add",
                correlation_id="c1",
                duration_ms=10.0,
                result_size_bytes=5,
                identity_context={"user_id": "alice", "agent_id": None, "session_id": None, "principal_type": "user"},
            )
        )
        handler.handle(
            ToolInvocationCompleted(
                mcp_server_id="math",
                tool_name="sub",
                correlation_id="c2",
                duration_ms=20.0,
                result_size_bytes=5,
                identity_context={"user_id": "bob", "agent_id": None, "session_id": None, "principal_type": "user"},
            )
        )
        handler.handle(
            ToolInvocationCompleted(
                mcp_server_id="math",
                tool_name="mul",
                correlation_id="c3",
                duration_ms=30.0,
                result_size_bytes=5,
                identity_context={"user_id": "alice", "agent_id": None, "session_id": None, "principal_type": "user"},
            )
        )

        alice_records = handler.query(caller_user_id="alice")
        assert len(alice_records) == 2
        assert all(r.caller_user_id == "alice" for r in alice_records)

        bob_records = handler.query(caller_user_id="bob")
        assert len(bob_records) == 1

    def test_include_event_types_filter(self):
        """Test filtering by included event types."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store, include_event_types=["McpServerStarted", "McpServerStopped"])

        handler.handle(McpServerStarted("p1", "subprocess", 1, 100.0))
        handler.handle(McpServerStopped("p1", "idle"))
        handler.handle(McpServerDegraded("p1", 3, 5, "error"))  # Should be excluded

        records = store.query()
        assert len(records) == 2

    def test_exclude_event_types_filter(self):
        """Test filtering by excluded event types."""
        store = InMemoryAuditStore()
        handler = AuditEventHandler(store=store, exclude_event_types=["HealthCheckPassed"])

        handler.handle(McpServerStarted("p1", "subprocess", 1, 100.0))
        handler.handle(HealthCheckPassed("p1", 50.0))  # Should be excluded

        records = store.query()
        assert len(records) == 1
        assert records[0].event_type == "McpServerStarted"


class TestAuditStoreInterface:
    """Test AuditStore abstract interface."""

    def test_store_requires_methods(self):
        """Test AuditStore requires all methods."""
        with pytest.raises(TypeError):

            class IncompleteStore(AuditStore):
                pass

            IncompleteStore()

    def test_custom_store_implementation(self):
        """Test custom store implementation."""

        class ListStore(AuditStore):
            def __init__(self):
                self.records = []

            def record(self, audit_record: AuditRecord) -> None:
                self.records.append(audit_record)

            def query(self, mcp_server_id=None, event_type=None, since=None, limit=100, caller_user_id=None):
                results = self.records.copy()
                if mcp_server_id:
                    results = [r for r in results if r.mcp_server_id == mcp_server_id]
                if event_type:
                    results = [r for r in results if r.event_type == event_type]
                if caller_user_id:
                    results = [r for r in results if r.caller_user_id == caller_user_id]
                return results[:limit]

        store = ListStore()
        record = AuditRecord("e1", "Event", datetime.now(UTC), "p1", {})
        store.record(record)

        assert len(store.query()) == 1
