"""Gemini API 集成测试

使用真实的 Gemini API 测试 GoogleClient 的各项功能：
1. 普通对话（流式 + 非流式）
2. 工具调用（function calling 完整流程）
3. 多轮对话
4. Token 使用量统计

需要环境变量: GEMINI_API_KEY

运行方式:
    pytest tests/test_gemini_integration.py -v -s
"""

import os
import asyncio
import pytest

from provider.google_client import GoogleClient, _normalize_schema
from provider.base import (
    Message,
    MessageRole,
    ToolCall,
    ToolResult,
    ToolDefinition,
    StreamChunk,
)

# 测试使用的模型
TEST_MODEL = "gemini-2.0-flash"

# 跳过条件：无 API key
skip_no_api_key = pytest.mark.skipif(
    not os.environ.get("GEMINI_API_KEY"),
    reason="GEMINI_API_KEY 环境变量未设置",
)


@pytest.fixture
def client():
    """创建 GoogleClient 实例"""
    api_key = os.environ.get("GEMINI_API_KEY", "")
    return GoogleClient(
        model=TEST_MODEL,
        api_key=api_key,
        temperature=0.1,
        max_tokens=256,
    )


@pytest.fixture
def weather_tools():
    """天气查询工具定义"""
    return [
        ToolDefinition(
            name="get_weather",
            description="获取指定城市的天气信息",
            parameters={
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称",
                    },
                },
                "required": ["city"],
            },
        )
    ]


@pytest.fixture
def calculator_tools():
    """计算器工具定义"""
    return [
        ToolDefinition(
            name="calculate",
            description="计算一个数学表达式的结果",
            parameters={
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式，如 '2 + 3'",
                    },
                },
                "required": ["expression"],
            },
        )
    ]


# ============================================================================
# 基础对话测试
# ============================================================================

@skip_no_api_key
@pytest.mark.asyncio
async def test_chat_basic(client):
    """测试基础非流式对话"""
    messages = [
        Message(role=MessageRole.USER, content="回答：1+1 等于几？只说数字。"),
    ]

    print("\n--- 测试: 基础非流式对话 ---")
    response = await client.chat(messages=messages)

    print(f"  响应内容: {response.content!r}")
    print(f"  finish_reason: {response.finish_reason}")
    print(f"  usage: {response.usage}")

    assert response.content, "响应内容不应为空"
    assert "2" in response.content, f"预期包含 '2'，实际: {response.content}"
    assert response.finish_reason == "stop"


@skip_no_api_key
@pytest.mark.asyncio
async def test_chat_stream_basic(client):
    """测试基础流式对话"""
    messages = [
        Message(role=MessageRole.USER, content="用一句话解释什么是Python。"),
    ]

    print("\n--- 测试: 基础流式对话 ---")
    chunks = []
    full_text = ""
    has_text_complete = False
    has_finished = False

    async for chunk in client.chat_stream(messages=messages):
        chunks.append(chunk)
        if chunk.text:
            full_text += chunk.text
            print(f"  [text chunk] {chunk.text!r}")
        if chunk.text_complete:
            has_text_complete = True
            print("  [text_complete]")
        if chunk.is_finished:
            has_finished = True
            print(f"  [finished] reason={chunk.finish_reason}, usage={chunk.usage}")

    print(f"  总 chunk 数: {len(chunks)}")
    print(f"  完整文本: {full_text!r}")

    assert full_text, "流式文本不应为空"
    assert has_text_complete, "应收到 text_complete 标记"
    assert has_finished, "应收到 is_finished 标记"
    assert "Python" in full_text or "python" in full_text.lower(), \
        f"响应应包含 Python 相关内容: {full_text}"


@skip_no_api_key
@pytest.mark.asyncio
async def test_chat_with_system_prompt(client):
    """测试带系统提示词的对话"""
    system_prompts = [
        Message(role=MessageRole.AGENT, content="你是一个只能用英文回答的助手。"),
    ]
    messages = [
        Message(role=MessageRole.USER, content="什么是AI？用一句话回答。"),
    ]

    print("\n--- 测试: 带系统提示词 ---")
    response = await client.chat(
        messages=messages,
        system_prompts=system_prompts,
    )

    print(f"  响应: {response.content!r}")

    assert response.content, "响应不应为空"
    # 系统提示词要求英文回答，应包含英文字符
    ascii_count = sum(1 for c in response.content if c.isascii() and c.isalpha())
    print(f"  ASCII 字母数: {ascii_count}")
    assert ascii_count > 5, "响应应主要包含英文字符"


# ============================================================================
# 工具调用测试
# ============================================================================

@skip_no_api_key
@pytest.mark.asyncio
async def test_tool_call_triggered(client, weather_tools):
    """测试工具调用是否被正确触发"""
    messages = [
        Message(role=MessageRole.USER, content="北京今天天气怎么样？"),
    ]

    print("\n--- 测试: 工具调用触发 ---")
    response = await client.chat(
        messages=messages,
        tools=weather_tools,
    )

    print(f"  响应文本: {response.content!r}")
    print(f"  tool_calls 数量: {len(response.tool_calls)}")
    for tc in response.tool_calls:
        print(f"  工具调用: name={tc.name}, args={tc.arguments}, id={tc.id}")

    assert len(response.tool_calls) > 0, "应触发工具调用"
    assert response.tool_calls[0].name == "get_weather", \
        f"应调用 get_weather，实际: {response.tool_calls[0].name}"
    assert "city" in response.tool_calls[0].arguments, \
        f"参数应包含 city，实际: {response.tool_calls[0].arguments}"


@skip_no_api_key
@pytest.mark.asyncio
async def test_tool_call_stream(client, weather_tools):
    """测试流式工具调用"""
    messages = [
        Message(role=MessageRole.USER, content="上海天气如何？"),
    ]

    print("\n--- 测试: 流式工具调用 ---")
    all_tool_calls = []
    finish_reason = None

    async for chunk in client.chat_stream(messages=messages, tools=weather_tools):
        if chunk.tool_calls:
            all_tool_calls.extend(chunk.tool_calls)
            for tc in chunk.tool_calls:
                print(f"  [tool_call] name={tc.name}, args={tc.arguments}")
        if chunk.is_finished:
            finish_reason = chunk.finish_reason
            print(f"  [finished] reason={finish_reason}")

    assert len(all_tool_calls) > 0, "流式模式应触发工具调用"
    assert all_tool_calls[0].name == "get_weather"
    assert finish_reason == "tool_use", f"finish_reason 应为 tool_use，实际: {finish_reason}"


@skip_no_api_key
@pytest.mark.asyncio
async def test_tool_call_full_round_trip(client, weather_tools):
    """测试完整的工具调用往返流程（调用 → 结果 → 最终回复）"""
    # 第 1 轮：用户提问，模型返回工具调用
    messages = [
        Message(role=MessageRole.USER, content="北京天气如何？"),
    ]

    print("\n--- 测试: 工具调用完整往返 ---")
    print("  [Round 1] 发送用户消息...")
    response1 = await client.chat(messages=messages, tools=weather_tools)

    print(f"  [Round 1] 文本: {response1.content!r}")
    print(f"  [Round 1] tool_calls: {len(response1.tool_calls)}")

    assert len(response1.tool_calls) > 0, "第 1 轮应触发工具调用"

    tc = response1.tool_calls[0]
    print(f"  [Round 1] 工具: {tc.name}({tc.arguments})")

    # 第 2 轮：发送工具结果，让模型总结
    messages.append(
        Message(
            role=MessageRole.ASSISTANT,
            content=response1.content,
            tool_calls=response1.tool_calls,
        )
    )
    messages.append(
        Message(
            role=MessageRole.TOOL,
            tool_result=ToolResult(
                tool_call_id=tc.id,
                name=tc.name,
                content='{"temperature": 25, "condition": "晴", "humidity": 45}',
                is_error=False,
            ),
        )
    )

    print("  [Round 2] 发送工具结果...")
    response2 = await client.chat(messages=messages, tools=weather_tools)

    print(f"  [Round 2] 最终回复: {response2.content!r}")
    print(f"  [Round 2] tool_calls: {len(response2.tool_calls)}")

    assert response2.content, "第 2 轮应有文本回复"
    assert len(response2.tool_calls) == 0, "第 2 轮不应再有工具调用"
    # 回复中应该提到温度或天气相关信息
    assert any(kw in response2.content for kw in ["25", "晴", "天气", "温度", "weather"]), \
        f"回复应包含天气信息: {response2.content}"


@skip_no_api_key
@pytest.mark.asyncio
async def test_multiple_tool_results(client, calculator_tools):
    """测试连续多个工具结果的合并（Gemini 要求 user/model 交替）"""
    # 模拟模型同时调用两次计算器的场景
    messages = [
        Message(role=MessageRole.USER, content="计算 2+3 和 4*5 的结果。"),
        Message(
            role=MessageRole.ASSISTANT,
            content="",
            tool_calls=[
                ToolCall(id="fc_1", name="calculate", arguments={"expression": "2+3"}),
                ToolCall(id="fc_2", name="calculate", arguments={"expression": "4*5"}),
            ],
        ),
        Message(
            role=MessageRole.TOOL,
            tool_result=ToolResult(
                tool_call_id="fc_1",
                name="calculate",
                content="5",
                is_error=False,
            ),
        ),
        Message(
            role=MessageRole.TOOL,
            tool_result=ToolResult(
                tool_call_id="fc_2",
                name="calculate",
                content="20",
                is_error=False,
            ),
        ),
    ]

    print("\n--- 测试: 多个工具结果合并 ---")
    response = await client.chat(messages=messages, tools=calculator_tools)

    print(f"  响应: {response.content!r}")

    assert response.content, "应有文本回复"
    assert "5" in response.content, f"回复应包含 5: {response.content}"
    assert "20" in response.content, f"回复应包含 20: {response.content}"


# ============================================================================
# 多轮对话测试
# ============================================================================

@skip_no_api_key
@pytest.mark.asyncio
async def test_multi_turn_conversation(client):
    """测试多轮对话上下文保持"""
    messages = [
        Message(role=MessageRole.USER, content="我的名字叫小明。记住这个。"),
    ]

    print("\n--- 测试: 多轮对话 ---")
    print("  [Turn 1] 自我介绍...")
    response1 = await client.chat(messages=messages)
    print(f"  [Turn 1] 回复: {response1.content!r}")

    messages.append(
        Message(role=MessageRole.ASSISTANT, content=response1.content)
    )
    messages.append(
        Message(role=MessageRole.USER, content="我叫什么名字？只说名字。")
    )

    print("  [Turn 2] 询问名字...")
    response2 = await client.chat(messages=messages)
    print(f"  [Turn 2] 回复: {response2.content!r}")

    assert "小明" in response2.content, \
        f"模型应记住名字 '小明': {response2.content}"


# ============================================================================
# Token 统计测试
# ============================================================================

@skip_no_api_key
@pytest.mark.asyncio
async def test_usage_stats(client):
    """测试 token 使用量统计"""
    messages = [
        Message(role=MessageRole.USER, content="说 hello"),
    ]

    print("\n--- 测试: Token 使用量 ---")
    usage = None
    async for chunk in client.chat_stream(messages=messages):
        if chunk.usage:
            usage = chunk.usage
            print(f"  usage: input={usage.input_tokens}, output={usage.output_tokens}")
        if chunk.text:
            print(f"  text: {chunk.text!r}")

    assert usage is not None, "应返回 usage 信息"
    assert usage.input_tokens > 0, f"input_tokens 应 > 0，实际: {usage.input_tokens}"
    assert usage.output_tokens > 0, f"output_tokens 应 > 0，实际: {usage.output_tokens}"


@skip_no_api_key
@pytest.mark.asyncio
async def test_count_tokens_basic(client):
    """测试 count_tokens 基础功能"""
    messages = [
        Message(role=MessageRole.USER, content="你好，世界"),
    ]

    print("\n--- 测试: count_tokens 基础 ---")
    token_count = await client.count_tokens(messages=messages)
    print(f"  token 数: {token_count}")

    assert isinstance(token_count, int), f"返回类型应为 int，实际: {type(token_count)}"
    assert token_count > 0, f"token 数应 > 0，实际: {token_count}"


@skip_no_api_key
@pytest.mark.asyncio
async def test_count_tokens_with_system_and_tools(client, weather_tools):
    """测试 count_tokens 包含系统提示词和工具定义"""
    system_prompts = [
        Message(role=MessageRole.AGENT, content="你是一个天气助手。"),
    ]
    messages = [
        Message(role=MessageRole.USER, content="北京天气如何？"),
    ]

    print("\n--- 测试: count_tokens 含系统提示词+工具 ---")

    # 只有消息
    count_msg_only = await client.count_tokens(messages=messages)
    print(f"  仅消息: {count_msg_only} tokens")

    # 消息 + 系统提示词
    count_with_sys = await client.count_tokens(
        messages=messages, system_prompts=system_prompts,
    )
    print(f"  消息+系统提示词: {count_with_sys} tokens")

    # 消息 + 系统提示词 + 工具
    count_with_all = await client.count_tokens(
        messages=messages, system_prompts=system_prompts, tools=weather_tools,
    )
    print(f"  消息+系统提示词+工具: {count_with_all} tokens")

    assert count_msg_only > 0
    assert count_with_sys > 0
    assert count_with_all > count_msg_only, \
        f"加工具后 token 数应 > 仅消息: {count_with_all} vs {count_msg_only}"


@skip_no_api_key
@pytest.mark.asyncio
async def test_count_tokens_multi_turn(client):
    """测试 count_tokens 多轮对话 token 随消息增加而增长"""
    messages_short = [
        Message(role=MessageRole.USER, content="Hi"),
    ]
    messages_long = [
        Message(role=MessageRole.USER, content="Hi"),
        Message(role=MessageRole.ASSISTANT, content="Hello! How can I help you?"),
        Message(role=MessageRole.USER, content="请帮我写一段 Python 代码实现冒泡排序算法。"),
    ]

    print("\n--- 测试: count_tokens 多轮对话 ---")
    count_short = await client.count_tokens(messages=messages_short)
    count_long = await client.count_tokens(messages=messages_long)
    print(f"  短对话: {count_short} tokens")
    print(f"  长对话: {count_long} tokens")

    assert count_long > count_short, \
        f"更多消息应有更多 token: {count_long} vs {count_short}"


# ============================================================================
# Schema 标准化单元测试（不需要 API key）
# ============================================================================

def test_schema_removes_title_and_default():
    """schema 标准化应移除 title 和 default"""
    schema = {
        "type": "object",
        "title": "MyModel",
        "properties": {
            "name": {
                "type": "string",
                "title": "Name Field",
                "default": "unknown",
            },
        },
    }
    result = _normalize_schema(schema)

    print(f"\n--- Schema 标准化: 移除 title/default ---")
    print(f"  输入: {schema}")
    print(f"  输出: {result}")

    assert "title" not in result
    assert "title" not in result["properties"]["name"]
    assert "default" not in result["properties"]["name"]
    assert result["type"] == "object"
    assert result["properties"]["name"]["type"] == "string"


def test_schema_renames_additional_properties():
    """schema 标准化应移除 additional_properties 和 additionalProperties"""
    schema = {
        "type": "object",
        "properties": {
            "data": {
                "type": "object",
                "additional_properties": {"type": "string"},
            },
            "items": {
                "type": "array",
                "items": {
                    "additionalProperties": True,
                    "type": "object",
                },
            },
        },
    }
    result = _normalize_schema(schema)

    print(f"\n--- Schema 标准化: 移除 additional_properties ---")
    print(f"  输出: {result}")

    data_schema = result["properties"]["data"]
    assert "additional_properties" not in data_schema
    assert "additionalProperties" not in data_schema

    items_inner = result["properties"]["items"]["items"]
    assert "additionalProperties" not in items_inner
    assert "additional_properties" not in items_inner


def test_schema_resolves_refs():
    """schema 标准化应解析 $ref 引用"""
    schema = {
        "type": "object",
        "properties": {
            "item": {"$ref": "#/$defs/Item"},
        },
        "$defs": {
            "Item": {
                "type": "object",
                "title": "Item",
                "properties": {
                    "name": {"type": "string"},
                },
            },
        },
    }
    result = _normalize_schema(schema)

    print(f"\n--- Schema 标准化: 解析 $ref ---")
    print(f"  输出: {result}")

    assert "$defs" not in result
    item_schema = result["properties"]["item"]
    assert item_schema["type"] == "object"
    assert "title" not in item_schema  # title 也应被移除
    assert "name" in item_schema["properties"]


def test_schema_simplifies_anyof_optional():
    """schema 标准化应简化 pydantic Optional 的 anyOf 模式"""
    schema = {
        "type": "object",
        "properties": {
            "name": {
                "anyOf": [
                    {"type": "string"},
                    {"type": "null"},
                ],
                "default": None,
                "description": "名称",
                "title": "Name",
            },
        },
    }
    result = _normalize_schema(schema)

    print(f"\n--- Schema 标准化: anyOf Optional 简化 ---")
    print(f"  输出: {result}")

    name_schema = result["properties"]["name"]
    # anyOf 应被简化为直接的 type: string
    assert "anyOf" not in name_schema
    assert name_schema["type"] == "string"
    assert name_schema["description"] == "名称"
    assert "title" not in name_schema
    assert "default" not in name_schema


def test_schema_todowrite_real_schema():
    """验证 todowrite 真实 schema 能被正确标准化（触发了线上 400 错误的 schema）"""
    # 这是 TodoWriteTool 实际生成的 schema
    schema = {
        "description": "Arguments for TodoWrite tool",
        "properties": {
            "todos": {
                "description": "The updated todo list",
                "items": {
                    "additionalProperties": True,
                    "type": "object",
                },
                "title": "Todos",
                "type": "array",
            },
        },
        "required": ["todos"],
        "title": "TodoWriteArgs",
        "type": "object",
    }
    result = _normalize_schema(schema)

    print(f"\n--- Schema 标准化: todowrite 真实 schema ---")
    print(f"  输出: {result}")

    # title 应被移除
    assert "title" not in result
    # items 内不应有 additionalProperties
    items_schema = result["properties"]["todos"]["items"]
    assert "additionalProperties" not in items_schema
    assert "additional_properties" not in items_schema
    assert items_schema["type"] == "object"


def test_schema_task_report_complex():
    """验证 TaskReport 复杂 schema（anyOf + additionalProperties 组合）"""
    schema = {
        "type": "object",
        "properties": {
            "steps": {
                "anyOf": [
                    {
                        "items": {
                            "additionalProperties": True,
                            "type": "object",
                        },
                        "type": "array",
                    },
                    {"type": "null"},
                ],
                "default": None,
                "description": "执行步骤列表",
                "title": "Steps",
            },
        },
        "required": ["action"],
        "title": "TaskReportArgs",
    }
    result = _normalize_schema(schema)

    print(f"\n--- Schema 标准化: task_report 复杂 schema ---")
    print(f"  输出: {result}")

    # anyOf[array, null] 应简化为 array
    steps = result["properties"]["steps"]
    assert "anyOf" not in steps
    assert steps["type"] == "array"
    # items 内不应有 additionalProperties
    assert "additionalProperties" not in steps["items"]
    assert "title" not in steps
    assert "default" not in steps


# ============================================================================
# 消息转换单元测试（不需要 API key）
# ============================================================================

def test_to_gemini_contents_merges_consecutive_user_roles():
    """测试连续同 role 消息合并"""
    from google.genai import types  # type: ignore

    c = object.__new__(GoogleClient)
    c._types = types

    messages = [
        Message(role=MessageRole.USER, content="你好"),
        Message(
            role=MessageRole.ASSISTANT,
            content="",
            tool_calls=[
                ToolCall(id="fc1", name="get_weather", arguments={"city": "北京"}),
            ],
        ),
        Message(
            role=MessageRole.TOOL,
            tool_result=ToolResult(
                tool_call_id="fc1",
                name="get_weather",
                content='{"temp": 25}',
                is_error=False,
            ),
        ),
        Message(
            role=MessageRole.TOOL,
            tool_result=ToolResult(
                tool_call_id="fc2",
                name="get_weather",
                content='{"temp": 30}',
                is_error=False,
            ),
        ),
    ]

    contents = c._to_gemini_contents(messages)

    print(f"\n--- 消息转换: 连续同 role 合并 ---")
    for i, item in enumerate(contents):
        print(f"  [{i}] role={item.role}, parts_count={len(item.parts)}")

    # 验证角色交替
    for i in range(1, len(contents)):
        assert contents[i].role != contents[i - 1].role, \
            f"索引 {i-1} 和 {i} 角色不应相同: {contents[i-1].role}"

    # user → model → user（两个 tool results 合并为一个 user 消息）
    assert len(contents) == 3, f"应有 3 条消息，实际: {len(contents)}"
    assert contents[0].role == "user"
    assert contents[1].role == "model"
    assert contents[2].role == "user"
    # 第 3 条消息的 parts 应包含 2 个 functionResponse
    assert len(contents[2].parts) == 2


def test_to_gemini_contents_includes_function_call_parts():
    """测试 assistant 消息包含 function_call parts"""
    from google.genai import types  # type: ignore

    c = object.__new__(GoogleClient)
    c._types = types

    messages = [
        Message(role=MessageRole.USER, content="查天气"),
        Message(
            role=MessageRole.ASSISTANT,
            content="我来查一下",
            tool_calls=[
                ToolCall(id="fc1", name="get_weather", arguments={"city": "上海"}),
            ],
        ),
    ]

    contents = c._to_gemini_contents(messages)

    print(f"\n--- 消息转换: function_call parts ---")
    for i, item in enumerate(contents):
        print(f"  [{i}] role={item.role}, parts={[type(p).__name__ for p in item.parts]}")

    assert len(contents) == 2
    model_msg = contents[1]
    assert model_msg.role == "model"
    # parts 应包含文本 + function_call
    assert len(model_msg.parts) == 2
    # 第一个是 Part(text=...)
    assert model_msg.parts[0].text == "我来查一下"
    # 第二个应该是 Part(function_call=...)
    part = model_msg.parts[1]
    assert part.function_call is not None, f"第二个 part 应有 function_call: {part}"


def test_thought_signature_preserved_in_tool_call():
    """测试 thought_signature 在 ToolCall 中正确保存和恢复"""
    import base64
    from google.genai import types  # type: ignore

    c = object.__new__(GoogleClient)
    c._types = types

    # 模拟 Gemini 返回带 thought_signature 的 function_call
    fake_sig = b"\x01\x02\x03\x04thought_test"
    fake_response = types.GenerateContentResponse(
        candidates=[
            types.Candidate(
                content=types.Content(
                    role="model",
                    parts=[
                        types.Part(
                            function_call=types.FunctionCall(
                                name="read", args={"file_path": "/tmp/test"}
                            ),
                            thought_signature=fake_sig,
                        )
                    ],
                )
            )
        ]
    )

    tool_calls = c._extract_tool_calls(fake_response)

    print(f"\n--- thought_signature 保存测试 ---")
    print(f"  提取到 {len(tool_calls)} 个工具调用")

    assert len(tool_calls) == 1
    tc = tool_calls[0]
    assert tc.name == "read"
    assert "thought_signature" in tc.metadata
    # base64 解码后应与原始值一致
    restored = base64.b64decode(tc.metadata["thought_signature"])
    assert restored == fake_sig, f"thought_signature 不匹配: {restored} != {fake_sig}"
    print(f"  thought_signature 保存正确: {tc.metadata['thought_signature'][:20]}...")


def test_thought_signature_restored_in_contents():
    """测试 thought_signature 在重建 Contents 时正确恢复"""
    import base64
    from google.genai import types  # type: ignore

    c = object.__new__(GoogleClient)
    c._types = types

    fake_sig = b"\xaa\xbb\xcc\xdd"
    sig_b64 = base64.b64encode(fake_sig).decode("ascii")

    messages = [
        Message(role=MessageRole.USER, content="读取文件"),
        Message(
            role=MessageRole.ASSISTANT,
            content="",
            tool_calls=[
                ToolCall(
                    id="fc1", name="read",
                    arguments={"file_path": "/tmp/x"},
                    metadata={"thought_signature": sig_b64},
                ),
            ],
        ),
        Message(
            role=MessageRole.TOOL,
            tool_result=ToolResult(
                tool_call_id="fc1", name="read",
                content="file content", is_error=False,
            ),
        ),
    ]

    contents = c._to_gemini_contents(messages)

    print(f"\n--- thought_signature 恢复测试 ---")
    for i, item in enumerate(contents):
        print(f"  [{i}] role={item.role}, parts_count={len(item.parts)}")

    # model 消息的 function_call Part 应携带 thought_signature
    model_msg = contents[1]
    assert model_msg.role == "model"
    fc_part = model_msg.parts[0]
    assert fc_part.function_call is not None
    assert fc_part.thought_signature is not None, "function_call Part 应携带 thought_signature"
    assert fc_part.thought_signature == fake_sig, \
        f"thought_signature 恢复不匹配: {fc_part.thought_signature} != {fake_sig}"
    print(f"  thought_signature 恢复正确: {fc_part.thought_signature!r}")


def test_thought_signature_serialization_round_trip():
    """测试 thought_signature 通过 Message 序列化/反序列化后保持一致"""
    import base64

    fake_sig = b"\x01\x02\x03"
    sig_b64 = base64.b64encode(fake_sig).decode("ascii")

    msg = Message(
        role=MessageRole.ASSISTANT,
        content="",
        tool_calls=[
            ToolCall(
                id="fc1", name="read",
                arguments={"file_path": "/tmp/x"},
                metadata={"thought_signature": sig_b64},
            ),
        ],
    )

    # 序列化 → 反序列化
    d = msg.to_dict()
    restored_msg = Message.from_dict(d)

    print(f"\n--- thought_signature 序列化往返测试 ---")
    print(f"  原始: {msg.tool_calls[0].metadata}")
    print(f"  恢复: {restored_msg.tool_calls[0].metadata}")

    assert restored_msg.tool_calls[0].metadata.get("thought_signature") == sig_b64
    # base64 解码后应与原始 bytes 一致
    restored_bytes = base64.b64decode(restored_msg.tool_calls[0].metadata["thought_signature"])
    assert restored_bytes == fake_sig
