"""
Comprehensive tests for search_engine_factory.py covering gaps not
addressed by existing test_search_engine_factory.py and
test_factory_full_search_wrapper.py.

Focus areas:
- API key retrieval paths (settings dict, plain value, engine_config fallback)
- LLM pass-through when not required but engine accepts it
- kwargs overriding default_params
- max_results default logic end-to-end
- Display label fallback end-to-end through create_search_engine
- use_full_search triggering the wrapper path
- LLM relevance filter application on engine instances
- get_search parameter routing for wikinews, max_filtered_results
- Edge cases: empty default_params, engine with **kwargs signature
"""

import inspect as _inspect
from unittest.mock import Mock, patch

import pytest


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


def _make_engine_class(*param_names, class_attrs=None):
    """Create a real class with a controlled __init__ signature.

    Builds a class whose inspect.signature(__init__) exposes exactly
    the requested parameter names, but whose __init__ actually accepts
    **kwargs so the factory can instantiate it.
    """
    params = [
        _inspect.Parameter("self", _inspect.Parameter.POSITIONAL_OR_KEYWORD)
    ]
    for name in param_names:
        params.append(
            _inspect.Parameter(
                name, _inspect.Parameter.POSITIONAL_OR_KEYWORD, default=None
            )
        )

    class _Eng:
        _call_kwargs = None

        def __init__(self, **kwargs):
            _Eng._call_kwargs = kwargs
            # Set kwargs as instance attributes so the factory can inspect them
            # (e.g. hasattr(engine, "llm") and engine.llm)
            for k, v in kwargs.items():
                setattr(self, k, v)

    _Eng.__init__.__signature__ = _inspect.Signature(params)

    # Attach optional class-level attributes (e.g. is_scientific)
    if class_attrs:
        for k, v in class_attrs.items():
            setattr(_Eng, k, v)

    return _Eng


def _engine_config(**overrides):
    """Return a minimal engine configuration dict."""
    base = {
        "module_path": "some.module",
        "class_name": "SomeEngine",
        "default_params": {},
    }
    base.update(overrides)
    return base


def _patches(registry_return=None, config_return=None, class_return=None):
    """Context-manager helper that patches registry, search_config, and get_safe_module_class."""
    import contextlib

    @contextlib.contextmanager
    def _ctx():
        # Mock evaluate_engine so these factory tests (which use fake
        # engine names like "myeng") don't get rejected by the egress
        # PEP for not being in ENGINE_REGISTRY. The PEP is exercised
        # separately in tests/security/test_egress_policy.py.
        from local_deep_research.security.egress.policy import Decision

        with (
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
            ) as mock_reg,
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.search_config"
            ) as mock_sc,
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class"
            ) as mock_gsmc,
            patch(
                "local_deep_research.security.egress.policy.evaluate_engine",
                return_value=Decision(True, "test_bypass"),
            ),
        ):
            mock_reg.get.return_value = registry_return
            if config_return is not None:
                mock_sc.return_value = config_return
            if class_return is not None:
                mock_gsmc.return_value = class_return
            yield mock_reg, mock_sc, mock_gsmc

    return _ctx()


# ---------------------------------------------------------------------------
# Tests: API key retrieval
# ---------------------------------------------------------------------------


class TestApiKeyRetrieval:
    """Test the various paths for obtaining API keys."""

    def test_api_key_from_settings_snapshot_dict_format(self):
        """API key found in settings_snapshot as {value: ...} dict."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("api_key", "max_results")
        snapshot = {
            "search.engine.web.myeng.api_key": {"value": "secret-key-123"},
        }

        with _patches(
            config_return={"myeng": _engine_config(requires_api_key=True)},
            class_return=EngCls,
        ):
            result = create_search_engine("myeng", settings_snapshot=snapshot)

        assert result is not None
        assert EngCls._call_kwargs["api_key"] == "secret-key-123"

    def test_api_key_from_settings_snapshot_plain_string(self):
        """API key found in settings_snapshot as a plain string."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("api_key", "max_results")
        snapshot = {
            "search.engine.web.myeng.api_key": "plain-key-456",
        }

        with _patches(
            config_return={"myeng": _engine_config(requires_api_key=True)},
            class_return=EngCls,
        ):
            result = create_search_engine("myeng", settings_snapshot=snapshot)

        assert result is not None
        assert EngCls._call_kwargs["api_key"] == "plain-key-456"

    def test_api_key_fallback_to_engine_config(self):
        """API key not in settings_snapshot but present in engine_config."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("api_key", "max_results")
        snapshot = {"dummy": "value"}  # no api_key setting

        with _patches(
            config_return={
                "myeng": _engine_config(
                    requires_api_key=True, api_key="from-config"
                )
            },
            class_return=EngCls,
        ):
            result = create_search_engine("myeng", settings_snapshot=snapshot)

        assert result is not None
        assert EngCls._call_kwargs["api_key"] == "from-config"


# ---------------------------------------------------------------------------
# Tests: LLM pass-through
# ---------------------------------------------------------------------------


class TestLlmPassThrough:
    """Test LLM injection into engine constructors."""

    def test_llm_passed_when_requires_llm_true(self):
        """LLM is always injected when requires_llm=True."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("llm", "max_results")
        mock_llm = Mock()

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            create_search_engine(
                "eng", llm=mock_llm, settings_snapshot={"x": 1}
            )

        assert EngCls._call_kwargs["llm"] is mock_llm

    def test_llm_passed_when_not_required_but_accepted_and_provided(self):
        """LLM is passed through when engine accepts it and it was provided,
        even without requires_llm=True."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("llm", "max_results")
        mock_llm = Mock()

        with _patches(
            config_return={"eng": _engine_config()},  # no requires_llm
            class_return=EngCls,
        ):
            create_search_engine(
                "eng", llm=mock_llm, settings_snapshot={"x": 1}
            )

        assert EngCls._call_kwargs["llm"] is mock_llm

    def test_llm_not_passed_when_engine_does_not_accept_it(self):
        """LLM is filtered out when engine __init__ doesn't list it."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")  # no 'llm' param
        mock_llm = Mock()

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            create_search_engine(
                "eng", llm=mock_llm, settings_snapshot={"x": 1}
            )

        assert "llm" not in EngCls._call_kwargs

    def test_llm_none_still_injected_when_requires_llm_true(self):
        """When requires_llm=True but llm=None, llm=None is still passed
        (engine handles degraded mode)."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("llm", "max_results")

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=None, settings_snapshot={"x": 1}
            )

        assert result is not None
        assert EngCls._call_kwargs["llm"] is None


# ---------------------------------------------------------------------------
# Tests: kwargs override default_params
# ---------------------------------------------------------------------------


class TestKwargsOverrideDefaults:
    """Test that kwargs override default_params from config."""

    def test_kwargs_override_default_params(self):
        """User-supplied kwargs should take precedence over default_params."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results", "region")

        with _patches(
            config_return={
                "eng": _engine_config(
                    default_params={"max_results": 5, "region": "us"}
                )
            },
            class_return=EngCls,
        ):
            create_search_engine(
                "eng",
                settings_snapshot={"x": 1},
                max_results=42,
            )

        # max_results from kwargs (42) should override default (5)
        assert EngCls._call_kwargs["max_results"] == 42
        # region from default_params should still be present
        assert EngCls._call_kwargs["region"] == "us"

    def test_empty_default_params(self):
        """Engine with no default_params should still work."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")

        with _patches(
            config_return={"eng": _engine_config(default_params={})},
            class_return=EngCls,
        ):
            create_search_engine(
                "eng", settings_snapshot={"x": 1}, max_results=10
            )

        assert EngCls._call_kwargs["max_results"] == 10

    def test_missing_default_params_key(self):
        """Config without default_params key should use empty dict."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")
        cfg = _engine_config()
        del cfg["default_params"]

        with _patches(
            config_return={"eng": cfg},
            class_return=EngCls,
        ):
            create_search_engine(
                "eng", settings_snapshot={"x": 1}, max_results=7
            )

        assert EngCls._call_kwargs["max_results"] == 7


# ---------------------------------------------------------------------------
# Tests: max_results defaults
# ---------------------------------------------------------------------------


class TestMaxResultsDefaults:
    """Test max_results default resolution through the actual function."""

    def test_max_results_from_settings_dict_value(self):
        """max_results from settings_snapshot as {value: N}."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")
        snapshot = {"search.max_results": {"value": 33}}

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            create_search_engine("eng", settings_snapshot=snapshot)

        assert EngCls._call_kwargs["max_results"] == 33

    def test_max_results_from_settings_plain_int(self):
        """max_results from settings_snapshot as plain integer."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")
        snapshot = {"search.max_results": 17}

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            create_search_engine("eng", settings_snapshot=snapshot)

        assert EngCls._call_kwargs["max_results"] == 17

    def test_max_results_default_20_when_not_in_settings(self):
        """max_results defaults to 20 when not in settings_snapshot."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")
        snapshot = {"dummy": "value"}

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            create_search_engine("eng", settings_snapshot=snapshot)

        assert EngCls._call_kwargs["max_results"] == 20

    def test_explicit_max_results_kwarg_not_overridden(self):
        """Explicit max_results kwarg should not be overridden by defaults."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")
        snapshot = {"search.max_results": {"value": 50}}

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            create_search_engine(
                "eng", settings_snapshot=snapshot, max_results=5
            )

        # Explicit kwarg should win
        assert EngCls._call_kwargs["max_results"] == 5


# ---------------------------------------------------------------------------
# Tests: Display label fallback (end-to-end)
# ---------------------------------------------------------------------------


class TestDisplayLabelFallback:
    """Test display label -> config key resolution through the actual function."""

    def test_display_label_resolved_to_config_key(self):
        """A display label like '🔬 OpenAlex (Scientific)' should resolve
        to the config key whose display_name matches."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")

        with _patches(
            config_return={
                "openalex": _engine_config(display_name="OpenAlex"),
            },
            class_return=EngCls,
        ):
            result = create_search_engine(
                "\U0001f52c OpenAlex (Scientific)",
                settings_snapshot={"x": 1},
            )

        assert result is not None

    def test_unresolvable_label_raises_value_error(self):
        """Display label that doesn't match any config FAILS CLOSED.
        Previously fell back to 'auto' silently — see plan C2."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")

        with _patches(
            config_return={
                "auto": _engine_config(),
                "other": _engine_config(display_name="Other"),
            },
            class_return=EngCls,
        ):
            with pytest.raises(ValueError, match="Unknown search engine"):
                create_search_engine(
                    "\U0001f50d Unknown (Category)",
                    settings_snapshot={"x": 1},
                )

    def test_plain_unknown_engine_raises_value_error(self):
        """Plain unknown engine name FAILS CLOSED (no more silent rewrite to 'auto').
        See plan C2."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")

        with _patches(
            config_return={"auto": _engine_config()},
            class_return=EngCls,
        ):
            with pytest.raises(ValueError, match="Unknown search engine"):
                create_search_engine(
                    "nonexistent_plain_name",
                    settings_snapshot={"x": 1},
                )


# ---------------------------------------------------------------------------
# Tests: use_full_search wrapper trigger
# ---------------------------------------------------------------------------


class TestUseFullSearchWrapper:
    """Test that use_full_search=True triggers _create_full_search_wrapper."""

    def test_use_full_search_calls_wrapper(self):
        """When use_full_search=True and engine supports it, wrapper is called."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")
        mock_wrapper_result = Mock()

        with _patches(
            config_return={"eng": _engine_config(supports_full_search=True)},
            class_return=EngCls,
        ):
            with patch(
                "local_deep_research.web_search_engines.search_engine_factory._create_full_search_wrapper"
            ) as mock_wrapper:
                mock_wrapper.return_value = mock_wrapper_result

                result = create_search_engine(
                    "eng",
                    settings_snapshot={"x": 1},
                    use_full_search=True,
                )

        assert mock_wrapper.called
        assert result is mock_wrapper_result

    def test_use_full_search_false_does_not_call_wrapper(self):
        """When use_full_search=False, wrapper is not called."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")

        with _patches(
            config_return={"eng": _engine_config(supports_full_search=True)},
            class_return=EngCls,
        ):
            with patch(
                "local_deep_research.web_search_engines.search_engine_factory._create_full_search_wrapper"
            ) as mock_wrapper:
                create_search_engine(
                    "eng",
                    settings_snapshot={"x": 1},
                    use_full_search=False,
                )

        assert not mock_wrapper.called

    def test_supports_full_search_false_does_not_call_wrapper(self):
        """Engine without supports_full_search does not trigger wrapper."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")

        with _patches(
            config_return={"eng": _engine_config(supports_full_search=False)},
            class_return=EngCls,
        ):
            with patch(
                "local_deep_research.web_search_engines.search_engine_factory._create_full_search_wrapper"
            ) as mock_wrapper:
                create_search_engine(
                    "eng",
                    settings_snapshot={"x": 1},
                    use_full_search=True,
                )

        assert not mock_wrapper.called


# ---------------------------------------------------------------------------
# Tests: LLM relevance filter
# ---------------------------------------------------------------------------


class TestLlmRelevanceFilter:
    """Test LLM relevance filter application on created engine instances."""

    def test_per_engine_filter_enabled(self):
        """Per-engine setting enables filter when engine has LLM."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("llm", "max_results")
        mock_llm = Mock()
        snapshot = {
            "search.engine.web.eng.default_params.enable_llm_relevance_filter": {
                "value": True
            },
        }

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot=snapshot
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is True

    def test_per_engine_filter_disabled(self):
        """Per-engine setting can explicitly disable filter."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "llm",
            "max_results",
            class_attrs={"needs_llm_relevance_filter": True},
        )
        mock_llm = Mock()
        snapshot = {
            "search.engine.web.eng.default_params.enable_llm_relevance_filter": {
                "value": False
            },
        }

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot=snapshot
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is False

    def test_auto_detection_needs_llm_relevance_filter_engine(self):
        """Engines with needs_llm_relevance_filter=True get filter auto-enabled."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "llm",
            "max_results",
            class_attrs={"needs_llm_relevance_filter": True},
        )
        mock_llm = Mock()

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot={"x": 1}
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is True

    def test_auto_detection_engine_without_needs_llm_relevance_filter(self):
        """Engines without needs_llm_relevance_filter do not get filter auto-enabled."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "llm",
            "max_results",
            class_attrs={"is_generic": True},
        )
        mock_llm = Mock()

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot={"x": 1}
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is False

    def test_global_skip_does_not_override_needs_llm_relevance_filter(self):
        """Global skip_relevance_filter=True does NOT disable for needs_llm_relevance_filter engines."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "llm",
            "max_results",
            class_attrs={"needs_llm_relevance_filter": True},
        )
        mock_llm = Mock()
        snapshot = {
            "search.skip_relevance_filter": {"value": True},
        }

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot=snapshot
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is True

    def test_global_skip_applies_to_unclassified_engine(self):
        """Global skip_relevance_filter=True disables for engines without needs_llm_relevance_filter."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "llm",
            "max_results",
        )
        mock_llm = Mock()
        snapshot = {
            "search.skip_relevance_filter": {"value": True},
        }

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot=snapshot
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is False

    def test_per_engine_overrides_needs_llm_relevance_filter(self):
        """Per-engine setting False disables filter even for needs_llm_relevance_filter engines."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "llm",
            "max_results",
            class_attrs={"needs_llm_relevance_filter": True},
        )
        mock_llm = Mock()
        snapshot = {
            "search.engine.web.eng.default_params.enable_llm_relevance_filter": {
                "value": False
            },
        }

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot=snapshot
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is False

    def test_no_llm_means_no_filter(self):
        """Filter is not applied when engine has no LLM."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "max_results",
            class_attrs={"needs_llm_relevance_filter": True},
        )

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            result = create_search_engine("eng", settings_snapshot={"x": 1})

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is False


# ---------------------------------------------------------------------------
# Tests: removed meta engines ('auto', 'meta', 'parallel', ...) are rejected
# ---------------------------------------------------------------------------


class TestRemovedMetaEnginesRejected:
    """The parallel/auto meta engines were removed — no special-case
    short-circuit remains, so they fail like any unknown engine."""

    @pytest.mark.parametrize(
        "engine_name", ["auto", "meta", "parallel", "parallel_scientific"]
    )
    def test_removed_engine_raises_value_error(self, engine_name):
        """Removed meta engine names raise ValueError mentioning the removal."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ) as mock_reg:
            mock_reg.get.return_value = None
            with pytest.raises(ValueError, match="meta engines were removed"):
                create_search_engine(
                    engine_name,
                    llm=Mock(),
                    settings_snapshot={"s": 1},
                )


# ---------------------------------------------------------------------------
# Tests: Retriever path
# ---------------------------------------------------------------------------


class TestRetrieverPath:
    """Test retriever registry path."""

    def test_retriever_max_results_from_kwargs(self):
        """max_results kwarg is forwarded to RetrieverSearchEngine."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        mock_retriever = Mock()
        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ) as mock_reg:
            mock_reg.get.return_value = mock_retriever
            with patch(
                "local_deep_research.web_search_engines.engines.search_engine_retriever.RetrieverSearchEngine"
            ) as mock_cls:
                mock_cls.return_value = Mock()
                create_search_engine(
                    "my_retriever",
                    settings_snapshot={"x": 1},
                    max_results=25,
                )
                call_kwargs = mock_cls.call_args[1]
                assert call_kwargs["max_results"] == 25

    def test_retriever_default_max_results(self):
        """Default max_results=10 when not specified."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        mock_retriever = Mock()
        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ) as mock_reg:
            mock_reg.get.return_value = mock_retriever
            with patch(
                "local_deep_research.web_search_engines.engines.search_engine_retriever.RetrieverSearchEngine"
            ) as mock_cls:
                mock_cls.return_value = Mock()
                create_search_engine(
                    "my_retriever",
                    settings_snapshot={"x": 1},
                )
                call_kwargs = mock_cls.call_args[1]
                assert call_kwargs["max_results"] == 10


# ---------------------------------------------------------------------------
# Tests: get_search parameter routing
# ---------------------------------------------------------------------------


class TestGetSearchParameterRouting:
    """Test get_search routes parameters correctly for various engines."""

    def test_get_search_wikinews_params(self):
        """Wikinews gets search_snippets_only, adaptive_search, time_period."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        snapshot = {
            "search.engine.web.wikinews.adaptive_search": {"value": False},
        }

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
        ) as mock_create:
            mock_create.return_value = Mock(run=Mock())

            get_search(
                search_tool="wikinews",
                llm_instance=Mock(),
                time_period="m",
                search_snippets_only=True,
                search_language="French",
                settings_snapshot=snapshot,
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["time_period"] == "m"
            assert call_kwargs["search_snippets_only"] is True
            assert call_kwargs["search_language"] == "French"
            assert call_kwargs["adaptive_search"] is False

    def test_get_search_max_filtered_results(self):
        """max_filtered_results is passed when provided."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
        ) as mock_create:
            mock_create.return_value = Mock(run=Mock())

            get_search(
                search_tool="some_engine",
                llm_instance=Mock(),
                max_filtered_results=5,
                settings_snapshot={"x": 1},
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["max_filtered_results"] == 5

    def test_get_search_max_filtered_results_none(self):
        """max_filtered_results=None is not passed as a parameter."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
        ) as mock_create:
            mock_create.return_value = Mock(run=Mock())

            get_search(
                search_tool="some_engine",
                llm_instance=Mock(),
                max_filtered_results=None,
                settings_snapshot={"x": 1},
            )

            call_kwargs = mock_create.call_args[1]
            assert "max_filtered_results" not in call_kwargs

    def test_get_search_google_pse_params(self):
        """google_pse gets region, safe_search, use_full_search, and search_language."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
        ) as mock_create:
            mock_create.return_value = Mock(run=Mock())

            get_search(
                search_tool="google_pse",
                llm_instance=Mock(),
                region="de",
                safe_search=False,
                search_snippets_only=False,
                search_language="German",
                settings_snapshot={"x": 1},
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["region"] == "de"
            assert call_kwargs["safe_search"] is False
            assert call_kwargs["use_full_search"] is True
            assert call_kwargs["search_language"] == "German"

    def test_get_search_mojeek_params(self):
        """mojeek gets region, safe_search, use_full_search but not search_language."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
        ) as mock_create:
            mock_create.return_value = Mock(run=Mock())

            get_search(
                search_tool="mojeek",
                llm_instance=Mock(),
                region="uk",
                safe_search=True,
                search_snippets_only=True,
                settings_snapshot={"x": 1},
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["region"] == "uk"
            assert call_kwargs["safe_search"] is True
            assert call_kwargs["use_full_search"] is False
            assert "search_language" not in call_kwargs

    def test_get_search_programmatic_mode_forwarded(self):
        """programmatic_mode is forwarded to create_search_engine."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
        ) as mock_create:
            mock_create.return_value = Mock(run=Mock())

            get_search(
                search_tool="some_engine",
                llm_instance=Mock(),
                programmatic_mode=True,
                settings_snapshot={"x": 1},
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["programmatic_mode"] is True

    def test_get_search_engine_without_region_params(self):
        """Engines not in the region list don't get region params."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine"
        ) as mock_create:
            mock_create.return_value = Mock(run=Mock())

            get_search(
                search_tool="arxiv",
                llm_instance=Mock(),
                region="us",
                safe_search=True,
                settings_snapshot={"x": 1},
            )

            call_kwargs = mock_create.call_args[1]
            assert "region" not in call_kwargs
            assert "safe_search" not in call_kwargs
            assert "use_full_search" not in call_kwargs


# ---------------------------------------------------------------------------
# Tests: Exception handling
# ---------------------------------------------------------------------------


class TestExceptionHandling:
    """Test exception handling in create_search_engine."""

    def test_engine_init_raises_returns_none(self):
        """Exception during __init__ of engine class returns None."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        class BrokenEngine:
            def __init__(self):
                raise RuntimeError("init failed")

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=BrokenEngine,
        ):
            result = create_search_engine("eng", settings_snapshot={"x": 1})

        assert result is None

    def test_missing_module_path_returns_none(self):
        """Config missing module_path should cause exception -> None."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        bad_config = {"class_name": "Foo", "default_params": {}}

        with _patches(
            config_return={"eng": bad_config},
        ):
            result = create_search_engine("eng", settings_snapshot={"x": 1})

        assert result is None


# ---------------------------------------------------------------------------
# Tests: settings_snapshot requirement
# ---------------------------------------------------------------------------


class TestSettingsSnapshotRequired:
    """Test that settings_snapshot=None raises RuntimeError for
    non-retriever engines."""

    def test_parallel_also_needs_settings_snapshot(self):
        """'parallel' no longer short-circuits — like every other engine name
        it requires a settings_snapshot."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ) as mock_reg:
            mock_reg.get.return_value = None
            with pytest.raises(RuntimeError, match="settings_snapshot"):
                create_search_engine(
                    "parallel", llm=Mock(), settings_snapshot=None
                )


# ---------------------------------------------------------------------------
# Tests: per-engine filter with plain bool (not dict)
# ---------------------------------------------------------------------------


class TestPerEngineFilterPlainBool:
    """Test per-engine LLM relevance filter setting as plain bool."""

    def test_per_engine_filter_plain_true(self):
        """Per-engine setting as plain True enables filter."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("llm", "max_results")
        mock_llm = Mock()
        snapshot = {
            "search.engine.web.eng.default_params.enable_llm_relevance_filter": True,
        }

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot=snapshot
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is True

    def test_global_skip_filter_plain_bool(self):
        """Global skip_relevance_filter as plain True works."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class(
            "llm",
            "max_results",
            class_attrs={"is_scientific": True},
        )
        mock_llm = Mock()
        snapshot = {
            "search.skip_relevance_filter": True,
        }

        with _patches(
            config_return={"eng": _engine_config(requires_llm=True)},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng", llm=mock_llm, settings_snapshot=snapshot
            )

        assert result is not None
        assert getattr(result, "enable_llm_relevance_filter", False) is False


# ---------------------------------------------------------------------------
# Tests: programmatic_mode propagation to engines that swallow **kwargs
# ---------------------------------------------------------------------------


class TestProgrammaticModePostConstructionPatch:
    """Most concrete engines (Serper, Tavily, Wikipedia, etc.) accept
    ``**kwargs`` in their constructor but do not forward them to
    ``BaseSearchEngine.__init__``. As a result, ``programmatic_mode``
    passed by the factory was silently dropped and the engine ended up
    with the BaseSearchEngine default (False), mismatching what the API
    caller asked for. The factory now applies the requested mode
    post-construction via ``_configure_programmatic_mode``."""

    def _make_kwargs_swallowing_engine(self):
        """Build a real BaseSearchEngine subclass that mirrors the
        Serper/Tavily/Wikipedia pattern: accepts ``**kwargs`` without
        forwarding to ``super().__init__``."""
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )

        class _SwallowingEngine(BaseSearchEngine):
            def __init__(self, max_results: int = 10, **kwargs):
                super().__init__(max_results=max_results)

            def _get_previews(self, query):
                return []

            def _get_full_content(self, items):
                return items

        return _SwallowingEngine

    def test_engine_swallowing_kwargs_still_gets_programmatic_mode(self):
        """Engine doesn't forward kwargs -> factory patches it post-construction."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = self._make_kwargs_swallowing_engine()

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            result = create_search_engine(
                "eng",
                settings_snapshot={"x": 1},
                programmatic_mode=True,
            )

        assert result is not None
        # Without the post-construction patch this would be False
        # because _SwallowingEngine.__init__ swallowed programmatic_mode.
        assert result.programmatic_mode is True
        # And the rate_tracker should be the per-instance programmatic one,
        # not the global shared tracker returned by get_tracker().
        from local_deep_research.web_search_engines.rate_limiting.tracker import (
            AdaptiveRateLimitTracker,
        )

        assert isinstance(result.rate_tracker, AdaptiveRateLimitTracker)
        assert result.rate_tracker.programmatic_mode is True

    def test_engine_swallowing_kwargs_default_mode_unchanged(self):
        """When programmatic_mode is False (default), no patch is needed
        and the engine ends up in shared-tracker mode."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = self._make_kwargs_swallowing_engine()

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=EngCls,
        ):
            result = create_search_engine("eng", settings_snapshot={"x": 1})

        assert result is not None
        assert result.programmatic_mode is False

    def test_retriever_path_propagates_programmatic_mode(self):
        """The retriever early-return path returns before the post-construction
        patch — verify programmatic_mode is passed at construction so the
        engine still ends up in the requested mode.
        """
        from langchain_core.retrievers import BaseRetriever
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        class _DummyRetriever(BaseRetriever):
            def _get_relevant_documents(self, query, *, run_manager):
                return []

        retriever = _DummyRetriever()

        # Retriever PEP now requires a snapshot (plan H3). Pass a
        # minimal snapshot + mock evaluate_retriever to allow the
        # registered retriever through so we can test programmatic_mode.
        from local_deep_research.security.egress.policy import Decision

        with (
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
            ) as mock_reg,
            patch(
                "local_deep_research.security.egress.policy.evaluate_retriever",
                return_value=Decision(True, "test_bypass"),
            ),
        ):
            mock_reg.get.return_value = retriever
            mock_reg.get_metadata.return_value = {"is_local": True}
            result = create_search_engine(
                "my_rag",
                programmatic_mode=True,
                settings_snapshot={"policy.egress_scope": "both"},
            )

        assert result is not None
        assert result.programmatic_mode is True


# ---------------------------------------------------------------------------
# Tests: factory enforces programmatic_mode regardless of subclass __init__
# pattern. Three patterns exist in the wild — see the Subclass contract
# section in BaseSearchEngine for the recommended approach.
# ---------------------------------------------------------------------------


class TestProgrammaticModeContract:
    """Factory must produce the requested ``programmatic_mode`` regardless
    of how an engine subclass handles its kwargs:

    1. ``swallow_no_forward``: Serper/Tavily/Wikipedia pattern. Subclass
       accepts ``**kwargs`` but doesn't forward them to ``super().__init__``.
       The factory's post-construction patch is the safety net here.
    2. ``forward_kwargs``: Subclass accepts ``**kwargs`` and forwards them.
       The base ``__init__`` sets ``programmatic_mode`` directly.
    3. ``named_param``: Subclass names ``programmatic_mode`` explicitly
       in its signature and forwards it. Base handles it during init."""

    @pytest.mark.parametrize(
        "swallow_pattern",
        ["swallow_no_forward", "forward_kwargs", "named_param"],
    )
    def test_factory_enforces_programmatic_mode_across_swallow_patterns(
        self, swallow_pattern
    ):
        from local_deep_research.web_search_engines.search_engine_base import (
            BaseSearchEngine,
        )
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        if swallow_pattern == "swallow_no_forward":

            class _Eng(BaseSearchEngine):
                def __init__(self, max_results: int = 10, **kwargs):
                    super().__init__(max_results=max_results)

                def _get_previews(self, query):
                    return []

                def _get_full_content(self, items):
                    return items

        elif swallow_pattern == "forward_kwargs":

            class _Eng(BaseSearchEngine):  # type: ignore[no-redef]
                def __init__(self, max_results: int = 10, **kwargs):
                    super().__init__(max_results=max_results, **kwargs)

                def _get_previews(self, query):
                    return []

                def _get_full_content(self, items):
                    return items

        else:  # named_param

            class _Eng(BaseSearchEngine):  # type: ignore[no-redef]
                def __init__(
                    self,
                    max_results: int = 10,
                    programmatic_mode: bool = False,
                    **kwargs,
                ):
                    super().__init__(
                        max_results=max_results,
                        programmatic_mode=programmatic_mode,
                    )

                def _get_previews(self, query):
                    return []

                def _get_full_content(self, items):
                    return items

        with _patches(
            config_return={"eng": _engine_config()},
            class_return=_Eng,
        ):
            result = create_search_engine(
                "eng",
                settings_snapshot={"x": 1},
                programmatic_mode=True,
            )

        from local_deep_research.web_search_engines.rate_limiting.tracker import (
            AdaptiveRateLimitTracker,
        )

        assert result is not None
        assert result.programmatic_mode is True, (
            f"Pattern {swallow_pattern!r}: factory did not enforce "
            f"programmatic_mode=True"
        )
        assert isinstance(result.rate_tracker, AdaptiveRateLimitTracker), (
            f"Pattern {swallow_pattern!r}: rate_tracker should be the "
            f"per-instance AdaptiveRateLimitTracker, got "
            f"{type(result.rate_tracker).__name__}"
        )
