"""
Coverage tests for GooglePSESearchEngine focusing on logic paths
not exercised by the existing test_search_engine_google_pse.py.

Covers:
- _make_request retry logic: first failure then success, all retries fail
  (with and without stored exception), and every rate-limit pattern
  (dailyLimitExceeded, rateLimitExceeded, 403, generic "limit"/"quota")
- _respect_rate_limit: sleep when within interval, no sleep when enough time
- _get_previews pagination: multiple pages of results, items without URL skipped
- Init: API key and engine ID resolved from settings_snapshot
"""

import time
from unittest.mock import Mock, call, patch

import pytest
from requests.exceptions import RequestException

from local_deep_research.web_search_engines.engines.search_engine_google_pse import (
    GooglePSESearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError


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


def _make_engine(**overrides):
    """Build a GooglePSESearchEngine with validation disabled."""
    defaults = dict(
        api_key="test-key",
        search_engine_id="test-id",
        max_retries=3,
        retry_delay=0.01,  # tiny delay so tests run fast
    )
    defaults.update(overrides)
    with patch.object(GooglePSESearchEngine, "_validate_connection"):
        return GooglePSESearchEngine(**defaults)


def _ok_response(json_body=None):
    """Return a mock response that behaves like a successful requests.Response."""
    resp = Mock()
    resp.json.return_value = json_body or {}
    resp.raise_for_status = Mock()
    return resp


# ---------------------------------------------------------------------------
# _make_request — retry: first failure then success
# ---------------------------------------------------------------------------


class TestMakeRequestRetryThenSuccess:
    """_make_request should retry on transient errors and succeed."""

    def test_first_request_fails_second_succeeds(self):
        engine = _make_engine(max_retries=3)
        ok = _ok_response({"items": [{"title": "ok"}]})

        with (
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get",
                side_effect=[RequestException("timeout"), ok],
            ),
            patch("time.sleep"),
        ):
            result = engine._make_request("query")

        assert result == {"items": [{"title": "ok"}]}

    def test_two_failures_then_success(self):
        engine = _make_engine(max_retries=3)
        ok = _ok_response({"searchInformation": {"totalResults": "5"}})

        with (
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get",
                side_effect=[
                    RequestException("err1"),
                    Exception("err2"),
                    ok,
                ],
            ),
            patch("time.sleep"),
        ):
            result = engine._make_request("query")

        assert result == {"searchInformation": {"totalResults": "5"}}


# ---------------------------------------------------------------------------
# _make_request — all retries fail
# ---------------------------------------------------------------------------


class TestMakeRequestAllRetriesFail:
    """_make_request should raise after exhausting retries."""

    def test_all_retries_fail_with_exception(self):
        engine = _make_engine(max_retries=2)

        with (
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get",
                side_effect=RequestException("server error"),
            ),
            patch("time.sleep"),
        ):
            with pytest.raises(
                RequestException, match="Failed to get response.*server error"
            ):
                engine._make_request("query")

    def test_all_retries_fail_generic_exception_stored(self):
        """When the last exception is a generic Exception (not RequestException)."""
        engine = _make_engine(max_retries=2)

        with (
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get",
                side_effect=Exception("unexpected"),
            ),
            patch("time.sleep"),
        ):
            with pytest.raises(
                RequestException, match="Failed to get response.*unexpected"
            ):
                engine._make_request("query")


# ---------------------------------------------------------------------------
# _make_request — rate limit patterns (RequestException branch)
# ---------------------------------------------------------------------------


class TestMakeRequestRateLimitPatterns:
    """Each rate-limit pattern in a RequestException must raise RateLimitError."""

    @pytest.mark.parametrize(
        "error_message",
        [
            "dailyLimitExceeded: quota used up",
            "rateLimitExceeded for this key",
            "HTTP 429 Too Many Requests",
            "HTTP 403 Forbidden",
            "quotaExceeded: API quota",
            "Request failed with quota issue",
        ],
        ids=[
            "dailyLimitExceeded",
            "rateLimitExceeded",
            "429",
            "403",
            "quotaExceeded",
            "quota_lowercase",
        ],
    )
    def test_request_exception_rate_limit(self, error_message):
        engine = _make_engine(max_retries=1)

        with (
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get",
                side_effect=RequestException(error_message),
            ),
            patch("time.sleep"),
        ):
            with pytest.raises(RateLimitError):
                engine._make_request("query")


# ---------------------------------------------------------------------------
# _make_request — rate limit patterns (generic Exception branch)
# ---------------------------------------------------------------------------


class TestMakeRequestGenericExceptionRateLimit:
    """Generic exceptions containing 'limit' or 'quota' raise RateLimitError."""

    @pytest.mark.parametrize(
        "error_message",
        [
            "rate limit reached",
            "API quota exceeded",
            "daily limit hit",
            "Quota for this project",
        ],
        ids=[
            "limit_lowercase",
            "quota_lowercase",
            "limit_word",
            "quota_capitalized",
        ],
    )
    def test_generic_exception_rate_limit(self, error_message):
        engine = _make_engine(max_retries=1)

        with (
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get",
                side_effect=Exception(error_message),
            ),
            patch("time.sleep"),
        ):
            with pytest.raises(RateLimitError):
                engine._make_request("query")

    def test_generic_exception_without_rate_pattern_retries(self):
        """A generic exception without quota/limit keywords should exhaust retries."""
        engine = _make_engine(max_retries=2)

        with (
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_google_pse.safe_get",
                side_effect=Exception("something else broke"),
            ),
            patch("time.sleep"),
        ):
            with pytest.raises(
                RequestException, match="Failed to get response"
            ):
                engine._make_request("query")


# ---------------------------------------------------------------------------
# _respect_rate_limit
# ---------------------------------------------------------------------------


class TestRespectRateLimit:
    """Tests for the _respect_rate_limit helper."""

    def test_sleeps_when_within_interval(self):
        engine = _make_engine()
        engine.last_request_time = time.time()  # just requested

        with patch("time.sleep") as mock_sleep:
            engine._respect_rate_limit()
            mock_sleep.assert_called_once()
            slept = mock_sleep.call_args[0][0]
            assert 0 < slept <= engine.min_request_interval

    def test_no_sleep_when_enough_time_elapsed(self):
        engine = _make_engine()
        engine.last_request_time = time.time() - 10  # 10 seconds ago

        with patch("time.sleep") as mock_sleep:
            engine._respect_rate_limit()
            mock_sleep.assert_not_called()


# ---------------------------------------------------------------------------
# _get_previews — pagination across multiple pages
# ---------------------------------------------------------------------------


class TestGetPreviewsPagination:
    """_get_previews should paginate when max_results > 10."""

    def test_multiple_pages(self):
        engine = _make_engine(max_results=15)

        page1_items = [
            {
                "title": f"R{i}",
                "snippet": f"S{i}",
                "link": f"https://example.com/{i}",
            }
            for i in range(10)
        ]
        page2_items = [
            {
                "title": f"R{i}",
                "snippet": f"S{i}",
                "link": f"https://example.com/{i}",
            }
            for i in range(10, 15)
        ]

        mock_request = Mock(
            side_effect=[
                {"items": page1_items},
                {"items": page2_items},
            ],
        )
        engine._make_request = mock_request
        previews = engine._get_previews("query")

        assert len(previews) == 15
        # Verify pagination indices: first call start=1, second start=11
        calls = mock_request.call_args_list
        assert calls[0] == call("query", 1)
        assert calls[1] == call("query", 11)

    def test_items_without_url_skipped_across_pages(self):
        """Items missing 'link' are skipped; pagination continues to fill quota."""
        engine = _make_engine(max_results=3)

        page1 = {
            "items": [
                {"title": "A", "snippet": "a", "link": "https://a.com"},
                {"title": "NoURL", "snippet": "x"},  # no link
                {"title": "B", "snippet": "b", "link": "https://b.com"},
            ]
        }
        page2 = {
            "items": [
                {"title": "C", "snippet": "c", "link": "https://c.com"},
            ]
        }

        with patch.object(
            engine,
            "_make_request",
            side_effect=[page1, page2],
        ):
            previews = engine._get_previews("query")

        assert len(previews) == 3
        assert previews[0]["title"] == "A"
        assert previews[1]["title"] == "B"
        assert previews[2]["title"] == "C"

    def test_stops_when_no_items_key(self):
        """Pagination stops when a response has no 'items' key."""
        engine = _make_engine(max_results=20)

        page1 = {
            "items": [
                {"title": "R1", "snippet": "s", "link": "https://r1.com"},
            ]
        }
        page2 = {"searchInformation": {"totalResults": "1"}}  # no items

        with patch.object(
            engine,
            "_make_request",
            side_effect=[page1, page2],
        ):
            previews = engine._get_previews("query")

        assert len(previews) == 1


# ---------------------------------------------------------------------------
# Init — API key and engine ID resolved from settings_snapshot
# ---------------------------------------------------------------------------


class TestInitFromSettingsSnapshot:
    """API key and engine ID can come from the settings snapshot."""

    def test_api_key_from_snapshot(self):
        snapshot = {
            "search.engine.web.google_pse.api_key": "snap-key",
            "search.engine.web.google_pse.engine_id": "snap-id",
        }

        def _fake_get(key, default=None, settings_snapshot=None):
            return (settings_snapshot or {}).get(key, default)

        with patch.object(GooglePSESearchEngine, "_validate_connection"):
            with patch(
                "local_deep_research.config.thread_settings.get_setting_from_snapshot",
                side_effect=_fake_get,
            ):
                engine = GooglePSESearchEngine(settings_snapshot=snapshot)

        assert engine.api_key == "snap-key"
        assert engine.search_engine_id == "snap-id"

    def test_explicit_params_override_snapshot(self):
        snapshot = {
            "search.engine.web.google_pse.api_key": "snap-key",
            "search.engine.web.google_pse.engine_id": "snap-id",
        }

        with patch.object(GooglePSESearchEngine, "_validate_connection"):
            engine = GooglePSESearchEngine(
                api_key="explicit-key",
                search_engine_id="explicit-id",
                settings_snapshot=snapshot,
            )

        assert engine.api_key == "explicit-key"
        assert engine.search_engine_id == "explicit-id"

    def test_no_settings_context_error_handled(self):
        """NoSettingsContextError is caught gracefully; explicit params still work."""
        from local_deep_research.config.thread_settings import (
            NoSettingsContextError,
        )

        def _raise_no_ctx(*args, **kwargs):
            raise NoSettingsContextError("no context")

        with patch.object(GooglePSESearchEngine, "_validate_connection"):
            with patch(
                "local_deep_research.config.thread_settings.get_setting_from_snapshot",
                side_effect=_raise_no_ctx,
            ):
                engine = GooglePSESearchEngine(
                    api_key="key-fallback",
                    search_engine_id="id-fallback",
                )

        assert engine.api_key == "key-fallback"
        assert engine.search_engine_id == "id-fallback"
