"""Tests for BaseSearchEngine retry logic, context management, and lifecycle.

The tenacity retry decorator path in search_engine_base.py:330-469 had zero
tests exercising actual retry behavior. These tests cover the retry flow,
rate limiting integration, and context manager lifecycle.
"""

from unittest.mock import Mock, patch


from local_deep_research.web_search_engines.rate_limiting import RateLimitError
from local_deep_research.web_search_engines.search_engine_base import (
    BaseSearchEngine,
)


class ConcreteSearchEngine(BaseSearchEngine):
    """Concrete implementation of BaseSearchEngine for testing."""

    def __init__(self, previews_func=None, **kwargs):
        super().__init__(**kwargs)
        self._previews_func = previews_func

    def _get_previews(self, query):
        if self._previews_func:
            return self._previews_func(query)
        return []

    def _get_full_content(self, relevant_items):
        return relevant_items


class TestRetryOnRateLimit:
    """Tests for retry behavior when rate limiting is enabled."""

    def test_retry_on_rate_limit_when_enabled(self):
        """With rate_tracker.enabled=True, RateLimitError triggers retry
        up to 3 attempts before exhaustion."""
        call_count = 0

        def previews_func(query):
            nonlocal call_count
            call_count += 1
            if call_count < 3:
                raise RateLimitError("rate limited")
            return [{"title": "Success", "snippet": "found it"}]

        tracker = Mock()
        tracker.enabled = True
        tracker.get_wait_time.return_value = 0.0

        engine = ConcreteSearchEngine(
            previews_func=previews_func,
            programmatic_mode=True,
            settings_snapshot={},
        )
        engine.rate_tracker = tracker

        results = engine.run("test query")

        assert call_count == 3
        assert len(results) == 1
        assert results[0]["title"] == "Success"

    def test_exhausted_retries_returns_empty_list(self):
        """All 3 attempts fail with RateLimitError -> RetryError caught -> returns []."""

        def always_fail(query):
            raise RateLimitError("always rate limited")

        tracker = Mock()
        tracker.enabled = True
        tracker.get_wait_time.return_value = 0.0

        engine = ConcreteSearchEngine(
            previews_func=always_fail,
            programmatic_mode=True,
            settings_snapshot={},
        )
        engine.rate_tracker = tracker

        results = engine.run("test query")

        assert results == []

    def test_first_attempt_success_records_outcome(self):
        """Successful first attempt calls record_outcome with success=True."""

        def success_func(query):
            return [{"title": "Result", "snippet": "text"}]

        tracker = Mock()
        tracker.enabled = True
        tracker.get_wait_time.return_value = 0.0

        engine = ConcreteSearchEngine(
            previews_func=success_func,
            programmatic_mode=True,
            settings_snapshot={},
        )
        engine.rate_tracker = tracker

        results = engine.run("test query")

        assert len(results) == 1
        # record_outcome should be called with success=True
        tracker.record_outcome.assert_called()
        call_kwargs = tracker.record_outcome.call_args
        assert call_kwargs[1].get("success") or call_kwargs[0][2] is True

    def test_rate_limit_disabled_treats_as_regular_error(self):
        """With rate_tracker.enabled=False, RateLimitError returns []
        without retry."""
        call_count = 0

        def rate_limited(query):
            nonlocal call_count
            call_count += 1
            raise RateLimitError("rate limited")

        tracker = Mock()
        tracker.enabled = False

        engine = ConcreteSearchEngine(
            previews_func=rate_limited,
            programmatic_mode=True,
            settings_snapshot={},
        )
        engine.rate_tracker = tracker

        results = engine.run("test query")

        assert results == []
        assert call_count == 1  # No retry


class TestContextManagerLifecycle:
    """Tests for __enter__/__exit__ and close() lifecycle."""

    def test_context_manager_enter_exit_lifecycle(self):
        """__enter__ returns self, __exit__ calls close()."""
        engine = ConcreteSearchEngine(
            programmatic_mode=True, settings_snapshot={}
        )

        result = engine.__enter__()
        assert result is engine

        # __exit__ should call close() and return False
        engine.session = Mock()
        exit_result = engine.__exit__(None, None, None)
        assert exit_result is False
        engine.session.close.assert_called_once()

    def test_close_handles_missing_session(self):
        """close() on engine without session attribute doesn't crash."""
        engine = ConcreteSearchEngine(
            programmatic_mode=True, settings_snapshot={}
        )
        # Don't set session attribute
        assert not hasattr(engine, "session")

        # Should not raise
        engine.close()

    def test_close_handles_none_session(self):
        """close() with session=None doesn't crash."""
        engine = ConcreteSearchEngine(
            programmatic_mode=True, settings_snapshot={}
        )
        engine.session = None

        # Should not raise
        engine.close()


class TestSearchContextPropagation:
    """Tests for research_context propagation in run()."""

    def test_search_context_set_and_cleared_in_finally(self):
        """research_context propagated via set_search_context / clear_search_context
        in run(), cleared even on exception."""

        def failing_previews(query):
            raise ValueError("search failed")

        engine = ConcreteSearchEngine(
            previews_func=failing_previews,
            programmatic_mode=False,
            settings_snapshot={},
        )
        engine.rate_tracker = Mock(enabled=False)

        context = {"research_id": "test-123"}

        with (
            patch(
                "local_deep_research.web_search_engines.search_engine_base.set_search_context"
            ) as mock_set,
            patch(
                "local_deep_research.web_search_engines.search_engine_base.clear_search_context"
            ) as mock_clear,
            patch(
                "local_deep_research.metrics.search_tracker.get_search_tracker",
                return_value=Mock(),
            ),
        ):
            results = engine.run("test query", research_context=context)

        # Context should have been set
        mock_set.assert_called_once_with(context)
        # Context should have been cleared in finally block
        mock_clear.assert_called_once()
        # Should return empty list after exception
        assert results == []
