"""High-value tests for Anthropic LLM provider edge cases and error paths.

These tests focus on error handling paths, credential validation edge cases,
connection failure scenarios, and other under-tested behaviors in the
AnthropicProvider implementation.
"""

import pytest
from unittest.mock import Mock, patch

from local_deep_research.llm.providers.implementations.anthropic import (
    AnthropicProvider,
)


# Module-level patch paths.
# get_setting_from_snapshot is patched at its source module so that calls
# from BaseLLMProvider.resolve_api_key (which lives in providers/base.py)
# are intercepted alongside calls from anthropic.py itself.
ANTHROPIC_MOD = "local_deep_research.llm.providers.implementations.anthropic"
GET_SETTING = (
    "local_deep_research.config.thread_settings.get_setting_from_snapshot"
)
CHAT_ANTHROPIC = f"{ANTHROPIC_MOD}.ChatAnthropic"

# Patch path for is_available(), which is inherited from
# OpenAICompatibleProvider and resolves the key via
# BaseLLMProvider.resolve_api_key — same source-module patch point.
BASE_GET_SETTING = GET_SETTING


def _make_setting_side_effect(overrides=None):
    """Helper to build a mock side_effect for get_setting_from_snapshot."""
    defaults = {
        "llm.anthropic.api_key": "test-anthropic-key",
        "llm.max_tokens": None,
    }
    if overrides:
        defaults.update(overrides)

    def side_effect(key, default=None, *args, **kwargs):
        return defaults.get(key, default)

    return side_effect


class TestAnthropicAPIKeyValidation:
    """Tests for API key validation edge cases in create_llm."""

    def test_empty_string_api_key_raises_value_error(self):
        """An empty-string API key should raise ValueError."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": ""}
            ),
        ):
            with pytest.raises(ValueError, match="API key not configured"):
                AnthropicProvider.create_llm()

    def test_whitespace_only_api_key_treated_as_missing(self):
        """A whitespace-only API key is normalized as missing and raises (required provider)."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": "   "}
            ),
        ):
            with pytest.raises(ValueError, match="API key not configured"):
                AnthropicProvider.create_llm(model_name="test-model")

    def test_none_api_key_raises_value_error(self):
        """A None API key should raise ValueError."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": None}
            ),
        ):
            with pytest.raises(ValueError, match="API key not configured"):
                AnthropicProvider.create_llm()

    def test_false_api_key_raises_value_error(self):
        """A boolean False API key should raise ValueError (falsy value)."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": False}
            ),
        ):
            with pytest.raises(ValueError):
                AnthropicProvider.create_llm()

    def test_error_message_includes_setting_key(self):
        """ValueError message should include the settings key."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": None}
            ),
        ):
            with pytest.raises(ValueError) as exc_info:
                AnthropicProvider.create_llm()
            assert "llm.anthropic.api_key" in str(exc_info.value)

    def test_error_message_includes_provider_name(self):
        """ValueError message should include provider name."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": None}
            ),
        ):
            with pytest.raises(ValueError) as exc_info:
                AnthropicProvider.create_llm()
            assert "Anthropic" in str(exc_info.value)

    def test_api_key_passed_as_anthropic_api_key_param(self):
        """API key must be passed as 'anthropic_api_key', not 'api_key'."""
        with patch(GET_SETTING, side_effect=_make_setting_side_effect()):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(model_name="test-model")
                call_kwargs = mock_chat.call_args[1]
                assert "anthropic_api_key" in call_kwargs
                assert "api_key" not in call_kwargs


class TestAnthropicMaxTokensHandling:
    """Tests for max_tokens configuration edge cases."""

    def test_max_tokens_not_included_when_none(self):
        """max_tokens should not be in params when settings return None."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect({"llm.max_tokens": None}),
        ):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(model_name="test-model")
                call_kwargs = mock_chat.call_args[1]
                assert "max_tokens" not in call_kwargs

    def test_max_tokens_not_included_when_zero(self):
        """max_tokens should not be in params when set to 0 (falsy)."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect({"llm.max_tokens": 0}),
        ):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(model_name="test-model")
                call_kwargs = mock_chat.call_args[1]
                assert "max_tokens" not in call_kwargs

    def test_max_tokens_converted_to_int(self):
        """max_tokens should be converted to int even if stored as string."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect({"llm.max_tokens": "8192"}),
        ):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(model_name="test-model")
                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["max_tokens"] == 8192
                assert isinstance(call_kwargs["max_tokens"], int)

    def test_max_tokens_converted_to_int_from_float(self):
        """max_tokens should be converted to int even if stored as float."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect({"llm.max_tokens": 4096.0}),
        ):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(model_name="test-model")
                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["max_tokens"] == 4096
                assert isinstance(call_kwargs["max_tokens"], int)


class TestAnthropicSettingsSnapshot:
    """Tests for settings_snapshot passthrough behavior."""

    def test_settings_snapshot_passed_through_kwargs(self):
        """settings_snapshot should be extracted from kwargs and passed to get_setting."""
        snapshot = {"llm.anthropic.api_key": "snap-key", "llm.max_tokens": None}

        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": "snap-key"}
            ),
        ) as mock_get:
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(
                    model_name="test-model", settings_snapshot=snapshot
                )

                for call in mock_get.call_args_list:
                    assert call.kwargs.get("settings_snapshot") == snapshot

    def test_create_llm_without_settings_snapshot(self):
        """create_llm should work when no settings_snapshot is provided."""
        with patch(GET_SETTING, side_effect=_make_setting_side_effect()):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                result = AnthropicProvider.create_llm(model_name="test-model")
                assert result is mock_chat.return_value


class TestAnthropicIsAvailableEdgeCases:
    """Tests for is_available edge cases."""

    def test_whitespace_only_key_returns_false(self):
        """Whitespace-only API key is stripped in is_available, returns False."""
        with patch(
            BASE_GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": "   \t\n  "}
            ),
        ):
            assert AnthropicProvider.is_available() is False

    def test_valid_key_with_leading_trailing_whitespace(self):
        """A valid key with surrounding whitespace should still be available."""
        with patch(
            BASE_GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": "  sk-ant-valid-key  "}
            ),
        ):
            assert AnthropicProvider.is_available() is True

    def test_is_available_catches_runtime_error(self):
        """is_available should catch RuntimeError from missing settings context."""
        with patch(BASE_GET_SETTING) as mock_get:
            mock_get.side_effect = RuntimeError("No settings context")
            assert AnthropicProvider.is_available() is False

    def test_is_available_catches_type_error(self):
        """is_available should catch TypeError."""
        with patch(BASE_GET_SETTING) as mock_get:
            mock_get.side_effect = TypeError("unexpected type")
            assert AnthropicProvider.is_available() is False


class TestAnthropicChatConstruction:
    """Tests verifying ChatAnthropic is constructed with correct params."""

    def test_default_temperature_is_0_7(self):
        """Default temperature should be 0.7 when not specified."""
        with patch(GET_SETTING, side_effect=_make_setting_side_effect()):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(model_name="test-model")
                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["temperature"] == 0.7

    def test_zero_temperature(self):
        """Temperature of 0.0 should be passed through correctly."""
        with patch(GET_SETTING, side_effect=_make_setting_side_effect()):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(
                    model_name="test-model", temperature=0.0
                )
                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["temperature"] == 0.0

    def test_chat_anthropic_exception_propagates(self):
        """If ChatAnthropic constructor raises, it should propagate."""
        with patch(GET_SETTING, side_effect=_make_setting_side_effect()):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.side_effect = Exception("Connection refused")
                with pytest.raises(Exception, match="Connection refused"):
                    AnthropicProvider.create_llm(model_name="test-model")

    def test_only_expected_params_passed_without_max_tokens(self):
        """Without max_tokens, only model, anthropic_api_key, temperature should be passed."""
        with patch(GET_SETTING, side_effect=_make_setting_side_effect()):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm(model_name="test-model")
                call_kwargs = mock_chat.call_args[1]
                expected_keys = {"model", "anthropic_api_key", "temperature"}
                assert set(call_kwargs.keys()) == expected_keys
