"""
Coverage tests for SerperSearchEngine focusing on logic paths not covered
by the existing test_search_engine_serper.py.

Covers:
- _get_previews: URL parse failure, sitelinks/date/attributes preserved,
  related searches stored, people also ask stored, unmapped time_period
- Rate limit raised from RequestException path
- Init: full_search creation with include_full_content, ImportError fallback
"""

from unittest.mock import Mock, patch

import pytest
import requests

from local_deep_research.web_search_engines.engines.search_engine_serper import (
    SerperSearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError


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


def _make_engine(**kwargs):
    """Create a SerperSearchEngine with sensible defaults for testing."""
    defaults = {"api_key": "test-key"}
    defaults.update(kwargs)
    return SerperSearchEngine(**defaults)


def _mock_response(json_data, status_code=200):
    """Build a mock response object mimicking requests.Response."""
    resp = Mock()
    resp.status_code = status_code
    resp.json.return_value = json_data
    resp.raise_for_status = Mock()
    return resp


SERPER_POST = "local_deep_research.web_search_engines.engines.search_engine_serper.safe_post"


# ---------------------------------------------------------------------------
# _get_previews -- URL parse failure
# ---------------------------------------------------------------------------


class TestGetPreviewsUrlParseFailure:
    """When urlparse raises, displayed_link should fall back to empty string."""

    def test_url_parse_exception_yields_empty_displayed_link(self):
        engine = _make_engine()
        resp = _mock_response(
            {
                "organic": [
                    {
                        "title": "Bad URL result",
                        "link": "not-a-valid-url",
                        "snippet": "Some snippet",
                        "position": 1,
                    }
                ]
            }
        )

        with (
            patch(SERPER_POST, return_value=resp),
            patch(
                "local_deep_research.web_search_engines.engines.search_engine_serper.urlparse",
                side_effect=Exception("parse boom"),
            ),
        ):
            previews = engine._get_previews("test query")

        assert len(previews) == 1
        assert previews[0]["displayed_link"] == ""
        assert previews[0]["title"] == "Bad URL result"


# ---------------------------------------------------------------------------
# _get_previews -- sitelinks, date, attributes preserved
# ---------------------------------------------------------------------------


class TestGetPreviewsOptionalFields:
    """Sitelinks, date, and attributes from the organic result are preserved."""

    def test_sitelinks_date_attributes_included(self):
        organic = {
            "title": "Rich result",
            "link": "https://example.com/page",
            "snippet": "A snippet",
            "position": 1,
            "sitelinks": [{"title": "Sub", "link": "https://example.com/sub"}],
            "date": "2025-01-15",
            "attributes": {"Author": "Jane Doe"},
        }
        resp = _mock_response({"organic": [organic]})

        with patch(SERPER_POST, return_value=resp):
            engine = _make_engine()
            previews = engine._get_previews("rich query")

        p = previews[0]
        assert p["sitelinks"] == organic["sitelinks"]
        assert p["date"] == "2025-01-15"
        assert p["attributes"] == {"Author": "Jane Doe"}

    def test_optional_fields_absent_when_not_in_result(self):
        organic = {
            "title": "Plain result",
            "link": "https://example.com",
            "snippet": "Plain",
            "position": 1,
        }
        resp = _mock_response({"organic": [organic]})

        with patch(SERPER_POST, return_value=resp):
            engine = _make_engine()
            previews = engine._get_previews("plain query")

        p = previews[0]
        assert "sitelinks" not in p
        assert "date" not in p
        assert "attributes" not in p


# ---------------------------------------------------------------------------
# _get_previews -- related searches stored
# ---------------------------------------------------------------------------


class TestGetPreviewsRelatedSearches:
    """relatedSearches from API response are stored on the engine instance."""

    def test_related_searches_stored(self):
        related = [{"query": "related 1"}, {"query": "related 2"}]
        resp = _mock_response({"organic": [], "relatedSearches": related})

        with patch(SERPER_POST, return_value=resp):
            engine = _make_engine()
            engine._get_previews("query")

        assert engine._related_searches == related

    def test_related_searches_none_when_absent(self):
        resp = _mock_response({"organic": []})

        with patch(SERPER_POST, return_value=resp):
            engine = _make_engine()
            engine._get_previews("query")

        assert engine._related_searches is None


# ---------------------------------------------------------------------------
# _get_previews -- people also ask stored
# ---------------------------------------------------------------------------


class TestGetPreviewsPeopleAlsoAsk:
    """peopleAlsoAsk from API response are stored on the engine instance."""

    def test_people_also_ask_stored(self):
        paa = [{"question": "What is X?", "snippet": "X is..."}]
        resp = _mock_response({"organic": [], "peopleAlsoAsk": paa})

        with patch(SERPER_POST, return_value=resp):
            engine = _make_engine()
            engine._get_previews("query")

        assert engine._people_also_ask == paa

    def test_people_also_ask_none_when_absent(self):
        resp = _mock_response({"organic": []})

        with patch(SERPER_POST, return_value=resp):
            engine = _make_engine()
            engine._get_previews("query")

        assert engine._people_also_ask is None


# ---------------------------------------------------------------------------
# _get_previews -- unmapped time_period
# ---------------------------------------------------------------------------


class TestGetPreviewsUnmappedTimePeriod:
    """An unrecognised time_period value should not add 'tbs' to the payload."""

    def test_unmapped_time_period_omits_tbs(self):
        resp = _mock_response({"organic": []})

        with patch(SERPER_POST, return_value=resp) as mock_post:
            engine = _make_engine(time_period="decade")
            engine._get_previews("query")

        payload = mock_post.call_args[1]["json"]
        assert "tbs" not in payload


# ---------------------------------------------------------------------------
# Rate limit from RequestException
# ---------------------------------------------------------------------------


class TestRateLimitFromRequestException:
    """When a RequestException contains rate-limit text, RateLimitError is raised."""

    def test_request_exception_with_rate_limit_raises(self):
        exc = requests.exceptions.RequestException(
            "429 Too Many Requests rate limit"
        )

        with patch(SERPER_POST, side_effect=exc):
            engine = _make_engine()
            # _raise_if_rate_limit is called with the exception; if it detects
            # rate-limit language it raises RateLimitError
            with pytest.raises(RateLimitError):
                engine._get_previews("query")


# ---------------------------------------------------------------------------
# Init -- full_search creation
# ---------------------------------------------------------------------------


class TestInitFullSearchCreation:
    """When include_full_content=True the engine creates a FullSearchResults."""

    def test_full_search_created_on_include_full_content(self):
        mock_full_cls = Mock()
        mock_full_instance = Mock()
        mock_full_cls.return_value = mock_full_instance

        with patch(
            "local_deep_research.web_search_engines.engines.full_search.FullSearchResults",
            mock_full_cls,
        ):
            engine = _make_engine(include_full_content=True, llm=Mock())

        assert engine.include_full_content is True
        assert engine.full_search is mock_full_instance
        mock_full_cls.assert_called_once()
        call_kwargs = mock_full_cls.call_args[1]
        assert call_kwargs["web_search"] is None
        assert call_kwargs["language"] == "en"


# ---------------------------------------------------------------------------
# Init -- ImportError fallback
# ---------------------------------------------------------------------------


class TestInitImportErrorFallback:
    """When FullSearchResults cannot be imported, full content is disabled."""

    def test_import_error_disables_full_content(self):
        with patch.dict(
            "sys.modules",
            {
                "local_deep_research.web_search_engines.engines.full_search": None,
            },
        ):
            engine = _make_engine(include_full_content=True, llm=Mock())

        # Should have been flipped to False due to ImportError
        assert engine.include_full_content is False
