"""
Unit tests for the observer module.

Tests for AgentObserver protocol and implementations.
"""

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

from persuade.agents.observer import (
    AgentEvent,
    CompositeObserver,
    LoggingObserver,
    NullObserver,
)


class TestAgentEvent:
    """Tests for AgentEvent dataclass."""

    def test_create_event(self) -> None:
        """Test creating an agent event."""
        event = AgentEvent(
            event_type="phase.started",
            agent_type="MockAgent",
            run_id="abc123",
            phase="plan",
        )

        assert event.event_type == "phase.started"
        assert event.agent_type == "MockAgent"
        assert event.run_id == "abc123"
        assert event.phase == "plan"
        assert event.duration_ms is None
        assert event.error is None

    def test_event_with_all_fields(self) -> None:
        """Test event with all optional fields."""
        event = AgentEvent(
            event_type="phase.completed",
            agent_type="TestAgent",
            run_id="xyz789",
            phase="execute",
            duration_ms=150.5,
            data={"key": "value"},
            error="Something failed",
        )

        assert event.duration_ms == 150.5
        assert event.data == {"key": "value"}
        assert event.error == "Something failed"

    def test_event_timestamp_default(self) -> None:
        """Test that timestamp is set automatically."""
        before = datetime.now(UTC).isoformat()
        event = AgentEvent(
            event_type="test",
            agent_type="TestAgent",
            run_id="test123",
        )
        after = datetime.now(UTC).isoformat()

        # Timestamp should be between before and after
        assert before <= event.timestamp <= after


class TestNullObserver:
    """Tests for NullObserver (no-op implementation)."""

    def test_on_phase_start_is_noop(self) -> None:
        """Test that on_phase_start does nothing."""
        observer = NullObserver()
        # Should not raise
        observer.on_phase_start("TestAgent", "run123", "plan")

    def test_on_phase_complete_is_noop(self) -> None:
        """Test that on_phase_complete does nothing."""
        observer = NullObserver()
        # Should not raise
        observer.on_phase_complete("TestAgent", "run123", "execute", 100.0, True, None)

    def test_on_phase_complete_with_error(self) -> None:
        """Test that on_phase_complete handles error parameter."""
        observer = NullObserver()
        # Should not raise
        observer.on_phase_complete("TestAgent", "run123", "plan", 50.0, False, "Error message")


class TestLoggingObserver:
    """Tests for LoggingObserver."""

    def test_on_phase_start_logs(self) -> None:
        """Test that on_phase_start logs correctly."""
        observer = LoggingObserver()
        # Mock the logger
        observer.logger = MagicMock()

        observer.on_phase_start("IteratePRAgent", "abc123", "plan")

        observer.logger.info.assert_called_once()
        call_args = observer.logger.info.call_args
        assert "phase started" in call_args[0][0].lower()
        assert call_args[1]["agent_type"] == "IteratePRAgent"
        assert call_args[1]["run_id"] == "abc123"
        assert call_args[1]["phase"] == "plan"

    def test_on_phase_complete_success_logs(self) -> None:
        """Test that on_phase_complete logs success correctly."""
        observer = LoggingObserver()
        observer.logger = MagicMock()

        observer.on_phase_complete("TestAgent", "xyz789", "execute", 123.456, True, None)

        observer.logger.info.assert_called_once()
        call_args = observer.logger.info.call_args
        assert "phase completed" in call_args[0][0].lower()
        assert call_args[1]["success"] is True
        assert call_args[1]["duration_ms"] == 123.46  # Rounded
        assert call_args[1]["error"] is None

    def test_on_phase_complete_failure_logs(self) -> None:
        """Test that on_phase_complete logs failure correctly."""
        observer = LoggingObserver()
        observer.logger = MagicMock()

        observer.on_phase_complete(
            "TestAgent", "run123", "plan", 50.0, False, "Something went wrong"
        )

        call_args = observer.logger.info.call_args
        assert call_args[1]["success"] is False
        assert call_args[1]["error"] == "Something went wrong"


class TestCompositeObserver:
    """Tests for CompositeObserver."""

    def test_forwards_to_all_observers(self) -> None:
        """Test that events are forwarded to all observers."""
        mock1 = MagicMock()
        mock2 = MagicMock()
        composite = CompositeObserver([mock1, mock2])

        composite.on_phase_start("TestAgent", "run123", "plan")

        mock1.on_phase_start.assert_called_once_with("TestAgent", "run123", "plan")
        mock2.on_phase_start.assert_called_once_with("TestAgent", "run123", "plan")

    def test_forwards_complete_to_all(self) -> None:
        """Test that complete events are forwarded to all."""
        mock1 = MagicMock()
        mock2 = MagicMock()
        composite = CompositeObserver([mock1, mock2])

        composite.on_phase_complete("TestAgent", "run123", "execute", 100.0, True, None)

        mock1.on_phase_complete.assert_called_once_with(
            "TestAgent", "run123", "execute", 100.0, True, None
        )
        mock2.on_phase_complete.assert_called_once_with(
            "TestAgent", "run123", "execute", 100.0, True, None
        )

    def test_empty_observers_list(self) -> None:
        """Test that empty list works without error."""
        composite = CompositeObserver([])
        # Should not raise
        composite.on_phase_start("TestAgent", "run123", "plan")
        composite.on_phase_complete("TestAgent", "run123", "plan", 50.0, True, None)

    def test_mixed_observer_types(self) -> None:
        """Test with mix of real observer types."""
        null = NullObserver()
        logging = LoggingObserver()
        logging.logger = MagicMock()

        composite = CompositeObserver([null, logging])
        composite.on_phase_start("TestAgent", "abc", "plan")

        # Only logging observer should have logged
        logging.logger.info.assert_called_once()


class TestObserverProtocol:
    """Tests for AgentObserver protocol compliance."""

    def test_null_observer_is_protocol_compliant(self) -> None:
        """Test NullObserver implements protocol methods."""
        observer = NullObserver()
        assert hasattr(observer, "on_phase_start")
        assert hasattr(observer, "on_phase_complete")
        assert callable(observer.on_phase_start)
        assert callable(observer.on_phase_complete)

    def test_logging_observer_is_protocol_compliant(self) -> None:
        """Test LoggingObserver implements protocol methods."""
        observer = LoggingObserver()
        assert hasattr(observer, "on_phase_start")
        assert hasattr(observer, "on_phase_complete")
        assert callable(observer.on_phase_start)
        assert callable(observer.on_phase_complete)

    def test_composite_observer_is_protocol_compliant(self) -> None:
        """Test CompositeObserver implements protocol methods."""
        observer = CompositeObserver([])
        assert hasattr(observer, "on_phase_start")
        assert hasattr(observer, "on_phase_complete")
        assert callable(observer.on_phase_start)
        assert callable(observer.on_phase_complete)
