"""
Coverage tests for WikinewsSearchEngine targeting statements not hit by
the existing test_search_engine_wikinews.py.

Covers:
- __init__: time_period="all" (from_date = datetime.min); unknown time_period
  falls back to 1-year delta; language not in WIKINEWS_LANGUAGES warning + default
- _adapt_date_range_for_query: UNCLEAR branch; LLM raises (exception handler);
  adaptive_search=True but no LLM returns early
- _optimize_query_for_wikinews: empty 'query' field raises ValueError; TypeError/
  AttributeError from LLM response
- _fetch_search_results: JSON decode error retries then returns []; all MAX_RETRIES
  exhausted returns []
- _process_search_result: invalid timestamp uses current date fallback; query word
  matching filter (false positive eliminated); search_snippets_only=False uses
  full_content; publication date > to_date filters out result
- _fetch_full_content_and_pubdate: no revisions key → fallback date used;
  invalid revision timestamp → fallback date; request exception returns ("", fallback)
- _get_full_content: just returns items unchanged (already in existing tests, but
  we cover it via a different path)
- _clean_wikinews_snippet: normalise multiple spaces
"""

from datetime import datetime, timedelta, UTC
from unittest.mock import Mock, patch
import json
import requests

MODULE = "local_deep_research.web_search_engines.engines.search_engine_wikinews"

from local_deep_research.web_search_engines.engines.search_engine_wikinews import (  # noqa: E402
    WikinewsSearchEngine,
    _clean_wikinews_snippet,
)


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


def _engine(**kw):
    return WikinewsSearchEngine(**kw)


def _mock_response(status=200, json_body=None, text=""):
    resp = Mock()
    resp.status_code = status
    resp.json.return_value = json_body if json_body is not None else {}
    resp.text = text
    resp.raise_for_status = Mock()
    return resp


# ---------------------------------------------------------------------------
# __init__
# ---------------------------------------------------------------------------


class TestInitExtra:
    def test_time_period_all_sets_datetime_min(self):
        engine = _engine(time_period="all")
        assert engine.from_date.year == 1

    def test_unknown_time_period_falls_back_to_one_year(self):
        engine = _engine(time_period="UNKNOWN_PERIOD")
        expected = datetime.now(UTC) - timedelta(days=365)
        delta = abs((engine.from_date - expected).total_seconds())
        assert delta < 60

    def test_language_not_in_wikinews_defaults_to_english(self):
        """A language code that maps OK but is absent from WIKINEWS_LANGUAGES → 'en'."""
        # 'thai' maps to 'th' which is not in WIKINEWS_LANGUAGES
        engine = _engine(search_language="thai")
        assert engine.lang_code == "en"

    def test_original_date_range_stored(self):
        engine = _engine(time_period="w")
        assert engine._original_date_range == (engine.from_date, engine.to_date)


# ---------------------------------------------------------------------------
# _adapt_date_range_for_query
# ---------------------------------------------------------------------------


class TestAdaptDateRangeForQueryExtra:
    def test_unclear_keeps_original_range(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="UNCLEAR")
        engine = _engine(llm=mock_llm, time_period="m")
        original_from = engine.from_date

        engine._adapt_date_range_for_query(
            "this is a sufficiently long query to trigger llm"
        )
        assert engine.from_date == original_from

    def test_llm_raises_keeps_original_range(self):
        mock_llm = Mock()
        mock_llm.invoke.side_effect = RuntimeError("llm down")
        engine = _engine(llm=mock_llm, time_period="m")
        original_from = engine.from_date

        engine._adapt_date_range_for_query(
            "this is a sufficiently long query to trigger llm"
        )
        assert engine.from_date == original_from

    def test_no_llm_returns_early_without_change(self):
        engine = _engine(adaptive_search=True)
        original_from = engine.from_date

        engine._adapt_date_range_for_query(
            "this is a sufficiently long query check"
        )
        assert engine.from_date == original_from

    def test_always_resets_to_original_before_classifying(self):
        """Even if from_date was modified by a previous call, it's reset first."""
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content="HISTORICAL")
        engine = _engine(llm=mock_llm, time_period="w")
        engine.from_date = datetime.now(UTC)  # tamper with it

        engine._adapt_date_range_for_query(
            "what happened in the distant past history"
        )
        # After HISTORICAL classification from_date should be datetime.min
        assert engine.from_date.year == 1


# ---------------------------------------------------------------------------
# _optimize_query_for_wikinews
# ---------------------------------------------------------------------------


class TestOptimizeQueryExtra:
    def test_empty_query_field_raises_falls_back(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content='{"query": ""}')
        engine = _engine(llm=mock_llm)
        result = engine._optimize_query_for_wikinews("original query text")
        assert result == "original query text"

    def test_attribute_error_falls_back(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = Mock(content=None)
        # get_llm_response_text will try to get .content which is None → triggers error path
        engine = _engine(llm=mock_llm)
        with patch(
            f"{MODULE}.get_llm_response_text",
            side_effect=AttributeError("no attr"),
        ):
            result = engine._optimize_query_for_wikinews("original query text")
        assert result == "original query text"

    def test_type_error_falls_back(self):
        mock_llm = Mock()
        mock_llm.invoke.return_value = None  # invoking None.content → TypeError
        engine = _engine(llm=mock_llm)
        with patch(
            f"{MODULE}.get_llm_response_text",
            side_effect=TypeError("none type"),
        ):
            result = engine._optimize_query_for_wikinews("original query text")
        assert result == "original query text"


# ---------------------------------------------------------------------------
# _fetch_search_results
# ---------------------------------------------------------------------------


class TestFetchSearchResultsExtra:
    def test_json_decode_error_retries_then_returns_empty(self):
        bad_resp = Mock()
        bad_resp.raise_for_status = Mock()
        bad_resp.json.side_effect = json.JSONDecodeError("bad json", "", 0)

        with patch(f"{MODULE}.safe_get", return_value=bad_resp):
            engine = _engine()
            result = engine._fetch_search_results("test", 0)
        assert result == []

    def test_all_retries_exhausted_returns_empty(self):
        with patch(
            f"{MODULE}.safe_get",
            side_effect=requests.exceptions.RequestException("fail"),
        ):
            engine = _engine()
            result = engine._fetch_search_results("test", 0)
        assert result == []


# ---------------------------------------------------------------------------
# _process_search_result
# ---------------------------------------------------------------------------


class TestProcessSearchResultExtra:
    def test_invalid_timestamp_uses_current_date_fallback(self):
        engine = _engine(time_period="all")
        # Use to_date - small delta so pub_date is definitely within range
        pub_date = engine.to_date - timedelta(seconds=10)
        result = {
            "pageid": 1,
            "title": "Article",
            "snippet": "snippet with article",
            "timestamp": "NOT-A-VALID-ISO-DATE",  # will trigger ValueError
        }
        with patch.object(
            engine,
            "_fetch_full_content_and_pubdate",
            return_value=("article content with snippet", pub_date),
        ):
            processed = engine._process_search_result(result, "article")
        # Should not raise; fallback path used and result within range
        assert processed is not None

    def test_publication_date_after_to_date_filtered(self):
        engine = _engine(time_period="all")
        engine.to_date = datetime(
            2020, 1, 1, tzinfo=UTC
        )  # restrict to_date to past
        now = datetime.now(UTC)
        result = {
            "pageid": 1,
            "title": "Future Article",
            "snippet": "future snippet",
            "timestamp": now.isoformat() + "Z",
        }
        with patch.object(
            engine,
            "_fetch_full_content_and_pubdate",
            return_value=("future content", now),  # pub_date > to_date
        ):
            processed = engine._process_search_result(result, "future")
        assert processed is None

    def test_query_word_mismatch_filtered_out(self):
        engine = _engine(time_period="all")
        now = datetime.now(UTC)
        result = {
            "pageid": 1,
            "title": "Unrelated Article",
            "snippet": "something",
            "timestamp": now.isoformat() + "Z",
        }
        with patch.object(
            engine,
            "_fetch_full_content_and_pubdate",
            return_value=("content without the specific keyword", now),
        ):
            processed = engine._process_search_result(result, "specificquery")
        assert processed is None

    def test_search_snippets_only_false_uses_full_content(self):
        engine = _engine(time_period="all", search_snippets_only=False)
        # pub_date must be strictly before to_date (which is captured at init time)
        pub_date = engine.to_date - timedelta(seconds=10)
        result = {
            "pageid": 1,
            "title": "Good Article",
            "snippet": "<b>good</b> snippet",
            "timestamp": pub_date.isoformat(),
        }
        with patch.object(
            engine,
            "_fetch_full_content_and_pubdate",
            return_value=("full good article content", pub_date),
        ):
            processed = engine._process_search_result(result, "good")
        assert processed is not None
        assert processed["full_content"] == "full good article content"
        assert processed["content"] == "full good article content"


# ---------------------------------------------------------------------------
# _fetch_full_content_and_pubdate
# ---------------------------------------------------------------------------


class TestFetchFullContentAndPubdateExtra:
    def test_no_revisions_uses_fallback_date(self):
        resp = _mock_response(
            status=200,
            json_body={
                "query": {
                    "pages": {
                        "42": {"extract": "content here", "revisions": []}
                    }
                }
            },
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine = _engine()
            fallback = datetime(2023, 6, 1, tzinfo=UTC)
            content, pub_date = engine._fetch_full_content_and_pubdate(
                42, fallback
            )
        assert content == "content here"
        assert pub_date == fallback

    def test_invalid_revision_timestamp_uses_fallback(self):
        resp = _mock_response(
            status=200,
            json_body={
                "query": {
                    "pages": {
                        "42": {
                            "extract": "content",
                            "revisions": [{"timestamp": "NOT-A-DATE"}],
                        }
                    }
                }
            },
        )
        with patch(f"{MODULE}.safe_get", return_value=resp):
            engine = _engine()
            fallback = datetime(2023, 6, 1, tzinfo=UTC)
            content, pub_date = engine._fetch_full_content_and_pubdate(
                42, fallback
            )
        assert pub_date == fallback

    def test_request_exception_returns_empty_and_fallback(self):
        with patch(
            f"{MODULE}.safe_get",
            side_effect=requests.exceptions.RequestException("timeout"),
        ):
            engine = _engine()
            fallback = datetime(2023, 6, 1, tzinfo=UTC)
            content, pub_date = engine._fetch_full_content_and_pubdate(
                42, fallback
            )
        assert content == ""
        assert pub_date == fallback


# ---------------------------------------------------------------------------
# _clean_wikinews_snippet — extra
# ---------------------------------------------------------------------------


class TestCleanWikinewsSnippetExtra:
    def test_multiple_spaces_normalized(self):
        result = _clean_wikinews_snippet("word   another    word")
        assert result == "word another word"

    def test_html_entity_and_tag_combined(self):
        result = _clean_wikinews_snippet("<b>R&amp;D</b> report")
        assert result == "R&D report"
