"""
Branch-coverage tests for search_engine_factory.py and llm_config.py.

Targets specific uncovered branch paths not exercised by existing test suites:

search_engine_factory.py:
  - Display label fallback: space_idx <= 0 (no icon prefix) skips base_name extraction
  - LLM pass-through when engine accepts 'llm' but requires_llm is False
  - LLM relevance filter: per-engine plain bool, auto-detect scientific with
    global skip_relevance_filter=False (no override), and generic engine
  - _create_full_search_wrapper: SerpAPI api_key + language + safesearch mapping
  - _create_full_search_wrapper: Brave safesearch mapping (True -> "moderate",
    False -> "off")

llm_config.py:
  - Registered custom LLM: callable factory TypeError re-wrap
  - Registered custom LLM: non-callable, non-BaseChatModel invalid type error
  - wrap_llm_without_think_tags: context_limit injection for registered provider
"""

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

import pytest

# ---------------------------------------------------------------------------
# Helpers (search_engine_factory tests)
# ---------------------------------------------------------------------------

_FACTORY_MOD = "local_deep_research.web_search_engines.search_engine_factory"
_LLM_MOD = "local_deep_research.config.llm_config"
# wrap_llm reads rate_limiting via the llm_config binding; context-window
# resolution now delegates to the _helpers twin which reads via
# thread_settings. Cross-cutting wrap_llm tests therefore patch BOTH.
_THREAD_SETTINGS_MOD = "local_deep_research.config.thread_settings"


def _make_engine_class(*param_names, class_attrs=None):
    """Build a class with a controlled __init__ signature for factory tests."""
    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
            for k, v in kwargs.items():
                setattr(self, k, v)

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


def _engine_config(**overrides):
    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):
    import contextlib

    @contextlib.contextmanager
    def _ctx():
        # Mock evaluate_engine so factory branch-coverage tests using
        # fake engine names (not in ENGINE_REGISTRY) aren't rejected by
        # the egress PEP. The PEP is exercised in
        # tests/security/test_egress_policy.py.
        from local_deep_research.security.egress.policy import Decision

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

    return _ctx()


# ---------------------------------------------------------------------------
# search_engine_factory: display label fallback -- no icon prefix
# ---------------------------------------------------------------------------


class TestDisplayLabelNoIconPrefix:
    """When the display label has no icon (space_idx <= 0), the base_name
    extraction is skipped and the engine falls through to the 'auto' fallback.
    Covers the else branch at line 107 of search_engine_factory.py."""

    def test_label_without_icon_prefix_raises_value_error(self):
        """Label with no icon prefix is still an unknown engine name → FAIL CLOSED.
        Plan C2 removed the silent rewrite to 'auto'."""
        from local_deep_research.web_search_engines.search_engine_factory import (
            create_search_engine,
        )

        EngCls = _make_engine_class("max_results")
        config = {"auto": _engine_config()}

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


# ---------------------------------------------------------------------------
# search_engine_factory: LLM pass-through when not required but accepted
# ---------------------------------------------------------------------------


class TestLlmPassThroughNotRequired:
    """When requires_llm is False but the engine __init__ accepts 'llm'
    and an LLM is provided, the LLM should be injected. This covers the
    elif branch at lines 228-237."""

    def test_llm_injected_when_accepted_and_provided(self):
        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()},
            class_return=EngCls,
        ):
            create_search_engine(
                "eng", llm=mock_llm, settings_snapshot={"x": 1}
            )

        assert EngCls._call_kwargs["llm"] is mock_llm


# ---------------------------------------------------------------------------
# search_engine_factory: LLM relevance filter -- global skip False
# ---------------------------------------------------------------------------


class TestRelevanceFilterGlobalSkipFalse:
    """When a needs_llm_relevance_filter engine auto-enables the filter and the global
    skip_relevance_filter is present but False, the filter should remain
    enabled."""

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

        EngCls = _make_engine_class(
            "llm",
            "max_results",
            class_attrs={"needs_llm_relevance_filter": True},
        )
        mock_llm = Mock()
        snapshot = {
            "search.skip_relevance_filter": {"value": 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 True


# ---------------------------------------------------------------------------
# _create_full_search_wrapper: SerpAPI special case
# ---------------------------------------------------------------------------


class _FakeSerpApiWrapper:
    """Fake wrapper that accepts SerpAPI-specific params."""

    def __init__(
        self,
        serpapi_api_key=None,
        language=None,
        safesearch=None,
        llm=None,
        web_search=None,
    ):
        self.serpapi_api_key = serpapi_api_key
        self.language = language
        self.safesearch = safesearch
        self.llm = llm
        self.web_search = web_search


class TestFullSearchWrapperSerpApi:
    """Covers the SerpAPI special-case branch (lines 418-452) in
    _create_full_search_wrapper, including API key extraction, language
    mapping, and safesearch mapping."""

    def test_serpapi_api_key_and_params_mapped(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            _create_full_search_wrapper,
        )

        base_engine = Mock()
        mock_llm = Mock()
        engine_config = {
            "full_search_module": "some.module",
            "full_search_class": "SerpApiSearch",
        }
        settings_snapshot = {
            "search.engine.web.serpapi.api_key": {"value": "serpapi-key-789"},
        }
        params = {
            "language": "French",
            "safe_search": True,
        }

        with patch(
            f"{_FACTORY_MOD}.get_safe_module_class",
            return_value=_FakeSerpApiWrapper,
        ):
            result = _create_full_search_wrapper(
                "serpapi",
                base_engine,
                engine_config,
                mock_llm,
                params,
                settings_snapshot=settings_snapshot,
            )

        assert isinstance(result, _FakeSerpApiWrapper)
        assert result.serpapi_api_key == "serpapi-key-789"
        assert result.language == "French"
        assert result.safesearch == "active"
        assert result.llm is mock_llm
        assert result.web_search is base_engine

    def test_serpapi_safesearch_off_when_false(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            _create_full_search_wrapper,
        )

        base_engine = Mock()
        engine_config = {
            "full_search_module": "some.module",
            "full_search_class": "SerpApiSearch",
        }
        settings_snapshot = {
            "search.engine.web.serpapi.api_key": "serpapi-plain-key",
        }
        params = {"safe_search": False}

        with patch(
            f"{_FACTORY_MOD}.get_safe_module_class",
            return_value=_FakeSerpApiWrapper,
        ):
            result = _create_full_search_wrapper(
                "serpapi",
                base_engine,
                engine_config,
                Mock(),
                params,
                settings_snapshot=settings_snapshot,
            )

        assert result.safesearch == "off"


# ---------------------------------------------------------------------------
# _create_full_search_wrapper: Brave special case
# ---------------------------------------------------------------------------


class _FakeBraveWrapper:
    """Fake wrapper that accepts Brave-specific params."""

    def __init__(
        self,
        api_key=None,
        language=None,
        safesearch=None,
        llm=None,
        web_search=None,
    ):
        self.api_key = api_key
        self.language = language
        self.safesearch = safesearch
        self.llm = llm
        self.web_search = web_search


class TestFullSearchWrapperBrave:
    """Covers the Brave special-case branch (lines 454-487) in
    _create_full_search_wrapper, including safesearch mapping
    (True -> 'moderate', False -> 'off')."""

    def test_brave_safesearch_moderate_when_true(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            _create_full_search_wrapper,
        )

        engine_config = {
            "full_search_module": "some.module",
            "full_search_class": "BraveSearch",
        }
        settings_snapshot = {
            "search.engine.web.brave.api_key": {"value": "brave-key-abc"},
        }
        params = {"language": "German", "safe_search": True}

        with patch(
            f"{_FACTORY_MOD}.get_safe_module_class",
            return_value=_FakeBraveWrapper,
        ):
            result = _create_full_search_wrapper(
                "brave",
                Mock(),
                engine_config,
                Mock(),
                params,
                settings_snapshot=settings_snapshot,
            )

        assert isinstance(result, _FakeBraveWrapper)
        assert result.api_key == "brave-key-abc"
        assert result.language == "German"
        assert result.safesearch == "moderate"

    def test_brave_safesearch_off_when_false(self):
        from local_deep_research.web_search_engines.search_engine_factory import (
            _create_full_search_wrapper,
        )

        engine_config = {
            "full_search_module": "some.module",
            "full_search_class": "BraveSearch",
        }
        settings_snapshot = {
            "search.engine.web.brave.api_key": "brave-plain-key",
        }
        params = {"safe_search": False}

        with patch(
            f"{_FACTORY_MOD}.get_safe_module_class",
            return_value=_FakeBraveWrapper,
        ):
            result = _create_full_search_wrapper(
                "brave",
                Mock(),
                engine_config,
                Mock(),
                params,
                settings_snapshot=settings_snapshot,
            )

        assert result.safesearch == "off"


# ---------------------------------------------------------------------------
# llm_config: context_limit injection via wrap_llm_without_think_tags
#   for a registered custom provider (not a built-in like ollama/openai)
# ---------------------------------------------------------------------------


class TestWrapLlmContextLimitRegisteredProvider:
    """Covers the context_limit injection in wrap_llm_without_think_tags
    (lines 883-892) when the provider is a custom registered provider that
    goes through the registered-LLM path in get_llm (which returns before
    the get_llm code that sets context_limit)."""

    def test_context_limit_injected_for_custom_provider(self):
        """wrap_llm injects context_limit for a non-built-in provider when
        research_context has no existing context_limit."""
        llm = MagicMock()
        research_ctx = {}

        def fake_setting(key, default=None, settings_snapshot=None):
            if key == "rate_limiting.llm_enabled":
                return False
            if key == "llm.context_window_unrestricted":
                return False
            if key == "llm.context_window_size":
                return 50000
            return default

        with (
            patch(
                f"{_LLM_MOD}.get_setting_from_snapshot",
                side_effect=fake_setting,
            ),
            patch(
                f"{_THREAD_SETTINGS_MOD}.get_setting_from_snapshot",
                side_effect=fake_setting,
            ),
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrap_llm_without_think_tags(
                llm,
                provider="my_custom_provider",
                research_context=research_ctx,
            )

        assert research_ctx["context_limit"] == 50000

    def test_context_limit_not_injected_when_none(self):
        """When _get_context_window_for_provider returns None (unrestricted
        cloud), context_limit is NOT added to research_context."""
        llm = MagicMock()
        research_ctx = {}

        def fake_setting(key, default=None, settings_snapshot=None):
            if key == "rate_limiting.llm_enabled":
                return False
            if key == "llm.context_window_unrestricted":
                return True
            return default

        with (
            patch(
                f"{_LLM_MOD}.get_setting_from_snapshot",
                side_effect=fake_setting,
            ),
            patch(
                f"{_THREAD_SETTINGS_MOD}.get_setting_from_snapshot",
                side_effect=fake_setting,
            ),
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrap_llm_without_think_tags(
                llm,
                provider="anthropic",
                research_context=research_ctx,
            )

        assert "context_limit" not in research_ctx


# ---------------------------------------------------------------------------
# llm_config: Anthropic provider missing API key
# ---------------------------------------------------------------------------


class TestGetLlmAnthropicMissingKey:
    """Covers the ValueError raised by AnthropicProvider.resolve_api_key
    when the user hasn't configured llm.anthropic.api_key.

    Pre-collapse this test exercised the procedural ``if/elif`` chain in
    ``llm_config.get_llm`` by mocking ``is_llm_registered=False``. After
    the dead-code deletion the chain is gone; the same error now comes
    from ``AnthropicProvider.create_llm`` via the registered-LLM branch
    in ``get_llm``. Test goes through the real registered path now.
    """

    def test_anthropic_no_api_key_raises(self):
        settings = {
            # A real run always carries a primary; the inference PEP now fails
            # closed without it (resolve_run_primary_engine), which would raise
            # before this test reaches the Anthropic-key check.
            "search.tool": "searxng",
            "llm.model": "claude-3-haiku",
            "llm.temperature": 0.7,
            "llm.provider": "anthropic",
            "llm.anthropic.api_key": None,
            "llm.supports_max_tokens": False,
            "llm.context_window_unrestricted": True,
            "rate_limiting.llm_enabled": False,
        }

        from local_deep_research.llm.providers import discover_providers

        # Re-prime the registry in case a sibling test cleared it.
        discover_providers(force_refresh=True)

        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            side_effect=lambda key, default=None, settings_snapshot=None: (
                settings.get(key, default)
            ),
        ):
            from local_deep_research.config.llm_config import get_llm

            with pytest.raises(
                ValueError, match="Anthropic API key not configured"
            ):
                get_llm(provider="anthropic", settings_snapshot=settings)
