"""
Deep coverage tests for search_engine_factory.py

Focuses on gaps not addressed by test_search_engine_factory_coverage.py:
- Unknown engine that falls back to 'auto' when available
- Unknown engine with no 'auto' fallback → returns None
- Registered retriever path
- parallel / parallel_scientific special paths
- 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


# ---------------------------------------------------------------------------
# No settings_snapshot raises RuntimeError
# ---------------------------------------------------------------------------


class TestNoSettingsSnapshot:
    def test_raises_runtime_error_without_snapshot(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        with pytest.raises(RuntimeError, match="settings_snapshot is required"):
            create_search_engine("wikipedia", settings_snapshot=None)


# ---------------------------------------------------------------------------
# parallel / parallel_scientific special paths
# ---------------------------------------------------------------------------


class TestParallelEngineCreation:
    def test_parallel_scientific_returns_parallel_engine(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        mock_engine = MagicMock()
        with patch(
            "local_deep_research.web_search_engines.engines.parallel_search_engine.ParallelSearchEngine",
            return_value=mock_engine,
        ):
            result = create_search_engine(
                "parallel_scientific",
                settings_snapshot={"dummy": "x"},
            )
        # Either the mock engine or a ParallelSearchEngine instance
        assert result is not None

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

        mock_engine = MagicMock()
        with patch(
            "local_deep_research.web_search_engines.engines.parallel_search_engine.ParallelSearchEngine",
            return_value=mock_engine,
        ):
            result = create_search_engine(
                "parallel", settings_snapshot={"dummy": "x"}
            )
        assert result is not None


# ---------------------------------------------------------------------------
# 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 TestUnknownEngineFallback:
    def test_unknown_engine_falls_back_to_auto_when_available(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngClass = _make_engine_class("max_results")
        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,
            ),
            patch(
                "local_deep_research.web_search_engines.search_engine_factory.get_safe_module_class",
                return_value=EngClass,
            ),
        ):
            result = create_search_engine(
                "nonexistent_engine",
                settings_snapshot=_make_snapshot(),
            )
        # Fell back to 'auto', engine created
        assert result is not None

    def test_unknown_engine_without_auto_returns_none(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,
        ):
            result = create_search_engine(
                "totally_unknown",
                settings_snapshot=_make_snapshot(),
            )
        assert result is None


# ---------------------------------------------------------------------------
# API key required but missing
# ---------------------------------------------------------------------------


class TestApiKeyRequired:
    def test_missing_api_key_returns_none(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngClass = _make_engine_class("api_key", "max_results")
        config = _minimal_config(
            "brave", EngClass, extra={"requires_api_key": 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,
            ),
        ):
            result = create_search_engine(
                "brave",
                settings_snapshot=_make_snapshot(),  # no api_key in snapshot
            )
        assert result is None


# ---------------------------------------------------------------------------
# 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
