"""
测试 react_loop.py —— run() 原生 tool_calls 路径

验证：
1. 原生 tool_calls 被正确解析和执行
2. 工具结果通过 role:"tool" 消息正确回写
3. 无 tool_calls 时正常完成
4. compact_trigger_tokens 默认值正确传递
"""
import asyncio
import sys
import tempfile
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest
from aacode.core.react_loop import AsyncReActLoop
from aacode.utils.context_manager import ContextManager
from aacode.utils.tool_registry import get_global_registry, ToolSchema, ToolParameter


@pytest.fixture
def registry():
    """每次测试前清空全局注册表"""
    reg = get_global_registry()
    reg.tools.clear()
    reg.schemas.clear()
    yield reg


class TestNativeToolCalls:
    """原生 tool_calls 路径测试"""

    @pytest.mark.asyncio
    async def test_single_tool_call_executed(self, registry):
        """单次 tool_call 被正确执行，结果回写"""
        call_log = []

        async def test_tool(msg=None, **kwargs):
            call_log.append(f"called:{msg}")
            return f"result:{msg}"

        schema = ToolSchema(
            name="test_tool",
            description="A test tool",
            parameters=[
                ToolParameter(name="msg", type=str, required=True, description="msg"),
            ],
        )
        registry.register(test_tool, schema)

        async def model_caller(messages):
            if len(messages) <= 2:
                return {
                    "text": "Let me call the tool",
                    "tool_calls": [
                        {
                            "name": "test_tool",
                            "arguments": '{"msg": "hello"}',
                        }
                    ],
                }
            return {"text": "Done", "tool_calls": []}

        with tempfile.TemporaryDirectory() as td:
            project_path = Path(td)
            cm = ContextManager(project_path)
            loop = AsyncReActLoop(
                model_caller=model_caller,
                tools={"test_tool": test_tool},
                context_manager=cm,
                max_iterations=10,
                project_path=project_path,
            )
            result = await loop.run(
                initial_prompt="You are a test assistant.",
                task_description="Test calling a tool.",
            )

        assert result["status"] == "completed"
        assert len(call_log) == 1
        assert call_log[0] == "called:hello"
        assert result["iterations"] == 2

    @pytest.mark.asyncio
    async def test_multiple_tool_calls_in_one_response(self, registry):
        """一次响应多个 tool_calls 全部执行"""
        results = []

        async def tool_a(**kwargs):
            results.append("a")
            return "done a"

        async def tool_b(**kwargs):
            results.append("b")
            return "done b"

        for name, func in [("tool_a", tool_a), ("tool_b", tool_b)]:
            registry.register(
                func,
                ToolSchema(name=name, description="", parameters=[]),
            )

        async def model_caller(messages):
            if len(messages) <= 2:
                return {
                    "text": "Calling both tools",
                    "tool_calls": [
                        {"name": "tool_a", "arguments": "{}"},
                        {"name": "tool_b", "arguments": "{}"},
                    ],
                }
            return {"text": "Done", "tool_calls": []}

        with tempfile.TemporaryDirectory() as td:
            cm = ContextManager(Path(td))
            loop = AsyncReActLoop(
                model_caller=model_caller,
                tools={"tool_a": tool_a, "tool_b": tool_b},
                context_manager=cm,
                max_iterations=10,
            )
            result = await loop.run(
                initial_prompt="Test",
                task_description="Test multiple tools",
            )

        assert result["status"] == "completed"
        assert results == ["a", "b"]
        assert result["iterations"] == 2

    @pytest.mark.asyncio
    async def test_tool_result_appended_as_tool_role(self, registry):
        """tool 执行结果以 role:'tool' 追加到消息列表"""
        async def test_tool(**kwargs):
            return "tool_output"

        registry.register(
            test_tool,
            ToolSchema(name="test_tool", description="", parameters=[]),
        )

        seen_tool_role = False

        async def model_caller(messages):
            nonlocal seen_tool_role
            roles = [m["role"] for m in messages]
            if "tool" in roles:
                seen_tool_role = True
                return {"text": "Done", "tool_calls": []}
            return {
                "text": "Calling",
                "tool_calls": [{"name": "test_tool", "arguments": "{}"}],
            }

        with tempfile.TemporaryDirectory() as td:
            cm = ContextManager(Path(td))
            loop = AsyncReActLoop(
                model_caller=model_caller,
                tools={"test_tool": test_tool},
                context_manager=cm,
                max_iterations=10,
            )
            await loop.run(
                initial_prompt="Test",
                task_description="Test tool role",
            )

        assert seen_tool_role, "role:'tool' message was never appended"

    @pytest.mark.asyncio
    async def test_empty_tool_calls_completes_immediately(self, registry):
        """无 tool_calls 时立即完成"""
        async def model_caller(messages):
            return {"text": "Nothing to do", "tool_calls": []}

        with tempfile.TemporaryDirectory() as td:
            cm = ContextManager(Path(td))
            loop = AsyncReActLoop(
                model_caller=model_caller,
                tools={},
                context_manager=cm,
                max_iterations=10,
            )
            result = await loop.run(
                initial_prompt="Test",
                task_description="Empty task",
            )

        assert result["status"] == "completed"
        assert result["iterations"] == 1

    @pytest.mark.asyncio
    async def test_max_iterations_reached(self, registry):
        """超过最大迭代次数时正确返回"""
        call_count = 0

        async def test_tool(**kwargs):
            return "result"

        registry.register(
            test_tool,
            ToolSchema(name="test_tool", description="", parameters=[]),
        )

        async def model_caller(messages):
            nonlocal call_count
            call_count += 1
            return {
                "text": f"Call {call_count}",
                "tool_calls": [{"name": "test_tool", "arguments": "{}"}],
            }

        with tempfile.TemporaryDirectory() as td:
            cm = ContextManager(Path(td))
            loop = AsyncReActLoop(
                model_caller=model_caller,
                tools={"test_tool": test_tool},
                context_manager=cm,
                max_iterations=3,
            )
            result = await loop.run(
                initial_prompt="Test",
                task_description="Max iterations test",
            )

        assert result["status"] == "max_iterations_reached"
        assert result["iterations"] == 3

    @pytest.mark.asyncio
    async def test_tool_error_still_continues_loop(self, registry):
        """工具报错不会中断循环，错误信息会回写"""
        call_count = 0

        async def failing_tool(**kwargs):
            nonlocal call_count
            call_count += 1
            raise ValueError("something broke")

        registry.register(
            failing_tool,
            ToolSchema(name="failing_tool", description="", parameters=[]),
        )

        async def model_caller(messages):
            if len(messages) > 3:
                return {"text": "Recovered", "tool_calls": []}
            return {
                "text": "Call failing tool",
                "tool_calls": [{"name": "failing_tool", "arguments": "{}"}],
            }

        with tempfile.TemporaryDirectory() as td:
            cm = ContextManager(Path(td))
            loop = AsyncReActLoop(
                model_caller=model_caller,
                tools={"failing_tool": failing_tool},
                context_manager=cm,
                max_iterations=10,
            )
            result = await loop.run(
                initial_prompt="Test",
                task_description="Error recovery",
            )

        assert result["status"] == "completed"
        error_in_observation = any(
            "Error" in (a.observation or "")
            for step in result.get("steps", [])
            for a in (step.actions or [])
        )
        assert error_in_observation, "Error should be in tool observation"

    @pytest.mark.asyncio
    async def test_context_config_passed_to_react_loop(self):
        """compact_trigger_tokens 默认值正确传递"""
        from aacode.config import ContextConfig

        config = ContextConfig()
        async def dummy_caller(messages):
            return {"text": "done", "tool_calls": []}

        with tempfile.TemporaryDirectory() as td:
            cm = ContextManager(Path(td))
            loop = AsyncReActLoop(
                model_caller=dummy_caller,
                tools={},
                context_manager=cm,
                context_config=config,
            )

            trigger_tokens = (
                loop.context_config.compact_trigger_tokens
                if loop.context_config
                else 8000
            )
            assert trigger_tokens == 256000

    @pytest.mark.asyncio
    async def test_cancelled_error_propagates(self, registry):
        """CancelledError 正确向上传播"""
        async def model_caller(messages):
            raise asyncio.CancelledError()

        with tempfile.TemporaryDirectory() as td:
            cm = ContextManager(Path(td))
            loop = AsyncReActLoop(
                model_caller=model_caller,
                tools={},
                context_manager=cm,
            )
            with pytest.raises(asyncio.CancelledError):
                await loop.run(
                    initial_prompt="Test",
                    task_description="Cancel test",
                )
