"""
Comprehensive pytest tests for MetaSearchEngine targeting coverage gaps.

Focuses on:
- Config extraction edge cases (_get_search_config)
- Engine filtering/selection (_get_available_engines)
- Result merging and fallback logic (_get_previews, _get_full_content)
- Error handling paths
- close() method
- analyze_query LLM invocation path (reachable when SearXNG absent + LLM truthy
  but with engines_info populated)
- Edge cases in engine instance creation
"""

import pytest
from unittest.mock import Mock, patch

from local_deep_research.config.constants import DEFAULT_MAX_FILTERED_RESULTS
from local_deep_research.web_search_engines.engines.meta_search_engine import (
    MetaSearchEngine,
)


# ---------------------------------------------------------------------------
# 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 or []
    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


def _make_engine_with_init(
    available_engines, llm=None, settings=None, **kwargs
):
    """Create MetaSearchEngine via __init__ but mock _get_available_engines."""
    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,
            **kwargs,
        )
    return engine


# ===========================================================================
# 1. _get_search_config edge cases
# ===========================================================================


class TestGetSearchConfigEdgeCases:
    """Tests for _get_search_config (now delegates to get_available_engines)."""

    def test_delegates_to_get_available_engines(self):
        """_get_search_config returns output of get_available_engines."""
        expected = {"brave": {"reliability": 0.9}}
        engine = _bypass_init(settings_snapshot={"search.max_results": 10})

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_available_engines",
            return_value=expected,
        ):
            config = engine._get_search_config()
        assert config == expected

    def test_empty_settings_snapshot_returns_empty(self):
        """Empty settings_snapshot returns empty dict."""
        engine = _bypass_init(settings_snapshot={})
        config = engine._get_search_config()
        assert config == {}

    def test_none_settings_snapshot_returns_empty(self):
        """None settings_snapshot returns empty dict."""
        engine = _bypass_init(settings_snapshot=None)
        config = engine._get_search_config()
        assert config == {}


# ===========================================================================
# 2. _get_available_engines edge cases (now via shared get_available_engines)
# ===========================================================================


class TestGetAvailableEnginesEdgeCases:
    """Edge cases in engine filtering via shared get_available_engines."""

    def test_api_key_empty_string_excluded(self):
        """Engine with requires_api_key=True and no key is excluded by shared filter."""
        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        # Shared function already filters brave (no valid key)
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_available_engines",
            return_value={"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_api_key_none_excluded(self):
        """Engine with requires_api_key=True and api_key=None is excluded."""
        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_available_engines",
            return_value={"wikipedia": {}},
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                settings_snapshot=settings,
                programmatic_mode=True,
            )

        assert "brave" not in engine.available_engines

    def test_requires_api_key_false_included_regardless(self):
        """Engine with requires_api_key=False included even without api_key."""
        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_available_engines",
            return_value={"brave": {"requires_api_key": False}},
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                settings_snapshot=settings,
                programmatic_mode=True,
            )

        assert "brave" in engine.available_engines

    def test_only_meta_and_auto_raises_runtime_error(self):
        """Only meta and auto in config raises RuntimeError."""
        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={"meta": {}, "auto": {}},
            ):
                with pytest.raises(
                    RuntimeError, match="No search engines enabled"
                ):
                    MetaSearchEngine(
                        llm=mock_llm,
                        settings_snapshot=settings,
                        programmatic_mode=True,
                    )

    def test_mixed_enabled_and_disabled_engines(self):
        """Mix of enabled and disabled engines filters correctly via shared function."""
        mock_llm = Mock()
        settings = {"search.max_results": {"value": 10}}

        # Shared function returns only enabled engines (brave, searxng)
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_available_engines",
            return_value={"brave": {}, "searxng": {}},
        ):
            engine = MetaSearchEngine(
                llm=mock_llm,
                settings_snapshot=settings,
                programmatic_mode=True,
            )

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


# ===========================================================================
# 3. analyze_query - LLM invocation path
# ===========================================================================


class TestAnalyzeQueryLLMPath:
    """Test the LLM invocation path in analyze_query.

    Note: The LLM path is only reachable when SearXNG is available AND
    the code doesn't short-circuit on the SearXNG general path.
    Actually, looking at the code more carefully: lines 216-228 always
    return when SearXNG is available. So the LLM path (line 244+)
    is unreachable in the current code. But we test the exception
    and other edge cases.
    """

    def test_analyze_query_exception_with_searxng_available(self):
        """Exception in analyze_query with SearXNG available returns SearXNG first."""
        engine = _bypass_init(
            available_engines=["searxng", "brave"],
            settings_snapshot={},
        )
        # Pass None to trigger AttributeError on .lower()
        result = engine.analyze_query(None)
        assert result[0] == "searxng"
        assert "brave" in result

    def test_analyze_query_exception_without_searxng(self):
        """Exception in analyze_query without SearXNG returns reliability-sorted."""
        engine = _bypass_init(
            available_engines=["brave", "wikipedia"],
        )
        engine._get_search_config = Mock(
            return_value={
                "brave": {"reliability": 0.6},
                "wikipedia": {"reliability": 0.9},
            }
        )
        result = engine.analyze_query(None)
        assert result[0] == "wikipedia"
        assert result[1] == "brave"

    def test_specialized_domain_no_available_engines_falls_through(self):
        """Specialized domain match with none available falls through."""
        engine = _bypass_init(
            available_engines=["searxng", "brave"],
            settings_snapshot={},
        )
        # "scientific paper" maps to [arxiv, pubmed, wikipedia] - none available
        result = engine.analyze_query("scientific paper on physics")
        # Falls through to SearXNG general path
        assert result[0] == "searxng"

    def test_specialized_domain_partial_match(self):
        """Only some specialized engines available."""
        engine = _bypass_init(available_engines=["wikipedia", "brave"])
        result = engine.analyze_query("scientific paper on biology")
        # Only wikipedia from [arxiv, pubmed, wikipedia]
        assert result == ["wikipedia"]

    def test_all_specialized_domains(self):
        """All specialized domain keywords produce expected results."""
        test_cases = [
            ("scientific paper on AI", ["arxiv", "pubmed", "wikipedia"]),
            ("medical research findings", ["pubmed", "searxng"]),
            ("clinical trial data", ["pubmed", "searxng"]),
            ("github actions workflow", ["github", "searxng"]),
            ("code repository for ML", ["github", "searxng"]),
            ("repository for NLP", ["github", "searxng"]),
            ("programming language design", ["github", "searxng"]),
        ]

        all_engines = [
            "arxiv",
            "pubmed",
            "wikipedia",
            "github",
            "searxng",
            "brave",
        ]
        for query, expected_engines in test_cases:
            engine = _bypass_init(available_engines=list(all_engines))
            result = engine.analyze_query(query)
            assert result == expected_engines, f"Failed for query: {query}"

    def test_arxiv_keyword_not_in_available(self):
        """'arxiv' in query but arxiv not available falls through."""
        engine = _bypass_init(
            available_engines=["searxng", "brave"],
            settings_snapshot={},
        )
        result = engine.analyze_query("arxiv paper on attention")
        # arxiv not available, falls through to SearXNG general path
        assert result[0] == "searxng"

    def test_pubmed_keyword_not_in_available(self):
        """'pubmed' in query but pubmed not available falls through."""
        engine = _bypass_init(
            available_engines=["searxng", "brave"],
            settings_snapshot={},
        )
        result = engine.analyze_query("pubmed articles on genetics")
        assert result[0] == "searxng"

    def test_general_query_no_searxng_no_llm_reliability_sort(self):
        """General query without SearXNG or LLM uses reliability sort."""
        engine = _bypass_init(
            available_engines=["brave", "wikipedia"],
            llm=None,
        )
        engine._get_search_config = Mock(
            return_value={
                "brave": {"reliability": 0.5},
                "wikipedia": {"reliability": 0.8},
            }
        )
        result = engine.analyze_query("what is the meaning of life")
        assert result[0] == "wikipedia"
        assert result[1] == "brave"

    def test_general_query_searxng_present_sorts_remaining(self):
        """General query with SearXNG returns SearXNG first, rest sorted."""
        engine = _bypass_init(
            available_engines=["brave", "searxng", "wikipedia", "arxiv"],
        )
        engine._get_search_config = Mock(
            return_value={
                "brave": {"reliability": 0.5},
                "wikipedia": {"reliability": 0.8},
                "arxiv": {"reliability": 0.3},
                "searxng": {"reliability": 0.9},
            }
        )
        result = engine.analyze_query("general knowledge question")
        assert result[0] == "searxng"
        assert result[1] == "wikipedia"
        assert result[2] == "brave"
        assert result[3] == "arxiv"


# ===========================================================================
# 4. _get_previews edge cases
# ===========================================================================


class TestGetPreviewsEdgeCases:
    """Edge cases in _get_previews."""

    def test_empty_ranked_engines_uses_fallback(self):
        """Empty ranked engines list uses fallback."""
        engine = _bypass_init(available_engines=[])
        engine.fallback_engine._get_previews.return_value = [
            {"title": "Wiki", "url": "http://wiki.com"}
        ]

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

        assert len(result) == 1
        assert result[0]["title"] == "Wiki"

    def test_max_engines_to_try_limits_attempts(self):
        """max_engines_to_try limits the number of engines tried."""
        engine = _bypass_init(available_engines=["e1", "e2", "e3", "e4", "e5"])
        engine.max_engines_to_try = 2

        call_order = []

        def mock_get_instance(name):
            call_order.append(name)
            mock = Mock()
            mock._get_previews.return_value = []  # All return empty
            return mock

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

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

        # Only 2 engines should be tried
        assert len(call_order) == 2
        assert call_order == ["e1", "e2"]

    def test_first_engine_succeeds_no_more_tried(self):
        """When first engine succeeds, no more engines are tried."""
        engine = _bypass_init(available_engines=["brave", "wikipedia"])

        call_count = []

        def mock_get_instance(name):
            call_count.append(name)
            mock = Mock()
            mock._get_previews.return_value = [
                {"title": f"Result from {name}", "url": f"http://{name}.com"}
            ]
            return mock

        engine._get_engine_instance = mock_get_instance

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

        assert len(call_count) == 1
        assert call_count[0] == "brave"
        assert result[0]["title"] == "Result from brave"

    def test_engine_returns_none_from_get_instance_skipped(self):
        """Engine that returns None from _get_engine_instance is skipped."""
        engine = _bypass_init(available_engines=["broken", "working"])

        mock_working = Mock()
        mock_working._get_previews.return_value = [
            {"title": "Working result", "url": "http://work.com"}
        ]

        def mock_get_instance(name):
            if name == "broken":
                return None
            return mock_working

        engine._get_engine_instance = mock_get_instance

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

        assert len(result) == 1
        assert result[0]["title"] == "Working result"

    def test_engine_raises_exception_skipped(self):
        """Engine that raises exception is skipped to next."""
        engine = _bypass_init(available_engines=["failing", "ok"])

        mock_failing = Mock()
        mock_failing._get_previews.side_effect = ConnectionError(
            "Network error"
        )

        mock_ok = Mock()
        mock_ok._get_previews.return_value = [
            {"title": "OK result", "url": "http://ok.com"}
        ]

        def mock_get_instance(name):
            if name == "failing":
                return mock_failing
            return mock_ok

        engine._get_engine_instance = mock_get_instance

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

        assert len(result) == 1
        assert result[0]["title"] == "OK result"

    def test_all_engines_return_empty_results_uses_fallback(self):
        """All engines returning empty results uses fallback."""
        engine = _bypass_init(available_engines=["e1", "e2"])

        def mock_get_instance(name):
            mock = Mock()
            mock._get_previews.return_value = []
            return mock

        engine._get_engine_instance = mock_get_instance
        engine.fallback_engine._get_previews.return_value = [
            {"title": "Fallback result"}
        ]

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

        assert result[0]["title"] == "Fallback result"
        assert engine._selected_engine is engine.fallback_engine
        assert engine._selected_engine_name == "wikipedia"

    def test_successful_engine_stores_selected_engine(self):
        """Successful engine is stored as _selected_engine."""
        engine = _bypass_init(available_engines=["brave"])

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

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

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

    def test_socket_io_error_non_critical(self):
        """SocketIO emit failure does not affect results."""
        engine = _bypass_init(available_engines=["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.object(engine, "analyze_query", return_value=["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 broken")
                )
                result = engine._get_previews("test")

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

    def test_none_previews_treated_as_empty(self):
        """Engine returning None previews is treated as no results."""
        engine = _bypass_init(available_engines=["e1"])

        mock_e = Mock()
        mock_e._get_previews.return_value = None

        engine._get_engine_instance = lambda name: mock_e
        engine.fallback_engine._get_previews.return_value = [
            {"title": "Fallback"}
        ]

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

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


# ===========================================================================
# 5. _get_full_content edge cases
# ===========================================================================


class TestGetFullContentEdgeCases:
    """Edge cases in _get_full_content."""

    def test_snippets_only_mode_returns_items_unchanged(self):
        """snippets_only=True returns items as-is."""
        engine = _bypass_init(
            settings_snapshot={"search.snippets_only": {"value": True}}
        )

        items = [{"title": "Test", "snippet": "content"}]
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=True,
        ):
            result = engine._get_full_content(items)
        assert result is items

    def test_selected_engine_used_for_full_content(self):
        """Selected engine is used to get full content."""
        engine = _bypass_init()
        mock_selected = Mock()
        mock_selected._get_full_content.return_value = [
            {"title": "Full", "full_content": "Full text"}
        ]
        engine._selected_engine = mock_selected
        engine._selected_engine_name = "brave"

        items = [{"title": "Test"}]
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=False,
        ):
            result = engine._get_full_content(items)

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

    def test_selected_engine_exception_returns_items(self):
        """Exception in selected engine returns items unchanged."""
        engine = _bypass_init()
        mock_selected = Mock()
        mock_selected._get_full_content.side_effect = RuntimeError(
            "Fetch failed"
        )
        engine._selected_engine = mock_selected
        engine._selected_engine_name = "brave"

        items = [{"title": "Test", "url": "http://example.com"}]
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=False,
        ):
            result = engine._get_full_content(items)

        assert result is items

    def test_no_selected_engine_returns_items(self):
        """No _selected_engine attribute returns items unchanged."""
        engine = _bypass_init()
        # Ensure _selected_engine is not set
        assert not hasattr(engine, "_selected_engine")

        items = [{"title": "Test"}]
        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.get_setting_from_snapshot",
            return_value=False,
        ):
            result = engine._get_full_content(items)

        assert result is items


# ===========================================================================
# 6. _get_engine_instance edge cases
# ===========================================================================


class TestGetEngineInstanceEdgeCases:
    """Edge cases in _get_engine_instance."""

    def test_cached_engine_returned_without_creating(self):
        """Cached engine returned without calling create_search_engine."""
        engine = _bypass_init()
        mock_cached = Mock()
        engine.engine_cache["brave"] = mock_cached

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine"
        ) as mock_create:
            result = engine._get_engine_instance("brave")

        mock_create.assert_not_called()
        assert result is mock_cached

    def test_new_engine_created_and_cached(self):
        """New engine is created and added to cache."""
        engine = _bypass_init()
        mock_new = Mock()

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            return_value=mock_new,
        ):
            result = engine._get_engine_instance("brave")

        assert result is mock_new
        assert engine.engine_cache["brave"] is mock_new

    def test_creation_failure_returns_none(self):
        """Engine creation failure returns None."""
        engine = _bypass_init()

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            side_effect=ValueError("Bad config"),
        ):
            result = engine._get_engine_instance("bad_engine")

        assert result is None
        assert "bad_engine" not in engine.engine_cache

    def test_max_filtered_results_none_defaults_to_constant(self):
        """max_filtered_results=None is coerced to DEFAULT_MAX_FILTERED_RESULTS."""
        engine = _bypass_init()
        engine.max_filtered_results = None
        # The property setter in BaseSearchEngine converts None -> DEFAULT_MAX_FILTERED_RESULTS
        assert engine.max_filtered_results == DEFAULT_MAX_FILTERED_RESULTS

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            return_value=Mock(),
        ) as mock_create:
            engine._get_engine_instance("brave")

        _, kwargs = mock_create.call_args
        # Always passed since the property never returns None
        assert kwargs["max_filtered_results"] == DEFAULT_MAX_FILTERED_RESULTS

    def test_max_filtered_results_set_is_passed(self):
        """max_filtered_results when set is passed to create_search_engine."""
        engine = _bypass_init()
        engine.max_filtered_results = 5

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            return_value=Mock(),
        ) as mock_create:
            engine._get_engine_instance("brave")

        _, kwargs = mock_create.call_args
        assert kwargs["max_filtered_results"] == 5

    def test_max_filtered_results_zero_is_passed(self):
        """max_filtered_results=0 (falsy but not None) is passed."""
        engine = _bypass_init()
        engine.max_filtered_results = 0

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            return_value=Mock(),
        ) as mock_create:
            engine._get_engine_instance("brave")

        _, kwargs = mock_create.call_args
        assert "max_filtered_results" in kwargs
        assert kwargs["max_filtered_results"] == 0

    def test_create_engine_returns_none_not_cached(self):
        """If create_search_engine returns None, it's not cached."""
        engine = _bypass_init()

        with patch(
            "local_deep_research.web_search_engines.engines.meta_search_engine.create_search_engine",
            return_value=None,
        ):
            result = engine._get_engine_instance("brave")

        assert result is None
        assert "brave" not in engine.engine_cache


# ===========================================================================
# 7. close() method
# ===========================================================================


class TestClose:
    """Tests for close() method."""

    def test_close_closes_cached_engines(self):
        """close() calls close() on all cached engines."""
        engine = _bypass_init()
        mock_e1 = Mock()
        mock_e2 = Mock()
        engine.engine_cache = {"e1": mock_e1, "e2": mock_e2}

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ):
            engine.close()

        mock_e1.close.assert_called_once()
        mock_e2.close.assert_called_once()

    def test_close_clears_cache(self):
        """close() clears the engine cache."""
        engine = _bypass_init()
        engine.engine_cache = {"e1": Mock(), "e2": Mock()}

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ):
            engine.close()

        assert engine.engine_cache == {}

    def test_close_closes_fallback_engine(self):
        """close() closes the fallback engine."""
        engine = _bypass_init()
        mock_fallback = Mock()
        engine.fallback_engine = mock_fallback

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ):
            engine.close()

        mock_fallback.close.assert_called_once()

    def test_close_handles_cached_engine_close_exception(self):
        """close() handles exception from cached engine close()."""
        engine = _bypass_init()
        mock_e1 = Mock()
        mock_e1.close.side_effect = RuntimeError("Close failed")
        mock_e2 = Mock()
        engine.engine_cache = {"e1": mock_e1, "e2": mock_e2}

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ):
            # Should not raise
            engine.close()

        # e2.close should still be called despite e1 raising
        mock_e2.close.assert_called_once()

    def test_close_handles_fallback_close_exception(self):
        """close() handles exception from fallback engine close()."""
        engine = _bypass_init()
        mock_fallback = Mock()
        mock_fallback.close.side_effect = RuntimeError("Fallback close failed")
        engine.fallback_engine = mock_fallback

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ):
            # Should not raise
            engine.close()

    def test_close_with_no_fallback_engine(self):
        """close() handles missing fallback_engine gracefully."""
        engine = _bypass_init()
        engine.fallback_engine = None

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ):
            # Should not raise
            engine.close()

    def test_close_with_empty_cache(self):
        """close() works with empty cache."""
        engine = _bypass_init()
        engine.engine_cache = {}

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ):
            engine.close()

        assert engine.engine_cache == {}

    def test_close_calls_super_close(self):
        """close() calls super().close()."""
        engine = _bypass_init()
        engine.engine_cache = {}

        with patch.object(
            type(engine).__bases__[0], "close", return_value=None
        ) as mock_super_close:
            engine.close()

        mock_super_close.assert_called_once()


# ===========================================================================
# 8. invoke() method
# ===========================================================================


class TestInvoke:
    """Tests for invoke() method."""

    def test_invoke_delegates_to_run(self):
        """invoke() calls self.run() with the query."""
        engine = _bypass_init()

        with patch.object(
            engine, "run", return_value=[{"title": "Result"}]
        ) as mock_run:
            result = engine.invoke("test query")

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

    def test_invoke_propagates_run_exception(self):
        """invoke() propagates exceptions from run()."""
        engine = _bypass_init()

        with patch.object(
            engine, "run", side_effect=RuntimeError("Search failed")
        ):
            with pytest.raises(RuntimeError, match="Search failed"):
                engine.invoke("test query")


# ===========================================================================
# 9. Integration-style tests (using __init__ with mocks)
# ===========================================================================


class TestInitIntegration:
    """Integration tests using actual __init__."""

    def test_init_stores_settings_snapshot(self):
        """__init__ stores settings_snapshot."""
        settings = {"search.max_results": {"value": 10}}
        engine = _make_engine_with_init(["wikipedia"], settings=settings)
        assert engine.settings_snapshot is settings

    def test_init_default_use_api_key_services(self):
        """Default use_api_key_services is True."""
        engine = _make_engine_with_init(["wikipedia"])
        assert engine.use_api_key_services is True

    def test_init_custom_use_api_key_services(self):
        """Custom use_api_key_services is respected."""
        engine = _make_engine_with_init(
            ["wikipedia"], use_api_key_services=False
        )
        assert engine.use_api_key_services is False

    def test_init_default_max_engines_to_try(self):
        """Default max_engines_to_try is 3."""
        engine = _make_engine_with_init(["wikipedia"])
        assert engine.max_engines_to_try == 3

    def test_init_custom_max_engines_to_try(self):
        """Custom max_engines_to_try is stored."""
        engine = _make_engine_with_init(["wikipedia"], max_engines_to_try=7)
        assert engine.max_engines_to_try == 7

    def test_init_creates_empty_engine_cache(self):
        """__init__ creates empty engine cache."""
        engine = _make_engine_with_init(["wikipedia"])
        assert engine.engine_cache == {}

    def test_init_accepts_extra_kwargs(self):
        """__init__ accepts and ignores extra kwargs."""
        # Should not raise
        engine = _make_engine_with_init(
            ["wikipedia"],
            extra_param="ignored",
            another_param=42,
        )
        assert engine is not None

    def test_init_with_max_filtered_results(self):
        """__init__ stores max_filtered_results."""
        engine = _make_engine_with_init(["wikipedia"], max_filtered_results=15)
        assert engine.max_filtered_results == 15

    def test_init_with_max_filtered_results_default(self):
        """__init__ defaults max_filtered_results to DEFAULT_MAX_FILTERED_RESULTS."""
        engine = _make_engine_with_init(["wikipedia"])
        assert engine.max_filtered_results == DEFAULT_MAX_FILTERED_RESULTS


# ===========================================================================
# 10. Analyze query with empty available_engines
# ===========================================================================


class TestAnalyzeQueryEmptyEngines:
    """Test analyze_query behavior with edge case engine configurations."""

    def test_empty_available_engines_returns_empty_list(self):
        """analyze_query with empty available_engines returns empty list."""
        engine = _bypass_init(available_engines=[], llm=None)
        result = engine.analyze_query("anything")
        assert result == []

    def test_single_engine_available_returned(self):
        """Single available engine is returned regardless of query."""
        engine = _bypass_init(available_engines=["brave"])
        result = engine.analyze_query("general question")
        # No SearXNG, no LLM -> reliability sort with single engine
        assert result == ["brave"]

    def test_arxiv_keyword_with_only_arxiv_available(self):
        """'arxiv' keyword with only arxiv available returns just arxiv."""
        engine = _bypass_init(available_engines=["arxiv"])
        result = engine.analyze_query("arxiv paper on NLP")
        assert result == ["arxiv"]

    def test_pubmed_keyword_with_only_pubmed_available(self):
        """'pubmed' keyword with only pubmed available returns just pubmed."""
        engine = _bypass_init(available_engines=["pubmed"])
        result = engine.analyze_query("pubmed article on COVID")
        assert result == ["pubmed"]

    def test_case_insensitive_specialized_detection(self):
        """Specialized domain detection is case-insensitive."""
        engine = _bypass_init(available_engines=["github", "searxng"])
        result = engine.analyze_query("GITHUB Repository for Deep Learning")
        assert "github" in result

    def test_specialized_before_explicit_keyword(self):
        """Specialized domain match takes priority over explicit keyword match."""
        engine = _bypass_init(
            available_engines=["arxiv", "pubmed", "wikipedia", "searxng"]
        )
        # "scientific paper" matches specialized domain before "arxiv" keyword check
        result = engine.analyze_query("scientific paper on arxiv about AI")
        assert result == ["arxiv", "pubmed", "wikipedia"]
