"""
LLM 客户端测试

测试 Anthropic 和 OpenAI 客户端的功能
"""

import pytest
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch
from typing import List, Dict, Any

import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))

from core.llm.base import (
    Message,
    MessageRole,
    ToolCall,
    ToolResult,
    ToolDefinition,
    StreamChunk,
)
from core.llm.anthropic_client import AnthropicClient
from core.llm.openai_client import OpenAIClient


class TestAnthropicClient:
    """测试 Anthropic 客户端"""

    @pytest.fixture
    def client(self):
        """创建测试客户端"""
        return AnthropicClient(
            model="claude-3-5-sonnet-20241022",
            api_key="test-api-key",
        )

    def test_client_initialization(self, client):
        """测试客户端初始化"""
        assert client.model == "claude-3-5-sonnet-20241022"
        assert client.api_key == "test-api-key"
        assert client.provider == "anthropic"

    def test_convert_simple_messages(self, client):
        """测试简单消息转换"""
        messages = [
            Message(role=MessageRole.USER, content="Hello"),
            Message(role=MessageRole.ASSISTANT, content="Hi there"),
        ]

        system_prompt, anthropic_messages = client._convert_messages(messages)

        assert system_prompt is None
        assert len(anthropic_messages) == 2
        assert anthropic_messages[0]["role"] == "user"
        assert anthropic_messages[0]["content"] == "Hello"
        assert anthropic_messages[1]["role"] == "assistant"
        assert anthropic_messages[1]["content"] == "Hi there"

    def test_convert_messages_with_system(self, client):
        """测试带系统消息的转换"""
        messages = [
            Message(role=MessageRole.SYSTEM, content="You are a helpful assistant"),
            Message(role=MessageRole.USER, content="Hello"),
        ]

        system_prompt, anthropic_messages = client._convert_messages(messages)

        assert system_prompt == "You are a helpful assistant"
        assert len(anthropic_messages) == 1
        assert anthropic_messages[0]["role"] == "user"

    def test_convert_messages_with_tool_calls(self, client):
        """测试带工具调用的消息转换"""
        messages = [
            Message(role=MessageRole.USER, content="What's the weather?"),
            Message(
                role=MessageRole.ASSISTANT,
                content="",
                tool_calls=[
                    ToolCall(
                        id="call_123",
                        name="get_weather",
                        arguments={"location": "NYC"},
                    )
                ],
            ),
            Message(
                role=MessageRole.TOOL,
                tool_result=ToolResult(
                    tool_call_id="call_123",
                    name="get_weather",
                    content="Sunny, 72°F",
                    is_error=False,
                ),
            ),
        ]

        system_prompt, anthropic_messages = client._convert_messages(messages)

        assert len(anthropic_messages) == 3

        # 第一个消息：用户
        assert anthropic_messages[0]["role"] == "user"
        assert anthropic_messages[0]["content"] == "What's the weather?"

        # 第二个消息：助手（带工具调用）
        assert anthropic_messages[1]["role"] == "assistant"
        assert isinstance(anthropic_messages[1]["content"], list)
        assert anthropic_messages[1]["content"][0]["type"] == "tool_use"
        assert anthropic_messages[1]["content"][0]["name"] == "get_weather"
        assert anthropic_messages[1]["content"][0]["input"] == {"location": "NYC"}

        # 第三个消息：工具结果
        assert anthropic_messages[2]["role"] == "user"
        assert isinstance(anthropic_messages[2]["content"], list)
        assert anthropic_messages[2]["content"][0]["type"] == "tool_result"
        assert anthropic_messages[2]["content"][0]["tool_use_id"] == "call_123"
        assert anthropic_messages[2]["content"][0]["content"] == "Sunny, 72°F"
        assert anthropic_messages[2]["content"][0]["is_error"] is False

    def test_convert_tools(self, client):
        """测试工具定义转换"""
        tools = [
            ToolDefinition(
                name="get_weather",
                description="Get current weather",
                parameters={
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "City name",
                        },
                    },
                    "required": ["location"],
                },
            )
        ]

        anthropic_tools = client._convert_tools(tools)

        assert len(anthropic_tools) == 1
        assert anthropic_tools[0]["name"] == "get_weather"
        assert anthropic_tools[0]["description"] == "Get current weather"
        assert "input_schema" in anthropic_tools[0]
        assert anthropic_tools[0]["input_schema"]["type"] == "object"

    def test_convert_tools_with_pydantic_schema(self, client):
        """测试 Pydantic Schema 转换"""
        from pydantic import BaseModel, Field

        class GetWeatherArgs(BaseModel):
            location: str = Field(..., description="City name")
            units: str = Field("celsius", description="Temperature units")

        tools = [
            ToolDefinition(
                name="get_weather",
                description="Get current weather",
                parameters=GetWeatherArgs.model_json_schema(),
            )
        ]

        anthropic_tools = client._convert_tools(tools)

        assert len(anthropic_tools) == 1
        assert anthropic_tools[0]["name"] == "get_weather"

        # 检查 input_schema
        input_schema = anthropic_tools[0]["input_schema"]
        assert input_schema["type"] == "object"
        assert "properties" in input_schema
        assert "location" in input_schema["properties"]
        assert "units" in input_schema["properties"]
        assert input_schema["required"] == ["location"]

    @pytest.mark.asyncio
    async def test_chat_stream_with_mock(self, client):
        """测试流式对话（使用 mock）"""
        # 创建 mock 流
        mock_stream = AsyncMock()
        mock_stream.__aenter__ = AsyncMock(return_value=mock_stream)
        mock_stream.__aexit__ = AsyncMock(return_value=None)

        # 模拟流事件
        mock_stream.__aiter__ = AsyncMock(
            return_value=iter(
                [
                    MagicMock(
                        type="content_block_start", content_block=MagicMock(type="text")
                    ),
                    MagicMock(
                        type="content_block_delta",
                        delta=MagicMock(type="text_delta", text="Hello"),
                    ),
                    MagicMock(type="content_block_stop"),
                    MagicMock(type="message_stop"),
                ]
            )
        )

        # Mock get_final_message
        mock_final_message = MagicMock()
        mock_final_message.stop_reason = "end_turn"
        mock_final_message.usage.input_tokens = 10
        mock_final_message.usage.output_tokens = 5
        mock_stream.get_final_message = AsyncMock(return_value=mock_final_message)

        # Patch client.messages.stream
        with patch.object(client.client, "messages") as mock_messages:
            mock_messages.stream.return_value = mock_stream

            messages = [
                Message(role=MessageRole.USER, content="Hello"),
            ]

            chunks = []
            async for chunk in client.chat_stream(messages):
                chunks.append(chunk)

            # 验证结果
            assert len(chunks) > 0
            assert any(chunk.text for chunk in chunks)

    def test_assistant_message_with_empty_content(self, client):
        """测试助手消息内容为空时的处理"""
        messages = [
            Message(role=MessageRole.ASSISTANT, content=""),
            Message(role=MessageRole.USER, content="Continue"),
        ]

        system_prompt, anthropic_messages = client._convert_messages(messages)

        # 空内容应该被替换为空格
        assert anthropic_messages[0]["content"] == " "

    def test_message_with_both_text_and_tools(self, client):
        """测试同时有文本和工具调用的消息"""
        messages = [
            Message(
                role=MessageRole.ASSISTANT,
                content="I'll check the weather",
                tool_calls=[
                    ToolCall(
                        id="call_123",
                        name="get_weather",
                        arguments={"location": "NYC"},
                    )
                ],
            ),
        ]

        system_prompt, anthropic_messages = client._convert_messages(messages)

        assert len(anthropic_messages) == 1
        content = anthropic_messages[0]["content"]
        assert isinstance(content, list)
        assert len(content) == 2
        assert content[0]["type"] == "text"
        assert content[0]["text"] == "I'll check the weather"
        assert content[1]["type"] == "tool_use"


class TestToolDefinition:
    """测试工具定义"""

    def test_pydantic_schema_to_anthropic_format(self):
        """测试 Pydantic Schema 转换为 Anthropic 格式"""
        from pydantic import BaseModel, Field

        class ReadFileArgs(BaseModel):
            file_path: str = Field(..., description="Path to the file")
            start_line: int = Field(1, description="Starting line")
            end_line: int = Field(None, description="Ending line")

        # 生成 Pydantic schema
        pydantic_schema = ReadFileArgs.model_json_schema()

        # 转换为 Anthropic 格式
        anthropic_format = {
            "name": "read_file",
            "description": "Read a file",
            "input_schema": pydantic_schema,
        }

        # 验证格式
        assert "input_schema" in anthropic_format
        assert anthropic_format["input_schema"]["type"] == "object"
        assert "properties" in anthropic_format["input_schema"]
        assert "required" in anthropic_format["input_schema"]

        # 检查必需字段
        required = anthropic_format["input_schema"]["required"]
        assert "file_path" in required
        assert "start_line" not in required  # 有默认值
        assert "end_line" not in required  # 可选

    def test_schema_without_title(self):
        """测试移除 schema 中的 title 字段"""
        from pydantic import BaseModel, Field

        class SimpleArgs(BaseModel):
            name: str = Field(..., description="Name")

        schema = SimpleArgs.model_json_schema()

        # Pydantic 默认会添加 title
        assert "title" in schema

        # 创建干净的 schema（移除 title）
        clean_schema = {
            "type": schema["type"],
            "properties": schema["properties"],
            "required": schema["required"],
        }

        assert "title" not in clean_schema


class TestRequestValidation:
    """测试请求验证"""

    def test_validate_temperature_range(self):
        """测试 temperature 参数范围"""
        client = AnthropicClient(
            model="claude-3-5-sonnet-20241022",
            api_key="test-key",
        )

        # 有效的 temperature
        assert 0.0 <= 0.7 <= 1.0

        # 无效的 temperature（应该在调用时验证）
        invalid_temps = [-0.1, 1.1, 2.0]
        for temp in invalid_temps:
            assert not (0.0 <= temp <= 1.0)

    def test_validate_max_tokens(self):
        """测试 max_tokens 参数"""
        # 有效的 max_tokens
        valid_values = [1, 100, 1024, 4096, 8192, 16384]
        for value in valid_values:
            assert value >= 1

        # 无效的 max_tokens
        invalid_values = [0, -1, -100]
        for value in invalid_values:
            assert value < 1

    @pytest.mark.asyncio
    async def test_empty_messages_list(self):
        """测试空消息列表"""
        client = AnthropicClient(
            model="claude-3-5-sonnet-20241022",
            api_key="test-key",
        )

        messages = []
        system_prompt, anthropic_messages = client._convert_messages(messages)

        assert system_prompt is None
        assert anthropic_messages == []


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
