"""
Tests for the BaseSearchEngine class.

Tests cover:
- Initialization and property access
- Rate limiting integration
- API key checking
- Engine class loading
- Run method behavior
- Relevance filtering
"""

from unittest.mock import Mock

from local_deep_research.config.constants import DEFAULT_MAX_FILTERED_RESULTS


class TestBaseSearchEngineInit:
    """Tests for BaseSearchEngine initialization."""

    def test_init_with_defaults(self):
        """Initialize with default values."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        # Create a concrete implementation for testing
        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine()

        assert engine.max_results == 10
        assert engine.max_filtered_results == DEFAULT_MAX_FILTERED_RESULTS
        assert engine.search_snippets_only is True
        assert engine.llm is None
        assert engine.programmatic_mode is False

    def test_init_with_custom_values(self):
        """Initialize with custom values."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        mock_llm = Mock()
        engine = TestEngine(
            llm=mock_llm,
            max_results=25,
            max_filtered_results=10,
            search_snippets_only=False,
            programmatic_mode=True,
        )

        assert engine.max_results == 25
        assert engine.max_filtered_results == 10
        assert engine.search_snippets_only is False
        assert engine.llm is mock_llm
        assert engine.programmatic_mode is True

    def test_init_with_none_max_results(self):
        """Handle None max_results gracefully."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine(max_results=None)

        assert engine.max_results == 10  # Default value

    def test_init_with_settings_snapshot(self):
        """Initialize with settings snapshot."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        settings = {"search.max_results": {"value": 20}}
        engine = TestEngine(settings_snapshot=settings)

        assert engine.settings_snapshot == settings


class TestBaseSearchEngineProperties:
    """Tests for BaseSearchEngine property access."""

    def test_max_results_setter(self):
        """Set max_results property."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine()
        engine.max_results = 50

        assert engine.max_results == 50

    def test_max_results_setter_minimum_value(self):
        """Ensure max_results is at least 1."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine()
        engine.max_results = 0

        assert engine.max_results == 1

    def test_max_filtered_results_setter(self):
        """Set max_filtered_results property."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine()
        engine.max_filtered_results = 20

        assert engine.max_filtered_results == 20


class TestBaseSearchEngineClassMethods:
    """Tests for BaseSearchEngine class methods."""

    def test_check_api_key_not_required(self):
        """API key check passes when not required."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        config = {"requires_api_key": False}
        result = BaseSearchEngine._check_api_key_availability("test", config)

        assert result is True

    def test_check_api_key_required_and_present(self):
        """API key check passes when required and valid."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        config = {"requires_api_key": True, "api_key": "valid-api-key-123"}
        result = BaseSearchEngine._check_api_key_availability("test", config)

        assert result is True

    def test_check_api_key_required_but_missing(self):
        """API key check fails when required but missing."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        config = {"requires_api_key": True, "api_key": ""}
        result = BaseSearchEngine._check_api_key_availability("test", config)

        assert result is False

    def test_check_api_key_placeholder_values(self):
        """API key check fails for placeholder values."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        placeholders = [
            "PLACEHOLDER",
            "YOUR_API_KEY_HERE",
            "None",
            "BRAVE_API_KEY",
            "YOUR_KEY",
            "null",
        ]

        for placeholder in placeholders:
            config = {"requires_api_key": True, "api_key": placeholder}
            result = BaseSearchEngine._check_api_key_availability(
                "test", config
            )
            assert result is False, f"Failed for placeholder: {placeholder}"

    def test_load_engine_class_success(self):
        """Successfully load engine class."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        config = {
            "module_path": ".engines.search_engine_wikipedia",
            "class_name": "WikipediaSearchEngine",
        }

        success, engine_class, error = BaseSearchEngine._load_engine_class(
            "wikipedia", config
        )

        assert success is True
        assert engine_class is not None
        assert error is None

    def test_load_engine_class_missing_config(self):
        """Fail to load engine class with missing config."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        config = {"module_path": ".engines.search_engine_wikipedia"}

        success, engine_class, error = BaseSearchEngine._load_engine_class(
            "wikipedia", config
        )

        assert success is False
        assert engine_class is None
        assert error is not None

    def test_load_engine_class_invalid_module(self):
        """Fail to load engine class from invalid module."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        config = {
            "module_path": ".engines.nonexistent_engine",
            "class_name": "NonexistentEngine",
        }

        success, engine_class, error = BaseSearchEngine._load_engine_class(
            "nonexistent", config
        )

        assert success is False
        assert engine_class is None
        assert error is not None


class TestBaseSearchEngineRun:
    """Tests for BaseSearchEngine run method."""

    def test_run_returns_empty_for_no_results(self):
        """Run returns empty list when no results."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine(programmatic_mode=True)
        results = engine.run("test query")

        assert results == []

    def test_run_returns_previews_when_snippets_only(self):
        """Run returns previews when search_snippets_only is True."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return [
                    {
                        "title": "Result 1",
                        "snippet": "Snippet 1",
                        "url": "http://a.com",
                    },
                    {
                        "title": "Result 2",
                        "snippet": "Snippet 2",
                        "url": "http://b.com",
                    },
                ]

            def _get_full_content(self, relevant_items):
                for item in relevant_items:
                    item["full_content"] = "Full content here"
                return relevant_items

        engine = TestEngine(programmatic_mode=True, search_snippets_only=True)
        results = engine.run("test query")

        assert len(results) == 2
        assert "full_content" not in results[0]  # Should not have full content

    def test_run_gets_full_content_when_not_snippets_only(self):
        """Run gets full content when search_snippets_only is False."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return [
                    {
                        "title": "Result 1",
                        "snippet": "Snippet 1",
                        "url": "http://a.com",
                    },
                ]

            def _get_full_content(self, relevant_items):
                for item in relevant_items:
                    item["full_content"] = "Full content here"
                return relevant_items

        engine = TestEngine(programmatic_mode=True, search_snippets_only=False)
        results = engine.run("test query")

        assert len(results) == 1
        assert results[0]["full_content"] == "Full content here"

    def test_run_applies_preview_filters(self):
        """Run applies preview filters."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )
        from local_deep_research.advanced_search_system.filters.base_filter import (
            BaseFilter,
        )

        class TestFilter(BaseFilter):
            def filter_results(self, results, query):
                return [
                    r for r in results if "keep" in r.get("title", "").lower()
                ]

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return [
                    {
                        "title": "Keep Result",
                        "snippet": "S1",
                        "url": "http://a.com",
                    },
                    {
                        "title": "Drop Result",
                        "snippet": "S2",
                        "url": "http://b.com",
                    },
                ]

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine(
            programmatic_mode=True, preview_filters=[TestFilter()]
        )
        results = engine.run("test query")

        assert len(results) == 1
        assert results[0]["title"] == "Keep Result"

    def test_run_handles_exception(self):
        """Run handles exceptions gracefully."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                raise Exception("Search failed")

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine(programmatic_mode=True)
        results = engine.run("test query")

        assert results == []

    def test_invoke_calls_run(self):
        """Invoke method calls run."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return [
                    {"title": "Result", "snippet": "S", "url": "http://a.com"}
                ]

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine(programmatic_mode=True)
        results = engine.invoke("test query")

        assert len(results) == 1


class TestBaseSearchEngineRelevanceFiltering:
    """Tests for BaseSearchEngine relevance filtering."""

    def test_filter_for_relevance_without_llm(self):
        """Filter returns all results when no LLM."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        engine = TestEngine(programmatic_mode=True, llm=None)
        previews = [
            {
                "title": "Result 1",
                "snippet": "Snippet 1",
                "url": "http://a.com",
            },
            {
                "title": "Result 2",
                "snippet": "Snippet 2",
                "url": "http://b.com",
            },
        ]

        result = engine._filter_for_relevance(previews, "test query")

        assert result == previews

    def test_filter_for_relevance_with_single_result(self):
        """Filter returns single result without LLM call."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        mock_llm = Mock()
        engine = TestEngine(programmatic_mode=True, llm=mock_llm)
        previews = [
            {"title": "Result 1", "snippet": "Snippet 1", "url": "http://a.com"}
        ]

        result = engine._filter_for_relevance(previews, "test query")

        assert result == previews
        mock_llm.invoke.assert_not_called()

    def test_filter_for_relevance_with_llm(self):
        """Filter uses LLM for ranking."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        mock_llm = Mock()
        mock_llm.invoke.return_value = "1, 0"

        engine = TestEngine(
            programmatic_mode=True, llm=mock_llm, max_filtered_results=5
        )
        previews = [
            {
                "title": "Result 0",
                "snippet": "Snippet 0",
                "url": "http://a.com",
            },
            {
                "title": "Result 1",
                "snippet": "Snippet 1",
                "url": "http://b.com",
            },
        ]

        result = engine._filter_for_relevance(previews, "test query")

        assert len(result) == 2
        # Should return in ranked order: [1, 0] means Result 1 first
        assert result[0]["title"] == "Result 1"
        assert result[1]["title"] == "Result 0"

    def test_filter_for_relevance_handles_invalid_indices(self):
        """Filter handles out-of-range indices gracefully."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        mock_llm = Mock()
        mock_llm.invoke.return_value = "0, 5, 1, 100"

        engine = TestEngine(programmatic_mode=True, llm=mock_llm)
        previews = [
            {
                "title": "Result 0",
                "snippet": "Snippet 0",
                "url": "http://a.com",
            },
            {
                "title": "Result 1",
                "snippet": "Snippet 1",
                "url": "http://b.com",
            },
        ]

        result = engine._filter_for_relevance(previews, "test query")

        # Should only include valid indices
        assert len(result) == 2
        assert result[0]["title"] == "Result 0"
        assert result[1]["title"] == "Result 1"

    def test_filter_for_relevance_handles_llm_error(self):
        """Filter handles LLM errors gracefully."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class TestEngine(BaseSearchEngine):
            def _get_previews(self, query):
                return []

            def _get_full_content(self, relevant_items):
                return relevant_items

        mock_llm = Mock()
        mock_llm.invoke.side_effect = Exception("LLM Error")

        engine = TestEngine(
            programmatic_mode=True, llm=mock_llm, max_filtered_results=5
        )
        previews = [
            {
                "title": "Result 0",
                "snippet": "Snippet 0",
                "url": "http://a.com",
            },
            {
                "title": "Result 1",
                "snippet": "Snippet 1",
                "url": "http://b.com",
            },
        ]

        result = engine._filter_for_relevance(previews, "test query")

        # Should fallback to top results
        assert len(result) <= 5


class TestBaseSearchEngineClassAttributes:
    """Tests for BaseSearchEngine class attributes."""

    def test_default_class_attributes(self):
        """Verify default class attribute values."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        assert BaseSearchEngine.is_public is False
        assert BaseSearchEngine.is_generic is False
        assert BaseSearchEngine.is_scientific is False
        assert BaseSearchEngine.is_local is False
        assert BaseSearchEngine.is_news is False
        assert BaseSearchEngine.is_code is False


class TestEnsureList:
    """Tests for BaseSearchEngine._ensure_list static method."""

    def test_list_passthrough(self):
        """Already-parsed lists pass through unchanged."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list(["a", "b"])
        assert result == ["a", "b"]

    def test_json_string(self):
        """JSON-encoded array strings are parsed."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list('["http://localhost:9200"]')
        assert result == ["http://localhost:9200"]

    def test_json_string_multiple(self):
        """JSON-encoded array with multiple items."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list('["content", "title"]')
        assert result == ["content", "title"]

    def test_comma_separated(self):
        """Comma-separated strings are split."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list("content, title, description")
        assert result == ["content", "title", "description"]

    def test_none_returns_default(self):
        """None returns the default list."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list(None)
        assert result == []

        result = BaseSearchEngine._ensure_list(None, default=["x"])
        assert result == ["x"]

    def test_empty_string_returns_default(self):
        """Empty or whitespace-only strings return the default."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        assert BaseSearchEngine._ensure_list("") == []
        assert BaseSearchEngine._ensure_list("   ") == []

    def test_invalid_json_falls_through_to_comma_split(self):
        """Malformed JSON starting with [ falls back to comma split."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list("[not valid json")
        assert result == ["[not valid json"]

    def test_non_string_non_list_returns_default(self):
        """Non-string, non-list types return the default."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        assert BaseSearchEngine._ensure_list(42) == []
        assert BaseSearchEngine._ensure_list({"a": 1}) == []

    def test_json_string_with_whitespace(self):
        """JSON strings with leading/trailing whitespace are handled."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list('  ["a", "b"]  ')
        assert result == ["a", "b"]

    def test_json_integer_items_become_strings(self):
        """Non-string items in JSON arrays are converted to strings."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        result = BaseSearchEngine._ensure_list("[1, 2, 3]")
        assert result == ["1", "2", "3"]


class TestAdaptiveWait:
    """Tests for AdaptiveWait class."""

    def test_adaptive_wait_calls_function(self):
        """AdaptiveWait calls provided function."""
        from local_deep_research.web_search_engines.search_engine_base import (
            AdaptiveWait,
        )

        wait_func = Mock(return_value=2.5)
        adaptive_wait = AdaptiveWait(wait_func)

        mock_retry_state = Mock()
        result = adaptive_wait(mock_retry_state)

        assert result == 2.5
        wait_func.assert_called_once()


class TestDoiEnrichmentOrdering:
    """Ensure enrichment runs before preview filters.

    Regression guard: the OpenAlex source-id enrichment must populate
    ``openalex_source_id`` on each result BEFORE the preview filters
    (which include JournalReputationFilter) run. If the ordering
    slips back to post-content-fetch, Tier 2 journal lookups silently
    degrade to fragile name matching even when a DOI was available.
    """

    def test_preview_filter_sees_enriched_source_id(self, monkeypatch):
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class FakeEngine(BaseSearchEngine):
            is_scientific = True  # opts us into the enrichment branch

            def _get_previews(self, query):
                return [
                    {
                        "title": "Some paper",
                        "doi": "10.1234/abc",
                    }
                ]

            def _get_full_content(self, items):
                return items

        # Capture the previews the filter sees so we can assert
        # ordering: enrichment must have already run.
        seen_by_filter: list[list[dict]] = []

        class CaptureFilter:
            def filter_results(self, results, query):
                seen_by_filter.append(
                    [dict(r) for r in results]  # snapshot
                )
                return results

        def fake_enrich(results, email=None):
            for r in results:
                r["openalex_source_id"] = "S42"
            return results

        monkeypatch.setattr(
            "local_deep_research.utilities.openalex_enrichment."
            "enrich_results_with_source_ids",
            fake_enrich,
        )

        engine = FakeEngine(programmatic_mode=True)
        engine._preview_filters = [CaptureFilter()]

        engine.run("anything")

        assert seen_by_filter, "preview filter was never called"
        first_result = seen_by_filter[0][0]
        assert first_result.get("openalex_source_id") == "S42", (
            "enrichment must run before preview filters so the "
            "journal reputation filter can use the source_id"
        )

    def test_non_scientific_engine_skips_enrichment(self, monkeypatch):
        """Non-scientific engines don't pay the enrichment cost."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class FakeEngine(BaseSearchEngine):
            # is_scientific defaults False via getattr

            def _get_previews(self, query):
                return [{"title": "Web result", "url": "http://x.com"}]

            def _get_full_content(self, items):
                return items

        called = {"count": 0}

        def fake_enrich(results, email=None):
            called["count"] += 1
            return results

        monkeypatch.setattr(
            "local_deep_research.utilities.openalex_enrichment."
            "enrich_results_with_source_ids",
            fake_enrich,
        )

        engine = FakeEngine(programmatic_mode=True)
        engine.run("anything")

        assert called["count"] == 0, (
            "non-scientific engines should not trigger DOI enrichment"
        )
