"""High-value tests for filters module: base_filter.py and followup_relevance_filter.py.

Covers BaseFilter ABC enforcement, FollowUpRelevanceFilter logic with
mocked LLM responses, empty inputs, and fallback behavior.
"""

import pytest
from unittest.mock import MagicMock

from local_deep_research.advanced_search_system.filters.base_filter import (
    BaseFilter,
)
from local_deep_research.advanced_search_system.filters.followup_relevance_filter import (
    FollowUpRelevanceFilter,
)


class TestBaseFilterABC:
    """Test BaseFilter abstract base class."""

    def test_cannot_instantiate_directly(self):
        with pytest.raises(TypeError):
            BaseFilter()

    def test_incomplete_subclass_raises(self):
        class Incomplete(BaseFilter):
            pass

        with pytest.raises(TypeError):
            Incomplete()

    def test_complete_subclass_works(self):
        class Complete(BaseFilter):
            def filter_results(self, results, query, **kwargs):
                return results

        f = Complete()
        assert isinstance(f, BaseFilter)

    def test_model_stored(self):
        class Complete(BaseFilter):
            def filter_results(self, results, query, **kwargs):
                return results

        model = MagicMock()
        f = Complete(model=model)
        assert f.model is model

    def test_model_default_none(self):
        class Complete(BaseFilter):
            def filter_results(self, results, query, **kwargs):
                return results

        f = Complete()
        assert f.model is None


class TestFollowUpRelevanceFilterEmptyInput:
    """Test FollowUpRelevanceFilter with empty/None inputs."""

    def test_empty_results_returns_empty(self):
        """Empty results list returns empty list."""
        f = FollowUpRelevanceFilter(model=MagicMock())
        assert f.filter_results([], "query") == []

    def test_no_model_returns_first_n(self):
        """Without model, returns first max_results sources."""
        f = FollowUpRelevanceFilter(model=None)
        results = [
            {"title": f"R{i}", "url": f"http://{i}.com"} for i in range(20)
        ]
        filtered = f.filter_results(results, "query", max_results=5)
        assert len(filtered) == 5
        assert filtered[0]["title"] == "R0"


class TestFollowUpRelevanceFilterWithModel:
    """Test FollowUpRelevanceFilter with mocked LLM."""

    def _make_filter(self, llm_content="[0, 2, 4]"):
        model = MagicMock()
        model.invoke.return_value = MagicMock(content=llm_content)
        return FollowUpRelevanceFilter(model=model)

    def test_selects_indices_from_llm(self):
        """Uses LLM-selected indices to filter."""
        f = self._make_filter("[0, 2]")
        results = [
            {"title": "A", "url": "http://a.com"},
            {"title": "B", "url": "http://b.com"},
            {"title": "C", "url": "http://c.com"},
        ]
        filtered = f.filter_results(results, "follow-up query")
        assert len(filtered) == 2
        assert filtered[0]["title"] == "A"
        assert filtered[1]["title"] == "C"

    def test_out_of_range_indices_skipped(self):
        """Indices >= len(results) are silently skipped."""
        f = self._make_filter("[0, 99]")
        results = [{"title": "Only", "url": "http://only.com"}]
        filtered = f.filter_results(results, "query")
        assert len(filtered) == 1

    def test_llm_failure_falls_back_to_first_n(self):
        """LLM exception falls back to first max_results."""
        model = MagicMock()
        model.invoke.side_effect = RuntimeError("LLM error")
        f = FollowUpRelevanceFilter(model=model)
        results = [
            {"title": f"R{i}", "url": f"http://{i}.com"} for i in range(10)
        ]
        filtered = f.filter_results(results, "query", max_results=3)
        assert len(filtered) == 3

    def test_context_included_in_prompt(self):
        """past_findings and original_query are passed through."""
        model = MagicMock()
        model.invoke.return_value = MagicMock(content="[0]")
        f = FollowUpRelevanceFilter(model=model)
        results = [{"title": "A", "url": "http://a.com"}]
        f.filter_results(
            results,
            "follow-up",
            past_findings="previous research",
            original_query="original question",
        )
        prompt = model.invoke.call_args[0][0]
        assert "previous research" in prompt
        assert "original question" in prompt

    def test_non_json_response_regex_fallback(self):
        """Non-JSON LLM response falls back to regex number extraction."""
        f = self._make_filter("The relevant sources are indices 0, 2, and 4.")
        results = [
            {"title": f"R{i}", "url": f"http://{i}.com"} for i in range(5)
        ]
        filtered = f.filter_results(results, "query")
        assert len(filtered) == 3
        assert filtered[0]["title"] == "R0"
        assert filtered[1]["title"] == "R2"
        assert filtered[2]["title"] == "R4"


class TestFollowUpRelevanceFilterMaxResults:
    """Test max_results parameter."""

    def test_default_max_results_is_ten(self):
        """Default max_results parameter is 10."""
        f = FollowUpRelevanceFilter(model=None)
        results = [
            {"title": f"R{i}", "url": f"http://{i}.com"} for i in range(20)
        ]
        filtered = f.filter_results(results, "query")
        assert len(filtered) == 10

    def test_custom_max_results(self):
        """Custom max_results limits output."""
        f = FollowUpRelevanceFilter(model=None)
        results = [
            {"title": f"R{i}", "url": f"http://{i}.com"} for i in range(20)
        ]
        filtered = f.filter_results(results, "query", max_results=3)
        assert len(filtered) == 3
