"""High-value edge case tests for LM Studio, OpenRouter, xAI, and IONOS providers.

Focuses on error handling paths, cross-provider consistency, edge cases in
configuration, list_models behavior, and provider-specific quirks.
"""

import pytest
from unittest.mock import Mock, patch

from local_deep_research.llm.providers.implementations.lmstudio import (
    LMStudioProvider,
)
from local_deep_research.llm.providers.implementations.openrouter import (
    OpenRouterProvider,
)
from local_deep_research.llm.providers.implementations.xai import (
    XAIProvider,
)
from local_deep_research.llm.providers.implementations.ionos import (
    IONOSProvider,
)
from local_deep_research.llm.providers.openai_base import (
    OpenAICompatibleProvider,
)
from local_deep_research.llm.providers.base import BaseLLMProvider


def _lmstudio_settings(url_value, extras=None):
    """Side-effect factory for LM Studio (no API key, URL-based)."""
    base = {
        "llm.lmstudio.url": url_value,
        "llm.max_tokens": None,
    }
    if extras:
        base.update(extras)

    def _side_effect(*args, **kwargs):
        key = args[0] if args else kwargs.get("key", "")
        default = args[1] if len(args) > 1 else kwargs.get("default", None)
        return base.get(key, default)

    return _side_effect


def _cloud_settings(api_key_setting, api_key_value, extras=None):
    """Build a mock side-effect for cloud provider settings."""
    base = {
        api_key_setting: api_key_value,
        "llm.max_tokens": None,
        "llm.streaming": None,
        "llm.max_retries": None,
        "llm.request_timeout": None,
    }
    if extras:
        base.update(extras)

    def _side_effect(key, default=None, *args, **kwargs):
        return base.get(key, default)

    return _side_effect


# ===========================================================================
# LM Studio edge cases
# ===========================================================================


class TestLMStudioEdgeCases:
    """Edge cases unique to the LM Studio provider."""

    def test_is_available_with_non_200_status_codes(self):
        """Various non-200 codes return False."""
        for status in [301, 403, 404, 502, 503]:
            with patch(
                "local_deep_research.config.thread_settings.get_setting_from_snapshot"
            ) as mock_gs:
                mock_gs.side_effect = _lmstudio_settings(
                    "http://localhost:1234/v1"
                )
                with patch("local_deep_research.security.safe_get") as mock_get:
                    mock_response = Mock(status_code=status)
                    mock_get.return_value = mock_response
                    assert LMStudioProvider.is_available() is False, (
                        f"Expected False for HTTP {status}"
                    )

    def test_is_available_timeout_returns_false(self):
        """A connection timeout is handled gracefully."""
        with patch(
            "local_deep_research.config.thread_settings.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _lmstudio_settings("http://localhost:1234/v1")
            with patch("local_deep_research.security.safe_get") as mock_get:
                mock_get.side_effect = TimeoutError("Connection timed out")
                assert LMStudioProvider.is_available() is False

    def test_lmstudio_inherits_from_openai_compatible(self):
        """LM Studio provider inherits from OpenAICompatibleProvider."""
        assert issubclass(LMStudioProvider, OpenAICompatibleProvider)
        assert issubclass(LMStudioProvider, BaseLLMProvider)


# ===========================================================================
# OpenRouter edge cases
# ===========================================================================


class TestOpenRouterEdgeCases:
    """Edge cases unique to the OpenRouter provider."""

    def test_create_llm_with_whitespace_only_api_key(self):
        """Whitespace-only API key passes truthiness check (not stripped in create_llm)."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _cloud_settings(
                "llm.openrouter.api_key", "   "
            )
            with patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI"
            ) as mock_chat:
                mock_chat.return_value = Mock()
                OpenRouterProvider.create_llm()
                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["api_key"] == "   "

    def test_create_llm_error_message_contains_setting_name(self):
        """Error message tells user which setting to configure."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _cloud_settings(
                "llm.openrouter.api_key", None
            )
            with pytest.raises(ValueError) as exc_info:
                OpenRouterProvider.create_llm()
            assert "llm.openrouter.api_key" in str(exc_info.value)

    def test_list_models_for_api_connection_error_returns_empty(self):
        """Network error returns empty list, not exception."""
        with patch("openai.OpenAI") as mock_openai:
            mock_openai.side_effect = ConnectionError("Network unreachable")
            result = OpenRouterProvider.list_models_for_api(api_key="key")
            assert result == []

    def test_openrouter_inherits_from_openai_compatible(self):
        """OpenRouter provider inherits from OpenAICompatibleProvider."""
        assert issubclass(OpenRouterProvider, OpenAICompatibleProvider)
        assert issubclass(OpenRouterProvider, BaseLLMProvider)

    def test_create_llm_passes_max_tokens_when_configured(self):
        """max_tokens from settings is forwarded to ChatOpenAI."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _cloud_settings(
                "llm.openrouter.api_key",
                "test-key",
                extras={"llm.max_tokens": 2048},
            )
            with patch(
                "local_deep_research.llm.providers.openai_base.ChatOpenAI"
            ) as mock_chat:
                mock_chat.return_value = Mock()
                OpenRouterProvider.create_llm()
                call_kwargs = mock_chat.call_args[1]
                assert call_kwargs["max_tokens"] == 2048


# ===========================================================================
# xAI edge cases
# ===========================================================================


class TestXAIEdgeCases:
    """Edge cases unique to the xAI Grok provider."""

    def test_create_llm_with_empty_string_api_key(self):
        """Empty string API key raises ValueError."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _cloud_settings("llm.xai.api_key", "")
            with pytest.raises(ValueError, match="(?i)api key"):
                XAIProvider.create_llm()

    def test_create_llm_error_message_contains_xai_setting(self):
        """Error message references the correct xAI setting key."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _cloud_settings("llm.xai.api_key", None)
            with pytest.raises(ValueError) as exc_info:
                XAIProvider.create_llm()
            assert "llm.xai.api_key" in str(exc_info.value)

    def test_list_models_for_api_requires_key(self):
        """xAI requires auth so list_models_for_api returns [] without key."""
        result = XAIProvider.list_models_for_api(api_key=None)
        assert result == []

    def test_is_available_with_exception_in_settings(self):
        """Exception during settings retrieval returns False."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = RuntimeError("settings DB corrupted")
            assert XAIProvider.is_available() is False

    def test_xai_inherits_from_openai_compatible(self):
        """xAI provider inherits from OpenAICompatibleProvider."""
        assert issubclass(XAIProvider, OpenAICompatibleProvider)
        assert issubclass(XAIProvider, BaseLLMProvider)


# ===========================================================================
# IONOS edge cases
# ===========================================================================


class TestIONOSEdgeCases:
    """Edge cases unique to the IONOS provider."""

    def test_create_llm_with_none_api_key_raises(self):
        """Explicitly None API key raises ValueError."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _cloud_settings("llm.ionos.api_key", None)
            with pytest.raises(ValueError, match="(?i)api key"):
                IONOSProvider.create_llm()

    def test_create_llm_error_message_references_ionos_setting(self):
        """Error message tells the user exactly which setting is missing."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.side_effect = _cloud_settings("llm.ionos.api_key", None)
            with pytest.raises(ValueError) as exc_info:
                IONOSProvider.create_llm()
            assert "llm.ionos.api_key" in str(exc_info.value)

    def test_list_models_for_api_without_key_returns_empty(self):
        """IONOS requires auth so returns [] without a key."""
        result = IONOSProvider.list_models_for_api(api_key=None)
        assert result == []

    def test_is_available_with_whitespace_only_key(self):
        """Whitespace-only API key is treated as unavailable."""
        with patch(
            "local_deep_research.llm.providers.openai_base.get_setting_from_snapshot"
        ) as mock_gs:
            mock_gs.return_value = "   "
            assert IONOSProvider.is_available() is False

    def test_ionos_inherits_from_openai_compatible(self):
        """IONOS provider inherits from OpenAICompatibleProvider."""
        assert issubclass(IONOSProvider, OpenAICompatibleProvider)
        assert issubclass(IONOSProvider, BaseLLMProvider)


# ===========================================================================
# Cross-provider consistency tests
# ===========================================================================


class TestCrossProviderConsistency:
    """Verify consistent behaviour across all four providers."""

    @pytest.mark.parametrize(
        "provider_cls,expected_key",
        [
            (LMStudioProvider, "LMSTUDIO"),
            (OpenRouterProvider, "OPENROUTER"),
            (XAIProvider, "XAI"),
            (IONOSProvider, "IONOS"),
        ],
    )
    def test_provider_key_is_uppercase(self, provider_cls, expected_key):
        """All provider_key values should be uppercase identifiers."""
        assert provider_cls.provider_key == expected_key

    @pytest.mark.parametrize(
        "provider_cls,is_cloud",
        [
            (LMStudioProvider, False),
            (OpenRouterProvider, True),
            (XAIProvider, True),
            (IONOSProvider, True),
        ],
    )
    def test_cloud_flag_consistency(self, provider_cls, is_cloud):
        """Cloud flag matches expected value for each provider."""
        assert provider_cls.is_cloud is is_cloud

    @pytest.mark.parametrize(
        "provider_cls",
        [LMStudioProvider, OpenRouterProvider, XAIProvider, IONOSProvider],
    )
    def test_default_base_url_is_valid(self, provider_cls):
        """All providers must have a non-empty default_base_url with a scheme."""
        url = provider_cls.default_base_url
        assert url is not None
        assert url.startswith("http://") or url.startswith("https://")

    @pytest.mark.parametrize(
        "provider_cls",
        [LMStudioProvider, OpenRouterProvider, XAIProvider, IONOSProvider],
    )
    def test_default_model_is_non_empty(self, provider_cls):
        """Every provider must declare a non-empty default model."""
        assert provider_cls.default_model
        assert len(provider_cls.default_model) > 0

    @pytest.mark.parametrize(
        "provider_cls",
        [OpenRouterProvider, XAIProvider, IONOSProvider],
    )
    def test_cloud_providers_require_api_key_setting(self, provider_cls):
        """Cloud providers must define an api_key_setting."""
        assert provider_cls.api_key_setting is not None
        assert provider_cls.api_key_setting.startswith("llm.")

    def test_local_provider_does_not_require_api_key(self):
        """LM Studio (local) should not require an API key."""
        assert LMStudioProvider.api_key_setting is None

    @pytest.mark.parametrize(
        "provider_cls",
        [LMStudioProvider, OpenRouterProvider, XAIProvider, IONOSProvider],
    )
    def test_all_providers_have_company_name(self, provider_cls):
        """Every provider must have a company_name for UI display."""
        assert provider_cls.company_name
        assert isinstance(provider_cls.company_name, str)
        assert len(provider_cls.company_name) > 0
