"""
Tests for web_search_engines/search_engine_factory.py

Tests cover:
- create_search_engine function
- get_search function
- _create_full_search_wrapper function
- Removed meta engines ('auto', 'parallel', ...) rejected with ValueError
- API key and LLM requirements
- LLM relevance filter settings
"""

import pytest
from unittest.mock import Mock, patch


class TestRemovedMetaEnginesRejected:
    """The 'auto'/'meta'/'parallel' meta engines were removed — the factory
    must reject them like any unknown engine, with a message pointing at
    the removal."""

    @pytest.mark.parametrize(
        "engine_name", ["auto", "meta", "parallel", "parallel_scientific"]
    )
    def test_removed_engine_raises_value_error(self, engine_name):
        """Creating a removed meta engine raises 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_registry:
            mock_registry.get.return_value = None

            with pytest.raises(ValueError, match="meta engines were removed"):
                create_search_engine(
                    engine_name=engine_name,
                    llm=Mock(),
                    settings_snapshot={"test": "value"},
                )


class TestCreateSearchEngineRetriever:
    """Tests for create_search_engine with registered retrievers."""

    def test_create_engine_with_registered_retriever(self):
        """Test creating engine from registered retriever."""
        mock_retriever = Mock()

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ) as mock_registry:
            mock_registry.get.return_value = mock_retriever

            with patch(
                "local_deep_research.web_search_engines.engines.search_engine_retriever.RetrieverSearchEngine"
            ) as mock_class:
                mock_engine = Mock()
                mock_class.return_value = mock_engine

                from local_deep_research.web_search_engines.search_engine_factory import (
                    create_search_engine,
                )

                create_search_engine(
                    engine_name="custom_retriever",
                    settings_snapshot={"test": "value"},
                )

                mock_class.assert_called_once()
                call_kwargs = mock_class.call_args[1]
                assert call_kwargs["retriever"] == mock_retriever
                assert call_kwargs["name"] == "custom_retriever"


@pytest.fixture(autouse=True)
def _module_wide_bypass_engine_pdp():
    """Auto-applied to every test in this module — bypasses the PEP so
    factory tests using mock engine names don't get rejected by the
    egress policy. The PEP itself is exercised in
    tests/security/test_egress_policy.py.
    """
    from local_deep_research.security.egress.policy import Decision

    with (
        patch(
            "local_deep_research.security.egress.policy.evaluate_engine",
            return_value=Decision(True, "test_bypass"),
        ),
        patch(
            "local_deep_research.security.egress.policy.evaluate_retriever",
            return_value=Decision(True, "test_bypass"),
        ),
    ):
        yield


class TestCreateSearchEngineRequirements:
    """Tests for API key and LLM requirements."""

    def test_missing_settings_snapshot_raises_error(self):
        """Test that missing settings_snapshot raises RuntimeError."""
        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ) as mock_registry:
            mock_registry.get.return_value = None

            from local_deep_research.web_search_engines.search_engine_factory import (
                create_search_engine,
            )

            with pytest.raises(RuntimeError) as exc_info:
                create_search_engine(
                    engine_name="test_engine",
                    settings_snapshot=None,
                )

            assert "settings_snapshot is required" in str(exc_info.value)

    def test_missing_api_key_returns_none(self):
        """Test that missing required API key returns None."""
        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.retriever_registry"
        ) as mock_registry:
            mock_registry.get.return_value = None

            with patch(
                "local_deep_research.web_search_engines.search_engine_factory.search_config"
            ) as mock_config:
                mock_config.return_value = {
                    "test_engine": {
                        "module_path": ".engines.test",
                        "class_name": "TestEngine",
                        "requires_api_key": True,
                    }
                }

                from local_deep_research.web_search_engines.search_engine_factory import (
                    create_search_engine,
                )

                result = create_search_engine(
                    engine_name="test_engine",
                    settings_snapshot={"dummy": "value"},  # Non-empty snapshot
                )

                assert result is None


class TestDisplayLabelFallback:
    """Tests for display label fallback functionality."""

    def test_extract_base_name_from_label(self):
        """Test extracting base name from display label."""
        engine_name = "🔬 OpenAlex (Scientific)"

        base_name = None
        if " (" in engine_name and engine_name.endswith(")"):
            parts = engine_name.rsplit(" (", 1)
            if len(parts) == 2:
                before_paren = parts[0]
                space_idx = before_paren.find(" ")
                if space_idx > 0:
                    base_name = before_paren[space_idx + 1 :].strip()

        assert base_name == "OpenAlex"

    def test_label_without_parentheses(self):
        """Test label without parentheses format."""
        engine_name = "simple_engine"

        extracted = None
        if " (" in engine_name and engine_name.endswith(")"):
            extracted = "something"

        assert extracted is None

    def test_label_matching_config(self):
        """Test matching extracted label to config."""
        config = {
            "openalex": {
                "display_name": "OpenAlex",
                "module_path": ".engines.search_engine_openalex",
                "class_name": "OpenAlexSearchEngine",
            }
        }

        base_name = "OpenAlex"
        matched_key = None

        for config_key, config_data in config.items():
            if isinstance(config_data, dict):
                display_name = config_data.get("display_name", config_key)
                if display_name == base_name:
                    matched_key = config_key
                    break

        assert matched_key == "openalex"


class TestMaxResultsDefault:
    """Tests for max_results default handling."""

    def test_max_results_from_settings_dict(self):
        """Test max_results from settings as dict."""
        settings_snapshot = {"search.max_results": {"value": 25}}

        max_results = None
        if "search.max_results" in settings_snapshot:
            max_results = (
                settings_snapshot["search.max_results"].get("value", 20)
                if isinstance(settings_snapshot["search.max_results"], dict)
                else settings_snapshot["search.max_results"]
            )

        assert max_results == 25

    def test_max_results_from_settings_direct(self):
        """Test max_results from settings as direct value."""
        settings_snapshot = {"search.max_results": 30}

        max_results = None
        if "search.max_results" in settings_snapshot:
            max_results = (
                settings_snapshot["search.max_results"].get("value", 20)
                if isinstance(settings_snapshot["search.max_results"], dict)
                else settings_snapshot["search.max_results"]
            )

        assert max_results == 30

    def test_max_results_default(self):
        """Test max_results default when not in settings."""
        settings_snapshot = {}
        kwargs = {}

        if "max_results" not in kwargs:
            if settings_snapshot and "search.max_results" in settings_snapshot:
                max_results = settings_snapshot["search.max_results"]
            else:
                max_results = 20
            kwargs["max_results"] = max_results

        assert kwargs["max_results"] == 20


class TestGetSearch:
    """Tests for get_search function."""

    def test_get_search_basic(self):
        """Test basic get_search call."""
        mock_llm = Mock()
        mock_engine = Mock()
        mock_engine.run = Mock()

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

            from local_deep_research.web_search_engines.search_engine_factory import (
                get_search,
            )

            result = get_search(
                search_tool="duckduckgo",
                llm_instance=mock_llm,
                max_results=10,
                settings_snapshot={"test": "value"},
            )

            mock_create.assert_called_once()
            assert result == mock_engine

    def test_get_search_with_duckduckgo_params(self):
        """Test get_search with DuckDuckGo specific params."""
        mock_llm = Mock()
        mock_engine = Mock()

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

            from local_deep_research.web_search_engines.search_engine_factory import (
                get_search,
            )

            get_search(
                search_tool="duckduckgo",
                llm_instance=mock_llm,
                max_results=20,
                region="uk",
                safe_search=False,
                search_snippets_only=True,
                settings_snapshot={"test": "value"},
            )

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

    def test_get_search_with_serpapi_params(self):
        """Test get_search with SerpAPI specific params."""
        mock_llm = Mock()
        mock_engine = Mock()

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

            from local_deep_research.web_search_engines.search_engine_factory import (
                get_search,
            )

            get_search(
                search_tool="serpapi",
                llm_instance=mock_llm,
                max_results=15,
                time_period="m",
                search_language="Spanish",
                settings_snapshot={"test": "value"},
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs["time_period"] == "m"
            assert call_kwargs["search_language"] == "Spanish"

    def test_get_search_returns_none(self):
        """Test get_search when engine creation fails."""
        mock_llm = Mock()

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

            from local_deep_research.web_search_engines.search_engine_factory import (
                get_search,
            )

            result = get_search(
                search_tool="nonexistent",
                llm_instance=mock_llm,
                settings_snapshot={"test": "value"},
            )

            assert result is None

    def test_get_search_adds_region_params(self):
        """get_search adds region parameters for supported engines."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        mock_llm = Mock()

        settings_snapshot = {
            "search.max_results": {"value": 10},
        }

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

            get_search(
                search_tool="duckduckgo",
                llm_instance=mock_llm,
                max_results=10,
                region="uk",
                safe_search=True,
                settings_snapshot=settings_snapshot,
            )

            # Check that region was passed
            call_kwargs = mock_create.call_args[1]
            assert call_kwargs.get("region") == "uk"
            assert call_kwargs.get("safe_search") is True

    def test_get_search_adds_language_params(self):
        """get_search adds language parameters for supported engines."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        mock_llm = Mock()

        settings_snapshot = {
            "search.max_results": {"value": 10},
        }

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

            get_search(
                search_tool="brave",
                llm_instance=mock_llm,
                max_results=10,
                search_language="German",
                settings_snapshot=settings_snapshot,
            )

            call_kwargs = mock_create.call_args[1]
            assert call_kwargs.get("search_language") == "German"


class TestFullSearchWrapper:
    """Tests for _create_full_search_wrapper function."""

    def test_wrapper_returns_base_on_missing_config(self):
        """Test wrapper returns base engine when config missing."""
        mock_base_engine = Mock()
        config = {}
        engine_name = "test_engine"

        if engine_name not in config:
            result = mock_base_engine
        else:
            result = None

        assert result == mock_base_engine

    def test_wrapper_config_extraction(self):
        """Test config extraction from settings snapshot."""
        settings_snapshot = {
            "search.engine.web.serpapi.api_key": {"value": "test-key"},
            "search.engine.web.serpapi.class_name": {"value": "SerpApiSearch"},
        }

        web_engines = {}
        for key, value in settings_snapshot.items():
            if key.startswith("search.engine.web."):
                parts = key.split(".")
                if len(parts) >= 4:
                    engine_name = parts[3]
                    if engine_name not in web_engines:
                        web_engines[engine_name] = {}
                    remaining_key = (
                        ".".join(parts[4:]) if len(parts) > 4 else ""
                    )
                    if remaining_key:
                        web_engines[engine_name][remaining_key] = (
                            value.get("value")
                            if isinstance(value, dict)
                            else value
                        )

        assert "serpapi" in web_engines
        assert web_engines["serpapi"]["api_key"] == "test-key"


class TestParameterFiltering:
    """Tests for parameter filtering logic."""

    def test_filter_unsupported_params(self):
        """Test filtering of unsupported parameters."""
        engine_init_params = [
            "self",
            "max_results",
            "api_key",
            "settings_snapshot",
        ]
        all_params = {
            "max_results": 20,
            "api_key": "test-key",
            "unsupported_param": "value",
            "another_unsupported": "value2",
        }

        filtered_params = {
            k: v for k, v in all_params.items() if k in engine_init_params[1:]
        }

        assert "max_results" in filtered_params
        assert "api_key" in filtered_params
        assert "unsupported_param" not in filtered_params
        assert "another_unsupported" not in filtered_params

    def test_add_settings_snapshot_to_params(self):
        """Test adding settings_snapshot to params if accepted."""
        engine_init_params = ["self", "max_results", "settings_snapshot"]
        filtered_params = {"max_results": 20}
        settings_snapshot = {"test": "value"}

        if "settings_snapshot" in engine_init_params[1:] and settings_snapshot:
            filtered_params["settings_snapshot"] = settings_snapshot

        assert "settings_snapshot" in filtered_params

    def test_add_programmatic_mode_to_params(self):
        """Test adding programmatic_mode to params if accepted."""
        engine_init_params = ["self", "max_results", "programmatic_mode"]
        filtered_params = {"max_results": 20}
        programmatic_mode = True

        if "programmatic_mode" in engine_init_params[1:]:
            filtered_params["programmatic_mode"] = programmatic_mode

        assert filtered_params["programmatic_mode"] is True


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

    The code under test calls inspect.signature(cls.__init__),
    so we need a real class whose __init__ has the desired parameters.
    The engine is instantiated with **filtered_params (kwargs style),
    so the __init__ must accept **kwargs. We use functools.wraps to
    preserve the signature for inspect while accepting any kwargs.
    """
    import inspect as _inspect

    # Build a signature object that the factory code will inspect
    params = [
        _inspect.Parameter("self", _inspect.Parameter.POSITIONAL_OR_KEYWORD)
    ]
    for name in param_names:
        params.append(
            _inspect.Parameter(name, _inspect.Parameter.POSITIONAL_OR_KEYWORD)
        )

    class _Eng:
        _call_kwargs = None

        def __init__(self, **kwargs):
            _Eng._call_kwargs = kwargs

    # Attach the desired signature so inspect.signature(cls.__init__) sees our params
    _Eng.__init__.__signature__ = _inspect.Signature(params)

    return _Eng


@pytest.fixture
def _bypass_engine_pdp():
    """Patch evaluate_engine to allow unknown mock engine names through.
    The PEP is exercised separately in tests/security/test_egress_policy.py;
    factory tests below use mock names (myengine, eng) that aren't in the
    static ENGINE_REGISTRY and would otherwise be rejected as engine_unknown.
    """
    from local_deep_research.security.egress.policy import Decision

    with patch(
        "local_deep_research.security.egress.policy.evaluate_engine",
        return_value=Decision(True, "test_bypass"),
    ):
        yield


class TestCreateEngineInstantiation:
    """Tests for the real engine instantiation path (lines 194-339)."""

    @pytest.fixture(autouse=True)
    def _auto_bypass(self, _bypass_engine_pdp):
        """Auto-apply the PEP bypass to every test in this class."""
        yield

    def _make_engine_config(self, **overrides):
        """Build a minimal engine config dict."""
        base = {
            "module_path": "some.module",
            "class_name": "SomeEngine",
            "default_params": {},
        }
        base.update(overrides)
        return base

    def test_calls_get_safe_module_class(self):
        """get_safe_module_class is called with module_path and class_name."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class()

        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 = None
            mock_sc.return_value = {"myengine": self._make_engine_config()}
            mock_gsmc.return_value = EngCls

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

            mock_gsmc.assert_called_once_with("some.module", "SomeEngine")

    def test_filters_unsupported_params(self):
        """Parameters not in engine __init__ are filtered out."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")

        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 = None
            mock_sc.return_value = {
                "eng": self._make_engine_config(
                    default_params={"max_results": 5, "bogus_param": "nope"}
                )
            }
            mock_gsmc.return_value = EngCls

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

            assert "max_results" in EngCls._call_kwargs
            assert "bogus_param" not in EngCls._call_kwargs

    def test_passes_settings_snapshot(self):
        """settings_snapshot is passed when engine accepts it."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("settings_snapshot")
        snapshot = {"key": "val"}

        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 = None
            mock_sc.return_value = {"eng": self._make_engine_config()}
            mock_gsmc.return_value = EngCls

            create_search_engine("eng", settings_snapshot=snapshot)

            assert EngCls._call_kwargs["settings_snapshot"] is snapshot

    def test_passes_programmatic_mode(self):
        """programmatic_mode flag is passed when engine accepts it."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("programmatic_mode")

        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 = None
            mock_sc.return_value = {"eng": self._make_engine_config()}
            mock_gsmc.return_value = EngCls

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

            assert EngCls._call_kwargs["programmatic_mode"] is True

    def test_adds_llm_when_requires_llm(self):
        """LLM is injected when engine config has requires_llm=True."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

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

        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 = None
            mock_sc.return_value = {
                "eng": self._make_engine_config(requires_llm=True)
            }
            mock_gsmc.return_value = EngCls

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

            assert EngCls._call_kwargs["llm"] is mock_llm

    def test_exception_returns_none(self):
        """Exception during instantiation returns None."""
        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,
            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 = None
            mock_sc.return_value = {"eng": self._make_engine_config()}
            mock_gsmc.side_effect = ImportError("module not found")

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

    def test_unknown_engine_raises_value_error(self):
        """Unknown engine name FAILS CLOSED (was 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()

        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 = None
            mock_sc.return_value = {"auto": self._make_engine_config()}
            mock_gsmc.return_value = EngCls

            with pytest.raises(ValueError, match="Unknown search engine"):
                create_search_engine("nonexistent", settings_snapshot={"x": 1})

    def test_no_config_raises_value_error(self):
        """Missing engine in config (and no 'auto') raises ValueError (was silent None — plan C2)."""
        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,
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.search_config"
            ) as mock_sc,
        ):
            mock_reg.get.return_value = None
            mock_sc.return_value = {"other": {}}

            with pytest.raises(ValueError, match="Unknown search engine"):
                create_search_engine("nonexistent", settings_snapshot={"x": 1})

    def test_default_params_merged(self):
        """default_params from config are merged with kwargs."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

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

        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 = None
            mock_sc.return_value = {
                "eng": self._make_engine_config(default_params={"region": "us"})
            }
            mock_gsmc.return_value = EngCls

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

            # default_params region should be present
            assert EngCls._call_kwargs["region"] == "us"
            # kwargs max_results should be present
            assert EngCls._call_kwargs["max_results"] == 15

    def test_none_engine_rejected(self):
        """search.tool='none' raises ValueError instead of silent auto-fallback."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with pytest.raises(ValueError, match="search.tool='none'"):
            create_search_engine("none", settings_snapshot={"x": 1})
