"""
Tests for the MetaSearchEngine class.

Tests cover:
- Initialization and configuration
- Query analysis and engine selection
- Preview retrieval with fallback
- Full content retrieval
- Engine caching
"""

from unittest.mock import Mock, patch


class TestMetaSearchEngineInit:
    """Tests for MetaSearchEngine initialization."""

    def test_init_with_required_params(self):
        """Initialize with required parameters."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.wikipedia.use_in_auto_search": {"value": True},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            assert engine.llm is mock_llm
            assert engine.max_results == 10
            assert engine.programmatic_mode is True

    def test_init_with_custom_max_results(self):
        """Initialize with custom max_results."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.wikipedia.use_in_auto_search": {"value": True},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                max_results=25,
                settings_snapshot=settings,
                programmatic_mode=True,
            )

            assert engine.max_results == 25

    def test_init_with_max_engines_to_try(self):
        """Initialize with custom max_engines_to_try."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.wikipedia.use_in_auto_search": {"value": True},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                max_engines_to_try=5,
                settings_snapshot=settings,
                programmatic_mode=True,
            )

            assert engine.max_engines_to_try == 5

    def test_init_creates_engine_cache(self):
        """Initialize creates empty engine cache."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.wikipedia.use_in_auto_search": {"value": True},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            assert engine.engine_cache == {}

    def test_init_creates_fallback_engine(self):
        """Initialize creates Wikipedia fallback engine."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.wikipedia.use_in_auto_search": {"value": True},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            assert engine.fallback_engine is not None


class TestMetaSearchEngineQueryAnalysis:
    """Tests for MetaSearchEngine query analysis."""

    def test_analyze_query_scientific_paper(self):
        """Analyze query detects scientific paper queries."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["arxiv", "pubmed", "wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            result = engine.analyze_query(
                "scientific paper about quantum computing"
            )

            assert "arxiv" in result or "pubmed" in result

    def test_analyze_query_github_code(self):
        """Analyze query detects code queries."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["github", "searxng"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            result = engine.analyze_query("code for sorting algorithms")

            assert "github" in result

    def test_analyze_query_medical_research(self):
        """Analyze query detects medical queries."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["pubmed", "searxng"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            result = engine.analyze_query("clinical trial results for diabetes")

            assert "pubmed" in result

    def test_analyze_query_prioritizes_searxng_for_general(self):
        """Analyze query prioritizes SearXNG for general queries."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["searxng", "wikipedia", "brave"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            result = engine.analyze_query("best restaurants in Paris")

            assert result[0] == "searxng"

    def test_analyze_query_arxiv_keyword(self):
        """Analyze query detects arxiv keyword."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["arxiv", "wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            result = engine.analyze_query(
                "find arxiv paper on machine learning"
            )

            assert result[0] == "arxiv"

    def test_analyze_query_pubmed_keyword(self):
        """Analyze query detects pubmed keyword."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["pubmed", "wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            result = engine.analyze_query("search pubmed for cancer research")

            assert result[0] == "pubmed"


class TestMetaSearchEngineGetPreviews:
    """Tests for MetaSearchEngine _get_previews method."""

    def test_get_previews_returns_results(self):
        """_get_previews returns results from successful engine."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        mock_engine = Mock()
        mock_engine._get_previews.return_value = [
            {"title": "Result 1", "url": "http://a.com", "snippet": "Snippet 1"}
        ]

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_engine_instance",
                return_value=mock_engine,
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                results = engine._get_previews("test query")

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

    def test_get_previews_falls_back_on_empty_results(self):
        """_get_previews falls back to Wikipedia on empty results."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        mock_engine = Mock()
        mock_engine._get_previews.return_value = []

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["brave"]
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_engine_instance",
                return_value=mock_engine,
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                # Mock fallback engine
                engine.fallback_engine = Mock()
                engine.fallback_engine._get_previews.return_value = [
                    {"title": "Wikipedia Result", "url": "http://wiki.com"}
                ]

                results = engine._get_previews("test query")

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

    def test_get_previews_falls_back_on_exception(self):
        """_get_previews falls back to Wikipedia on exception."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        mock_engine = Mock()
        mock_engine._get_previews.side_effect = Exception("Search failed")

        with patch.object(
            MetaSearchEngine, "_get_available_engines", return_value=["brave"]
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_engine_instance",
                return_value=mock_engine,
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                # Mock fallback engine
                engine.fallback_engine = Mock()
                engine.fallback_engine._get_previews.return_value = [
                    {"title": "Wikipedia Fallback", "url": "http://wiki.com"}
                ]

                results = engine._get_previews("test query")

                assert len(results) == 1
                assert results[0]["title"] == "Wikipedia Fallback"

    def test_get_previews_tries_multiple_engines(self):
        """_get_previews tries multiple engines before fallback."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        call_count = [0]

        def engine_factory(name):
            call_count[0] += 1
            mock = Mock()
            if call_count[0] < 3:
                mock._get_previews.return_value = []
            else:
                mock._get_previews.return_value = [
                    {"title": "Result from engine 3", "url": "http://c.com"}
                ]
            return mock

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["engine1", "engine2", "engine3"],
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_engine_instance",
                side_effect=engine_factory,
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    max_engines_to_try=3,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                results = engine._get_previews("test query")

                assert call_count[0] == 3
                assert len(results) == 1


class TestMetaSearchEngineGetFullContent:
    """Tests for MetaSearchEngine _get_full_content method."""

    def test_get_full_content_uses_selected_engine(self):
        """_get_full_content uses the engine that provided previews."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.snippets_only": {"value": False},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            with patch(
                "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
                return_value=False,  # Not snippets_only mode
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                # Simulate having selected an engine during previews
                mock_selected = Mock()
                mock_selected._get_full_content.return_value = [
                    {"title": "Result", "full_content": "Full text here"}
                ]
                engine._selected_engine = mock_selected
                engine._selected_engine_name = "brave"

                items = [{"title": "Result", "url": "http://a.com"}]
                results = engine._get_full_content(items)

                mock_selected._get_full_content.assert_called_once_with(items)
                assert results[0]["full_content"] == "Full text here"

    def test_get_full_content_returns_items_when_snippets_only(self):
        """_get_full_content returns items as-is when snippets_only is True."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.snippets_only": {"value": True},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            items = [
                {"title": "Result", "url": "http://a.com", "snippet": "Snippet"}
            ]
            results = engine._get_full_content(items)

            assert results == items

    def test_get_full_content_returns_items_when_no_selected_engine(self):
        """_get_full_content returns items when no engine was selected."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.snippets_only": {"value": False},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

            # Don't set _selected_engine
            items = [{"title": "Result", "url": "http://a.com"}]
            results = engine._get_full_content(items)

            assert results == items


class TestMetaSearchEngineEngineCaching:
    """Tests for MetaSearchEngine engine caching."""

    def test_get_engine_instance_creates_new_engine(self):
        """_get_engine_instance creates new engine when not cached."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        mock_created_engine = Mock()

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            with patch(
                "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
                return_value=mock_created_engine,
            ) as mock_create:
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                result = engine._get_engine_instance("brave")

                mock_create.assert_called_once()
                assert result is mock_created_engine

    def test_get_engine_instance_returns_cached(self):
        """_get_engine_instance returns cached engine."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        cached_engine = Mock()

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            with patch(
                "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine"
            ) as mock_create:
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                # Pre-populate cache
                engine.engine_cache["brave"] = cached_engine

                result = engine._get_engine_instance("brave")

                # Should not call create_search_engine
                mock_create.assert_not_called()
                assert result is cached_engine

    def test_get_engine_instance_handles_creation_error(self):
        """_get_engine_instance handles engine creation errors."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            with patch(
                "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
                side_effect=Exception("Creation failed"),
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                result = engine._get_engine_instance("failing_engine")

                assert result is None


class TestMetaSearchEngineInvoke:
    """Tests for MetaSearchEngine invoke method."""

    def test_invoke_calls_run(self):
        """invoke method calls run."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            with patch.object(
                MetaSearchEngine, "run", return_value=[{"title": "Result"}]
            ) as mock_run:
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                result = engine.invoke("test query")

                mock_run.assert_called_once_with("test query")
                assert result == [{"title": "Result"}]


class TestMetaSearchEngineGetAvailableEngines:
    """Tests for MetaSearchEngine _get_available_engines method."""

    def test_get_available_engines_filters_meta_and_auto(self):
        """_get_available_engines filters out meta and auto."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.wikipedia.use_in_auto_search": {"value": True},
            "search.engine.web.meta.use_in_auto_search": {"value": True},
            "search.engine.web.auto.use_in_auto_search": {"value": True},
        }

        # Use a real instance to test _get_available_engines
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot"
        ) as mock_get_setting:

            def setting_side_effect(key, default, settings_snapshot=None):
                if "wikipedia" in key and "use_in_auto_search" in key:
                    return True
                return default

            mock_get_setting.side_effect = setting_side_effect

            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={"wikipedia": {}, "meta": {}, "auto": {}},
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

                assert "meta" not in engine.available_engines
                assert "auto" not in engine.available_engines

    def test_get_available_engines_raises_when_none_available(self):
        """_get_available_engines raises RuntimeError when no engines available."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )
        import pytest

        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=False,  # All engines disabled
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={"wikipedia": {}, "brave": {}},
            ):
                with pytest.raises(RuntimeError) as exc_info:
                    MetaSearchEngine(
                        llm=mock_llm,
                        settings_snapshot=settings,
                        programmatic_mode=True,
                    )

                assert "No search engines enabled" in str(exc_info.value)


class TestMetaSearchEngineGetSearchConfig:
    """Tests for MetaSearchEngine _get_search_config method."""

    def test_get_search_config_without_snapshot_returns_empty(self):
        """_get_search_config() returns {} when settings_snapshot=None.

        Regression test: previously this method called itself recursively
        when settings_snapshot was None, causing infinite recursion
        (RecursionError / stack overflow).
        """
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        mock_llm = Mock()
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.wikipedia.use_in_auto_search": {"value": True},
        }

        with patch.object(
            MetaSearchEngine,
            "_get_available_engines",
            return_value=["wikipedia"],
        ):
            engine = MetaSearchEngine(
                llm=mock_llm, settings_snapshot=settings, programmatic_mode=True
            )

        # Now set settings_snapshot to None and call _get_search_config
        engine.settings_snapshot = None

        # This should return {} instead of recursing infinitely
        result = engine._get_search_config()
        assert result == {}
