"""Extended tests for meta_search_engine.py - targeting untested paths.

Covers:
- analyze_query() LLM invocation path (lines 256-346)
- analyze_query() LLM response parsing (content attr vs str)
- analyze_query() SearXNG fallback when LLM omits it
- analyze_query() no valid engines from LLM → reliability sort
- analyze_query() exception → fallback
- analyze_query() no engines_info → reliability sort
- _get_search_config() from settings_snapshot
- _get_available_engines() API key filtering, local engine path
- _get_full_content() exception in selected engine
- _get_previews() engine init failure → skip
"""

from unittest.mock import Mock, patch


def _make_engine(available_engines, llm=None, settings=None):
    """Helper to create MetaSearchEngine with mocked _get_available_engines."""
    from local_deep_research.web_search_engines.engines.meta_search_engine import (
        MetaSearchEngine,
    )

    if llm is None:
        llm = Mock()
    if settings is None:
        settings = {"search.max_results": {"value": 10}}

    with patch.object(
        MetaSearchEngine,
        "_get_available_engines",
        return_value=available_engines,
    ):
        engine = MetaSearchEngine(
            llm=llm,
            settings_snapshot=settings,
            programmatic_mode=True,
        )

    return engine


# ── analyze_query: LLM invocation path ──────────────────────────


class TestAnalyzeQueryAdvancedPaths:
    """Tests for analyze_query paths not covered by existing tests.

    Note: The LLM invocation path (lines 255-346) is currently unreachable
    because line 227 catches SearXNG-present case and line 242 catches
    SearXNG-absent case, both returning before LLM is invoked. These tests
    cover the reachable but untested paths.
    """

    def test_no_llm_no_searxng_reliability_sort(self):
        """When LLM is None and SearXNG unavailable, reliability sort is used."""
        engine = _make_engine(["brave", "wikipedia"], llm=None)

        engine._get_search_config = lambda: {
            "brave": {"reliability": 0.8},
            "wikipedia": {"reliability": 0.95},
        }

        result = engine.analyze_query("query without llm")

        assert result[0] == "wikipedia"
        assert result[1] == "brave"

    def test_truthy_llm_no_searxng_reliability_sort(self):
        """When LLM is truthy but SearXNG absent, reliability sort is used (line 242)."""
        mock_llm = Mock()

        engine = _make_engine(["brave", "wikipedia"], llm=mock_llm)
        engine._get_search_config = lambda: {
            "brave": {"reliability": 0.6},
            "wikipedia": {"reliability": 0.9},
        }

        result = engine.analyze_query("general question about history")

        # Line 242 catches this: "searxng" not in available_engines → True
        assert result[0] == "wikipedia"
        assert result[1] == "brave"
        mock_llm.invoke.assert_not_called()

    def test_searxng_present_reliability_sort_for_remaining(self):
        """SearXNG present → first, remaining sorted by reliability (line 227)."""
        engine = _make_engine(["searxng", "brave", "wikipedia"])
        engine._get_search_config = lambda: {
            "brave": {"reliability": 0.9},
            "wikipedia": {"reliability": 0.7},
        }

        result = engine.analyze_query("general web search")

        assert result[0] == "searxng"
        assert result[1] == "brave"
        assert result[2] == "wikipedia"

    def test_multiple_specialized_terms_first_match_wins(self):
        """First matching specialized term in dict order determines engines."""
        engine = _make_engine(["github", "pubmed", "searxng"])

        # "clinical" appears before "code" in the specialized_domains dict,
        # so "clinical" match wins
        result = engine.analyze_query("code for clinical analysis")

        assert result[0] == "pubmed"

    def test_specialized_match_skips_unavailable_engines(self):
        """Specialized match only includes available engines."""
        engine = _make_engine(["wikipedia", "searxng"])

        # "scientific paper" wants arxiv, pubmed, wikipedia
        result = engine.analyze_query("scientific paper on climate")

        # Only wikipedia is available from the specialized list
        assert result == ["wikipedia"]

    def test_exception_in_analyze_with_searxng_available(self):
        """Exception in try block with SearXNG → SearXNG first + reliability."""
        engine = _make_engine(["searxng", "brave"])
        engine._get_search_config = lambda: {
            "searxng": {"reliability": 0.7},
            "brave": {"reliability": 0.9},
        }

        # Cause exception by passing non-string query (no .lower() method)
        result = engine.analyze_query(None)

        assert result[0] == "searxng"
        assert result[1] == "brave"

    def test_exception_in_analyze_without_searxng(self):
        """Exception in try block without SearXNG → reliability sort."""
        engine = _make_engine(["brave", "wikipedia"])
        engine._get_search_config = lambda: {
            "brave": {"reliability": 0.6},
            "wikipedia": {"reliability": 0.9},
        }

        # Cause exception by passing non-string query
        result = engine.analyze_query(None)

        assert result[0] == "wikipedia"
        assert result[1] == "brave"

    def test_arxiv_keyword_puts_arxiv_first_includes_others(self):
        """'arxiv' keyword → arxiv first, all others follow."""
        engine = _make_engine(["arxiv", "wikipedia", "brave"])

        result = engine.analyze_query("find arxiv papers on transformers")

        assert result[0] == "arxiv"
        assert set(result[1:]) == {"wikipedia", "brave"}

    def test_pubmed_keyword_puts_pubmed_first_includes_others(self):
        """'pubmed' keyword → pubmed first, all others follow."""
        engine = _make_engine(["pubmed", "wikipedia", "brave"])

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

        assert result[0] == "pubmed"
        assert set(result[1:]) == {"wikipedia", "brave"}


# ── _get_search_config from settings_snapshot ────────────────────


class TestGetSearchConfigFromSnapshot:
    """Tests for _get_search_config using settings_snapshot."""

    def test_extracts_web_engine_configs(self):
        """Engine configs are extracted from settings_snapshot keys."""
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.brave.class_name": {"value": "BraveSearch"},
            "search.engine.web.brave.api_key": {"value": "key123"},
            "search.engine.web.wikipedia.class_name": {"value": "WikiSearch"},
        }

        engine = _make_engine(["brave", "wikipedia"], settings=settings)
        config = engine._get_search_config()

        assert "brave" in config
        assert config["brave"]["class_name"] == "BraveSearch"
        assert config["brave"]["api_key"] == "key123"
        assert "wikipedia" in config

    def test_extracts_auto_engine_config(self):
        """Auto engine config is extracted separately."""
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.auto.class_name": "MetaSearchEngine",
            "search.engine.auto.max_engines": "3",
        }

        engine = _make_engine(["wikipedia"], settings=settings)
        config = engine._get_search_config()

        assert "auto" in config
        assert config["auto"]["class_name"] == "MetaSearchEngine"

    def test_handles_dict_value_format(self):
        """Values that are dicts have their 'value' key extracted."""
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.brave.timeout": {"value": 30},
        }

        engine = _make_engine(["brave"], settings=settings)
        config = engine._get_search_config()

        assert config["brave"]["timeout"] == 30

    def test_handles_plain_value_format(self):
        """Plain (non-dict) values are used directly."""
        settings = {
            "search.max_results": {"value": 10},
            "search.engine.web.brave.timeout": 30,
        }

        engine = _make_engine(["brave"], settings=settings)
        config = engine._get_search_config()

        assert config["brave"]["timeout"] == 30


# ── _get_available_engines: API key and local engines ────────────


class TestGetAvailableEnginesFiltering:
    """Tests for _get_available_engines API key and local engine handling."""

    def test_requires_api_key_but_missing_excluded(self):
        """Engine with requires_api_key=True but no key is excluded."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        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"
        ) as mock_gs:
            mock_gs.return_value = True  # All engines enabled for auto search

            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={
                    "brave": {"requires_api_key": True, "api_key": None},
                    "wikipedia": {},
                },
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

        assert "brave" not in engine.available_engines
        assert "wikipedia" in engine.available_engines

    def test_requires_api_key_with_key_present_included(self):
        """Engine with requires_api_key=True and key present is included."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        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"
        ) as mock_gs:
            mock_gs.return_value = True

            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={
                    "brave": {"requires_api_key": True, "api_key": "sk-123"},
                },
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

        assert "brave" in engine.available_engines

    def test_use_api_key_services_false_skips_api_engines(self):
        """When use_api_key_services=False, API key engines are excluded."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

        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"
        ) as mock_gs:
            mock_gs.return_value = True

            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={
                    "brave": {"requires_api_key": True, "api_key": "key"},
                    "wikipedia": {},
                },
            ):
                engine = MetaSearchEngine(
                    llm=mock_llm,
                    use_api_key_services=False,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

        assert "brave" not in engine.available_engines
        assert "wikipedia" in engine.available_engines

    def test_local_engine_setting_path(self):
        """Local engines use correct setting path format."""
        from local_deep_research.web_search_engines.engines.meta_search_engine import (
            MetaSearchEngine,
        )

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

        setting_keys_checked = []

        def capture_setting(key, default, settings_snapshot=None):
            setting_keys_checked.append(key)
            return True  # Enable all

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            side_effect=capture_setting,
        ):
            with patch.object(
                MetaSearchEngine,
                "_get_search_config",
                return_value={
                    "local.myfiles": {},
                    "wikipedia": {},
                },
            ):
                MetaSearchEngine(
                    llm=mock_llm,
                    settings_snapshot=settings,
                    programmatic_mode=True,
                )

        # local engine should check "search.engine.local.myfiles.use_in_auto_search"
        assert any(
            "search.engine.local.myfiles.use_in_auto_search" in k
            for k in setting_keys_checked
        )
        # web engine should check "search.engine.web.wikipedia.use_in_auto_search"
        assert any(
            "search.engine.web.wikipedia.use_in_auto_search" in k
            for k in setting_keys_checked
        )


# ── _get_full_content: exception in selected engine ──────────────


class TestGetFullContentException:
    """Tests for _get_full_content exception handling."""

    def test_exception_in_selected_engine_returns_items(self):
        """Exception in selected engine → returns items without full content."""
        engine = _make_engine(["wikipedia"])

        mock_selected = Mock()
        mock_selected._get_full_content.side_effect = RuntimeError(
            "Fetch failed"
        )
        engine._selected_engine = mock_selected
        engine._selected_engine_name = "wikipedia"

        items = [{"title": "Result", "url": "http://example.com"}]

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=False,  # Not snippets_only
        ):
            result = engine._get_full_content(items)

        assert result == items


# ── _get_previews: engine init failure ───────────────────────────


class TestGetPreviewsEngineInitFailure:
    """Tests for _get_previews when engine initialization fails."""

    def test_init_failure_skips_to_next_engine(self):
        """Failed engine init skips to next engine in list."""
        engine = _make_engine(["broken", "working"])

        mock_working = Mock()
        mock_working._get_previews.return_value = [
            {"title": "From working", "url": "http://ok.com"}
        ]

        def get_instance(name):
            if name == "broken":
                return None  # Init failed
            return mock_working

        engine._get_engine_instance = get_instance

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.SocketIOService"
        ):
            results = engine._get_previews("test query")

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

    def test_all_engines_fail_uses_fallback(self):
        """All engines failing → Wikipedia fallback is used."""
        engine = _make_engine(["bad1", "bad2"])

        engine._get_engine_instance = lambda name: None  # All fail

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

        results = engine._get_previews("test query")

        assert results[0]["title"] == "Wiki fallback"
        assert engine._selected_engine_name == "wikipedia"

    def test_successful_engine_stores_selection(self):
        """Successful engine is stored as _selected_engine."""
        engine = _make_engine(["brave"])

        mock_brave = Mock()
        mock_brave._get_previews.return_value = [
            {"title": "Brave result", "url": "http://brave.com"}
        ]
        engine._get_engine_instance = lambda name: mock_brave

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.SocketIOService"
        ):
            engine._get_previews("test query")

        assert engine._selected_engine is mock_brave
        assert engine._selected_engine_name == "brave"

    def test_socket_emit_failure_non_critical(self):
        """SocketIO emit failure doesn't affect results."""
        engine = _make_engine(["brave"])

        mock_brave = Mock()
        mock_brave._get_previews.return_value = [
            {"title": "Result", "url": "http://test.com"}
        ]
        engine._get_engine_instance = lambda name: mock_brave

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.SocketIOService"
        ) as mock_sio:
            mock_sio.return_value.emit_socket_event.side_effect = RuntimeError(
                "Socket error"
            )
            results = engine._get_previews("test query")

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