"""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 path for the anthropic implementation
ANTHROPIC_MOD = "local_deep_research.llm.providers.implementations.anthropic"
GET_SETTING = f"{ANTHROPIC_MOD}.get_setting_from_snapshot"
CHAT_ANTHROPIC = f"{ANTHROPIC_MOD}.ChatAnthropic"


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_passes_through(self):
        """A whitespace-only API key passes the truthiness check and reaches ChatAnthropic."""
        with patch(
            GET_SETTING,
            side_effect=_make_setting_side_effect(
                {"llm.anthropic.api_key": "   "}
            ),
        ):
            with patch(CHAT_ANTHROPIC) as mock_chat:
                mock_chat.return_value = Mock()
                AnthropicProvider.create_llm()
                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["anthropic_api_key"] == "   "

    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()
                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()
                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()
                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()
                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()
                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(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()
                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(
            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(
            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(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(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()
                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(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()

    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()
                call_kwargs = mock_chat.call_args[1]
                expected_keys = {"model", "anthropic_api_key", "temperature"}
                assert set(call_kwargs.keys()) == expected_keys
