"""Tests for the API-key helpers on BaseLLMProvider.

Covers ``resolve_api_key`` (None vs raise vs string), the
``resolve_api_key_or_placeholder`` companion that always returns a string,
``build_bearer_header``, and the ``has_api_key`` predicate.
"""

from unittest.mock import patch

import pytest

from local_deep_research.llm.providers.base import (
    BaseLLMProvider,
    OPTIONAL_API_KEY_PLACEHOLDER,
)


class _RequiredProvider(BaseLLMProvider):
    """Test fixture: a provider that requires an API key."""

    provider_name = "RequiredProvider"
    api_key_setting = "test.required.api_key"
    api_key_optional = False


class _OptionalProvider(BaseLLMProvider):
    """Test fixture: a provider that allows missing API key."""

    provider_name = "OptionalProvider"
    api_key_setting = "test.optional.api_key"
    api_key_optional = True


class _NoSettingProvider(BaseLLMProvider):
    """Test fixture: a provider with no api_key_setting at all."""

    provider_name = "NoSettingProvider"
    api_key_setting = None


SETTINGS = (
    "local_deep_research.config.thread_settings.get_setting_from_snapshot"
)


def _set(value):
    """Patch get_setting_from_snapshot to return ``value`` for any key."""

    def side_effect(key, default=None, *args, **kwargs):
        if key == _RequiredProvider.api_key_setting:
            return value
        if key == _OptionalProvider.api_key_setting:
            return value
        return default

    return side_effect


class TestResolveApiKey:
    def test_required_with_real_key_returns_stripped(self):
        with patch(SETTINGS, side_effect=_set("  sk-key  ")):
            assert _RequiredProvider.resolve_api_key() == "sk-key"

    def test_required_with_missing_key_raises(self):
        with patch(SETTINGS, side_effect=_set(None)):
            with pytest.raises(ValueError, match="API key not configured"):
                _RequiredProvider.resolve_api_key()

    def test_required_with_empty_string_raises(self):
        with patch(SETTINGS, side_effect=_set("")):
            with pytest.raises(ValueError):
                _RequiredProvider.resolve_api_key()

    def test_required_with_whitespace_only_raises(self):
        with patch(SETTINGS, side_effect=_set("   ")):
            with pytest.raises(ValueError):
                _RequiredProvider.resolve_api_key()

    def test_optional_with_real_key_returns_stripped(self):
        with patch(SETTINGS, side_effect=_set(" sk-opt ")):
            assert _OptionalProvider.resolve_api_key() == "sk-opt"

    def test_optional_with_missing_key_returns_none(self):
        with patch(SETTINGS, side_effect=_set(None)):
            assert _OptionalProvider.resolve_api_key() is None

    def test_optional_with_whitespace_only_returns_none(self):
        with patch(SETTINGS, side_effect=_set("\t  \n")):
            assert _OptionalProvider.resolve_api_key() is None

    def test_no_setting_returns_none(self):
        # Patching is unnecessary — api_key_setting is None so no read happens
        assert _NoSettingProvider.resolve_api_key() is None


class TestResolveApiKeyOrPlaceholder:
    def test_returns_real_key_when_set(self):
        with patch(SETTINGS, side_effect=_set("real")):
            assert _OptionalProvider.resolve_api_key_or_placeholder() == "real"

    def test_returns_placeholder_when_optional_and_missing(self):
        with patch(SETTINGS, side_effect=_set("")):
            assert (
                _OptionalProvider.resolve_api_key_or_placeholder()
                == OPTIONAL_API_KEY_PLACEHOLDER
            )

    def test_required_missing_still_raises(self):
        # The "_or_placeholder" variant only swallows missing for OPTIONAL
        # providers; required providers must still raise so cloud-API
        # callers get a clear error instead of silently sending a fake key.
        with patch(SETTINGS, side_effect=_set("")):
            with pytest.raises(ValueError):
                _RequiredProvider.resolve_api_key_or_placeholder()


class TestBuildBearerHeader:
    def test_returns_empty_when_no_key(self):
        with patch(SETTINGS, side_effect=_set(None)):
            assert _OptionalProvider.build_bearer_header() == {}

    def test_returns_empty_for_whitespace_only(self):
        with patch(SETTINGS, side_effect=_set("   ")):
            assert _OptionalProvider.build_bearer_header() == {}

    def test_returns_bearer_when_key_set(self):
        with patch(SETTINGS, side_effect=_set("abc")):
            assert _OptionalProvider.build_bearer_header() == {
                "Authorization": "Bearer abc"
            }

    def test_strips_whitespace_in_header_value(self):
        with patch(SETTINGS, side_effect=_set("  abc  ")):
            assert _OptionalProvider.build_bearer_header() == {
                "Authorization": "Bearer abc"
            }

    def test_required_provider_with_no_key_returns_empty(self):
        # ValueError from resolve_api_key is swallowed — caller can decide
        # what to do (e.g., is_available returns False elsewhere).
        with patch(SETTINGS, side_effect=_set(None)):
            assert _RequiredProvider.build_bearer_header() == {}


class TestHasApiKey:
    def test_true_when_real_key_set(self):
        with patch(SETTINGS, side_effect=_set("real")):
            assert _RequiredProvider.has_api_key() is True

    def test_false_when_required_and_missing(self):
        with patch(SETTINGS, side_effect=_set(None)):
            assert _RequiredProvider.has_api_key() is False

    def test_false_when_optional_and_missing(self):
        with patch(SETTINGS, side_effect=_set("")):
            assert _OptionalProvider.has_api_key() is False

    def test_false_when_settings_raise_unexpectedly(self):
        # Defense-in-depth: settings infrastructure failures should not
        # propagate from is_available()-style callers.
        with patch(SETTINGS, side_effect=RuntimeError("settings down")):
            assert _RequiredProvider.has_api_key() is False
            assert _OptionalProvider.has_api_key() is False
