"""
Deep coverage tests for MetaSearchEngine targeting remaining uncovered statements.

Focuses on:
- analyze_query LLM invocation path (no SearXNG, LLM present, engines_info populated)
- LLM response parsing (.content attribute vs plain string)
- SearXNG fallback injection when LLM omits it
- empty engines_info branch (no matching engines in config)
- use_api_key_services=False filtering path
- _get_available_engines with all engines needing API keys disabled
- _get_engine_instance with max_filtered_results=None (not passed)
- analyze_query exception path when SearXNG absent
- all-engines-fail then fallback in _get_previews with error accumulation
"""

from unittest.mock import Mock, patch

from local_deep_research.web_search_engines.engines.meta_search_engine import (
    MetaSearchEngine,
)

MODULE = "local_deep_research.web_search_engines.engines.meta_search_engine"

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _bypass_init(available_engines=None, settings_snapshot=None, llm=None):
    """Create MetaSearchEngine bypassing __init__ for pure logic tests."""
    with patch.object(
        MetaSearchEngine, "__init__", lambda self, *a, **kw: None
    ):
        engine = MetaSearchEngine.__new__(MetaSearchEngine)
    engine.available_engines = (
        available_engines if available_engines is not None else []
    )
    engine.settings_snapshot = settings_snapshot
    engine.llm = llm
    engine.engine_cache = {}
    engine.max_results = 10
    engine.max_filtered_results = None
    engine.max_engines_to_try = 3
    engine.use_api_key_services = True
    engine.programmatic_mode = True
    engine.fallback_engine = Mock()
    return engine


# ===========================================================================
# 1. analyze_query - LLM invocation path (no SearXNG, LLM present)
# ===========================================================================


class TestAnalyzeQueryNoSearxngPath:
    """
    Tests for the analyze_query path when SearXNG is absent.

    Note: Lines 244-335 (LLM invocation) are structurally dead code in the
    current implementation — when SearXNG is absent line 231 always returns.
    These tests cover the reliability-sort path (lines 231-242) and the
    exception handler (lines 337-355) which are reached in practice.
    """

    def _make_engine_with_config(self, available_engines, config, llm=None):
        engine = _bypass_init(
            available_engines=available_engines,
            settings_snapshot={},
            llm=llm or Mock(),
        )
        engine._get_search_config = Mock(return_value=config)
        return engine

    def test_no_searxng_llm_present_uses_reliability_sort(self):
        """No SearXNG + LLM present still uses reliability-based sort (line 231 branch)."""
        config = {
            "brave": {"reliability": 0.6},
            "wikipedia": {"reliability": 0.9},
        }
        mock_llm = Mock()
        engine = self._make_engine_with_config(
            available_engines=["brave", "wikipedia"],
            config=config,
            llm=mock_llm,
        )

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

        # Line 231 fires (searxng not available), returns reliability sort
        mock_llm.invoke.assert_not_called()
        assert result[0] == "wikipedia"
        assert result[1] == "brave"

    def test_no_searxng_no_llm_uses_reliability_sort(self):
        """No SearXNG, no LLM: reliability sort is used."""
        config = {
            "brave": {"reliability": 0.5},
            "arxiv": {"reliability": 0.85},
        }
        engine = self._make_engine_with_config(
            available_engines=["brave", "arxiv"],
            config=config,
            llm=None,
        )

        result = engine.analyze_query("what is machine learning")
        assert result[0] == "arxiv"
        assert result[1] == "brave"

    def test_reliability_missing_defaults_to_zero(self):
        """Engines with no reliability in config default to 0 for sorting."""
        config = {
            "brave": {},  # no reliability key
            "wikipedia": {"reliability": 0.5},
        }
        engine = self._make_engine_with_config(
            available_engines=["brave", "wikipedia"],
            config=config,
            llm=None,
        )

        result = engine.analyze_query("some query without domain")
        assert result[0] == "wikipedia"
        assert result[1] == "brave"

    def test_searxng_present_sorts_remaining_by_reliability(self):
        """SearXNG available: searxng first, others sorted by reliability."""
        config = {
            "searxng": {"reliability": 1.0},
            "brave": {"reliability": 0.4},
            "wikipedia": {"reliability": 0.8},
        }
        engine = self._make_engine_with_config(
            available_engines=["brave", "searxng", "wikipedia"],
            config=config,
        )

        result = engine.analyze_query("general tech question")
        assert result[0] == "searxng"
        assert result[1] == "wikipedia"
        assert result[2] == "brave"

    def test_engines_info_config_keys_irrelevant_to_no_searxng_path(self):
        """Config has entries but no SearXNG still triggers reliability sort (not LLM)."""
        mock_llm = Mock()
        config = {
            "brave": {
                "description": "Brave",
                "strengths": "speed",
                "weaknesses": "none",
                "reliability": 0.5,
            },
            "wikipedia": {
                "description": "Wikipedia",
                "strengths": "accuracy",
                "weaknesses": "none",
                "reliability": 0.9,
            },
        }
        engine = self._make_engine_with_config(
            available_engines=["brave", "wikipedia"],
            config=config,
            llm=mock_llm,
        )

        result = engine.analyze_query("what is artificial intelligence")
        # No searxng -> line 231 branch, reliability sort, LLM never called
        assert set(result) == {"brave", "wikipedia"}
        mock_llm.invoke.assert_not_called()

    def test_searxng_first_with_no_config_for_others(self):
        """SearXNG + engines with no config entry still returns searxng first."""
        # Config has no entries for brave/arxiv
        engine = self._make_engine_with_config(
            available_engines=["brave", "searxng", "arxiv"],
            config={"searxng": {"reliability": 0.9}},
        )

        result = engine.analyze_query("general question")
        assert result[0] == "searxng"
        assert set(result) == {"brave", "searxng", "arxiv"}

    def test_query_exception_no_searxng_reliability_sort(self):
        """analyze_query exception without SearXNG returns reliability-ordered."""
        config = {
            "brave": {"reliability": 0.5},
            "wikipedia": {"reliability": 0.9},
        }
        mock_llm = Mock()
        engine = _bypass_init(
            available_engines=["brave", "wikipedia"],
            settings_snapshot={},
            llm=mock_llm,
        )
        engine._get_search_config = Mock(return_value=config)

        # Pass None to trigger exception in query.lower()
        result = engine.analyze_query(None)
        assert result[0] == "wikipedia"
        assert result[1] == "brave"

    def test_query_exception_with_searxng_reliability_sort_rest(self):
        """analyze_query exception with SearXNG returns searxng first, rest by reliability."""
        config = {
            "searxng": {"reliability": 1.0},
            "brave": {"reliability": 0.3},
            "wikipedia": {"reliability": 0.8},
        }
        mock_llm = Mock()
        engine = _bypass_init(
            available_engines=["searxng", "brave", "wikipedia"],
            settings_snapshot={},
            llm=mock_llm,
        )
        engine._get_search_config = Mock(return_value=config)

        result = engine.analyze_query(None)
        assert result[0] == "searxng"
        assert result[1] == "wikipedia"
        assert result[2] == "brave"


# ===========================================================================
# 2. _get_available_engines - use_api_key_services=False path
# ===========================================================================


class TestGetAvailableEnginesApiKeyServices:
    """Test the use_api_key_services flag via the shared get_available_engines."""

    def test_api_key_service_disabled_excludes_requiring_key(self):
        """use_api_key_services=False excludes engines requiring an API key."""
        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        # Simulate get_available_engines returning only wikipedia when
        # use_api_key_services=False (brave filtered out by shared fn)
        with patch(
            f"{MODULE}.get_available_engines",
            return_value={"wikipedia": {}},
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                settings_snapshot=settings,
                use_api_key_services=False,
                programmatic_mode=True,
            )

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

    def test_api_key_service_enabled_includes_valid_key_engine(self):
        """use_api_key_services=True includes engines with valid API keys."""
        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch(
            f"{MODULE}.get_available_engines",
            return_value={
                "brave": {
                    "requires_api_key": True,
                    "api_key": "valid_key_123",
                },
                "wikipedia": {},
            },
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                settings_snapshot=settings,
                use_api_key_services=True,
                programmatic_mode=True,
            )

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


# ===========================================================================
# 3. _get_engine_instance - max_filtered_results=None not passed
# ===========================================================================


class TestGetEngineInstanceMaxFilteredResults:
    """Test max_filtered_results handling in _get_engine_instance."""

    def test_max_filtered_results_value_passed_to_create(self):
        """max_filtered_results value is forwarded to create_search_engine."""
        engine = _bypass_init()
        engine.max_filtered_results = 7

        mock_created = Mock()
        with patch(
            f"{MODULE}.create_search_engine", return_value=mock_created
        ) as mock_create:
            result = engine._get_engine_instance("wikipedia")

        _, kwargs = mock_create.call_args
        assert kwargs["max_filtered_results"] == 7
        assert result is mock_created

    def test_programmatic_mode_forwarded_to_create(self):
        """programmatic_mode is forwarded to create_search_engine."""
        engine = _bypass_init()
        engine.max_filtered_results = 5

        mock_created = Mock()
        with patch(
            f"{MODULE}.create_search_engine", return_value=mock_created
        ) as mock_create:
            engine._get_engine_instance("brave")

        _, kwargs = mock_create.call_args
        assert kwargs["programmatic_mode"] is True

    def test_settings_snapshot_forwarded_to_create(self):
        """settings_snapshot is forwarded to create_search_engine."""
        snapshot = {"key": "value"}
        engine = _bypass_init(settings_snapshot=snapshot)
        engine.max_filtered_results = 5

        mock_created = Mock()
        with patch(
            f"{MODULE}.create_search_engine", return_value=mock_created
        ) as mock_create:
            engine._get_engine_instance("brave")

        _, kwargs = mock_create.call_args
        assert kwargs["settings_snapshot"] is snapshot


# ===========================================================================
# 4. _get_previews - all engines fail with accumulated errors
# ===========================================================================


class TestGetPreviewsAllEnginesFail:
    """Test the path where all engines fail and errors accumulate."""

    def test_all_engines_raise_exception_fallback_used(self):
        """When every engine raises, fallback is used and errors are accumulated."""
        engine = _bypass_init(available_engines=["e1", "e2", "e3"])
        engine.max_engines_to_try = 3

        call_log = []

        def mock_get_instance(name):
            call_log.append(name)
            mock = Mock()
            mock._get_previews.side_effect = TimeoutError(f"{name} timed out")
            return mock

        engine._get_engine_instance = mock_get_instance
        engine.fallback_engine._get_previews.return_value = [
            {"title": "Fallback Wikipedia", "url": "http://wiki.org"}
        ]

        with patch.object(
            engine, "analyze_query", return_value=["e1", "e2", "e3"]
        ):
            result = engine._get_previews("test query")

        # All 3 should have been tried
        assert call_log == ["e1", "e2", "e3"]
        # Fallback result returned
        assert result[0]["title"] == "Fallback Wikipedia"
        # Fallback engine is stored
        assert engine._selected_engine is engine.fallback_engine
        assert engine._selected_engine_name == "wikipedia"

    def test_mix_of_none_instance_and_exception_and_empty(self):
        """Mix of None instance, exception, and empty results all fall through to fallback."""
        engine = _bypass_init(
            available_engines=["none_engine", "exc_engine", "empty_engine"]
        )
        engine.max_engines_to_try = 3

        mock_exc = Mock()
        mock_exc._get_previews.side_effect = ValueError("bad config")
        mock_empty = Mock()
        mock_empty._get_previews.return_value = []

        def mock_get_instance(name):
            if name == "none_engine":
                return None
            if name == "exc_engine":
                return mock_exc
            return mock_empty

        engine._get_engine_instance = mock_get_instance
        engine.fallback_engine._get_previews.return_value = [
            {"title": "Wiki fallback"}
        ]

        with patch.object(
            engine,
            "analyze_query",
            return_value=["none_engine", "exc_engine", "empty_engine"],
        ):
            result = engine._get_previews("some query")

        assert result[0]["title"] == "Wiki fallback"


# ===========================================================================
# 5. _get_search_config with dict values containing nested dict
# ===========================================================================


class TestGetSearchConfigValueExtraction:
    """Test _get_search_config delegates to get_available_engines."""

    def test_delegates_to_shared_function(self):
        """_get_search_config returns whatever get_available_engines returns."""
        expected = {"brave": {"api_key": "mykey", "reliability": 0.85}}
        engine = _bypass_init(settings_snapshot={"search.max_results": 10})

        with patch(
            f"{MODULE}.get_available_engines",
            return_value=expected,
        ):
            config = engine._get_search_config()

        assert config == expected

    def test_returns_empty_without_snapshot(self):
        """Returns empty dict when settings_snapshot is empty/falsy."""
        engine = _bypass_init(settings_snapshot={})
        config = engine._get_search_config()
        assert config == {}


# ===========================================================================
# 6. analyze_query - searxng in exception handler with no searxng available
# ===========================================================================


class TestAnalyzeQueryExceptionHandlerPaths:
    """Test both branches of the exception handler in analyze_query."""

    def test_exception_with_searxng_available_sorts_rest_by_reliability(self):
        """Exception + SearXNG available: searxng first, rest sorted by reliability."""
        engine = _bypass_init(
            available_engines=["searxng", "brave", "wikipedia"],
        )
        engine._get_search_config = Mock(
            return_value={
                "brave": {"reliability": 0.3},
                "wikipedia": {"reliability": 0.8},
                "searxng": {"reliability": 0.5},
            }
        )
        # Pass None to trigger AttributeError on .lower()
        result = engine.analyze_query(None)

        assert result[0] == "searxng"
        # wikipedia has higher reliability
        assert result[1] == "wikipedia"
        assert result[2] == "brave"

    def test_exception_without_searxng_returns_reliability_ordered(self):
        """Exception + no SearXNG: returns all engines sorted by reliability."""
        engine = _bypass_init(
            available_engines=["brave", "arxiv", "pubmed"],
        )
        engine._get_search_config = Mock(
            return_value={
                "brave": {"reliability": 0.4},
                "arxiv": {"reliability": 0.9},
                "pubmed": {"reliability": 0.7},
            }
        )
        result = engine.analyze_query(None)

        assert result[0] == "arxiv"
        assert result[1] == "pubmed"
        assert result[2] == "brave"
