"""
Tests for uncovered code paths in StackExchangeSearchEngine.

Targets:
- _get_full_content: content building, HTML cleaning, tags, answer fetching
- _fetch_top_answers: success, error_id, rate limit, exception
- get_question: success, rate limit, exception, empty items
- get_answers: success, rate limit, exception
- search_by_tag: tag switching, cleanup on exception
"""

from unittest.mock import Mock, patch

import pytest

from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
    StackExchangeSearchEngine,
)
from local_deep_research.web_search_engines.rate_limiting import RateLimitError

MODULE = (
    "local_deep_research.web_search_engines.engines.search_engine_stackexchange"
)


@pytest.fixture
def engine():
    eng = StackExchangeSearchEngine(max_results=5, site="stackoverflow")
    eng.rate_tracker = Mock()
    eng.rate_tracker.apply_rate_limit.return_value = 0
    return eng


def _mock_response(status_code=200, json_data=None):
    resp = Mock()
    resp.status_code = status_code
    resp.json.return_value = json_data or {}
    resp.raise_for_status = Mock()
    if status_code >= 400:
        resp.raise_for_status.side_effect = Exception(f"HTTP {status_code}")
    return resp


# ---------------------------------------------------------------------------
# _get_full_content
# ---------------------------------------------------------------------------


class TestGetFullContent:
    def test_builds_content_from_raw(self, engine):
        """Builds content string with question body, tags, and answers."""
        items = [
            {
                "title": "How to sort a list",
                "tags": ["python", "sorting"],
                "_raw": {
                    "body": "<p>I want to sort a <b>list</b> in Python.</p>",
                    "question_id": 123,
                },
            }
        ]

        with patch.object(
            engine,
            "_fetch_top_answers",
            return_value=[
                {
                    "body": "<p>Use sorted()</p>",
                    "score": 10,
                    "is_accepted": True,
                }
            ],
        ):
            results = engine._get_full_content(items)

        assert len(results) == 1
        content = results[0]["content"]
        assert "Question: How to sort a list" in content
        assert "Tags: python, sorting" in content
        assert "sort a list in Python" in content
        assert "<p>" not in content  # HTML stripped
        assert "--- Top Answers (1) ---" in content
        assert "[Score: 10, Accepted]" in content
        assert "Use sorted()" in content
        assert "_raw" not in results[0]

    def test_no_question_id_skips_answers(self, engine):
        """Content omits answers when question_id is absent."""
        items = [
            {
                "title": "Q",
                "tags": [],
                "_raw": {
                    "body": "<p>Body text.</p>",
                },
            }
        ]

        results = engine._get_full_content(items)
        assert "Top Answers" not in results[0].get("content", "")

    def test_without_raw(self, engine):
        """Items without _raw get no content added."""
        items = [{"title": "Q", "snippet": "existing"}]

        results = engine._get_full_content(items)
        assert len(results) == 1
        assert "content" not in results[0]


# ---------------------------------------------------------------------------
# _fetch_top_answers
# ---------------------------------------------------------------------------


class TestFetchTopAnswers:
    @patch(f"{MODULE}.safe_get")
    def test_success(self, mock_get, engine):
        """Returns answer items on success."""
        answers = [
            {"body": "<p>Answer 1</p>", "score": 5, "is_accepted": True},
            {"body": "<p>Answer 2</p>", "score": 3, "is_accepted": False},
        ]
        mock_get.return_value = _mock_response(
            200, {"items": answers, "quota_remaining": 100}
        )

        result = engine._fetch_top_answers(123, max_answers=2)

        assert len(result) == 2
        assert result[0]["score"] == 5

    @patch(f"{MODULE}.safe_get")
    def test_error_id_returns_empty(self, mock_get, engine):
        """Returns empty list when API returns error_id (e.g. quota exhausted)."""
        mock_get.return_value = _mock_response(
            200,
            {"items": [], "error_id": 502, "error_message": "Quota Exhausted"},
        )

        result = engine._fetch_top_answers(123)
        assert result == []

    @patch(f"{MODULE}.safe_get")
    def test_rate_limit_propagates(self, mock_get, engine):
        """RateLimitError is re-raised, not swallowed."""
        mock_get.return_value = _mock_response(429)
        engine._raise_if_rate_limit = Mock(side_effect=RateLimitError("429"))

        with pytest.raises(RateLimitError):
            engine._fetch_top_answers(123)

    @patch(f"{MODULE}.safe_get")
    def test_value_error_propagates(self, mock_get, engine):
        """ValueError from safe_get (SSRF, oversize) is re-raised."""
        mock_get.side_effect = ValueError("SSRF blocked")

        with pytest.raises(ValueError):
            engine._fetch_top_answers(123)

    @patch(f"{MODULE}.safe_get")
    def test_network_error_returns_empty(self, mock_get, engine):
        """Network errors return empty list gracefully."""
        mock_get.side_effect = ConnectionError("fail")

        result = engine._fetch_top_answers(123)
        assert result == []

    @patch(f"{MODULE}.safe_get")
    def test_low_quota_warning(self, mock_get, engine):
        """Logs warning when quota is low."""
        mock_get.return_value = _mock_response(
            200, {"items": [], "quota_remaining": 5}
        )

        result = engine._fetch_top_answers(123)
        assert result == []


# ---------------------------------------------------------------------------
# get_question
# ---------------------------------------------------------------------------


class TestGetQuestion:
    @patch(f"{MODULE}.safe_get")
    def test_success(self, mock_get, engine):
        """Returns question dict on success."""
        question = {"question_id": 12345, "title": "Test"}
        mock_get.return_value = _mock_response(
            200, {"items": [question], "backoff": None}
        )

        result = engine.get_question(12345)

        assert result["question_id"] == 12345

    @patch(f"{MODULE}.safe_get")
    def test_empty_items(self, mock_get, engine):
        """Returns None when no items."""
        mock_get.return_value = _mock_response(200, {"items": []})

        result = engine.get_question(99999)
        assert result is None

    @patch(f"{MODULE}.safe_get")
    def test_rate_limit_propagates(self, mock_get, engine):
        """RateLimitError is re-raised."""
        mock_get.return_value = _mock_response(429)
        engine._raise_if_rate_limit = Mock(side_effect=RateLimitError("429"))

        with pytest.raises(RateLimitError):
            engine.get_question(12345)

    @patch(f"{MODULE}.safe_get")
    def test_exception_returns_none(self, mock_get, engine):
        """Other exceptions return None."""
        mock_get.side_effect = ConnectionError("fail")

        result = engine.get_question(12345)
        assert result is None


# ---------------------------------------------------------------------------
# get_answers
# ---------------------------------------------------------------------------


class TestGetAnswers:
    @patch(f"{MODULE}.safe_get")
    def test_success(self, mock_get, engine):
        """Returns list of answers."""
        answers = [
            {"answer_id": 1, "body": "Answer 1"},
            {"answer_id": 2, "body": "Answer 2"},
        ]
        mock_get.return_value = _mock_response(200, {"items": answers})

        result = engine.get_answers(12345)

        assert len(result) == 2
        assert result[0]["answer_id"] == 1

    @patch(f"{MODULE}.safe_get")
    def test_rate_limit_propagates(self, mock_get, engine):
        """RateLimitError is re-raised."""
        mock_get.return_value = _mock_response(429)
        engine._raise_if_rate_limit = Mock(side_effect=RateLimitError("429"))

        with pytest.raises(RateLimitError):
            engine.get_answers(12345)

    @patch(f"{MODULE}.safe_get")
    def test_exception_returns_empty(self, mock_get, engine):
        """Other exceptions return empty list."""
        mock_get.side_effect = ConnectionError("fail")

        result = engine.get_answers(12345)
        assert result == []


# ---------------------------------------------------------------------------
# search_by_tag
# ---------------------------------------------------------------------------


class TestSearchByTag:
    def test_sets_tag_and_restores(self, engine):
        """Temporarily sets tagged filter and restores."""
        engine.tagged = None
        with patch.object(engine, "run", return_value=[]) as mock_run:
            engine.search_by_tag("python", "sorting")

        mock_run.assert_called_once_with("sorting")
        assert engine.tagged is None

    def test_restores_on_exception(self, engine):
        """Tagged is restored even on exception."""
        engine.tagged = "original"
        with patch.object(engine, "run", side_effect=Exception("fail")):
            with pytest.raises(Exception, match="fail"):
                engine.search_by_tag("python")

        assert engine.tagged == "original"
