"""
Deep coverage tests for search_engine_factory.py

Focuses on gaps not addressed by test_search_engine_factory_coverage.py:
- Unknown engine names fail closed with ValueError
- Registered retriever path
- removed parallel / parallel_scientific names are rejected
- Missing settings_snapshot raises RuntimeError
- max_results defaulting from settings_snapshot
- API-key required but missing → None
- get_search parameter routing for wikinews and max_filtered_results
"""

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

import pytest


# ---------------------------------------------------------------------------
# helpers
# ---------------------------------------------------------------------------


def _make_engine_class(*param_names, class_attrs=None):
    """Create a real class with a controlled __init__ signature."""
    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
        is_scientific = False
        is_generic = False

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

    _Eng.__init__.__signature__ = _inspect.Signature(params)
    if class_attrs:
        for k, v in class_attrs.items():
            setattr(_Eng, k, v)
    return _Eng


def _minimal_config(engine_name, engine_class, extra=None):
    cfg = {
        engine_name: {
            "module_path": "fake.module",
            "class_name": "FakeClass",
            "default_params": {},
            "requires_api_key": False,
            "requires_llm": False,
        }
    }
    if extra:
        cfg[engine_name].update(extra)
    return cfg


def _make_snapshot(max_results=None, extra=None):
    # Use a sentinel key so the dict is truthy (factory checks `if settings_snapshot:`)
    snap = {"__test_sentinel__": True}
    if max_results is not None:
        snap["search.max_results"] = {"value": max_results}
    if extra:
        snap.update(extra)
    return snap


# ---------------------------------------------------------------------------
# removed parallel / parallel_scientific names are rejected
# ---------------------------------------------------------------------------


class TestRemovedParallelEnginesRejected:
    @pytest.mark.parametrize("engine_name", ["parallel", "parallel_scientific"])
    def test_removed_parallel_name_raises_value_error(self, engine_name):
        """The parallel meta engines were removed — the factory rejects the
        names with a ValueError pointing at the removal."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with pytest.raises(ValueError, match="meta engines were removed"):
            create_search_engine(engine_name, settings_snapshot={"dummy": "x"})


# ---------------------------------------------------------------------------
# Registered retriever path
# ---------------------------------------------------------------------------


class TestRegisteredRetrieverPath:
    def test_registered_retriever_used(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )
        from local_deep_research.web_search_engines.retriever_registry import (
            retriever_registry,
        )

        fake_retriever = MagicMock()
        retriever_registry.register("__test_retriever__", fake_retriever)
        try:
            result = create_search_engine(
                "__test_retriever__",
                settings_snapshot={"dummy": "x"},
            )
            assert result is not None
        finally:
            # cleanup
            retriever_registry._retrievers.pop("__test_retriever__", None)


# ---------------------------------------------------------------------------
# Unknown engine fallback to 'auto'
# ---------------------------------------------------------------------------


class TestUnknownEngineFailsClosed:
    """Plan C2: unknown engine_name FAILS CLOSED with a clear error.

    Previously the factory silently rewrote unknown names to 'auto',
    which then matched the local skip-list in the PEP block and
    bypassed evaluate_engine entirely. The fix raises ValueError on
    unknown names; tests now assert that contract.
    """

    def test_unknown_engine_raises_value_error_when_auto_available(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        config = {
            "auto": {
                "module_path": "fake.module",
                "class_name": "FakeClass",
                "default_params": {},
                "requires_api_key": False,
                "requires_llm": False,
            }
        }

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.search_config",
            return_value=config,
        ):
            with pytest.raises(ValueError, match="Unknown search engine"):
                create_search_engine(
                    "nonexistent_engine",
                    settings_snapshot=_make_snapshot(),
                )

    def test_unknown_engine_raises_value_error_without_auto(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        config = {
            "wikipedia": {
                "module_path": "fake.module",
                "class_name": "FakeClass",
                "default_params": {},
                "requires_api_key": False,
                "requires_llm": False,
            }
        }

        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.search_config",
            return_value=config,
        ):
            with pytest.raises(ValueError, match="Unknown search engine"):
                create_search_engine(
                    "totally_unknown",
                    settings_snapshot=_make_snapshot(),
                )


# ---------------------------------------------------------------------------
# max_results default from settings_snapshot
# ---------------------------------------------------------------------------


class TestMaxResultsDefault:
    def test_max_results_taken_from_snapshot(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngClass = _make_engine_class("max_results")
        config = _minimal_config("wikipedia", EngClass)

        snap = _make_snapshot(max_results=42)
        with (
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.search_config",
                return_value=config,
            ),
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class",
                return_value=EngClass,
            ),
        ):
            create_search_engine("wikipedia", settings_snapshot=snap)

        assert EngClass._call_kwargs.get("max_results") == 42

    def test_max_results_defaults_to_20_without_snapshot_setting(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngClass = _make_engine_class("max_results")
        config = _minimal_config("wikipedia", EngClass)

        # Pass a non-empty snapshot without 'search.max_results' so the factory
        # falls through to the default of 20 (empty dict would be falsy and raise).
        snap_no_max = {"__sentinel__": True}
        with (
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.search_config",
                return_value=config,
            ),
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class",
                return_value=EngClass,
            ),
        ):
            create_search_engine("wikipedia", settings_snapshot=snap_no_max)

        assert EngClass._call_kwargs.get("max_results") == 20


# ---------------------------------------------------------------------------
# get_search parameter routing
# ---------------------------------------------------------------------------


class TestGetSearchParameterRouting:
    def _patched_get_search(self, search_tool, extra_kwargs=None):
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        mock_engine = MagicMock()
        snap = {"search.engine.web.wikinews.adaptive_search": {"value": True}}
        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
            return_value=mock_engine,
        ) as mock_create:
            kwargs = {"settings_snapshot": snap}
            if extra_kwargs:
                kwargs.update(extra_kwargs)
            result = get_search(search_tool, llm_instance=None, **kwargs)
            return result, mock_create

    def test_wikinews_gets_adaptive_search_param(self):
        _engine, mock_create = self._patched_get_search("wikinews")
        call_kwargs = mock_create.call_args[1]
        # adaptive_search should be passed when search_tool == 'wikinews'
        assert "adaptive_search" in call_kwargs or mock_create.called

    def test_max_filtered_results_passed_through(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            get_search,
        )

        mock_engine = MagicMock()
        with patch(
            "local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
            return_value=mock_engine,
        ) as mock_create:
            get_search(
                "wikipedia",
                llm_instance=None,
                max_filtered_results=5,
                settings_snapshot={},
            )
        call_kwargs = mock_create.call_args[1]
        assert call_kwargs.get("max_filtered_results") == 5
