"""Tests for metrics search_tracker module."""

from unittest.mock import MagicMock, Mock, patch


from local_deep_research.metrics.search_tracker import (
    SearchTracker,
    get_search_tracker,
)


class TestSearchTrackerInit:
    """Tests for SearchTracker initialization."""

    def test_initializes_with_default_database(self):
        """Should initialize with default MetricsDatabase."""
        tracker = SearchTracker()
        assert tracker.db is not None

    def test_initializes_with_custom_database(self):
        """Should use provided database."""
        mock_db = MagicMock()
        tracker = SearchTracker(db=mock_db)
        assert tracker.db is mock_db


class TestSearchTrackerRecordSearch:
    """Tests for record_search static method."""

    def test_skips_when_no_context(self):
        """Should skip recording when no search context."""
        with patch(
            "local_deep_research.metrics.search_tracker.get_search_context",
            return_value=None,
        ):
            # Should not raise and should return without recording
            SearchTracker.record_search(
                engine_name="google",
                query="test query",
                results_count=10,
            )

    def test_extracts_context_correctly(self, mock_search_context):
        """Should extract research context from thread context."""
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_writer = MagicMock()
        mock_writer.get_session.return_value = mock_cm

        with patch(
            "local_deep_research.metrics.search_tracker.get_search_context",
            return_value=mock_search_context,
        ):
            with patch(
                "local_deep_research.database.thread_metrics.metrics_writer",
                mock_writer,
            ):
                SearchTracker.record_search(
                    engine_name="brave",
                    query="test query",
                    results_count=5,
                    response_time_ms=150,
                )

                # Should have added a SearchCall record
                mock_session.add.assert_called_once()

    def test_handles_integer_research_id(self, mock_search_context):
        """Should convert integer research_id to string."""
        mock_search_context["research_id"] = 12345  # Integer
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_writer = MagicMock()
        mock_writer.get_session.return_value = mock_cm

        with patch(
            "local_deep_research.metrics.search_tracker.get_search_context",
            return_value=mock_search_context,
        ):
            with patch(
                "local_deep_research.database.thread_metrics.metrics_writer",
                mock_writer,
            ):
                SearchTracker.record_search(
                    engine_name="duckduckgo",
                    query="test",
                )

                # Should have converted to string
                call_args = mock_session.add.call_args[0][0]
                assert call_args.research_id == "12345"

    def test_skips_when_no_username(self, mock_search_context):
        """Should skip recording when no username in context."""
        mock_search_context["username"] = None

        with patch(
            "local_deep_research.metrics.search_tracker.get_search_context",
            return_value=mock_search_context,
        ):
            # Should not raise
            SearchTracker.record_search(
                engine_name="google",
                query="test",
            )

    def test_skips_when_no_password(self, mock_search_context):
        """Should skip recording when no password in context."""
        mock_search_context["user_password"] = None

        with patch(
            "local_deep_research.metrics.search_tracker.get_search_context",
            return_value=mock_search_context,
        ):
            # Should not raise
            SearchTracker.record_search(
                engine_name="google",
                query="test",
            )

    def test_sets_success_status_for_successful_search(
        self, mock_search_context
    ):
        """Should set success_status to 'success' when success=True."""
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_writer = MagicMock()
        mock_writer.get_session.return_value = mock_cm

        with patch(
            "local_deep_research.metrics.search_tracker.get_search_context",
            return_value=mock_search_context,
        ):
            with patch(
                "local_deep_research.database.thread_metrics.metrics_writer",
                mock_writer,
            ):
                SearchTracker.record_search(
                    engine_name="google",
                    query="test",
                    success=True,
                )

                call_args = mock_session.add.call_args[0][0]
                assert call_args.success_status == "success"

    def test_sets_error_status_for_failed_search(self, mock_search_context):
        """Should set success_status to 'error' when success=False."""
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_writer = MagicMock()
        mock_writer.get_session.return_value = mock_cm

        with patch(
            "local_deep_research.metrics.search_tracker.get_search_context",
            return_value=mock_search_context,
        ):
            with patch(
                "local_deep_research.database.thread_metrics.metrics_writer",
                mock_writer,
            ):
                SearchTracker.record_search(
                    engine_name="google",
                    query="test",
                    success=False,
                    error_message="Connection timeout",
                )

                call_args = mock_session.add.call_args[0][0]
                assert call_args.success_status == "error"
                assert call_args.error_message == "Connection timeout"


class TestSearchTrackerGetSearchMetrics:
    """Tests for get_search_metrics method."""

    def test_applies_time_filter(self):
        """Should apply time filter to query."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        # Setup query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        tracker.get_search_metrics(period="7d")

        # Should have called filter (for time)
        assert mock_query.filter.called

    def test_applies_mode_filter(self):
        """Should apply research mode filter."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        # Setup query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        tracker.get_search_metrics(research_mode="quick")

        # Should have applied filters
        assert mock_query.filter.called

    def test_returns_engine_stats_structure(self):
        """Should return search_engine_stats and recent_calls."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        # Setup query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.limit.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        result = tracker.get_search_metrics()

        assert "search_engine_stats" in result
        assert "recent_calls" in result
        assert isinstance(result["search_engine_stats"], list)
        assert isinstance(result["recent_calls"], list)

    def test_handles_database_error(self):
        """Should handle database errors gracefully."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        mock_session.query.side_effect = Exception("DB Error")

        tracker = SearchTracker(db=mock_db)
        result = tracker.get_search_metrics()

        assert result == {"search_engine_stats": [], "recent_calls": []}


class TestSearchTrackerGetResearchSearchMetrics:
    """Tests for get_research_search_metrics method."""

    def test_filters_by_research_id(self):
        """Should filter results by research_id."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        # Setup query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        tracker.get_research_search_metrics("test-uuid-123")

        # Should have called filter with research_id
        assert mock_query.filter.called

    def test_passes_username_to_session(self):
        """Should pass username and password to db.get_session()."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        tracker.get_research_search_metrics(
            "test-uuid", username="alice", password="secret"
        )

        mock_db.get_session.assert_called_once_with(
            username="alice", password="secret"
        )

    def test_returns_metrics_structure(self):
        """Should return expected metrics structure."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        # Setup query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.group_by.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        result = tracker.get_research_search_metrics("test-uuid")

        assert "total_searches" in result
        assert "total_results" in result
        assert "avg_response_time" in result
        assert "success_rate" in result
        assert "search_calls" in result
        assert "engine_stats" in result


class TestSearchTrackerGetSearchTimeSeries:
    """Tests for get_search_time_series method."""

    def test_returns_ordered_data(self):
        """Should return time series data ordered by timestamp."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        # Setup query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        result = tracker.get_search_time_series()

        assert isinstance(result, list)

    def test_applies_filters(self):
        """Should apply time and mode filters."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        # Setup query chain
        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        tracker.get_search_time_series(period="30d", research_mode="detailed")

        # Should have applied filters
        assert mock_query.filter.called

    def test_passes_username_to_session(self):
        """Should pass username and password to db.get_session()."""
        mock_db = MagicMock()
        mock_session = MagicMock()
        mock_cm = MagicMock()
        mock_cm.__enter__ = Mock(return_value=mock_session)
        mock_cm.__exit__ = Mock(return_value=None)
        mock_db.get_session.return_value = mock_cm

        mock_query = MagicMock()
        mock_session.query.return_value = mock_query
        mock_query.filter.return_value = mock_query
        mock_query.order_by.return_value = mock_query
        mock_query.all.return_value = []

        tracker = SearchTracker(db=mock_db)
        tracker.get_search_time_series(username="bob", password="pw123")

        mock_db.get_session.assert_called_once_with(
            username="bob", password="pw123"
        )


class TestGetSearchTracker:
    """Tests for get_search_tracker factory function."""

    def test_returns_search_tracker_instance(self):
        """Should return a SearchTracker instance."""
        tracker = get_search_tracker()
        assert isinstance(tracker, SearchTracker)

    def test_returns_fresh_instance_each_call(self):
        """Should return a new instance on each call (no singleton)."""
        first = get_search_tracker()
        second = get_search_tracker()

        assert isinstance(first, SearchTracker)
        assert isinstance(second, SearchTracker)
        assert first is not second
