"""
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():
        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,
        ):
            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"

    def test_missing_api_key_returns_none(self):
        """No API key anywhere -> returns None."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        snapshot = {"dummy": "value"}

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

        assert result is None


# ---------------------------------------------------------------------------
# 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_falls_back_to_auto(self):
        """Display label that doesn't match any config falls back to 'auto'."""
        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,
        ):
            result = create_search_engine(
                "\U0001f50d Unknown (Category)",
                settings_snapshot={"x": 1},
            )

        assert result is not None

    def test_unknown_engine_no_auto_returns_none(self):
        """Unknown engine with no 'auto' in config returns None."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with _patches(
            config_return={"other_engine": _engine_config()},
        ):
            result = create_search_engine(
                "totally_unknown",
                settings_snapshot={"x": 1},
            )

        assert result is None

    def test_plain_unknown_engine_falls_back_to_auto(self):
        """Plain unknown engine name (no label format) falls back to 'auto'."""
        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,
        ):
            result = create_search_engine(
                "nonexistent_plain_name",
                settings_snapshot={"x": 1},
            )

        assert result is not None


# ---------------------------------------------------------------------------
# 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={"is_scientific": 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_scientific_engine(self):
        """Scientific engines 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_scientific": 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_generic_engine(self):
        """Generic engines 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_filter_overrides_auto(self):
        """Global skip_relevance_filter=True disables even for scientific engines."""
        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": {"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_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={"is_scientific": 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: Parallel engine creation
# ---------------------------------------------------------------------------


class TestParallelEngineCreation:
    """Test parallel engine special-case handling."""

    def test_parallel_scientific_passes_kwargs(self):
        """Extra kwargs are forwarded to ParallelSearchEngine."""
        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"
        ):
            with patch(
                "local_deep_research.web_search_engines.engines.parallel_search_engine.ParallelSearchEngine"
            ) as mock_cls:
                mock_cls.return_value = Mock()
                create_search_engine(
                    "parallel_scientific",
                    llm=Mock(),
                    settings_snapshot={"s": 1},
                    max_results=15,
                )
                call_kwargs = mock_cls.call_args[1]
                assert call_kwargs["max_results"] == 15

    def test_parallel_passes_settings_snapshot(self):
        """settings_snapshot is forwarded to ParallelSearchEngine."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        snapshot = {"key": "val"}
        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ):
            with patch(
                "local_deep_research.web_search_engines.engines.parallel_search_engine.ParallelSearchEngine"
            ) as mock_cls:
                mock_cls.return_value = Mock()
                create_search_engine(
                    "parallel",
                    llm=Mock(),
                    settings_snapshot=snapshot,
                )
                call_kwargs = mock_cls.call_args[1]
                assert call_kwargs["settings_snapshot"] is snapshot


# ---------------------------------------------------------------------------
# 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_instantiation_exception_returns_none(self):
        """Exception during engine class instantiation returns None."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with _patches(
            config_return={"eng": _engine_config()},
        ) as (_, _, mock_gsmc):
            mock_gsmc.side_effect = ValueError("bad class")

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

        assert result is None

    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-parallel,
    non-retriever engines."""

    def test_none_settings_snapshot_raises(self):
        """settings_snapshot=None raises RuntimeError."""
        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 is required"
            ):
                create_search_engine("some_engine", settings_snapshot=None)

    def test_parallel_does_not_need_settings_snapshot(self):
        """Parallel engines work without settings_snapshot (they short-circuit)."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with patch(
            "local_deep_research.web_search_engines.engines.parallel_search_engine.ParallelSearchEngine"
        ) as mock_cls:
            mock_cls.return_value = Mock()
            # Should not raise even with settings_snapshot=None
            result = create_search_engine(
                "parallel", llm=Mock(), settings_snapshot=None
            )
            assert result is not 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
