"""Tests for Kimi provider implementation."""

import json
import os
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from oclawma.providers import (
    AuthenticationError,
    CompletionError,
    CompletionRequest,
    KimiProvider,
    Message,
    ModelNotFoundError,
    ProviderConnectionError,
    RateLimitError,
    UsageStats,
)
from oclawma.providers.fallback import FallbackProvider


class TestKimiProviderInit:
    """Test KimiProvider initialization."""

    def test_init_with_api_key(self) -> None:
        """Test initialization with explicit API key."""
        provider = KimiProvider(api_key="test-key")
        assert provider.api_key == "test-key"
        assert provider.base_url == "https://api.moonshot.cn/v1"

    def test_init_with_custom_base_url(self) -> None:
        """Test initialization with custom base URL."""
        provider = KimiProvider(api_key="test-key", base_url="https://custom.moonshot.cn/v1")
        assert provider.base_url == "https://custom.moonshot.cn/v1"

    def test_init_from_env_var(self) -> None:
        """Test initialization from environment variable."""
        with patch.dict(os.environ, {"KIMI_API_KEY": "env-test-key"}):
            provider = KimiProvider()
            assert provider.api_key == "env-test-key"

    def test_init_missing_api_key(self) -> None:
        """Test that missing API key raises AuthenticationError."""
        with patch.dict(os.environ, {}, clear=True):
            with pytest.raises(AuthenticationError) as exc_info:
                KimiProvider()
            assert "KIMI_API_KEY" in str(exc_info.value)


class TestKimiProviderTokenCounting:
    """Test KimiProvider token counting."""

    def test_count_tokens_empty(self) -> None:
        """Test token counting for empty string."""
        provider = KimiProvider(api_key="test")
        assert provider.count_tokens("") == 0

    def test_count_tokens_short(self) -> None:
        """Test token counting for short text."""
        provider = KimiProvider(api_key="test")
        # Minimum 1 token
        assert provider.count_tokens("Hi") == 1

    def test_count_tokens_long(self) -> None:
        """Test token counting for longer text."""
        provider = KimiProvider(api_key="test")
        text = "a" * 100  # 100 characters
        # ~3.5 chars per token = 29 tokens
        assert provider.count_tokens(text) == 29

    def test_count_message_tokens(self) -> None:
        """Test token counting for messages."""
        provider = KimiProvider(api_key="test")
        messages = [
            Message(role="system", content="You are helpful"),
            Message(role="user", content="Hello there"),
        ]
        tokens = provider.count_message_tokens(messages)
        # Should be greater than 0
        assert tokens > 0


class TestKimiProviderConvertMessages:
    """Test KimiProvider message conversion."""

    def test_convert_messages_basic(self) -> None:
        """Test basic message conversion."""
        provider = KimiProvider(api_key="test")
        messages = [
            Message(role="user", content="Hello"),
            Message(role="assistant", content="Hi there"),
        ]
        kimi_msgs = provider._convert_messages(messages)
        assert len(kimi_msgs) == 2
        assert kimi_msgs[0]["role"] == "user"
        assert kimi_msgs[0]["content"] == "Hello"
        assert kimi_msgs[1]["role"] == "assistant"

    def test_convert_messages_with_system(self) -> None:
        """Test message conversion with system prompt."""
        provider = KimiProvider(api_key="test")
        messages = [
            Message(role="system", content="You are helpful"),
            Message(role="user", content="Hello"),
        ]
        kimi_msgs = provider._convert_messages(messages)
        assert len(kimi_msgs) == 2
        assert kimi_msgs[0]["role"] == "system"
        assert kimi_msgs[1]["role"] == "user"

    def test_convert_messages_with_name(self) -> None:
        """Test message conversion with name field."""
        provider = KimiProvider(api_key="test")
        messages = [
            Message(role="assistant", content="Hi", name="Kimi"),
        ]
        kimi_msgs = provider._convert_messages(messages)
        assert kimi_msgs[0]["name"] == "Kimi"


class TestKimiProviderResolveModel:
    """Test KimiProvider model resolution."""

    def test_resolve_model_k2_5_alias(self) -> None:
        """Test k2.5 alias resolution."""
        provider = KimiProvider(api_key="test")
        assert provider._resolve_model("k2.5") == "kimi-k2-5"
        assert provider._resolve_model("k2p5") == "kimi-k2-5"

    def test_resolve_model_k1_5_alias(self) -> None:
        """Test k1.5 alias resolution."""
        provider = KimiProvider(api_key="test")
        assert provider._resolve_model("k1.5") == "kimi-k1-5"
        assert provider._resolve_model("k1p5") == "kimi-k1-5"

    def test_resolve_model_full_name(self) -> None:
        """Test full model name passthrough."""
        provider = KimiProvider(api_key="test")
        assert provider._resolve_model("kimi-k2-5") == "kimi-k2-5"


@pytest.mark.asyncio
class TestKimiProviderComplete:
    """Test KimiProvider complete method."""

    async def test_complete_success(self) -> None:
        """Test successful completion."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "id": "chatcmpl-test",
            "object": "chat.completion",
            "created": 1234567890,
            "model": "kimi-k2-5",
            "choices": [
                {
                    "index": 0,
                    "message": {"role": "assistant", "content": "Hello! How can I help?"},
                    "finish_reason": "stop",
                }
            ],
            "usage": {
                "prompt_tokens": 10,
                "completion_tokens": 7,
                "total_tokens": 17,
            },
        }
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="kimi-k2-5",
        )

        response = await provider.complete(request)

        assert response.content == "Hello! How can I help?"
        assert response.model == "kimi-k2-5"
        assert response.finish_reason == "stop"
        assert isinstance(response.usage, UsageStats)
        assert response.usage.prompt_tokens == 10
        assert response.usage.completion_tokens == 7
        assert response.usage.total_tokens == 17

    async def test_complete_with_max_tokens(self) -> None:
        """Test completion with max_tokens."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "choices": [{"message": {"content": "Response"}, "finish_reason": "stop"}],
            "usage": {"prompt_tokens": 5, "completion_tokens": 5, "total_tokens": 10},
        }
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="kimi-k2-5",
            max_tokens=100,
        )

        await provider.complete(request)

        call_args = mock_client.post.call_args
        payload = call_args[1]["json"]
        assert payload["max_tokens"] == 100

    async def test_complete_auth_error(self) -> None:
        """Test authentication error handling."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.status_code = 401
        mock_response.text = '{"error": {"message": "Invalid API key"}}'

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(
            side_effect=httpx.HTTPStatusError(
                "401",
                request=MagicMock(),
                response=mock_response,
            )
        )
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="kimi-k2-5",
        )

        with pytest.raises(AuthenticationError) as exc_info:
            await provider.complete(request)

        assert "Authentication failed" in str(exc_info.value)

    async def test_complete_rate_limit_error(self) -> None:
        """Test rate limit error handling."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.status_code = 429
        mock_response.text = '{"error": {"message": "Rate limit exceeded"}}'

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(
            side_effect=httpx.HTTPStatusError(
                "429",
                request=MagicMock(),
                response=mock_response,
            )
        )
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="kimi-k2-5",
        )

        with pytest.raises(RateLimitError) as exc_info:
            await provider.complete(request)

        assert "Rate limit exceeded" in str(exc_info.value)

    async def test_complete_model_not_found(self) -> None:
        """Test model not found error handling."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.text = '{"error": {"message": "Model not found"}}'

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(
            side_effect=httpx.HTTPStatusError(
                "404",
                request=MagicMock(),
                response=mock_response,
            )
        )
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="invalid-model",
        )

        with pytest.raises(ModelNotFoundError) as exc_info:
            await provider.complete(request)

        assert "Model not found" in str(exc_info.value)

    async def test_complete_connection_error(self) -> None:
        """Test connection error handling."""
        provider = KimiProvider(api_key="test")

        mock_client = AsyncMock()
        mock_client.post = AsyncMock(side_effect=httpx.ConnectError("Connection refused"))
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="kimi-k2-5",
        )

        with pytest.raises(ProviderConnectionError) as exc_info:
            await provider.complete(request)

        assert "Cannot connect to Kimi API" in str(exc_info.value)


@pytest.mark.asyncio
class TestKimiProviderStreamComplete:
    """Test KimiProvider streaming completion."""

    async def test_stream_complete_success(self) -> None:
        """Test successful streaming completion."""
        provider = KimiProvider(api_key="test")

        # Create mock stream data in SSE format
        chunks = [
            "data: "
            + json.dumps(
                {
                    "id": "chatcmpl-test",
                    "model": "kimi-k2-5",
                    "choices": [{"delta": {"content": "Hello"}, "finish_reason": None}],
                }
            ),
            "data: "
            + json.dumps(
                {
                    "choices": [{"delta": {"content": " world"}, "finish_reason": None}],
                }
            ),
            "data: "
            + json.dumps(
                {
                    "choices": [{"delta": {"content": "!"}, "finish_reason": "stop"}],
                }
            ),
        ]

        async def async_chunk_iterator():
            for chunk in chunks:
                yield chunk

        mock_stream = MagicMock()
        mock_stream.aiter_lines = MagicMock(return_value=async_chunk_iterator())
        mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)
        mock_stream.__aexit__ = AsyncMock(return_value=None)

        mock_client = AsyncMock()
        mock_client.stream = MagicMock(return_value=mock_stream)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="kimi-k2-5",
        )

        responses = []
        async for response in provider.stream_complete(request):
            responses.append(response)

        assert len(responses) == 3
        assert responses[0].content == "Hello"
        assert responses[1].content == " world"
        assert responses[2].content == "!"
        assert responses[2].finish_reason == "stop"

    async def test_stream_with_done_message(self) -> None:
        """Test streaming with [DONE] message."""
        provider = KimiProvider(api_key="test")

        chunks = [
            "data: "
            + json.dumps(
                {
                    "choices": [{"delta": {"content": "Hello"}, "finish_reason": None}],
                }
            ),
            "data: [DONE]",
        ]

        async def async_chunk_iterator():
            for chunk in chunks:
                yield chunk

        mock_stream = MagicMock()
        mock_stream.aiter_lines = MagicMock(return_value=async_chunk_iterator())
        mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)
        mock_stream.__aexit__ = AsyncMock(return_value=None)

        mock_client = AsyncMock()
        mock_client.stream = MagicMock(return_value=mock_stream)
        mock_client.is_closed = False

        provider._client = mock_client

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="kimi-k2-5",
        )

        responses = []
        async for response in provider.stream_complete(request):
            responses.append(response)

        assert len(responses) == 1
        assert responses[0].content == "Hello"


@pytest.mark.asyncio
class TestKimiProviderListModels:
    """Test KimiProvider list_models method."""

    async def test_list_models_success(self) -> None:
        """Test successful model listing."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.json.return_value = {
            "data": [
                {"id": "kimi-k2-5", "object": "model"},
                {"id": "kimi-k1-5", "object": "model"},
            ]
        }
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        models = await provider.list_models()

        assert "kimi-k2-5" in models
        assert "kimi-k1-5" in models

    async def test_list_models_auth_error(self) -> None:
        """Test authentication error during model listing."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.status_code = 401

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(
            side_effect=httpx.HTTPStatusError(
                "401",
                request=MagicMock(),
                response=mock_response,
            )
        )
        mock_client.is_closed = False

        provider._client = mock_client

        with pytest.raises(AuthenticationError):
            await provider.list_models()


@pytest.mark.asyncio
class TestKimiProviderHealthCheck:
    """Test KimiProvider health_check method."""

    async def test_health_check_healthy(self) -> None:
        """Test healthy status."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.json.return_value = {"data": [{"id": "kimi-k2-5"}]}
        mock_response.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=mock_response)
        mock_client.is_closed = False

        provider._client = mock_client

        health = await provider.health_check()

        assert health["status"] == "healthy"
        assert health["api_key_configured"] is True
        assert "kimi-k2-5" in health["models"]

    async def test_health_check_unauthenticated(self) -> None:
        """Test unauthenticated status."""
        provider = KimiProvider(api_key="test")

        mock_response = MagicMock()
        mock_response.status_code = 401

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(
            side_effect=httpx.HTTPStatusError(
                "401",
                request=MagicMock(),
                response=mock_response,
            )
        )
        mock_client.is_closed = False

        provider._client = mock_client

        health = await provider.health_check()

        assert health["status"] == "unauthenticated"


class TestKimiProviderClose:
    """Test KimiProvider close method."""

    async def test_close_client(self) -> None:
        """Test closing the client."""
        provider = KimiProvider(api_key="test")

        mock_client = AsyncMock()
        mock_client.is_closed = False
        mock_client.aclose = AsyncMock()

        provider._client = mock_client

        await provider.close()

        mock_client.aclose.assert_called_once()
        assert provider._client is None


class TestFallbackProvider:
    """Test FallbackProvider functionality."""

    def test_init(self) -> None:
        """Test fallback provider initialization."""
        primary = MagicMock(spec=KimiProvider)
        fallback = MagicMock(spec=KimiProvider)

        provider = FallbackProvider(primary, fallback, context_threshold=5000)

        assert provider.primary == primary
        assert provider.fallback == fallback
        assert provider.context_threshold == 5000
        assert provider.last_used_fallback is False

    def test_should_use_fallback(self) -> None:
        """Test fallback detection logic."""
        primary = MagicMock(spec=KimiProvider)
        primary.count_message_tokens = MagicMock(return_value=100)

        fallback = MagicMock(spec=KimiProvider)
        provider = FallbackProvider(primary, fallback, context_threshold=5000)

        # Small request should not trigger fallback
        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="test",
        )

        # Temporarily patch count_message_tokens
        provider.count_message_tokens = MagicMock(return_value=100)
        should, reason = provider._should_use_fallback(request)

        assert should is False
        assert reason is None

    def test_should_use_fallback_large_context(self) -> None:
        """Test fallback triggered by large context."""
        primary = MagicMock(spec=KimiProvider)
        fallback = MagicMock(spec=KimiProvider)
        provider = FallbackProvider(primary, fallback, context_threshold=5000)

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="test",
        )

        # Large request should trigger fallback
        provider.count_message_tokens = MagicMock(return_value=6000)
        should, reason = provider._should_use_fallback(request)

        assert should is True
        assert reason is not None
        assert "6000" in reason

    @pytest.mark.asyncio
    async def test_complete_uses_primary(self) -> None:
        """Test that primary provider is used for normal requests."""
        primary = MagicMock(spec=KimiProvider)
        primary.complete = AsyncMock(
            return_value=MagicMock(
                content="Primary response",
                model="test",
                usage=MagicMock(),
            )
        )

        fallback = MagicMock(spec=KimiProvider)

        provider = FallbackProvider(primary, fallback)
        provider._should_use_fallback = MagicMock(return_value=(False, None))

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="test",
        )

        await provider.complete(request)

        assert provider.last_used_fallback is False
        primary.complete.assert_called_once_with(request)
        fallback.complete.assert_not_called()

    @pytest.mark.asyncio
    async def test_complete_uses_fallback_on_threshold(self) -> None:
        """Test that fallback is used when context exceeds threshold."""
        primary = MagicMock(spec=KimiProvider)
        fallback = MagicMock(spec=KimiProvider)
        fallback.complete = AsyncMock(
            return_value=MagicMock(
                content="Fallback response",
                model="cloud",
                usage=MagicMock(),
            )
        )

        provider = FallbackProvider(primary, fallback)
        provider._should_use_fallback = MagicMock(return_value=(True, "Context too large"))

        request = CompletionRequest(
            messages=[Message(role="user", content="Large content...")],
            model="test",
        )

        await provider.complete(request)

        assert provider.last_used_fallback is True
        assert provider.fallback_reason == "Context too large"
        primary.complete.assert_not_called()
        fallback.complete.assert_called_once_with(request)

    @pytest.mark.asyncio
    async def test_complete_fallback_on_error(self) -> None:
        """Test fallback when primary raises context error."""

        primary = MagicMock(spec=KimiProvider)
        primary.complete = AsyncMock(side_effect=CompletionError("Context length exceeded"))

        fallback = MagicMock(spec=KimiProvider)
        fallback.complete = AsyncMock(
            return_value=MagicMock(
                content="Fallback response",
                model="cloud",
                usage=MagicMock(),
            )
        )

        provider = FallbackProvider(primary, fallback)
        provider._should_use_fallback = MagicMock(return_value=(False, None))

        request = CompletionRequest(
            messages=[Message(role="user", content="Hi")],
            model="test",
        )

        await provider.complete(request)

        assert provider.last_used_fallback is True
        assert "Context length exceeded" in str(provider.fallback_reason)
        fallback.complete.assert_called_once()

    @pytest.mark.asyncio
    async def test_health_check(self) -> None:
        """Test health check combines both providers."""
        primary = MagicMock(spec=KimiProvider)
        primary.health_check = AsyncMock(return_value={"status": "healthy", "models": ["local"]})

        fallback = MagicMock(spec=KimiProvider)
        fallback.health_check = AsyncMock(return_value={"status": "healthy", "models": ["cloud"]})

        provider = FallbackProvider(primary, fallback)

        health = await provider.health_check()

        assert health["status"] == "healthy"
        assert health["primary"]["status"] == "healthy"
        assert health["fallback"]["status"] == "healthy"

    def test_get_fallback_info(self) -> None:
        """Test fallback info method."""
        # Use actual provider instances for this test
        primary = KimiProvider(api_key="test-primary")
        fallback = KimiProvider(api_key="test-fallback")

        provider = FallbackProvider(primary, fallback, context_threshold=5000)

        info = provider.get_fallback_info()

        assert info["context_threshold"] == 5000
        assert info["primary_provider"] == "KimiProvider"
        assert info["fallback_provider"] == "KimiProvider"
        assert info["last_used_fallback"] is False
