"""
测试 DefaultContext._repair_incomplete_tool_calls() 方法

验证场景：
1. 正常消息不被修改
2. 缺失全部 tool_result 时追加合成结果
3. 部分缺失时只补全缺失的
4. 多轮工具调用只检查最近一次
5. 从磁盘加载时自动修复
"""

import json
import asyncio
import pytest

from context.base import ContextConfig
from context.default import DefaultContext
from provider import Message, MessageRole, ToolCall, ToolResult


async def empty_chat_fn(*_args, **_kwargs):
    """空的 chat 函数，不 yield 任何内容"""
    if False:
        yield None


def _make_assistant_with_tools(content: str, tool_calls: list[tuple[str, str]]) -> Message:
    """构造带 tool_calls 的 assistant 消息

    Args:
        content: 文本内容
        tool_calls: [(tool_call_id, tool_name), ...] 列表
    """
    return Message(
        role=MessageRole.ASSISTANT,
        content=content,
        tool_calls=[
            ToolCall(id=tc_id, name=tc_name, arguments={"dummy": True})
            for tc_id, tc_name in tool_calls
        ],
    )


def _make_tool_result(tool_call_id: str, tool_name: str, content: str = "ok") -> Message:
    """构造 tool_result 消息"""
    return Message(
        role=MessageRole.TOOL,
        tool_result=ToolResult(
            tool_call_id=tool_call_id,
            name=tool_name,
            content=content,
            is_error=False,
        ),
    )


class TestRepairIncompleteToolCalls:
    """测试 _repair_incomplete_tool_calls 方法"""

    def _create_context(self) -> DefaultContext:
        return DefaultContext(empty_chat_fn, config=ContextConfig(max_tokens=100000))

    def test_no_tool_calls_returns_unchanged(self):
        """没有工具调用的消息列表直接返回"""
        ctx = self._create_context()
        messages = [
            Message(role=MessageRole.USER, content="你好"),
            Message(role=MessageRole.ASSISTANT, content="你好！"),
        ]

        result = ctx._repair_incomplete_tool_calls(messages)

        assert len(result) == 2
        # 内容不变
        assert result[0].content == "你好"
        assert result[1].content == "你好！"

    def test_complete_tool_calls_returns_unchanged(self):
        """工具调用链完整时不修改"""
        ctx = self._create_context()
        messages = [
            Message(role=MessageRole.USER, content="请读取文件"),
            _make_assistant_with_tools("好的", [("tc_1", "read")]),
            _make_tool_result("tc_1", "read", "文件内容"),
        ]

        result = ctx._repair_incomplete_tool_calls(messages)

        assert len(result) == 3
        # 最后一条是原始的 tool_result，不是合成的
        assert result[2].tool_result.content == "文件内容"
        assert result[2].tool_result.is_error is False

    def test_all_tool_results_missing(self):
        """全部 tool_result 缺失时追加合成结果"""
        ctx = self._create_context()
        messages = [
            Message(role=MessageRole.USER, content="请读取文件"),
            _make_assistant_with_tools("好的", [
                ("tc_1", "read"),
                ("tc_2", "write"),
            ]),
            # 缺失 tc_1 和 tc_2 的 tool_result
        ]

        result = ctx._repair_incomplete_tool_calls(messages)

        # 追加了 2 条合成结果
        assert len(result) == 4
        print(f"[测试日志] 原始消息数: {len(messages)}, 修复后: {len(result)}")

        # 验证合成结果
        for i, (expected_id, expected_name) in enumerate([("tc_1", "read"), ("tc_2", "write")]):
            synthetic = result[2 + i]
            assert synthetic.role == MessageRole.TOOL
            assert synthetic.tool_result.tool_call_id == expected_id
            assert synthetic.tool_result.name == expected_name
            assert synthetic.tool_result.is_error is True
            assert "进程中断" in synthetic.tool_result.content
            print(f"[测试日志] 合成结果 {i}: id={expected_id}, name={expected_name}, content={synthetic.tool_result.content}")

    def test_partial_tool_results_missing(self):
        """部分 tool_result 缺失时只补全缺失的"""
        ctx = self._create_context()
        messages = [
            Message(role=MessageRole.USER, content="请执行"),
            _make_assistant_with_tools("好的", [
                ("tc_1", "read"),
                ("tc_2", "write"),
                ("tc_3", "bash"),
            ]),
            _make_tool_result("tc_1", "read", "文件内容"),
            # tc_2 和 tc_3 缺失
        ]

        result = ctx._repair_incomplete_tool_calls(messages)

        assert len(result) == 5  # 原始 3 + 补 2
        print(f"[测试日志] 原始消息数: {len(messages)}, 修复后: {len(result)}")

        # tc_1 的结果保持不变
        assert result[2].tool_result.tool_call_id == "tc_1"
        assert result[2].tool_result.is_error is False

        # tc_2 和 tc_3 被补全
        assert result[3].tool_result.tool_call_id == "tc_2"
        assert result[3].tool_result.is_error is True
        assert result[4].tool_result.tool_call_id == "tc_3"
        assert result[4].tool_result.is_error is True

    def test_only_checks_last_assistant_with_tool_calls(self):
        """只检查最近一个带 tool_calls 的 assistant 消息"""
        ctx = self._create_context()
        messages = [
            # 第一轮：完整的工具调用
            Message(role=MessageRole.USER, content="第一个问题"),
            _make_assistant_with_tools("好的", [("tc_1", "read")]),
            _make_tool_result("tc_1", "read", "结果1"),
            # 第二轮：不完整的工具调用（最近一次）
            Message(role=MessageRole.USER, content="第二个问题"),
            _make_assistant_with_tools("好的", [("tc_2", "write")]),
            # tc_2 缺失 tool_result
        ]

        result = ctx._repair_incomplete_tool_calls(messages)

        # 只补全 tc_2
        assert len(result) == 6  # 原始 5 + 补 1
        assert result[-1].tool_result.tool_call_id == "tc_2"
        assert result[-1].tool_result.is_error is True
        print(f"[测试日志] 只修复了最近一个缺失的 tc_2，未触碰已完整的 tc_1")

    def test_earlier_incomplete_not_touched(self):
        """更早的不完整调用不被修复（理论上不应出现，但确认逻辑正确）"""
        ctx = self._create_context()
        messages = [
            # 第一轮：故意制造不完整（正常不会出现，但验证只查最近）
            Message(role=MessageRole.USER, content="第一个问题"),
            _make_assistant_with_tools("好的", [("tc_old", "read")]),
            # tc_old 缺失
            Message(role=MessageRole.USER, content="第二个问题"),
            # 第二轮：没有工具调用
            Message(role=MessageRole.ASSISTANT, content="直接回答"),
        ]

        result = ctx._repair_incomplete_tool_calls(messages)

        # 最近的 assistant 没有 tool_calls，但从后往前找到 tc_old 是最近的带 tool_calls 的
        # tc_old 缺失 → 补全
        assert len(result) == 5
        assert result[-1].tool_result.tool_call_id == "tc_old"
        print(f"[测试日志] tc_old 被修复: {result[-1].tool_result.content}")

    def test_empty_messages_returns_empty(self):
        """空消息列表直接返回"""
        ctx = self._create_context()

        result = ctx._repair_incomplete_tool_calls([])

        assert len(result) == 0

    def test_synthetic_result_content_is_informative(self):
        """合成的错误结果包含有用信息"""
        ctx = self._create_context()
        messages = [
            Message(role=MessageRole.USER, content="请执行"),
            _make_assistant_with_tools("", [("tc_1", "bash")]),
        ]

        result = ctx._repair_incomplete_tool_calls(messages)

        synthetic = result[-1]
        assert synthetic.tool_result.is_error is True
        assert "系统恢复" in synthetic.tool_result.content
        assert "进程中断" in synthetic.tool_result.content
        assert "重新执行" in synthetic.tool_result.content
        print(f"[测试日志] 合成结果内容: {synthetic.tool_result.content}")


class TestLoadFromDiskWithRepair:
    """测试 load_from_disk 加载后自动修复"""

    @pytest.mark.asyncio
    async def test_load_repairs_incomplete_tool_calls(self, tmp_path):
        """从磁盘加载时自动修复不完整的工具调用"""
        session_id = "test_repair"
        storage_dir = tmp_path / "sessions"
        context_file = storage_dir / session_id / "context.jsonl"
        context_file.parent.mkdir(parents=True, exist_ok=True)

        # 模拟中断场景：手动写入不完整的消息
        incomplete_messages = [
            Message(role=MessageRole.USER, content="请读取文件"),
            _make_assistant_with_tools("好的", [("tc_1", "read"), ("tc_2", "write")]),
            _make_tool_result("tc_1", "read", "文件内容"),
            # tc_2 缺失
        ]

        with open(context_file, "w", encoding="utf-8") as f:
            for msg in incomplete_messages:
                f.write(json.dumps(msg.to_dict(), ensure_ascii=False) + "\n")

        print(f"[测试日志] 写入 {len(incomplete_messages)} 条不完整消息到 {context_file}")

        # 创建 Context 并加载
        ctx = DefaultContext(
            empty_chat_fn,
            config=ContextConfig(max_tokens=100000),
            session_id=session_id,
            storage_dir=storage_dir,
        )

        loaded = await ctx.load_from_disk()
        assert loaded is True

        messages = ctx.get_messages()
        print(f"[测试日志] 加载后消息数: {len(messages)}")

        # 原始 3 条 + 补全 1 条 = 4 条
        assert len(messages) == 4

        # 验证补全的 tc_2
        assert messages[-1].role == MessageRole.TOOL
        assert messages[-1].tool_result.tool_call_id == "tc_2"
        assert messages[-1].tool_result.is_error is True
        assert "进程中断" in messages[-1].tool_result.content
        print(f"[测试日志] 自动修复了 tc_2: {messages[-1].tool_result.content}")

    @pytest.mark.asyncio
    async def test_load_complete_messages_unchanged(self, tmp_path):
        """完整的消息历史加载后不被修改"""
        session_id = "test_complete"
        storage_dir = tmp_path / "sessions"
        context_file = storage_dir / session_id / "context.jsonl"
        context_file.parent.mkdir(parents=True, exist_ok=True)

        complete_messages = [
            Message(role=MessageRole.USER, content="请读取文件"),
            _make_assistant_with_tools("好的", [("tc_1", "read")]),
            _make_tool_result("tc_1", "read", "文件内容"),
            Message(role=MessageRole.ASSISTANT, content="文件内容如上。"),
        ]

        with open(context_file, "w", encoding="utf-8") as f:
            for msg in complete_messages:
                f.write(json.dumps(msg.to_dict(), ensure_ascii=False) + "\n")

        ctx = DefaultContext(
            empty_chat_fn,
            config=ContextConfig(max_tokens=100000),
            session_id=session_id,
            storage_dir=storage_dir,
        )

        loaded = await ctx.load_from_disk()
        assert loaded is True

        messages = ctx.get_messages()
        # 消息数量不变
        assert len(messages) == 4
        print(f"[测试日志] 完整消息加载后未被修改，消息数: {len(messages)}")
