"""
Tests for the Stack Exchange search engine.
Tests initialization, search functionality, and error handling.
"""

import pytest
from unittest.mock import Mock


class TestStackExchangeSearchEngineInit:
    """Tests for Stack Exchange search engine initialization."""

    def test_init_default_parameters(self):
        """Test initialization with default parameters."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine()

        assert engine.max_results == 10
        assert engine.site == "stackoverflow"
        assert engine.sort == "relevance"
        assert engine.accepted_only is False
        assert engine.has_answers is False
        assert engine.min_score is None
        assert engine.tagged is None
        assert engine.is_public is True
        assert engine.is_code is True

    def test_init_custom_parameters(self):
        """Test initialization with custom parameters."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(
            max_results=50,
            site="serverfault",
            sort="votes",
            accepted_only=True,
            has_answers=True,
            min_score=5,
            tagged="python;django",
        )

        assert engine.max_results == 50
        assert engine.site == "serverfault"
        assert engine.sort == "votes"
        assert engine.accepted_only is True
        assert engine.has_answers is True
        assert engine.min_score == 5
        assert engine.tagged == "python;django"

    def test_init_rejects_min_score_with_relevance_sort(self):
        """Test that min_score + sort='relevance' raises ValueError."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        with pytest.raises(
            ValueError, match="min_score requires a numeric sort"
        ):
            StackExchangeSearchEngine(min_score=10, sort="relevance")

    def test_init_allows_min_score_with_non_relevance_sort(self):
        """Test that min_score works with votes, creation, and activity sorts."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        for sort in ("votes", "creation", "activity"):
            engine = StackExchangeSearchEngine(min_score=5, sort=sort)
            assert engine.min_score == 5
            assert engine.sort == sort

    def test_base_url_set(self):
        """Test that API base URL is correctly set."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine()
        assert engine.base_url == "https://api.stackexchange.com/2.3"
        assert (
            engine.search_url
            == "https://api.stackexchange.com/2.3/search/advanced"
        )


class TestStackExchangeQueryBuilding:
    """Tests for Stack Exchange query parameter building."""

    def test_build_query_params_basic(self):
        """Test basic query params building."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=20)
        params = engine._build_query_params("python list comprehension")

        assert params["q"] == "python list comprehension"
        assert params["site"] == "stackoverflow"
        assert params["pagesize"] == 20
        assert params["sort"] == "relevance"

    def test_build_query_params_with_site(self):
        """Test query params with different site."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(site="unix")
        params = engine._build_query_params("bash script")

        assert params["site"] == "unix"

    def test_build_query_params_with_accepted_only(self):
        """Test query params with accepted only filter."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(accepted_only=True)
        params = engine._build_query_params("test")

        assert params["accepted"] == "True"

    def test_build_query_params_with_min_score(self):
        """Test query params with minimum score filter."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(min_score=10, sort="votes")
        params = engine._build_query_params("test")

        assert params["min"] == 10
        assert params["sort"] == "votes"

    def test_build_query_params_with_tags(self):
        """Test query params with tag filter."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(tagged="python;django")
        params = engine._build_query_params("test")

        assert params["tagged"] == "python;django"


class TestStackExchangeHtmlDecoding:
    """Tests for Stack Exchange HTML decoding."""

    def test_decode_html_entities(self):
        """Test HTML entity decoding."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine()
        result = engine._decode_html("What does &quot;yield&quot; do?")
        assert result == 'What does "yield" do?'

    def test_decode_html_ampersand(self):
        """Test ampersand decoding."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine()
        result = engine._decode_html("A &amp; B")
        assert result == "A & B"


class TestStackExchangeSiteName:
    """Tests for Stack Exchange site name resolution."""

    def test_get_site_name_stackoverflow(self):
        """Test site name for Stack Overflow."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(site="stackoverflow")
        assert engine._get_site_name() == "Stack Overflow"

    def test_get_site_name_unix(self):
        """Test site name for Unix & Linux."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(site="unix")
        assert engine._get_site_name() == "Unix & Linux"

    def test_invalid_site_raises_error(self):
        """Test that invalid site raises ValueError."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        with pytest.raises(ValueError) as excinfo:
            StackExchangeSearchEngine(site="customsite")
        assert "Invalid site" in str(excinfo.value)
        assert "customsite" in str(excinfo.value)

    def test_invalid_sort_raises_error(self):
        """Test that invalid sort raises ValueError."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        with pytest.raises(ValueError) as excinfo:
            StackExchangeSearchEngine(sort="invalid_sort")
        assert "Invalid sort" in str(excinfo.value)


class TestStackExchangeBackoffHandling:
    """Tests for Stack Exchange API backoff handling."""

    def test_backoff_until_initialized_to_zero(self):
        """Test that backoff is initialized to zero."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine()
        assert engine._backoff_until == 0

    def test_handle_backoff_sets_future_time(self):
        """Test that handle_backoff sets a future time when backoff is present."""
        import time

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

        engine = StackExchangeSearchEngine()
        before_time = time.time()

        engine._handle_backoff({"backoff": 10})

        assert engine._backoff_until > before_time
        assert engine._backoff_until <= before_time + 11  # Allow some tolerance

    def test_handle_backoff_ignores_missing_field(self):
        """Test that handle_backoff does nothing when backoff field is missing."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine()
        engine._backoff_until = 0

        engine._handle_backoff({"items": [], "quota_remaining": 100})

        assert engine._backoff_until == 0

    def test_apply_backoff_resets_after_waiting(self, monkeypatch):
        """Test that apply_backoff resets backoff_until after waiting."""
        import time

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

        engine = StackExchangeSearchEngine()
        # Set backoff to a past time so we don't actually wait
        engine._backoff_until = time.time() - 1

        engine._apply_backoff()

        assert engine._backoff_until == 0

    def test_get_previews_handles_backoff_in_response(self, monkeypatch):
        """Test that backoff in response is properly handled."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json = Mock(
            return_value={
                "items": [
                    {
                        "question_id": 12345,
                        "title": "Test Question",
                        "body": "<p>Test</p>",
                        "link": "https://stackoverflow.com/questions/12345",
                        "owner": {"display_name": "Test User"},
                        "score": 10,
                        "tags": ["python"],
                    }
                ],
                "quota_remaining": 299,
                "backoff": 5,  # API requests backoff of 5 seconds
            }
        )
        mock_response.raise_for_status = Mock()

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        # Should still return results even with backoff
        previews = engine._get_previews("test")
        assert len(previews) == 1

        # Backoff should be set for next request
        assert engine._backoff_until > 0


class TestStackExchangeSearchExecution:
    """Tests for Stack Exchange search execution."""

    @pytest.fixture
    def engine(self):
        """Create a Stack Exchange engine."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        return StackExchangeSearchEngine(max_results=10)

    def test_get_previews_success(self, engine, monkeypatch):
        """Test successful preview retrieval."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json = Mock(
            return_value={
                "items": [
                    {
                        "question_id": 231767,
                        "title": "What does the &quot;yield&quot; keyword do?",
                        "body": "<p>Test question body</p>",
                        "link": "https://stackoverflow.com/questions/231767",
                        "owner": {
                            "display_name": "John Doe",
                            "link": "https://stackoverflow.com/users/123",
                            "reputation": 5000,
                        },
                        "score": 100,
                        "view_count": 50000,
                        "answer_count": 10,
                        "is_answered": True,
                        "accepted_answer_id": 231855,
                        "tags": ["python", "yield", "generator"],
                        "creation_date": 1224800471,
                        "last_activity_date": 1711253482,
                    }
                ],
                "has_more": True,
                "quota_max": 300,
                "quota_remaining": 299,
            }
        )
        mock_response.raise_for_status = Mock()

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        previews = engine._get_previews("python yield")

        assert len(previews) == 1
        assert previews[0]["title"] == 'What does the "yield" keyword do?'
        assert previews[0]["author"] == "John Doe"
        assert previews[0]["score"] == 100
        assert previews[0]["answer_count"] == 10
        assert previews[0]["has_accepted_answer"] is True
        assert "python" in previews[0]["tags"]
        assert previews[0]["source"] == "Stack Overflow"

    def test_get_previews_rate_limit_error(self, engine, monkeypatch):
        """Test that 429 errors raise RateLimitError."""
        from local_deep_research.web_search_engines.rate_limiting import (
            RateLimitError,
        )

        mock_response = Mock()
        mock_response.status_code = 429

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        with pytest.raises(RateLimitError):
            engine._get_previews("test")

    def test_get_previews_handles_exception(self, engine, monkeypatch):
        """Test that exceptions are handled gracefully."""
        import requests

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(side_effect=requests.RequestException("Network error")),
        )

        previews = engine._get_previews("test")
        assert previews == []

    def test_get_previews_handles_api_error(self, engine, monkeypatch):
        """Test that API errors are handled gracefully."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json = Mock(
            return_value={
                "error_id": 502,
                "error_message": "Throttle violation",
                "error_name": "throttle_violation",
            }
        )
        mock_response.raise_for_status = Mock()

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        previews = engine._get_previews("test")
        assert previews == []


class TestStackExchangeFullContent:
    """Tests for Stack Exchange full content retrieval."""

    def test_get_full_content_builds_content(self):
        """Test that full content builds proper content string."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine()

        items = [
            {
                "title": "How to use Python generators?",
                "author": "Author One",
                "score": 50,
                "answer_count": 5,
                "has_accepted_answer": True,
                "tags": ["python", "generators"],
                "_raw": {
                    "body": "<p>This is the question body with <code>code</code>.</p>",
                },
            }
        ]

        results = engine._get_full_content(items)

        assert len(results) == 1
        assert (
            "Question: How to use Python generators?" in results[0]["content"]
        )
        assert "Tags: python, generators" in results[0]["content"]
        assert "question body with code" in results[0]["content"]
        assert "_raw" not in results[0]


class TestStackExchangeEdgeCases:
    """Tests for Stack Exchange edge cases and error handling."""

    @pytest.fixture
    def engine(self):
        """Create a Stack Exchange engine."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        return StackExchangeSearchEngine(max_results=10)

    def test_all_valid_sites_can_be_instantiated(self):
        """Test that all sites in SITES dict can be used."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        valid_sites = [
            "stackoverflow",
            "serverfault",
            "superuser",
            "askubuntu",
            "unix",
            "math",
            "physics",
            "stats",
            "security",
            "dba",
        ]

        for site in valid_sites:
            engine = StackExchangeSearchEngine(site=site)
            assert engine.site == site

    def test_all_valid_sorts_can_be_used(self):
        """Test that all valid sort orders can be used."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        valid_sorts = ["relevance", "votes", "creation", "activity"]

        for sort in valid_sorts:
            engine = StackExchangeSearchEngine(sort=sort)
            assert engine.sort == sort

    def test_get_previews_empty_results(self, engine, monkeypatch):
        """Test handling of empty search results."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json = Mock(
            return_value={
                "items": [],
                "has_more": False,
                "quota_remaining": 299,
            }
        )
        mock_response.raise_for_status = Mock()

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        previews = engine._get_previews("xyznonexistentquery123")
        assert previews == []

    def test_get_previews_low_quota_returns_empty(self, engine, monkeypatch):
        """Test that low quota still returns results (warning is logged via loguru)."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json = Mock(
            return_value={
                "items": [],
                "has_more": False,
                "quota_remaining": 5,  # Low quota
            }
        )
        mock_response.raise_for_status = Mock()

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        # Should still work even with low quota
        previews = engine._get_previews("test")
        assert previews == []  # Empty because no items in response

    def test_get_previews_unicode_query(self, engine, monkeypatch):
        """Test handling of Unicode characters in query."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json = Mock(
            return_value={
                "items": [
                    {
                        "question_id": 12345,
                        "title": "How to print 日本語 in Python?",
                        "body": "<p>Unicode test</p>",
                        "link": "https://stackoverflow.com/questions/12345",
                        "owner": {"display_name": "用户"},
                        "score": 10,
                        "tags": ["python", "unicode"],
                    }
                ],
                "quota_remaining": 299,
            }
        )
        mock_response.raise_for_status = Mock()

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        previews = engine._get_previews("日本語 Python")
        assert len(previews) == 1
        assert "日本語" in previews[0]["title"]

    def test_get_previews_missing_optional_fields(self, engine, monkeypatch):
        """Test handling of missing optional fields in API response."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json = Mock(
            return_value={
                "items": [
                    {
                        "question_id": 12345,
                        "title": "Minimal question",
                        # Missing: body, owner, tags, score, etc.
                    }
                ],
                "quota_remaining": 299,
            }
        )
        mock_response.raise_for_status = Mock()

        monkeypatch.setattr(
            "local_deep_research.web_search_engines.engines.search_engine_stackexchange.safe_get",
            Mock(return_value=mock_response),
        )

        previews = engine._get_previews("test")
        assert len(previews) == 1
        assert previews[0]["title"] == "Minimal question"
        assert previews[0]["author"] == "Unknown"
        assert previews[0]["score"] == 0

    def test_get_full_content_without_raw(self, engine):
        """Test full content handling when _raw is missing."""
        items = [
            {
                "title": "Test Question",
                "author": "Test Author",
                "score": 10,
            }
        ]

        results = engine._get_full_content(items)
        assert len(results) == 1
        # Should not crash, content might be minimal

    def test_decode_html_complex_entities(self, engine):
        """Test decoding of various HTML entities."""
        test_cases = [
            ("&lt;code&gt;", "<code>"),
            ("&#39;test&#39;", "'test'"),
            ("&nbsp;space&nbsp;", "\xa0space\xa0"),
            ("&amp;amp;", "&amp;"),
        ]

        for input_text, expected in test_cases:
            result = engine._decode_html(input_text)
            assert result == expected, f"Failed for {input_text}"


@pytest.mark.integration
class TestStackExchangeIntegration:
    """Integration tests making real API calls to Stack Exchange."""

    def test_real_search_python(self):
        """Test real search for Python questions."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        results = engine._get_previews("python list comprehension")

        # Verify we got results
        assert len(results) > 0, "Should find Python questions"

        # Verify result structure
        for r in results:
            assert "title" in r, "Each result should have a title"
            assert "link" in r, "Each result should have a link"
            assert r["source"] == "Stack Overflow", (
                "Source should be Stack Overflow"
            )
            assert "stackoverflow.com" in r["link"], "Link should be SO URL"

    def test_real_search_returns_scores(self):
        """Test that real search returns scores."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        results = engine._get_previews("javascript async await")

        assert len(results) > 0, "Should find JavaScript questions"

        # All should have score
        for r in results:
            assert "score" in r, "Should have score"
            assert isinstance(r["score"], int), "Score should be int"

    def test_real_search_returns_tags(self):
        """Test that real search returns tags."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        results = engine._get_previews("react hooks")

        assert len(results) > 0, "Should find React questions"

        # At least some should have tags
        has_tags = any(r.get("tags") for r in results)
        assert has_tags, (
            f"At least one should have tags, got: {[r.get('tags') for r in results]}"
        )

    def test_real_search_different_site(self):
        """Test real search on different Stack Exchange site."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5, site="unix")
        results = engine._get_previews("bash script loop")

        assert len(results) > 0, "Should find Unix questions"

        # Source should reflect the site
        assert results[0]["source"] == "Unix & Linux", (
            f"Source should be Unix & Linux, got: {results[0]['source']}"
        )
        assert results[0]["site"] == "unix"

    def test_real_search_with_high_score(self):
        """Test real search with minimum score filter."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(
            max_results=5, min_score=100, sort="votes"
        )
        results = engine._get_previews("python")

        assert len(results) > 0, "Should find high-score Python questions"

        # All should have score >= 100
        for r in results:
            assert r["score"] >= 100, (
                f"Score should be >= 100, got: {r['score']}"
            )

    def test_real_search_returns_answer_count(self):
        """Test that real search returns answer counts."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5, has_answers=True)
        results = engine._get_previews("docker container")

        assert len(results) > 0, "Should find Docker questions with answers"

        # All should have at least one answer
        for r in results:
            assert r["answer_count"] >= 1, (
                f"Should have answers, got: {r['answer_count']}"
            )

    def test_real_search_returns_author_info(self):
        """Test that real search returns author information."""
        from local_deep_research.web_search_engines.engines.search_engine_stackexchange import (
            StackExchangeSearchEngine,
        )

        engine = StackExchangeSearchEngine(max_results=5)
        results = engine._get_previews("git merge conflict")

        assert len(results) > 0, "Should find Git questions"

        # At least some should have author
        has_author = any(r.get("author") for r in results)
        assert has_author, "At least one should have author"
