# -*- coding: utf-8 -*-
"""agent_loop 模块测试"""
import json
from unittest.mock import MagicMock

import pytest

from abyss.agent_loop import (
    _extract_missing_param,
    execute_tool,
    execute_tool_batch,
    process_tool_calls,
)
from abyss.tools.registry import ToolRegistry, format_tool_result
from abyss.tools.base import Tool


class FakeTool(Tool):
    """测试用 Fake Tool，echo 返回参数。"""
    @property
    def name(self) -> str:
        return "fake_echo"

    @property
    def description(self) -> str:
        return "Echo back the message"

    @property
    def parameters(self) -> dict:
        return {
            "type": "object",
            "properties": {
                "message": {"type": "string", "description": "Message to echo"},
            },
            "required": ["message"],
        }

    def execute(self, message: str, **kwargs) -> dict:
        return {"success": True, "echo": message}


class TestExtractMissingParam:
    """_extract_missing_param 工具函数测试"""

    def test_extract_named_param(self):
        """从 TypeError 提取具名参数。"""
        exc = TypeError("FakeTool.execute() missing 1 required positional argument: 'message'")
        result = _extract_missing_param(exc, lambda message: None)
        assert result == "message"

    def test_extract_generic_missing(self):
        """无具名参数时列函数签名。"""
        exc = TypeError("missing 2 required positional arguments")
        result = _extract_missing_param(exc, lambda a, b, c=1: None)
        assert result == "a, b"

    def test_no_match(self):
        """不匹配任何模式返回空串。"""
        exc = TypeError("unexpected keyword argument 'foo'")
        result = _extract_missing_param(exc, lambda: None)
        assert result == ""


class TestExecuteTool:
    """execute_tool 核心逻辑测试"""

    def make_registry(self):
        reg = ToolRegistry()
        reg.register(FakeTool())
        return reg

    def test_dispatches_to_tool(self):
        """正确派发到已注册工具。"""
        reg = self.make_registry()
        tc = {
            "function": {
                "name": "fake_echo",
                "arguments": json.dumps({"message": "hello"}),
            }
        }
        result = json.loads(execute_tool(tc, reg))
        assert result["success"] is True
        assert result["echo"] == "hello"

    def test_unknown_tool(self):
        """未知工具返回错误。"""
        reg = self.make_registry()
        tc = {
            "function": {
                "name": "no_such_tool",
                "arguments": "{}",
            }
        }
        result = json.loads(execute_tool(tc, reg))
        assert result["success"] is False
        assert "未知工具" in result["error"]

    def test_bad_json_arguments(self):
        """参数 JSON 解析失败返回错误。"""
        reg = self.make_registry()
        tc = {
            "function": {
                "name": "fake_echo",
                "arguments": "not json",
            }
        }
        result = json.loads(execute_tool(tc, reg))
        assert result["success"] is False
        assert "参数解析失败" in result["error"]

    def test_missing_required_param(self):
        """缺必要参数时返回结构化错误，不抛 TypeError。"""
        reg = self.make_registry()
        tc = {
            "function": {
                "name": "fake_echo",
                "arguments": "{}",
            }
        }
        result = json.loads(execute_tool(tc, reg))
        assert result["success"] is False
        assert "缺失必要参数" in result["error"]

    def test_pre_tool_hook_blocks(self):
        """PreToolUse 钩子阻断工具调用。"""
        reg = self.make_registry()
        host = MagicMock()
        host.pre_tool.return_value = {"block": True, "reason": "test block"}

        tc = {
            "function": {
                "name": "fake_echo",
                "arguments": json.dumps({"message": "hello"}),
            }
        }
        result = json.loads(execute_tool(tc, reg, host=host))
        assert result["success"] is False
        assert "被钩子阻断" in result["error"]
        assert "test block" in result["error"]

    def test_pre_tool_hook_allows(self):
        """PreToolUse 钩子不阻断时正常执行。"""
        reg = self.make_registry()
        host = MagicMock()
        host.pre_tool.return_value = {"block": False}

        tc = {
            "function": {
                "name": "fake_echo",
                "arguments": json.dumps({"message": "hello"}),
            }
        }
        result = json.loads(execute_tool(tc, reg, host=host))
        assert result["success"] is True

    def test_no_host_nohook(self):
        """host 为 None 时跳过钩子，正常执行。"""
        reg = self.make_registry()
        tc = {
            "function": {
                "name": "fake_echo",
                "arguments": json.dumps({"message": "hello"}),
            }
        }
        result = json.loads(execute_tool(tc, reg, host=None))
        assert result["success"] is True

    def test_post_tool_call(self):
        """PostToolUse 钩子在执行后被调用。"""
        reg = self.make_registry()
        host = MagicMock()
        host.pre_tool.return_value = {"block": False}
        host.post_tool = MagicMock()

        tc = {
            "function": {
                "name": "fake_echo",
                "arguments": json.dumps({"message": "hello"}),
            }
        }
        execute_tool(tc, reg, host=host)
        host.post_tool.assert_called_once()


class TestExecuteToolBatch:
    """execute_tool_batch 测试"""

    def test_runs_all_tools(self):
        """批量执行所有工具调用。"""
        reg = ToolRegistry()
        reg.register(FakeTool())
        tcs = [
            {"function": {"name": "fake_echo", "arguments": json.dumps({"message": "a"})}},
            {"function": {"name": "fake_echo", "arguments": json.dumps({"message": "b"})}},
        ]
        results = execute_tool_batch(1, tcs, reg)
        assert len(results) == 2
        assert json.loads(results[0])["echo"] == "a"
        assert json.loads(results[1])["echo"] == "b"


class TestProcessToolCalls:
    """process_tool_calls 循环逻辑测试"""

    def make_session(self):
        from abyss.session import Session
        return Session("You are a test assistant.")

    def make_registry(self):
        reg = ToolRegistry()
        reg.register(FakeTool())
        return reg

    def test_single_call_no_more_tools(self):
        """工具调用后 AI 不再要求更多工具 → 返回最终 content。"""
        session = self.make_session()
        reg = self.make_registry()
        client = MagicMock()

        # 模拟 stream_handler：返回内容无新 tool_calls
        def fake_stream(client, msgs, schema, cfg, sr):
            return ("done", "", [], None)

        tc = {"function": {"name": "fake_echo", "arguments": json.dumps({"message": "hi"})}}

        result = process_tool_calls(
            session=session,
            client=client,
            tools_schemas=reg.to_openai_schemas(),
            tool_calls=[tc],
            cfg={"tool_result_max_chars": None},
            registry=reg,
            stream_handler=fake_stream,
        )
        assert result == "done"

    def test_follow_up_tool_loops(self):
        """多轮工具调用循环正确。"""
        session = self.make_session()
        reg = self.make_registry()
        client = MagicMock()

        call_count = [0]

        def fake_stream(client, msgs, schema, cfg, sr):
            call_count[0] += 1
            if call_count[0] == 1:
                # 第 2 轮（首次 follow-up）：继续要工具
                new_tc = [
                    {"index": 0, "id": "call_2", "function": {"name": "fake_echo", "arguments": json.dumps({"message": "again"})}}
                ]
                return ("", "", new_tc, None)
            else:
                # 第 3 轮：最终回复
                return ("all done", "", [], None)

        tc = {"function": {"name": "fake_echo", "arguments": json.dumps({"message": "hi"})}}

        result = process_tool_calls(
            session=session,
            client=client,
            tools_schemas=reg.to_openai_schemas(),
            tool_calls=[tc],
            cfg={"tool_result_max_chars": None},
            registry=reg,
            stream_handler=fake_stream,
        )
        assert result == "all done"
        assert call_count[0] == 2

    def test_engine_usage_update(self):
        """上下文引擎接收 usage 更新。"""
        session = self.make_session()
        reg = self.make_registry()
        engine = MagicMock()

        def fake_stream(client, msgs, schema, cfg, sr):
            return ("done", "", [], {"prompt_tokens": 500, "total_tokens": 600})

        tc = {"function": {"name": "fake_echo", "arguments": json.dumps({"message": "hi"})}}

        process_tool_calls(
            session=session,
            client=MagicMock(),
            tools_schemas=reg.to_openai_schemas(),
            tool_calls=[tc],
            cfg={"tool_result_max_chars": None},
            registry=reg,
            stream_handler=fake_stream,
            engine=engine,
        )
        engine.update_from_response.assert_called_once_with({"prompt_tokens": 500, "total_tokens": 600})
