"""
Comprehensive tests for provider availability functions and related helpers
in local_deep_research/config/llm_config.py.

Focuses on gaps not covered by existing test files:
- is_*_available() import-error and exception branches
- get_available_providers() multi-provider and cache behaviour
- wrap_llm_without_think_tags() context_limit injection, token counter, string responses
- _get_context_window_for_provider() edge cases
- get_selected_llm_provider() with snapshot parameter
"""

import sys
from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.config.llm_config"


def _patch_all_availability(**overrides):
    """Return a context manager that patches every is_*_available to False
    unless overridden.  Usage:  _patch_all_availability(ollama=True, openai=True)
    """
    defaults = {
        "ollama": False,
        "openai": False,
        "anthropic": False,
        "google": False,
        "openrouter": False,
        "openai_endpoint": False,
        "lmstudio": False,
        "llamacpp": False,
    }
    defaults.update(overrides)

    from contextlib import ExitStack

    stack = ExitStack()
    for name, val in defaults.items():
        fn_name = f"is_{name}_available"
        stack.enter_context(patch(f"{MODULE}.{fn_name}", return_value=val))
    return stack


# ===================================================================
# is_openai_available
# ===================================================================


class TestIsOpenAIAvailable:
    """Edge-case coverage for is_openai_available()."""

    def test_import_error_returns_false(self):
        """When OpenAIProvider cannot be imported, returns False."""
        with patch.dict(
            sys.modules,
            {"local_deep_research.llm.providers.implementations.openai": None},
        ):
            # Force re-import by calling the function which does a local import
            from local_deep_research.config.llm_config import (
                is_openai_available,
            )

            result = is_openai_available()
            assert result is False

    def test_passes_settings_snapshot_through(self):
        """settings_snapshot is forwarded to OpenAIProvider.is_available."""
        snap = {"llm.openai.api_key": "sk-test"}
        with patch(
            "local_deep_research.llm.providers.implementations.openai.OpenAIProvider.is_available",
            return_value=True,
        ) as mock_avail:
            from local_deep_research.config.llm_config import (
                is_openai_available,
            )

            is_openai_available(settings_snapshot=snap)
            mock_avail.assert_called_once_with(snap)


# ===================================================================
# is_anthropic_available
# ===================================================================


class TestIsAnthropicAvailable:
    """Edge-case coverage for is_anthropic_available()."""

    def test_import_error_returns_false(self):
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.anthropic": None
            },
        ):
            from local_deep_research.config.llm_config import (
                is_anthropic_available,
            )

            result = is_anthropic_available()
            assert result is False

    def test_passes_settings_snapshot_through(self):
        snap = {"llm.anthropic.api_key": "sk-ant-x"}
        with patch(
            "local_deep_research.llm.providers.implementations.anthropic.AnthropicProvider.is_available",
            return_value=True,
        ) as mock_avail:
            from local_deep_research.config.llm_config import (
                is_anthropic_available,
            )

            is_anthropic_available(settings_snapshot=snap)
            mock_avail.assert_called_once_with(snap)


# ===================================================================
# is_ollama_available
# ===================================================================


class TestIsOllamaAvailable:
    """Edge-case coverage for is_ollama_available()."""

    def test_import_error_returns_false(self):
        with patch.dict(
            sys.modules,
            {"local_deep_research.llm.providers.implementations.ollama": None},
        ):
            from local_deep_research.config.llm_config import (
                is_ollama_available,
            )

            result = is_ollama_available()
            assert result is False

    def test_passes_settings_snapshot_through(self):
        snap = {"llm.ollama.url": "http://localhost:11434"}
        with patch(
            "local_deep_research.llm.providers.implementations.ollama.OllamaProvider.is_available",
            return_value=True,
        ) as mock_avail:
            from local_deep_research.config.llm_config import (
                is_ollama_available,
            )

            is_ollama_available(settings_snapshot=snap)
            mock_avail.assert_called_once_with(snap)


# ===================================================================
# is_openai_endpoint_available
# ===================================================================


class TestIsOpenAIEndpointAvailable:
    """Edge-case coverage for is_openai_endpoint_available()."""

    def test_import_error_returns_false(self):
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.custom_openai_endpoint": None
            },
        ):
            from local_deep_research.config.llm_config import (
                is_openai_endpoint_available,
            )

            result = is_openai_endpoint_available()
            assert result is False

    def test_exception_returns_false(self):
        with patch(
            "local_deep_research.llm.providers.implementations.custom_openai_endpoint.CustomOpenAIEndpointProvider.is_available",
            side_effect=RuntimeError("boom"),
        ):
            from local_deep_research.config.llm_config import (
                is_openai_endpoint_available,
            )

            assert is_openai_endpoint_available() is False


# ===================================================================
# is_lmstudio_available
# ===================================================================


class TestIsLMStudioAvailable:
    """Edge-case coverage for is_lmstudio_available()."""

    def test_import_error_returns_false(self):
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.lmstudio": None
            },
        ):
            from local_deep_research.config.llm_config import (
                is_lmstudio_available,
            )

            result = is_lmstudio_available()
            assert result is False

    def test_exception_returns_false(self):
        with patch(
            "local_deep_research.llm.providers.implementations.lmstudio.LMStudioProvider.is_available",
            side_effect=RuntimeError("boom"),
        ):
            from local_deep_research.config.llm_config import (
                is_lmstudio_available,
            )

            assert is_lmstudio_available() is False


# ===================================================================
# is_llamacpp_available
# ===================================================================


class TestIsLlamaCppAvailable:
    """Edge-case coverage for is_llamacpp_available() — now HTTP-based."""

    LLAMACPP_PROVIDER = "local_deep_research.llm.providers.implementations.llamacpp.LlamaCppProvider"

    def test_returns_true_when_provider_available(self):
        """Returns True when LlamaCppProvider.is_available() returns True."""
        with patch(f"{self.LLAMACPP_PROVIDER}.is_available", return_value=True):
            from local_deep_research.config.llm_config import (
                is_llamacpp_available,
            )

            assert is_llamacpp_available() is True

    def test_returns_false_when_provider_unavailable(self):
        """Returns False when llama-server isn't reachable."""
        with patch(
            f"{self.LLAMACPP_PROVIDER}.is_available", return_value=False
        ):
            from local_deep_research.config.llm_config import (
                is_llamacpp_available,
            )

            assert is_llamacpp_available() is False

    def test_generic_exception_returns_false(self):
        """Unexpected exception in is_llamacpp_available returns False."""
        with patch(
            f"{self.LLAMACPP_PROVIDER}.is_available",
            side_effect=RuntimeError("unexpected"),
        ):
            from local_deep_research.config.llm_config import (
                is_llamacpp_available,
            )

            assert is_llamacpp_available() is False


# ===================================================================
# is_google_available
# ===================================================================


class TestIsGoogleAvailable:
    """Edge-case coverage for is_google_available()."""

    def test_import_error_returns_false(self):
        with patch.dict(
            sys.modules,
            {"local_deep_research.llm.providers.implementations.google": None},
        ):
            from local_deep_research.config.llm_config import (
                is_google_available,
            )

            assert is_google_available() is False

    def test_exception_returns_false(self):
        """When GoogleProvider.is_available raises, returns False."""
        mock_module = MagicMock()
        mock_module.GoogleProvider.is_available.side_effect = RuntimeError(
            "boom"
        )
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.google": mock_module
            },
        ):
            from local_deep_research.config.llm_config import (
                is_google_available,
            )

            assert is_google_available() is False

    def test_returns_true_when_provider_reports_available(self):
        mock_module = MagicMock()
        mock_module.GoogleProvider.is_available.return_value = True
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.google": mock_module
            },
        ):
            from local_deep_research.config.llm_config import (
                is_google_available,
            )

            assert is_google_available() is True

    def test_returns_false_when_provider_reports_unavailable(self):
        mock_module = MagicMock()
        mock_module.GoogleProvider.is_available.return_value = False
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.google": mock_module
            },
        ):
            from local_deep_research.config.llm_config import (
                is_google_available,
            )

            assert is_google_available() is False


# ===================================================================
# is_openrouter_available
# ===================================================================


class TestIsOpenRouterAvailable:
    """Edge-case coverage for is_openrouter_available()."""

    def test_import_error_returns_false(self):
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.openrouter": None
            },
        ):
            from local_deep_research.config.llm_config import (
                is_openrouter_available,
            )

            assert is_openrouter_available() is False

    def test_exception_returns_false(self):
        mock_module = MagicMock()
        mock_module.OpenRouterProvider.is_available.side_effect = RuntimeError(
            "boom"
        )
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.openrouter": mock_module
            },
        ):
            from local_deep_research.config.llm_config import (
                is_openrouter_available,
            )

            assert is_openrouter_available() is False

    def test_returns_true_when_provider_reports_available(self):
        mock_module = MagicMock()
        mock_module.OpenRouterProvider.is_available.return_value = True
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.openrouter": mock_module
            },
        ):
            from local_deep_research.config.llm_config import (
                is_openrouter_available,
            )

            assert is_openrouter_available() is True

    def test_returns_false_when_provider_reports_unavailable(self):
        mock_module = MagicMock()
        mock_module.OpenRouterProvider.is_available.return_value = False
        with patch.dict(
            sys.modules,
            {
                "local_deep_research.llm.providers.implementations.openrouter": mock_module
            },
        ):
            from local_deep_research.config.llm_config import (
                is_openrouter_available,
            )

            assert is_openrouter_available() is False


# ===================================================================
# ===================================================================
# get_available_providers  (cached function)
# ===================================================================


class TestGetAvailableProviders:
    """Tests for get_available_providers() with multiple providers and cache."""

    def setup_method(self):
        """Clear the LRU cache before each test."""
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        get_available_providers.cache_clear()

    def test_no_providers_returns_none_key(self):
        """When nothing is available, dict contains only 'none'."""
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability():
            result = get_available_providers()
            assert result == {"none": "No model providers available"}

    def test_only_ollama_available(self):
        """Only ollama available."""
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(ollama=True):
            result = get_available_providers()
            assert "ollama" in result
            assert result["ollama"] == "Ollama (local models)"
            assert "none" not in result

    def test_only_openai_available(self):
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(openai=True):
            result = get_available_providers()
            assert "openai" in result
            assert result["openai"] == "OpenAI API"
            assert "none" not in result

    def test_only_anthropic_available(self):
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(anthropic=True):
            result = get_available_providers()
            assert "anthropic" in result
            assert result["anthropic"] == "Anthropic API"

    def test_only_google_available(self):
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(google=True):
            result = get_available_providers()
            assert "google" in result
            assert result["google"] == "Google Gemini API"

    def test_only_openrouter_available(self):
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(openrouter=True):
            result = get_available_providers()
            assert "openrouter" in result
            assert result["openrouter"] == "OpenRouter API"

    def test_only_openai_endpoint_available(self):
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(openai_endpoint=True):
            result = get_available_providers()
            assert "openai_endpoint" in result
            assert result["openai_endpoint"] == "OpenAI-Compatible Endpoint"

    def test_only_lmstudio_available(self):
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(lmstudio=True):
            result = get_available_providers()
            assert "lmstudio" in result
            assert result["lmstudio"] == "LM Studio (local models)"

    def test_only_llamacpp_available(self):
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(llamacpp=True):
            result = get_available_providers()
            assert "llamacpp" in result
            assert result["llamacpp"] == "LlamaCpp (local models)"

    def test_multiple_providers_available(self):
        """Multiple providers yields dict with all of them, no 'none'."""
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(ollama=True, openai=True, anthropic=True):
            result = get_available_providers()
            assert "ollama" in result
            assert "openai" in result
            assert "anthropic" in result
            assert "none" not in result

    def test_all_providers_available(self):
        """All providers enabled results in all keys present."""
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(
            ollama=True,
            openai=True,
            anthropic=True,
            google=True,
            openrouter=True,
            openai_endpoint=True,
            lmstudio=True,
            llamacpp=True,
        ):
            result = get_available_providers()
            assert len(result) == 8
            assert "none" not in result

    def test_cache_returns_same_object(self):
        """Calling twice without cache_clear returns the cached dict."""
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(openai=True):
            first = get_available_providers()
            second = get_available_providers()
            assert first is second

    def test_cache_clear_resets(self):
        """After cache_clear, function re-evaluates availability."""
        from local_deep_research.config.llm_config import (
            get_available_providers,
        )

        with _patch_all_availability(openai=True):
            first = get_available_providers()
            assert "openai" in first

        get_available_providers.cache_clear()

        with _patch_all_availability(anthropic=True):
            second = get_available_providers()
            assert "anthropic" in second
            assert "openai" not in second


# ===================================================================
# get_selected_llm_provider
# ===================================================================


class TestGetSelectedLlmProvider:
    """Additional coverage for get_selected_llm_provider()."""

    def test_with_explicit_snapshot(self):
        from local_deep_research.config.llm_config import (
            get_selected_llm_provider,
        )

        result = get_selected_llm_provider(
            settings_snapshot={"llm.provider": "Google"}
        )
        assert result == "google"

    def test_mixed_case_normalised(self):
        from local_deep_research.config.llm_config import (
            get_selected_llm_provider,
        )

        result = get_selected_llm_provider(
            settings_snapshot={"llm.provider": "OpenAI_Endpoint"}
        )
        assert result == "openai_endpoint"

    def test_default_when_key_missing(self):
        from local_deep_research.config.llm_config import (
            get_selected_llm_provider,
        )

        result = get_selected_llm_provider(settings_snapshot={})
        assert result == "ollama"


# ===================================================================
# _get_context_window_for_provider  (additional edge cases)
# ===================================================================


class TestGetContextWindowForProvider:
    """Additional edge-case coverage for _get_context_window_for_provider."""

    def test_openrouter_treated_as_cloud(self):
        with patch(
            f"{MODULE}.get_setting_from_snapshot",
            return_value=True,
        ):
            from local_deep_research.config.llm_config import (
                _get_context_window_for_provider,
            )

            assert _get_context_window_for_provider("openrouter") is None

    def test_local_provider_with_float_value(self):
        """Float value from settings is coerced to int."""
        with patch(
            f"{MODULE}.get_setting_from_snapshot",
            return_value=8192.7,
        ):
            from local_deep_research.config.llm_config import (
                _get_context_window_for_provider,
            )

            result = _get_context_window_for_provider("lmstudio")
            assert result == 8192
            assert isinstance(result, int)

    def test_cloud_restricted_with_float_value(self):
        """Float cloud window is coerced to int."""
        call_num = [0]

        def fake_setting(key, default, settings_snapshot=None):
            call_num[0] += 1
            if key == "llm.context_window_unrestricted":
                return False
            if key == "llm.context_window_size":
                return 65536.9
            return default

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

            result = _get_context_window_for_provider("openai")
            assert result == 65536
            assert isinstance(result, int)


# ===================================================================
# wrap_llm_without_think_tags
# ===================================================================


class TestWrapLlmWithoutThinkTags:
    """Comprehensive tests for the ProcessingLLMWrapper created by wrap_llm_without_think_tags."""

    def _make_wrapper(self, mock_llm, **kwargs):
        """Create wrapper with rate-limiting disabled."""
        defaults = {
            "research_id": None,
            "provider": None,
            "research_context": None,
            "settings_snapshot": None,
        }
        defaults.update(kwargs)
        with patch(
            f"{MODULE}.get_setting_from_snapshot",
            return_value=False,
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            return wrap_llm_without_think_tags(mock_llm, **defaults)

    # --- basic wrapper behaviour ---

    def test_wrapper_has_base_llm(self):
        llm = MagicMock()
        w = self._make_wrapper(llm)
        assert w.base_llm is llm

    def test_invoke_calls_base_llm(self):
        llm = MagicMock()
        resp = MagicMock()
        resp.content = "hello"
        llm.invoke.return_value = resp
        w = self._make_wrapper(llm)
        w.invoke("prompt")
        llm.invoke.assert_called_once_with("prompt")

    def test_think_tags_removed_from_content(self):
        llm = MagicMock()
        resp = MagicMock()
        resp.content = "<think>internal reasoning</think>final answer"
        llm.invoke.return_value = resp
        w = self._make_wrapper(llm)
        result = w.invoke("prompt")
        assert "<think>" not in result.content
        assert "final answer" in result.content

    def test_think_tags_removed_from_string_response(self):
        llm = MagicMock()
        llm.invoke.return_value = "<think>thought</think>answer"
        w = self._make_wrapper(llm)
        result = w.invoke("prompt")
        assert isinstance(result, str)
        assert "<think>" not in result
        assert "answer" in result

    def test_response_without_content_attr_returned_as_is(self):
        """Response that is neither string nor has .content is passed through."""
        llm = MagicMock()
        resp = 42  # int has no .content
        llm.invoke.return_value = resp
        w = self._make_wrapper(llm)
        result = w.invoke("prompt")
        assert result == 42

    def test_invoke_exception_propagated(self):
        llm = MagicMock()
        llm.invoke.side_effect = ConnectionError("timeout")
        w = self._make_wrapper(llm)
        with pytest.raises(ConnectionError, match="timeout"):
            w.invoke("prompt")

    # --- __getattr__ delegation ---

    def test_getattr_delegates_to_base_llm(self):
        llm = MagicMock()
        llm.model_name = "gpt-4"
        llm.some_custom_attr = "custom_value"
        w = self._make_wrapper(llm)
        assert w.model_name == "gpt-4"
        assert w.some_custom_attr == "custom_value"

    # --- context_limit injection ---

    def test_context_limit_set_in_research_context(self):
        """wrap_llm sets context_limit in research_context when provider is local."""
        llm = MagicMock()
        research_ctx = {}

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

        with patch(
            f"{MODULE}.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="ollama", research_context=research_ctx
            )
        assert research_ctx.get("context_limit") == 4096

    def test_context_limit_not_overwritten_if_already_set(self):
        """If research_context already has context_limit, it should NOT be overwritten."""
        llm = MagicMock()
        research_ctx = {"context_limit": 9999}

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

        with patch(
            f"{MODULE}.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="ollama", research_context=research_ctx
            )
        assert research_ctx["context_limit"] == 9999

    def test_context_limit_not_set_for_unrestricted_cloud(self):
        """Cloud unrestricted provider returns None window, so context_limit not set."""
        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"{MODULE}.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="openai", research_context=research_ctx
            )
        assert "context_limit" not in research_ctx

    def test_no_crash_when_research_context_is_none(self):
        """No crash when research_context=None."""
        llm = MagicMock()
        w = self._make_wrapper(llm, provider="openai", research_context=None)
        assert w is not None

    # --- rate limiting integration ---

    def test_rate_limiting_applied_when_enabled(self):
        llm = MagicMock()
        rate_limited_llm = MagicMock()

        with (
            patch(
                f"{MODULE}.get_setting_from_snapshot",
                return_value=True,
            ),
            patch(
                "local_deep_research.web_search_engines.rate_limiting.llm.create_rate_limited_llm_wrapper",
                return_value=rate_limited_llm,
            ) as mock_rl,
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrapper = wrap_llm_without_think_tags(llm, provider="openai")
            mock_rl.assert_called_once_with(llm, "openai")
            # The wrapper wraps the rate-limited LLM
            assert wrapper.base_llm is rate_limited_llm

    def test_rate_limiting_not_applied_when_disabled(self):
        llm = MagicMock()

        with (
            patch(
                f"{MODULE}.get_setting_from_snapshot",
                return_value=False,
            ),
            patch(
                "local_deep_research.web_search_engines.rate_limiting.llm.create_rate_limited_llm_wrapper",
            ) as mock_rl,
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrapper = wrap_llm_without_think_tags(llm, provider="openai")
            mock_rl.assert_not_called()
            assert wrapper.base_llm is llm

    # --- token counter callback ---

    def test_token_counter_attached_when_research_id_given(self):
        """When research_id is set, a token counting callback is added."""
        llm = MagicMock()
        llm.callbacks = None
        llm.model_name = "test-model"

        mock_counter = MagicMock()
        mock_callback = MagicMock()
        mock_counter.create_callback.return_value = mock_callback

        with (
            patch(
                f"{MODULE}.get_setting_from_snapshot",
                return_value=False,
            ),
            patch(
                "local_deep_research.metrics.TokenCounter",
                return_value=mock_counter,
            ),
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrap_llm_without_think_tags(llm, research_id=42, provider="openai")
        mock_counter.create_callback.assert_called_once_with(42, None)
        assert mock_callback.preset_provider == "openai"
        assert mock_callback.preset_model == "test-model"

    def test_token_counter_uses_model_attr_fallback(self):
        """If llm has .model but not .model_name, uses .model."""
        llm = MagicMock(spec=["invoke", "callbacks", "model"])
        llm.callbacks = None
        llm.model = "claude-3-opus"

        mock_counter = MagicMock()
        mock_callback = MagicMock()
        mock_counter.create_callback.return_value = mock_callback

        with (
            patch(
                f"{MODULE}.get_setting_from_snapshot",
                return_value=False,
            ),
            patch(
                "local_deep_research.metrics.TokenCounter",
                return_value=mock_counter,
            ),
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrap_llm_without_think_tags(
                llm, research_id=1, provider="anthropic"
            )
        assert mock_callback.preset_model == "claude-3-opus"

    def test_callbacks_extended_when_existing(self):
        """If llm.callbacks already has entries, new callback is appended."""
        existing_cb = MagicMock()
        llm = MagicMock()
        llm.callbacks = [existing_cb]
        llm.model_name = "m"

        mock_counter = MagicMock()
        mock_callback = MagicMock()
        mock_counter.create_callback.return_value = mock_callback

        with (
            patch(
                f"{MODULE}.get_setting_from_snapshot",
                return_value=False,
            ),
            patch(
                "local_deep_research.metrics.TokenCounter",
                return_value=mock_counter,
            ),
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrap_llm_without_think_tags(llm, research_id=10)
        assert mock_callback in llm.callbacks
        assert existing_cb in llm.callbacks

    def test_no_callbacks_when_no_research_id(self):
        """When research_id is None, no callbacks are attached."""
        llm = MagicMock()
        llm.callbacks = None

        with patch(
            f"{MODULE}.get_setting_from_snapshot",
            return_value=False,
        ):
            from local_deep_research.config.llm_config import (
                wrap_llm_without_think_tags,
            )

            wrap_llm_without_think_tags(llm, research_id=None)
        # callbacks should remain None (nothing to attach)
        assert llm.callbacks is None


# ===================================================================
# VALID_PROVIDERS constant
# ===================================================================


class TestValidProviders:
    """Sanity checks for the VALID_PROVIDERS list."""

    def test_contains_all_expected(self):
        from local_deep_research.config.llm_config import VALID_PROVIDERS

        expected = {
            "ollama",
            "openai",
            "anthropic",
            "google",
            "openrouter",
            "openai_endpoint",
            "lmstudio",
            "llamacpp",
            "none",
        }
        assert set(VALID_PROVIDERS) == expected

    def test_is_list(self):
        from local_deep_research.config.llm_config import VALID_PROVIDERS

        assert isinstance(VALID_PROVIDERS, list)

    def test_no_duplicates(self):
        from local_deep_research.config.llm_config import VALID_PROVIDERS

        assert len(VALID_PROVIDERS) == len(set(VALID_PROVIDERS))
