"""Tests for CrewAISentinelAdapter. No crewai install required."""

import threading
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch

import pytest

from agentcop import Sentinel

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_adapter(run_id=None):
    """Construct a CrewAISentinelAdapter with the crewai guard bypassed."""
    with patch("agentcop.adapters.crewai._require_crewai"):
        from agentcop.adapters.crewai import CrewAISentinelAdapter

        return CrewAISentinelAdapter(run_id=run_id)


@pytest.fixture()
def adapter():
    return _make_adapter(run_id="run-001")


@pytest.fixture()
def adapter_no_run():
    return _make_adapter(run_id=None)


# ---------------------------------------------------------------------------
# Guard / import
# ---------------------------------------------------------------------------


class TestRequireCrewAI:
    def test_raises_import_error_when_crewai_missing(self):
        import builtins

        real_import = builtins.__import__

        def mock_import(name, *args, **kwargs):
            if name == "crewai":
                raise ImportError("No module named 'crewai'")
            return real_import(name, *args, **kwargs)

        with patch("builtins.__import__", side_effect=mock_import):
            from agentcop.adapters.crewai import _require_crewai

            with pytest.raises(ImportError, match="pip install agentcop\\[crewai\\]"):
                _require_crewai()

    def test_does_not_raise_when_crewai_present(self):
        with patch("builtins.__import__", return_value=MagicMock()):
            from agentcop.adapters.crewai import _require_crewai

            _require_crewai()

    def test_constructor_calls_require_crewai(self):
        with patch("agentcop.adapters.crewai._require_crewai") as mock_guard:
            from agentcop.adapters.crewai import CrewAISentinelAdapter

            CrewAISentinelAdapter()
            mock_guard.assert_called_once()


# ---------------------------------------------------------------------------
# Adapter init
# ---------------------------------------------------------------------------


class TestAdapterInit:
    def test_source_system_is_crewai(self, adapter):
        assert adapter.source_system == "crewai"

    def test_run_id_stored(self):
        a = _make_adapter(run_id="my-run")
        assert a._run_id == "my-run"

    def test_run_id_defaults_to_none(self, adapter_no_run):
        assert adapter_no_run._run_id is None

    def test_buffer_starts_empty(self, adapter):
        assert adapter.drain() == []


# ---------------------------------------------------------------------------
# Crew events
# ---------------------------------------------------------------------------


class TestFromCrewEvents:
    def test_kickoff_started_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_started", "crew_name": "Research Crew"}
        )
        assert e.event_type == "crew_kickoff_started"

    def test_kickoff_started_severity_info(self, adapter):
        e = adapter.to_sentinel_event({"type": "crew_kickoff_started", "crew_name": "RC"})
        assert e.severity == "INFO"

    def test_kickoff_started_body_contains_crew_name(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_started", "crew_name": "Research Crew"}
        )
        assert "Research Crew" in e.body

    def test_kickoff_started_attributes(self, adapter):
        e = adapter.to_sentinel_event({"type": "crew_kickoff_started", "crew_name": "RC"})
        assert e.attributes["crew_name"] == "RC"

    def test_kickoff_started_source_system(self, adapter):
        e = adapter.to_sentinel_event({"type": "crew_kickoff_started", "crew_name": "RC"})
        assert e.source_system == "crewai"

    def test_kickoff_started_trace_id(self, adapter):
        e = adapter.to_sentinel_event({"type": "crew_kickoff_started", "crew_name": "RC"})
        assert e.trace_id == "run-001"

    def test_kickoff_started_event_id_prefixed(self, adapter):
        e = adapter.to_sentinel_event({"type": "crew_kickoff_started", "crew_name": "RC"})
        assert e.event_id.startswith("crewai-crew-")

    def test_kickoff_completed_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_completed", "crew_name": "RC", "output": "done"}
        )
        assert e.event_type == "crew_kickoff_completed"

    def test_kickoff_completed_severity_info(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_completed", "crew_name": "RC", "output": "done"}
        )
        assert e.severity == "INFO"

    def test_kickoff_completed_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_completed", "crew_name": "RC", "output": "final answer"}
        )
        assert e.attributes["crew_name"] == "RC"
        assert e.attributes["output"] == "final answer"

    def test_kickoff_failed_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_failed", "crew_name": "RC", "error": "timeout"}
        )
        assert e.event_type == "crew_kickoff_failed"

    def test_kickoff_failed_severity_error(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_failed", "crew_name": "RC", "error": "timeout"}
        )
        assert e.severity == "ERROR"

    def test_kickoff_failed_body_contains_error(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_failed", "crew_name": "RC", "error": "timeout"}
        )
        assert "timeout" in e.body

    def test_kickoff_failed_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_failed", "crew_name": "RC", "error": "timeout"}
        )
        assert e.attributes["error"] == "timeout"
        assert e.attributes["crew_name"] == "RC"

    def test_missing_crew_name_defaults_to_unknown(self, adapter):
        e = adapter.to_sentinel_event({"type": "crew_kickoff_started"})
        assert e.attributes["crew_name"] == "unknown"


# ---------------------------------------------------------------------------
# Agent events
# ---------------------------------------------------------------------------


class TestFromAgentEvents:
    def test_agent_started_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_started", "agent_role": "Researcher"}
        )
        assert e.event_type == "agent_execution_started"

    def test_agent_started_severity_info(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_started", "agent_role": "Researcher"}
        )
        assert e.severity == "INFO"

    def test_agent_started_body_contains_role(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_started", "agent_role": "Researcher"}
        )
        assert "Researcher" in e.body

    def test_agent_started_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_started", "agent_role": "Researcher"}
        )
        assert e.attributes["agent_role"] == "Researcher"

    def test_agent_started_event_id_prefixed(self, adapter):
        e = adapter.to_sentinel_event({"type": "agent_execution_started", "agent_role": "R"})
        assert e.event_id.startswith("crewai-agent-")

    def test_agent_completed_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_completed", "agent_role": "R", "output": "result"}
        )
        assert e.event_type == "agent_execution_completed"

    def test_agent_completed_severity_info(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_completed", "agent_role": "R", "output": "x"}
        )
        assert e.severity == "INFO"

    def test_agent_completed_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_completed", "agent_role": "Writer", "output": "article"}
        )
        assert e.attributes["agent_role"] == "Writer"
        assert e.attributes["output"] == "article"

    def test_agent_error_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_error", "agent_role": "R", "error": "API fail"}
        )
        assert e.event_type == "agent_execution_error"

    def test_agent_error_severity_error(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_error", "agent_role": "R", "error": "fail"}
        )
        assert e.severity == "ERROR"

    def test_agent_error_body_contains_error(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_error", "agent_role": "R", "error": "API timeout"}
        )
        assert "API timeout" in e.body

    def test_agent_error_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "agent_execution_error", "agent_role": "Coder", "error": "SyntaxError"}
        )
        assert e.attributes["agent_role"] == "Coder"
        assert e.attributes["error"] == "SyntaxError"

    def test_missing_agent_role_defaults_to_unknown(self, adapter):
        e = adapter.to_sentinel_event({"type": "agent_execution_started"})
        assert e.attributes["agent_role"] == "unknown"

    def test_trace_id_propagated(self, adapter):
        e = adapter.to_sentinel_event({"type": "agent_execution_started", "agent_role": "R"})
        assert e.trace_id == "run-001"

    def test_trace_id_none_when_no_run_id(self, adapter_no_run):
        e = adapter_no_run.to_sentinel_event(
            {"type": "agent_execution_started", "agent_role": "R"}
        )
        assert e.trace_id is None


# ---------------------------------------------------------------------------
# Task events
# ---------------------------------------------------------------------------


class TestFromTaskEvents:
    def test_task_started_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_started", "task_description": "Research AI trends"}
        )
        assert e.event_type == "task_started"

    def test_task_started_severity_info(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_started", "task_description": "Research AI trends"}
        )
        assert e.severity == "INFO"

    def test_task_started_body_contains_description(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_started", "task_description": "Research AI trends"}
        )
        assert "Research AI trends" in e.body

    def test_task_started_attributes_with_agent_role(self, adapter):
        e = adapter.to_sentinel_event(
            {
                "type": "task_started",
                "task_description": "Write report",
                "agent_role": "Writer",
            }
        )
        assert e.attributes["task_description"] == "Write report"
        assert e.attributes["agent_role"] == "Writer"

    def test_task_started_no_agent_role_when_empty(self, adapter):
        e = adapter.to_sentinel_event({"type": "task_started", "task_description": "task"})
        assert "agent_role" not in e.attributes

    def test_task_started_event_id_prefixed(self, adapter):
        e = adapter.to_sentinel_event({"type": "task_started", "task_description": "x"})
        assert e.event_id.startswith("crewai-task-")

    def test_task_completed_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_completed", "task_description": "x", "output": "y"}
        )
        assert e.event_type == "task_completed"

    def test_task_completed_severity_info(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_completed", "task_description": "x", "output": "y"}
        )
        assert e.severity == "INFO"

    def test_task_completed_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_completed", "task_description": "research", "output": "report"}
        )
        assert e.attributes["task_description"] == "research"
        assert e.attributes["output"] == "report"

    def test_task_failed_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_failed", "task_description": "x", "error": "fail"}
        )
        assert e.event_type == "task_failed"

    def test_task_failed_severity_error(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_failed", "task_description": "x", "error": "fail"}
        )
        assert e.severity == "ERROR"

    def test_task_failed_body_contains_error(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_failed", "task_description": "x", "error": "RateLimit"}
        )
        assert "RateLimit" in e.body

    def test_task_failed_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "task_failed", "task_description": "task", "error": "boom"}
        )
        assert e.attributes["task_description"] == "task"
        assert e.attributes["error"] == "boom"


# ---------------------------------------------------------------------------
# Tool events
# ---------------------------------------------------------------------------


class TestFromToolEvents:
    def test_tool_started_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_started", "tool_name": "SerperDevTool", "agent_role": "R"}
        )
        assert e.event_type == "tool_usage_started"

    def test_tool_started_severity_info(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_started", "tool_name": "T", "agent_role": "R"}
        )
        assert e.severity == "INFO"

    def test_tool_started_body_contains_tool_and_agent(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_started", "tool_name": "WebSearch", "agent_role": "Researcher"}
        )
        assert "WebSearch" in e.body
        assert "Researcher" in e.body

    def test_tool_started_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_started", "tool_name": "WebSearch", "agent_role": "Researcher"}
        )
        assert e.attributes["tool_name"] == "WebSearch"
        assert e.attributes["agent_role"] == "Researcher"

    def test_tool_started_event_id_prefixed(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_started", "tool_name": "T", "agent_role": "R"}
        )
        assert e.event_id.startswith("crewai-tool-")

    def test_tool_finished_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_finished", "tool_name": "T", "agent_role": "R"}
        )
        assert e.event_type == "tool_usage_finished"

    def test_tool_finished_severity_info(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_finished", "tool_name": "T", "agent_role": "R"}
        )
        assert e.severity == "INFO"

    def test_tool_finished_from_cache_true(self, adapter):
        e = adapter.to_sentinel_event(
            {
                "type": "tool_usage_finished",
                "tool_name": "T",
                "agent_role": "R",
                "from_cache": True,
            }
        )
        assert e.attributes["from_cache"] is True

    def test_tool_finished_from_cache_defaults_false(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_finished", "tool_name": "T", "agent_role": "R"}
        )
        assert e.attributes["from_cache"] is False

    def test_tool_error_event_type(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_error", "tool_name": "T", "agent_role": "R", "error": "403"}
        )
        assert e.event_type == "tool_usage_error"

    def test_tool_error_severity_error(self, adapter):
        e = adapter.to_sentinel_event(
            {"type": "tool_usage_error", "tool_name": "T", "agent_role": "R", "error": "403"}
        )
        assert e.severity == "ERROR"

    def test_tool_error_body_contains_error(self, adapter):
        e = adapter.to_sentinel_event(
            {
                "type": "tool_usage_error",
                "tool_name": "WebSearch",
                "agent_role": "R",
                "error": "rate limit",
            }
        )
        assert "rate limit" in e.body

    def test_tool_error_attributes(self, adapter):
        e = adapter.to_sentinel_event(
            {
                "type": "tool_usage_error",
                "tool_name": "WebSearch",
                "agent_role": "Researcher",
                "error": "403",
            }
        )
        assert e.attributes["tool_name"] == "WebSearch"
        assert e.attributes["agent_role"] == "Researcher"
        assert e.attributes["error"] == "403"

    def test_missing_tool_name_defaults_to_unknown(self, adapter):
        e = adapter.to_sentinel_event({"type": "tool_usage_started", "agent_role": "R"})
        assert e.attributes["tool_name"] == "unknown"


# ---------------------------------------------------------------------------
# Unknown / empty events
# ---------------------------------------------------------------------------


class TestFromUnknown:
    def test_unknown_type_gives_unknown_crewai_event(self, adapter):
        e = adapter.to_sentinel_event({"type": "some_future_event"})
        assert e.event_type == "unknown_crewai_event"

    def test_unknown_severity_info(self, adapter):
        e = adapter.to_sentinel_event({"type": "whatever"})
        assert e.severity == "INFO"

    def test_unknown_event_id_prefixed(self, adapter):
        e = adapter.to_sentinel_event({"type": "x"})
        assert e.event_id.startswith("crewai-unknown-")

    def test_unknown_attributes_original_type(self, adapter):
        e = adapter.to_sentinel_event({"type": "mystery_event"})
        assert e.attributes["original_type"] == "mystery_event"

    def test_empty_dict_does_not_raise(self, adapter):
        e = adapter.to_sentinel_event({})
        assert e.event_type == "unknown_crewai_event"

    def test_missing_type_key_goes_to_unknown(self, adapter):
        e = adapter.to_sentinel_event({"crew_name": "RC"})
        assert e.event_type == "unknown_crewai_event"

    def test_trace_id_on_unknown_is_run_id(self, adapter):
        e = adapter.to_sentinel_event({"type": "x"})
        assert e.trace_id == "run-001"


# ---------------------------------------------------------------------------
# Timestamp parsing
# ---------------------------------------------------------------------------


class TestTimestampParsing:
    def test_iso_z_suffix_parsed(self, adapter):
        e = adapter.to_sentinel_event(
            {
                "type": "crew_kickoff_started",
                "crew_name": "RC",
                "timestamp": "2026-06-01T12:00:00Z",
            }
        )
        assert e.timestamp == datetime(2026, 6, 1, 12, 0, 0, tzinfo=UTC)

    def test_iso_offset_parsed(self, adapter):
        e = adapter.to_sentinel_event(
            {
                "type": "crew_kickoff_started",
                "crew_name": "RC",
                "timestamp": "2026-06-01T12:00:00+00:00",
            }
        )
        assert e.timestamp.year == 2026

    def test_missing_timestamp_uses_now(self, adapter):
        before = datetime.now(UTC)
        e = adapter.to_sentinel_event({"type": "crew_kickoff_started", "crew_name": "RC"})
        after = datetime.now(UTC)
        assert before <= e.timestamp <= after

    def test_invalid_timestamp_falls_back_to_now(self, adapter):
        before = datetime.now(UTC)
        e = adapter.to_sentinel_event(
            {"type": "crew_kickoff_started", "crew_name": "RC", "timestamp": "not-a-date"}
        )
        after = datetime.now(UTC)
        assert before <= e.timestamp <= after


# ---------------------------------------------------------------------------
# drain() and flush_into()
# ---------------------------------------------------------------------------


class TestDrainFlush:
    def _push(self, adapter, *types):
        for t in types:
            adapter._buffer_event(
                adapter.to_sentinel_event(
                    {
                        "type": t,
                        "crew_name": "RC",
                        "agent_role": "R",
                        "task_description": "x",
                        "tool_name": "T",
                    }
                )
            )

    def test_drain_returns_buffered_events(self, adapter):
        self._push(adapter, "crew_kickoff_started", "agent_execution_started")
        events = adapter.drain()
        assert len(events) == 2

    def test_drain_clears_buffer(self, adapter):
        self._push(adapter, "crew_kickoff_started")
        adapter.drain()
        assert adapter.drain() == []

    def test_drain_empty_buffer_returns_empty_list(self, adapter):
        assert adapter.drain() == []

    def test_drain_returns_correct_types(self, adapter):
        self._push(adapter, "crew_kickoff_started", "task_failed")
        events = adapter.drain()
        assert events[0].event_type == "crew_kickoff_started"
        assert events[1].event_type == "task_failed"

    def test_flush_into_ingests_events(self, adapter):
        self._push(adapter, "agent_execution_error")
        sentinel = Sentinel()
        adapter.flush_into(sentinel)
        assert len(sentinel._events) == 1

    def test_flush_into_clears_buffer(self, adapter):
        self._push(adapter, "crew_kickoff_started")
        sentinel = Sentinel()
        adapter.flush_into(sentinel)
        assert adapter.drain() == []

    def test_flush_into_empty_buffer_is_a_noop(self, adapter):
        sentinel = Sentinel()
        adapter.flush_into(sentinel)
        assert sentinel._events == []

    def test_multiple_flush_calls_accumulate_in_sentinel(self, adapter):
        sentinel = Sentinel()
        self._push(adapter, "crew_kickoff_started")
        adapter.flush_into(sentinel)
        self._push(adapter, "crew_kickoff_completed")
        adapter.flush_into(sentinel)
        # Second ingest replaces first (Sentinel.ingest replaces buffer)
        assert len(sentinel._events) == 1


# ---------------------------------------------------------------------------
# Buffer thread safety
# ---------------------------------------------------------------------------


class TestBufferThreadSafety:
    def test_concurrent_buffer_event_does_not_corrupt(self, adapter):
        errors = []

        def worker(i):
            try:
                adapter._buffer_event(
                    adapter.to_sentinel_event(
                        {"type": "crew_kickoff_started", "crew_name": f"crew-{i}"}
                    )
                )
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=worker, args=(i,)) for i in range(50)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert errors == []
        assert len(adapter.drain()) == 50

    def test_concurrent_drain_and_buffer_event(self, adapter):
        errors = []

        def bufferer(i):
            try:
                adapter._buffer_event(
                    adapter.to_sentinel_event(
                        {"type": "agent_execution_started", "agent_role": f"agent-{i}"}
                    )
                )
            except Exception as e:
                errors.append(e)

        def drainer():
            try:
                adapter.drain()
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=bufferer, args=(i,)) for i in range(25)] + [
            threading.Thread(target=drainer) for _ in range(25)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert errors == []


# ---------------------------------------------------------------------------
# setup() — mocked event bus
# ---------------------------------------------------------------------------


class TestSetup:
    def test_setup_registers_handlers_with_bus(self):
        adapter = _make_adapter()
        mock_bus = MagicMock()
        mock_module = MagicMock()

        with patch.dict(
            "sys.modules",
            {
                "crewai": MagicMock(),
                "crewai.utilities": MagicMock(),
                "crewai.utilities.events": mock_module,
            },
        ):
            adapter.setup(mock_bus)

        # bus.on() called once per registered event type (12 total)
        assert mock_bus.on.call_count == 12

    def test_setup_uses_default_bus_when_none_given(self):
        adapter = _make_adapter()
        mock_module = MagicMock()
        mock_default_bus = MagicMock()
        mock_module.crewai_event_bus = mock_default_bus

        with patch.dict(
            "sys.modules",
            {
                "crewai": MagicMock(),
                "crewai.utilities": MagicMock(),
                "crewai.utilities.events": mock_module,
            },
        ):
            adapter.setup()  # no bus argument

        assert mock_default_bus.on.call_count == 12


# ---------------------------------------------------------------------------
# Protocol conformance
# ---------------------------------------------------------------------------


class TestProtocolConformance:
    def test_satisfies_sentinel_adapter_protocol(self, adapter):
        from agentcop import SentinelAdapter

        assert isinstance(adapter, SentinelAdapter)

    def test_source_system_attribute(self, adapter):
        assert adapter.source_system == "crewai"

    def test_to_sentinel_event_callable(self, adapter):
        assert callable(adapter.to_sentinel_event)


# ---------------------------------------------------------------------------
# Integration with Sentinel
# ---------------------------------------------------------------------------


class TestSentinelIntegration:
    def test_all_error_events_collected(self, adapter):
        sentinel = Sentinel()

        for raw in [
            {"type": "crew_kickoff_failed", "crew_name": "RC", "error": "timeout"},
            {"type": "agent_execution_error", "agent_role": "Researcher", "error": "API fail"},
            {"type": "task_failed", "task_description": "research", "error": "no results"},
            {
                "type": "tool_usage_error",
                "tool_name": "WebSearch",
                "agent_role": "R",
                "error": "403",
            },
        ]:
            adapter._buffer_event(adapter.to_sentinel_event(raw))

        adapter.flush_into(sentinel)
        # All four are ERROR severity — a custom detector would fire on each
        assert len(sentinel._events) == 4
        assert all(e.severity == "ERROR" for e in sentinel._events)

    def test_custom_detector_fires_on_agent_error(self, adapter):
        from agentcop import ViolationRecord

        def detect_agent_failure(event):
            if event.event_type == "agent_execution_error":
                return ViolationRecord(
                    violation_type="agent_execution_failed",
                    severity="ERROR",
                    source_event_id=event.event_id,
                    trace_id=event.trace_id,
                    detail={
                        "agent_role": event.attributes.get("agent_role"),
                        "error": event.attributes.get("error"),
                    },
                )

        adapter._buffer_event(
            adapter.to_sentinel_event(
                {
                    "type": "agent_execution_error",
                    "agent_role": "Researcher",
                    "error": "OpenAI API timeout",
                }
            )
        )

        sentinel = Sentinel(detectors=[detect_agent_failure])
        adapter.flush_into(sentinel)
        violations = sentinel.detect_violations()

        assert len(violations) == 1
        assert violations[0].violation_type == "agent_execution_failed"
        assert violations[0].detail["agent_role"] == "Researcher"
        assert "timeout" in violations[0].detail["error"]

    def test_custom_detector_fires_on_task_failure(self, adapter):
        from agentcop import ViolationRecord

        def detect_task_failure(event):
            if event.event_type == "task_failed":
                return ViolationRecord(
                    violation_type="task_execution_failed",
                    severity="ERROR",
                    source_event_id=event.event_id,
                    trace_id=event.trace_id,
                    detail={
                        "task": event.attributes.get("task_description"),
                        "error": event.attributes.get("error"),
                    },
                )

        adapter._buffer_event(
            adapter.to_sentinel_event(
                {
                    "type": "task_failed",
                    "task_description": "Summarize quarterly report",
                    "error": "context length exceeded",
                }
            )
        )

        sentinel = Sentinel(detectors=[detect_task_failure])
        adapter.flush_into(sentinel)
        violations = sentinel.detect_violations()

        assert len(violations) == 1
        assert violations[0].detail["task"] == "Summarize quarterly report"

    def test_trace_id_consistent_across_run(self):
        a = _make_adapter(run_id="session-xyz")
        events_raw = [
            {"type": "crew_kickoff_started", "crew_name": "RC"},
            {"type": "agent_execution_started", "agent_role": "Researcher"},
            {"type": "task_completed", "task_description": "research", "output": "done"},
            {"type": "crew_kickoff_completed", "crew_name": "RC", "output": "final"},
        ]
        events = [a.to_sentinel_event(r) for r in events_raw]
        assert all(e.trace_id == "session-xyz" for e in events)

    def test_no_violations_for_non_error_events(self, adapter):
        sentinel = Sentinel()  # default detectors only — none match crewai types
        for raw in [
            {"type": "crew_kickoff_started", "crew_name": "RC"},
            {"type": "agent_execution_started", "agent_role": "R"},
            {"type": "task_started", "task_description": "research"},
            {"type": "tool_usage_started", "tool_name": "T", "agent_role": "R"},
        ]:
            adapter._buffer_event(adapter.to_sentinel_event(raw))
        adapter.flush_into(sentinel)
        assert sentinel.detect_violations() == []
