"""
Tests for advanced_search_system/filters/cross_engine_filter.py

Tests cover:
- CrossEngineFilter initialization
- filter_results method
- Result reordering and reindexing
- Error handling
"""

from unittest.mock import Mock, patch


class TestCrossEngineFilterInit:
    """Tests for CrossEngineFilter initialization."""

    @patch(
        "local_deep_research.config.thread_settings.get_setting_from_snapshot"
    )
    def test_init_with_default_max_results(self, mock_get_setting):
        """Test initialization with default max results from settings."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_get_setting.return_value = 50

        filter_instance = CrossEngineFilter(model=Mock())

        assert filter_instance.max_results == 50

    def test_init_with_custom_max_results(self):
        """Test initialization with custom max results."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(model=Mock(), max_results=25)

        assert filter_instance.max_results == 25

    def test_init_default_reorder_setting(self):
        """Test default reorder setting."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(model=Mock(), max_results=100)

        assert filter_instance.default_reorder is True

    def test_init_default_reindex_setting(self):
        """Test default reindex setting."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(model=Mock(), max_results=100)

        assert filter_instance.default_reindex is True

    def test_init_custom_reorder_reindex(self):
        """Test custom reorder and reindex settings."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(
            model=Mock(),
            max_results=100,
            default_reorder=False,
            default_reindex=False,
        )

        assert filter_instance.default_reorder is False
        assert filter_instance.default_reindex is False


class TestFilterResultsNoModel:
    """Tests for filter_results when no model is provided."""

    def test_returns_limited_results_without_model(self):
        """Test that results are limited when no model."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(model=None, max_results=5)

        results = [{"title": f"Result {i}"} for i in range(10)]

        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) == 5

    def test_reindexes_results_without_model(self):
        """Test that results are reindexed when no model."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(model=None, max_results=5)

        results = [{"title": f"Result {i}"} for i in range(3)]

        filtered = filter_instance.filter_results(results, "query")

        assert filtered[0]["index"] == "1"
        assert filtered[1]["index"] == "2"
        assert filtered[2]["index"] == "3"


class TestFilterResultsFewResults:
    """Tests for filter_results with few results."""

    def test_skips_llm_for_few_results(self):
        """Test that LLM is skipped for <= 10 results."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [{"title": f"Result {i}"} for i in range(5)]

        filter_instance.filter_results(results, "query")

        # LLM should not be called
        mock_model.invoke.assert_not_called()

    def test_returns_all_results_for_few_results(self):
        """Test all results returned when <= 10."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(model=Mock(), max_results=100)

        results = [{"title": f"Result {i}"} for i in range(5)]

        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) == 5


class TestFilterResultsWithLLM:
    """Tests for filter_results with LLM filtering."""

    def test_calls_llm_for_many_results(self):
        """Test that LLM is called for > 10 results."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.return_value = Mock(content="[0, 1, 2]")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [
            {"title": f"Result {i}", "snippet": f"Snippet {i}"}
            for i in range(15)
        ]

        filter_instance.filter_results(results, "test query")

        mock_model.invoke.assert_called_once()

    def test_reorders_results_based_on_llm_response(self):
        """Test that results are reordered based on LLM response."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.return_value = Mock(content="[2, 0, 1]")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [
            {"title": "First"},
            {"title": "Second"},
            {"title": "Third"},
        ] + [{"title": f"Result {i}"} for i in range(12)]  # Need > 10 total

        filtered = filter_instance.filter_results(results, "query")

        assert filtered[0]["title"] == "Third"
        assert filtered[1]["title"] == "First"
        assert filtered[2]["title"] == "Second"

    def test_respects_max_results(self):
        """Test that max_results is respected."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.return_value = Mock(content="[0, 1, 2, 3, 4, 5]")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=3)

        results = [{"title": f"Result {i}"} for i in range(15)]

        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) <= 3


class TestFilterResultsReindex:
    """Tests for result reindexing."""

    def test_reindex_updates_indices(self):
        """Test that reindex updates result indices."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.return_value = Mock(content="[2, 0]")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [{"title": f"Result {i}"} for i in range(15)]

        filtered = filter_instance.filter_results(
            results, "query", reindex=True
        )

        assert filtered[0]["index"] == "1"
        assert filtered[1]["index"] == "2"

    def test_start_index_offset(self):
        """Test that start_index offsets indices correctly."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        filter_instance = CrossEngineFilter(model=None, max_results=100)

        results = [{"title": f"Result {i}"} for i in range(3)]

        filtered = filter_instance.filter_results(
            results, "query", start_index=5
        )

        assert filtered[0]["index"] == "6"
        assert filtered[1]["index"] == "7"
        assert filtered[2]["index"] == "8"


class TestFilterResultsErrorHandling:
    """Tests for error handling in filter_results."""

    def test_handles_llm_exception(self):
        """Test that LLM exceptions are handled gracefully."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.side_effect = RuntimeError("LLM error")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [{"title": f"Result {i}"} for i in range(15)]

        # Should not raise, returns original results
        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) > 0

    def test_llm_exception_fallback_limited_to_context_items(self):
        """Exception fallback should stay within the evaluated context window."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.side_effect = RuntimeError("LLM error")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=10)
        filter_instance.max_context_items = 3

        results = [{"title": f"Result {i}"} for i in range(15)]

        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) == 3
        assert filtered == results[:3]

    def test_handles_invalid_json_response(self):
        """Test handling of invalid JSON in LLM response."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.return_value = Mock(content="not valid json")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [{"title": f"Result {i}"} for i in range(15)]

        # Should not raise
        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) > 0

    def test_invalid_json_fallback_limited_to_context_items(self):
        """Invalid JSON fallback should stay within the evaluated context window."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.return_value = Mock(content="not valid json")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=10)
        filter_instance.max_context_items = 3

        results = [{"title": f"Result {i}"} for i in range(15)]

        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) == 3
        assert filtered == results[:3]

    def test_handles_empty_json_array(self):
        """Test handling of empty JSON array response."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        mock_model.invoke.return_value = Mock(content="[]")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [{"title": f"Result {i}"} for i in range(15)]

        # Should return top 10 original results as fallback
        filtered = filter_instance.filter_results(results, "query")

        assert len(filtered) == 10
        # Verify they are the first 10 original results
        for i in range(10):
            assert filtered[i] is results[i]

    def test_empty_array_fallback_limited_to_context_window(self):
        """Empty-array fallback should stay within the evaluated context window."""
        f, results = _make_filter_and_results("[]")
        f.max_context_items = 5

        filtered = f.filter_results(results, "query", reorder=True)

        assert len(filtered) == 5
        assert filtered == results[:5]


class TestFilterResultsReorder:
    """Tests for reorder parameter."""

    def test_no_reorder_maintains_original_order(self):
        """Test that reorder=False maintains original order."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        # LLM wants to reorder as [2, 0, 1]
        mock_model.invoke.return_value = Mock(content="[2, 0, 1]")

        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        results = [
            {"title": "First"},
            {"title": "Second"},
            {"title": "Third"},
        ] + [{"title": f"Result {i}"} for i in range(12)]

        filtered = filter_instance.filter_results(
            results, "query", reorder=False
        )

        # When not reordering, results should be sorted by original index
        # So order would be: 0, 1, 2 (sorted indices from [2, 0, 1])
        assert filtered[0]["title"] == "First"
        assert filtered[1]["title"] == "Second"
        assert filtered[2]["title"] == "Third"


class TestInheritance:
    """Tests for CrossEngineFilter inheritance."""

    def test_inherits_from_base_filter(self):
        """Test that CrossEngineFilter inherits from BaseFilter."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )
        from local_deep_research.advanced_search_system.filters.base_filter import (
            BaseFilter,
        )

        assert issubclass(CrossEngineFilter, BaseFilter)

    def test_has_model_attribute(self):
        """Test that instance has model attribute from base class."""
        from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
            CrossEngineFilter,
        )

        mock_model = Mock()
        filter_instance = CrossEngineFilter(model=mock_model, max_results=100)

        assert filter_instance.model is mock_model


def _make_filter_and_results(content, *, reorder=True, num_results=15):
    """Helper: create a CrossEngineFilter with a mock LLM returning *content*."""
    from local_deep_research.advanced_search_system.filters.cross_engine_filter import (
        CrossEngineFilter,
    )

    mock_model = Mock()
    mock_model.invoke.return_value = Mock(content=content)
    f = CrossEngineFilter(model=mock_model, max_results=100)
    results = [
        {"title": f"Result {i}", "snippet": f"Snippet {i}"}
        for i in range(num_results)
    ]
    return f, results


class TestFilterResultsValidation:
    """Tests for index validation and json_utils integration."""

    def test_filters_negative_indices(self):
        """Negative indices in LLM response are rejected."""
        f, results = _make_filter_and_results("[-1, 0, 1, -2]")
        filtered = f.filter_results(results, "query", reorder=False)
        # Only 0 and 1 are valid
        assert len(filtered) == 2
        assert filtered[0] is results[0]
        assert filtered[1] is results[1]

    def test_filters_out_of_range_indices(self):
        """Out-of-range indices in LLM response are rejected."""
        f, results = _make_filter_and_results("[0, 1, 999, 2]")
        filtered = f.filter_results(results, "query", reorder=False)
        assert len(filtered) == 3
        assert filtered[0] is results[0]
        assert filtered[1] is results[1]
        assert filtered[2] is results[2]

    def test_deduplicates_repeated_llm_indices_reorder_true(self):
        """Repeated indices should only contribute one ranked result."""
        f, results = _make_filter_and_results("[2, 2, 0, 2, 1]")
        filtered = f.filter_results(results, "query", reorder=True)
        assert filtered == [results[2], results[0], results[1]]

    def test_deduplicates_repeated_llm_indices_reorder_false(self):
        """Repeated indices should be deduplicated before original-order sorting."""
        f, results = _make_filter_and_results("[2, 2, 0, 2, 1]")
        filtered = f.filter_results(results, "query", reorder=False)
        assert filtered == [results[0], results[1], results[2]]

    def test_indices_outside_context_window_are_rejected(self):
        """LLM indices cannot reference results outside the evaluated context."""
        f, results = _make_filter_and_results("[0, 4]")
        f.max_context_items = 3

        filtered = f.filter_results(results, "query", reorder=True)

        assert filtered == [results[0]]

    def test_indices_outside_context_window_rejected_reorder_false(self):
        """Out-of-window indices should also be rejected without reordering."""
        f, results = _make_filter_and_results("[0, 4]")
        f.max_context_items = 3

        filtered = f.filter_results(results, "query", reorder=False)

        assert filtered == [results[0]]

    @patch(
        "local_deep_research.advanced_search_system.filters.cross_engine_filter.logger.warning"
    )
    def test_logs_warning_for_non_integer_indices(self, mock_warning):
        """Non-integer indices are skipped with a warning."""
        f, results = _make_filter_and_results('[0, "1", true, 2]')

        filtered = f.filter_results(results, "query", reorder=True)

        assert filtered == [results[0], results[2]]
        assert mock_warning.call_count == 2

    def test_all_invalid_indices_reorder_false_returns_fallback(self):
        """When all indices are invalid with reorder=False, return top 10."""
        f, results = _make_filter_and_results("[999, 998]")
        filtered = f.filter_results(results, "query", reorder=False)
        assert len(filtered) == 10
        for i in range(10):
            assert filtered[i] is results[i]

    def test_handles_trailing_comma(self):
        """json_utils strips trailing commas so LLM output like [0, 1, 2,] works."""
        f, results = _make_filter_and_results("[0, 1, 2,]")
        filtered = f.filter_results(results, "query", reorder=True)
        assert len(filtered) == 3

    def test_handles_inline_comments(self):
        """json_utils strips inline comments so LLM output works."""
        f, results = _make_filter_and_results("[3, 0, 7] // most relevant")
        filtered = f.filter_results(results, "query", reorder=True)
        assert len(filtered) == 3
        assert filtered[0] is results[3]

    def test_handles_ellipsis_in_array(self):
        """json_utils strips ellipsis so LLM output like [1, 2, ...] works."""
        f, results = _make_filter_and_results("[1, 2, ...]")
        filtered = f.filter_results(results, "query", reorder=True)
        assert len(filtered) == 2
        assert filtered[0] is results[1]
        assert filtered[1] is results[2]
