"""
Coverage tests for local_deep_research/llm/providers/openai_base.py

Tests focus on branches NOT already exercised by tests/llm_providers/test_openai_base.py:
- create_llm() raises ValueError when api_key_setting is set but key is missing
- create_llm() uses 'dummy-key' when api_key_setting is None (local providers)
- create_llm() respects default_model when model_name is None
- create_llm() passes base_url kwarg override correctly
- _create_llm_instance() uses dummy-key by default
- is_available() returns True when api_key_setting is None
- is_available() returns False when key is blank/missing
- requires_auth_for_models() returns True by default
- _get_base_url_for_models() returns default_base_url when url_setting is None
- _get_base_url_for_models() returns configured URL when url_setting is present
- list_models_for_api() returns [] when auth required but api_key is None
- list_models() propagates exceptions as []
"""

from unittest.mock import MagicMock, patch

import pytest

from local_deep_research.llm.providers.openai_base import (
    OpenAICompatibleProvider,
)


# ---------------------------------------------------------------------------
# Subclass helpers for testing
# ---------------------------------------------------------------------------


class _NoKeyProvider(OpenAICompatibleProvider):
    """Provider that doesn't require an API key (like LM Studio)."""

    provider_name = "no_key_provider"
    api_key_setting = None
    default_base_url = "http://localhost:1234/v1"
    default_model = "local-model"


class _KeyedProvider(OpenAICompatibleProvider):
    """Provider that requires an API key."""

    provider_name = "keyed_provider"
    api_key_setting = "llm.keyed_provider.api_key"
    default_base_url = "https://api.example.com/v1"
    default_model = "default-model"


class _UrlSettingProvider(OpenAICompatibleProvider):
    """Provider with a configurable URL setting."""

    provider_name = "url_provider"
    api_key_setting = None
    url_setting = "llm.url_provider.url"
    default_base_url = "http://default.local/v1"
    default_model = "url-model"


# ---------------------------------------------------------------------------
# create_llm()
# ---------------------------------------------------------------------------


class TestCreateLlm:
    def test_raises_when_api_key_missing(self):
        """If api_key_setting is set but key not found, ValueError is raised."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            return_value=None,
        ):
            with pytest.raises(ValueError, match="not configured"):
                _KeyedProvider.create_llm()

    def test_no_api_key_setting_uses_dummy_key(self):
        """Providers with api_key_setting=None get a dummy key."""
        mock_llm = MagicMock()
        with (
            patch(
                "local_deep_research.config.thread_settings.get_setting_from_snapshot",
                return_value=None,
            ),
            patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI",
                return_value=mock_llm,
            ) as MockChat,
        ):
            _NoKeyProvider.create_llm(model_name="test-model")
        # dummy-key was passed
        call_kwargs = MockChat.call_args[1]
        assert call_kwargs["api_key"] == "not-required"

    def test_uses_default_model_when_none_given(self):
        """model_name=None → raises ValueError (no silent default)."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            return_value=None,
        ):
            with pytest.raises(ValueError, match="model not configured"):
                _NoKeyProvider.create_llm(model_name=None)

    def test_base_url_kwarg_overrides_default(self):
        """Passing base_url kwarg overrides cls.default_base_url."""
        mock_llm = MagicMock()
        with (
            patch(
                "local_deep_research.config.thread_settings.get_setting_from_snapshot",
                return_value=None,
            ),
            patch(
                "local_deep_research.llm.providers.openai_base.normalize_url",
                side_effect=lambda u: u,
            ),
            patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI",
                return_value=mock_llm,
            ) as MockChat,
        ):
            _NoKeyProvider.create_llm(
                model_name="test-model", base_url="http://custom.local/v1"
            )
        call_kwargs = MockChat.call_args[1]
        assert "custom.local" in call_kwargs["base_url"]


# ---------------------------------------------------------------------------
# _create_llm_instance()
# ---------------------------------------------------------------------------


class TestCreateLlmInstance:
    def test_uses_dummy_key_by_default(self):
        mock_llm = MagicMock()
        with (
            patch(
                "local_deep_research.config.thread_settings.get_setting_from_snapshot",
                return_value=None,
            ),
            patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI",
                return_value=mock_llm,
            ) as MockChat,
        ):
            _NoKeyProvider._create_llm_instance(model_name="test-model")
        call_kwargs = MockChat.call_args[1]
        assert call_kwargs["api_key"] == "not-required"


# ---------------------------------------------------------------------------
# is_available()
# ---------------------------------------------------------------------------


class TestIsAvailable:
    def test_no_key_setting_always_available(self):
        assert _NoKeyProvider.is_available() is True

    def test_keyed_provider_available_when_key_present(self):
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            return_value="sk-real-key",
        ):
            assert _KeyedProvider.is_available() is True

    def test_keyed_provider_not_available_when_key_missing(self):
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            return_value=None,
        ):
            assert _KeyedProvider.is_available() is False

    def test_keyed_provider_not_available_when_key_blank(self):
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            return_value="   ",
        ):
            assert _KeyedProvider.is_available() is False

    def test_exception_during_check_returns_false(self):
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            side_effect=Exception("boom"),
        ):
            assert _KeyedProvider.is_available() is False


# ---------------------------------------------------------------------------
# requires_auth_for_models()
# ---------------------------------------------------------------------------


class TestRequiresAuthForModels:
    def test_default_returns_true(self):
        assert OpenAICompatibleProvider.requires_auth_for_models() is True


# ---------------------------------------------------------------------------
# _get_base_url_for_models()
# ---------------------------------------------------------------------------


class TestGetBaseUrlForModels:
    def test_returns_default_url_when_no_url_setting(self):
        url = _KeyedProvider._get_base_url_for_models()
        assert url == _KeyedProvider.default_base_url

    def test_returns_configured_url_when_url_setting_present(self):
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot",
            return_value="http://configured.local/v1",
        ):
            url = _UrlSettingProvider._get_base_url_for_models()
        assert "configured.local" in url


# ---------------------------------------------------------------------------
# list_models_for_api()
# ---------------------------------------------------------------------------


class TestListModelsForApi:
    def test_returns_empty_list_when_auth_required_but_no_key(self):
        result = _KeyedProvider.list_models_for_api(api_key=None)
        assert result == []

    def test_returns_empty_list_on_openai_exception(self):
        mock_client = MagicMock()
        mock_client.models.list.side_effect = Exception("network error")
        with patch(
            "openai.OpenAI",
            return_value=mock_client,
        ):
            result = _NoKeyProvider.list_models_for_api(api_key="dummy")
        assert result == []

    @pytest.mark.parametrize(
        "bad_key",
        [
            {"llm.openai.api_key": "sk-leaked"},  # the issue #3800 case
            123,
            b"bytes-not-string",
            ["list", "of", "things"],
        ],
        ids=["dict", "int", "bytes", "list"],
    )
    def test_rejects_non_string_api_key_without_calling_sdk(self, bad_key):
        """Defense-in-depth (issue #3800): a non-string api_key would land
        in ``Authorization: Bearer <repr>`` and leak its contents to the
        endpoint. The provider must refuse before constructing the client.
        """
        with patch("openai.OpenAI") as mock_openai:
            result = _NoKeyProvider.list_models_for_api(
                api_key=bad_key, base_url="http://localhost:1234/v1"
            )
        assert result == []
        mock_openai.assert_not_called()


# ---------------------------------------------------------------------------
# list_models()
# ---------------------------------------------------------------------------


class TestListModels:
    def test_returns_empty_list_on_exception(self):
        with patch.object(
            _KeyedProvider,
            "list_models_for_api",
            side_effect=Exception("fail"),
        ):
            result = _KeyedProvider.list_models()
        assert result == []
