"""Deep coverage tests for llm_config.py targeting uncovered branches.

Focuses on:
- get_llm() with registered custom LLMs (callable factory and BaseChatModel instance)
- get_llm() with invalid provider
- ProcessingLLMWrapper.close()
- _get_context_window_for_provider() with None window_size from settings
"""

from unittest.mock import MagicMock, patch

import pytest
from langchain_core.language_models import BaseChatModel

MODULE = "local_deep_research.config.llm_config"
# Context-window resolution delegates to the _helpers twin, which reads
# settings via thread_settings.get_setting_from_snapshot. Patch this module
# (not MODULE) for context-window tests.
THREAD_SETTINGS = "local_deep_research.config.thread_settings"


def _make_settings_snapshot(provider="openai", model="gpt-4", **extra):
    snap = {
        "llm.provider": provider,
        "llm.model": model,
        "llm.temperature": 0.7,
        "llm.max_tokens": 4096,
        "llm.supports_max_tokens": True,
        "llm.context_window_unrestricted": True,
        "rate_limiting.llm_enabled": False,
    }
    snap.update(extra)
    return snap


# ---------------------------------------------------------------------------
# get_llm with registered custom LLM – callable factory
# ---------------------------------------------------------------------------


class TestGetLlmRegisteredFactory:
    def test_callable_factory_invoked(self):
        mock_instance = MagicMock(spec=BaseChatModel)
        mock_factory = MagicMock(return_value=mock_instance)

        # Make isinstance(mock_factory, BaseChatModel) return False
        # mock_factory is already a MagicMock (not BaseChatModel)

        with (
            patch(f"{MODULE}.is_llm_registered", return_value=True),
            patch(f"{MODULE}.get_llm_from_registry", return_value=mock_factory),
            patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
            patch(f"{MODULE}.wrap_llm_without_think_tags") as mock_wrap,
        ):
            mock_setting.side_effect = (
                lambda key, default=None, settings_snapshot=None: {
                    "llm.model": "gpt-4",
                    "llm.temperature": 0.7,
                    "llm.provider": "my_custom",
                    "rate_limiting.llm_enabled": False,
                }.get(key, default)
            )

            from local_deep_research.config.llm_config import get_llm

            get_llm(
                provider="my_custom",
                settings_snapshot={"search.tool": "searxng"},
            )

        mock_factory.assert_called_once()
        mock_wrap.assert_called_once()

    def test_factory_returning_non_basechatmodel_raises(self):
        mock_factory = MagicMock(return_value="not_a_model")

        with (
            patch(f"{MODULE}.is_llm_registered", return_value=True),
            patch(f"{MODULE}.get_llm_from_registry", return_value=mock_factory),
            patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
        ):
            mock_setting.side_effect = (
                lambda key, default=None, settings_snapshot=None: {
                    "llm.model": "gpt-4",
                    "llm.temperature": 0.7,
                    "llm.provider": "my_custom",
                }.get(key, default)
            )

            from local_deep_research.config.llm_config import get_llm

            with pytest.raises(ValueError, match="must return a BaseChatModel"):
                get_llm(
                    provider="my_custom",
                    settings_snapshot={"search.tool": "searxng"},
                )

    def test_factory_with_bad_signature_raises_type_error(self):
        def bad_factory():
            return MagicMock(spec=BaseChatModel)

        with (
            patch(f"{MODULE}.is_llm_registered", return_value=True),
            patch(f"{MODULE}.get_llm_from_registry", return_value=bad_factory),
            patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
        ):
            mock_setting.side_effect = (
                lambda key, default=None, settings_snapshot=None: {
                    "llm.model": "gpt-4",
                    "llm.temperature": 0.7,
                    "llm.provider": "bad_factory",
                }.get(key, default)
            )

            from local_deep_research.config.llm_config import get_llm

            with pytest.raises(TypeError, match="invalid signature"):
                get_llm(
                    provider="bad_factory",
                    settings_snapshot={"search.tool": "searxng"},
                )

    def test_registered_basechatmodel_instance_used_directly(self):
        mock_instance = MagicMock(spec=BaseChatModel)

        with (
            patch(f"{MODULE}.is_llm_registered", return_value=True),
            patch(
                f"{MODULE}.get_llm_from_registry", return_value=mock_instance
            ),
            patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
            patch(f"{MODULE}.wrap_llm_without_think_tags") as mock_wrap,
        ):
            mock_setting.side_effect = (
                lambda key, default=None, settings_snapshot=None: {
                    "llm.model": "gpt-4",
                    "llm.temperature": 0.7,
                    "llm.provider": "my_instance",
                }.get(key, default)
            )

            from local_deep_research.config.llm_config import get_llm

            get_llm(
                provider="my_instance",
                settings_snapshot={"search.tool": "searxng"},
            )

        # The factory path is skipped for BaseChatModel instances
        mock_wrap.assert_called_once()

    def test_registered_invalid_type_raises_value_error(self):
        """Registered object that is not BaseChatModel and not callable raises ValueError."""

        class WeirdThing:
            pass

        with (
            patch(f"{MODULE}.is_llm_registered", return_value=True),
            patch(f"{MODULE}.get_llm_from_registry", return_value=WeirdThing()),
            patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
        ):
            mock_setting.side_effect = (
                lambda key, default=None, settings_snapshot=None: {
                    "llm.model": "gpt-4",
                    "llm.temperature": 0.7,
                    "llm.provider": "weird",
                }.get(key, default)
            )

            from local_deep_research.config.llm_config import get_llm

            with pytest.raises(
                ValueError, match="must be either a BaseChatModel"
            ):
                get_llm(
                    provider="weird",
                    settings_snapshot={"search.tool": "searxng"},
                )


# ---------------------------------------------------------------------------
# get_llm with invalid provider
# ---------------------------------------------------------------------------


class TestGetLlmInvalidProvider:
    def test_invalid_provider_raises_value_error(self):
        with (
            patch(f"{MODULE}.is_llm_registered", return_value=False),
            patch(f"{MODULE}.get_setting_from_snapshot") as mock_setting,
        ):
            mock_setting.side_effect = (
                lambda key, default=None, settings_snapshot=None: {
                    "llm.model": "gpt-4",
                    "llm.temperature": 0.7,
                    "llm.provider": "bogus_provider",
                    "llm.supports_max_tokens": False,
                    "llm.context_window_unrestricted": True,
                    "rate_limiting.llm_enabled": False,
                }.get(key, default)
            )

            from local_deep_research.config.llm_config import get_llm

            with pytest.raises(ValueError, match="Invalid provider"):
                get_llm(
                    provider="bogus_provider",
                    settings_snapshot={"search.tool": "searxng"},
                )


# ---------------------------------------------------------------------------
# _get_context_window_for_provider edge cases
# ---------------------------------------------------------------------------


class TestContextWindowEdgeCases:
    def test_none_window_size_for_local_provider_defaults_to_8192(self):
        """If local_context_window_size returns None, defaults to 8192."""
        with patch(
            f"{THREAD_SETTINGS}.get_setting_from_snapshot",
            return_value=None,
        ):
            from local_deep_research.config.llm_config import (
                _get_context_window_for_provider,
            )

            result = _get_context_window_for_provider("ollama")
            assert result == 8192

    def test_none_window_size_for_cloud_restricted_defaults_to_128000(self):
        """Restricted cloud: if context_window_size returns None, defaults to 128000."""
        call_num = [0]

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

        with patch(
            f"{THREAD_SETTINGS}.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 == 128000

    def test_llamacpp_is_local_provider(self):
        """llamacpp is in the local provider list."""
        with patch(
            f"{THREAD_SETTINGS}.get_setting_from_snapshot",
            return_value=8192,
        ):
            from local_deep_research.config.llm_config import (
                _get_context_window_for_provider,
            )

            result = _get_context_window_for_provider("llamacpp")
            assert result == 8192


# ---------------------------------------------------------------------------
# ProcessingLLMWrapper.close()
# ---------------------------------------------------------------------------


class TestProcessingLLMWrapperClose:
    def _make_wrapper(self, mock_llm):
        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)

    def test_close_called_without_error(self):
        mock_llm = MagicMock()
        wrapper = self._make_wrapper(mock_llm)
        with patch(
            "local_deep_research.utilities.llm_utils._close_base_llm"
        ) as mock_close:
            wrapper.close()
            mock_close.assert_called_once_with(mock_llm)

    def test_close_swallows_exceptions(self):
        mock_llm = MagicMock()
        wrapper = self._make_wrapper(mock_llm)
        with patch(
            "local_deep_research.utilities.llm_utils._close_base_llm",
            side_effect=RuntimeError("close failed"),
        ):
            # Should not raise
            wrapper.close()


# ---------------------------------------------------------------------------
# get_llm model/provider name cleaning
# ---------------------------------------------------------------------------


class TestGetLlmNameCleaning:
    def test_model_name_stripped_and_unquoted(self):
        """Model name with surrounding quotes/whitespace is cleaned."""
        captured = {}

        def fake_setting(key, default=None, settings_snapshot=None):
            return {
                "llm.model": '  "gpt-4"  ',
                "llm.temperature": 0.7,
                "llm.provider": "openai",
                "llm.openai.api_key": "sk-test",
                "llm.supports_max_tokens": False,
                "llm.context_window_unrestricted": True,
                "rate_limiting.llm_enabled": False,
            }.get(key, default)

        with (
            patch(f"{MODULE}.is_llm_registered", return_value=False),
            patch(
                f"{MODULE}.get_setting_from_snapshot", side_effect=fake_setting
            ),
            patch(
                "langchain_openai.ChatOpenAI.__init__", return_value=None
            ) as mock_init,
            patch(
                f"{MODULE}.wrap_llm_without_think_tags",
                return_value=MagicMock(),
            ),
        ):
            try:
                from local_deep_research.config.llm_config import get_llm

                get_llm(settings_snapshot={"search.tool": "searxng"})
                if mock_init.call_args:
                    captured["model"] = mock_init.call_args.kwargs.get(
                        "model",
                        mock_init.call_args.args[0]
                        if mock_init.call_args.args
                        else None,
                    )
            except Exception:
                pass  # ChatOpenAI init may fail, we just care about name cleaning
