"""Tests for MetaSearchEngine — engine filtering, API key validation, query analysis, content retrieval."""

from unittest.mock import MagicMock, patch

import pytest

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


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


def _make_settings_snapshot(engines, auto_settings=None):
    """Build a settings_snapshot dict that MetaSearchEngine expects.

    Args:
        engines: dict mapping engine_name -> {config keys like requires_api_key, api_key, ...}
        auto_settings: dict of per-engine use_in_auto_search overrides (default all True)
    """
    snapshot = {}
    for name, config in engines.items():
        for key, value in config.items():
            snapshot[f"search.engine.web.{name}.{key}"] = value
        # Enable for auto search by default unless overridden
        if auto_settings and name in auto_settings:
            snapshot[f"search.engine.web.{name}.use_in_auto_search"] = (
                auto_settings[name]
            )
        else:
            snapshot[f"search.engine.web.{name}.use_in_auto_search"] = True
    return snapshot


def _make_meta_engine(settings_snapshot, use_api_key_services=True, llm=None):
    """Create MetaSearchEngine with mocked dependencies."""
    if llm is None:
        llm = MagicMock()
    with patch(
        "local_deep_research.web_search_engines.engines.meta_search_engine.WikipediaSearchEngine"
    ):
        return MetaSearchEngine(
            llm=llm,
            max_results=5,
            use_api_key_services=use_api_key_services,
            settings_snapshot=settings_snapshot,
        )


# ===========================================================================
# 1. _get_available_engines — basic filtering
# ===========================================================================


class TestGetAvailableEngines:
    """Verify engine filtering logic in _get_available_engines."""

    def test_meta_and_auto_excluded(self):
        """Engines named 'meta' and 'auto' are always excluded."""
        snapshot = _make_settings_snapshot(
            {
                "searxng": {},
                "meta": {},
                "auto": {},
            }
        )
        engine = _make_meta_engine(snapshot)
        assert "meta" not in engine.available_engines
        assert "auto" not in engine.available_engines
        assert "searxng" in engine.available_engines

    def test_engine_disabled_for_auto_search(self):
        """Engines with use_in_auto_search=False are excluded."""
        snapshot = _make_settings_snapshot(
            {"searxng": {}, "brave": {}},
            auto_settings={"brave": False},
        )
        engine = _make_meta_engine(snapshot)
        assert "brave" not in engine.available_engines
        assert "searxng" in engine.available_engines

    def test_no_engines_available_raises(self):
        """RuntimeError when no engines pass the filter."""
        snapshot = _make_settings_snapshot(
            {"brave": {}},
            auto_settings={"brave": False},
        )
        with pytest.raises(RuntimeError, match="No search engines enabled"):
            _make_meta_engine(snapshot)


# ===========================================================================
# 2. API key validation combinations
# ===========================================================================


class TestAPIKeyValidation:
    """Verify API key filtering logic."""

    def test_engine_requires_key_and_key_present(self):
        """Engine with required key and key present is included."""
        snapshot = _make_settings_snapshot(
            {
                "brave": {"requires_api_key": True, "api_key": "sk-xxx"},
            }
        )
        engine = _make_meta_engine(snapshot)
        assert "brave" in engine.available_engines

    def test_engine_requires_key_but_key_missing(self):
        """Engine with required key but no key is excluded."""
        snapshot = _make_settings_snapshot(
            {
                "brave": {"requires_api_key": True},
                "searxng": {},
            }
        )
        engine = _make_meta_engine(snapshot)
        assert "brave" not in engine.available_engines

    def test_engine_requires_key_but_api_services_disabled(self):
        """Engine requiring key excluded when use_api_key_services=False."""
        snapshot = _make_settings_snapshot(
            {
                "brave": {"requires_api_key": True, "api_key": "sk-xxx"},
                "searxng": {},
            }
        )
        engine = _make_meta_engine(snapshot, use_api_key_services=False)
        assert "brave" not in engine.available_engines
        assert "searxng" in engine.available_engines


# ===========================================================================
# 3. analyze_query — specialized domain detection
# ===========================================================================


class TestAnalyzeQuery:
    """Verify query analysis and engine selection logic."""

    def test_scientific_paper_query(self):
        """Query containing 'scientific paper' selects arxiv/pubmed if available."""
        snapshot = _make_settings_snapshot({"arxiv": {}, "searxng": {}})
        engine = _make_meta_engine(snapshot)
        result = engine.analyze_query(
            "latest scientific paper on quantum computing"
        )
        assert result[0] == "arxiv"

    def test_code_query_selects_github(self):
        """Query about code selects github if available."""
        snapshot = _make_settings_snapshot({"github": {}, "searxng": {}})
        engine = _make_meta_engine(snapshot)
        result = engine.analyze_query("python code for sorting algorithms")
        assert "github" in result

    def test_arxiv_keyword_prioritizes_arxiv(self):
        """Query containing 'arxiv' puts arxiv first."""
        snapshot = _make_settings_snapshot({"arxiv": {}, "searxng": {}})
        engine = _make_meta_engine(snapshot)
        result = engine.analyze_query("find arxiv papers on transformers")
        assert result[0] == "arxiv"

    def test_pubmed_keyword_prioritizes_pubmed(self):
        """Query containing 'pubmed' puts pubmed first."""
        snapshot = _make_settings_snapshot({"pubmed": {}, "searxng": {}})
        engine = _make_meta_engine(snapshot)
        result = engine.analyze_query("search pubmed for covid studies")
        assert result[0] == "pubmed"

    def test_general_query_prefers_searxng(self):
        """General queries prefer searxng when available."""
        snapshot = _make_settings_snapshot({"searxng": {}, "brave": {}})
        engine = _make_meta_engine(snapshot)
        result = engine.analyze_query("what is the weather today")
        assert result[0] == "searxng"

    def test_no_llm_falls_back_to_reliability(self):
        """Without LLM and without SearXNG, engines sorted by reliability."""
        snapshot = _make_settings_snapshot(
            {
                "brave": {"reliability": 0.9},
                "wikipedia": {"reliability": 0.7},
            }
        )
        engine = _make_meta_engine(snapshot, llm=None)
        engine.llm = None
        result = engine.analyze_query("anything")
        # Should be sorted by reliability (descending)
        assert result[0] == "brave"


# ===========================================================================
# 4. analyze_query — exception handling
# ===========================================================================


class TestAnalyzeQueryExceptions:
    """Verify exception handling in analyze_query."""

    def test_llm_exception_falls_back_to_searxng(self):
        """LLM error falls back to searxng-first ordering."""
        snapshot = _make_settings_snapshot(
            {
                "searxng": {"strengths": "general", "description": "meta"},
                "brave": {"strengths": "web", "description": "search"},
            }
        )
        llm = MagicMock()
        llm.invoke.side_effect = RuntimeError("LLM down")
        engine = _make_meta_engine(snapshot, llm=llm)

        result = engine.analyze_query("test query")
        assert result[0] == "searxng"

    def test_llm_returns_empty_response(self):
        """LLM returns empty string — falls back gracefully."""
        snapshot = _make_settings_snapshot(
            {
                "searxng": {"strengths": "general", "description": "meta"},
            }
        )
        llm = MagicMock()
        llm.invoke.return_value = MagicMock(content="")
        engine = _make_meta_engine(snapshot, llm=llm)

        result = engine.analyze_query("test query")
        # searxng should still be included via the fallback
        assert "searxng" in result

    def test_llm_returns_invalid_engine_names(self):
        """LLM returns engine names that don't exist — falls back gracefully."""
        snapshot = _make_settings_snapshot(
            {
                "searxng": {"strengths": "general", "description": "meta"},
            }
        )
        llm = MagicMock()
        llm.invoke.return_value = MagicMock(content="nonexistent1,nonexistent2")
        engine = _make_meta_engine(snapshot, llm=llm)

        result = engine.analyze_query("test query")
        # searxng should be added as fallback since no valid engines were selected
        assert "searxng" in result


# ===========================================================================
# 5. _get_full_content — failure scenarios
# ===========================================================================


class TestGetFullContent:
    """Verify _get_full_content failure paths."""

    def test_snippets_only_skips_content(self):
        """When snippets_only=True, items returned as-is."""
        snapshot = _make_settings_snapshot({"searxng": {}})
        snapshot["search.snippets_only"] = True
        engine = _make_meta_engine(snapshot)

        items = [{"title": "Test", "snippet": "content"}]
        result = engine._get_full_content(items)
        assert result == items

    def test_selected_engine_exception_returns_items(self):
        """Exception in selected engine returns items unchanged."""
        snapshot = _make_settings_snapshot({"searxng": {}})
        snapshot["search.snippets_only"] = False
        engine = _make_meta_engine(snapshot)

        mock_selected = MagicMock()
        mock_selected._get_full_content.side_effect = RuntimeError("failed")
        engine._selected_engine = mock_selected
        engine._selected_engine_name = "searxng"

        items = [{"title": "Test"}]
        result = engine._get_full_content(items)
        assert result == items

    def test_no_selected_engine_returns_items(self):
        """No selected engine returns items unchanged."""
        snapshot = _make_settings_snapshot({"searxng": {}})
        snapshot["search.snippets_only"] = False
        engine = _make_meta_engine(snapshot)

        # Ensure _selected_engine is not set
        if hasattr(engine, "_selected_engine"):
            delattr(engine, "_selected_engine")

        items = [{"title": "Test"}]
        result = engine._get_full_content(items)
        assert result == items


# ===========================================================================
# 6. _get_engine_instance — caching and failure
# ===========================================================================


class TestGetEngineInstance:
    """Verify engine instance caching and creation failure."""

    def test_cached_engine_returned(self):
        """Cached engine instance is reused."""
        snapshot = _make_settings_snapshot({"searxng": {}})
        engine = _make_meta_engine(snapshot)

        mock_cached = MagicMock()
        engine.engine_cache["test_engine"] = mock_cached

        result = engine._get_engine_instance("test_engine")
        assert result is mock_cached

    @patch(
        "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine"
    )
    def test_creation_failure_returns_none(self, mock_create):
        """Engine creation failure returns None."""
        mock_create.side_effect = RuntimeError("init failed")
        snapshot = _make_settings_snapshot({"searxng": {}})
        engine = _make_meta_engine(snapshot)

        result = engine._get_engine_instance("bad_engine")
        assert result is None

    @patch(
        "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine"
    )
    def test_max_filtered_results_zero_passed(self, mock_create):
        """max_filtered_results=0 (falsy) should NOT be passed when it's None check."""
        mock_create.return_value = MagicMock()
        snapshot = _make_settings_snapshot({"searxng": {}})

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.WikipediaSearchEngine"
        ):
            engine = MetaSearchEngine(
                llm=MagicMock(),
                max_results=5,
                max_filtered_results=0,
                settings_snapshot=snapshot,
            )

        engine._get_engine_instance("new_engine")
        # max_filtered_results=0 is not None, so should be passed
        call_kwargs = mock_create.call_args
        # 0 is not None, so the "if self.max_filtered_results is not None" check passes
        # and max_filtered_results should be in the params
        assert "max_filtered_results" in (
            call_kwargs.kwargs if call_kwargs.kwargs else call_kwargs[1]
        )


# ===========================================================================
# 7. _get_previews — engine fallthrough
# ===========================================================================


class TestGetPreviews:
    """Verify _get_previews tries engines in order and falls back."""

    def test_first_engine_succeeds(self):
        """First engine returning results is used."""
        snapshot = _make_settings_snapshot({"searxng": {}})
        engine = _make_meta_engine(snapshot)

        mock_search = MagicMock()
        mock_search._get_previews.return_value = [{"title": "Result 1"}]
        engine._get_engine_instance = MagicMock(return_value=mock_search)

        with patch.object(engine, "analyze_query", return_value=["searxng"]):
            with patch(
                "local_deep_research.web_search_engines.engines.meta_search_engine.SocketIOService"
            ):
                result = engine._get_previews("test query")

        assert len(result) == 1

    def test_all_engines_fail_uses_fallback(self):
        """All engines failing falls back to Wikipedia."""
        snapshot = _make_settings_snapshot({"searxng": {}})
        engine = _make_meta_engine(snapshot)
        engine._get_engine_instance = MagicMock(return_value=None)

        mock_fallback = MagicMock()
        mock_fallback._get_previews.return_value = [
            {"title": "Wikipedia result"}
        ]
        engine.fallback_engine = mock_fallback

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

        assert result[0]["title"] == "Wikipedia result"
