"""U1 / G3 工具调用 schema 校验测试。

覆盖 R8-R10:
- R8 schema 校验在 execute 前运行
- R9 类型化错误码 (tool_call_invalid / schema_mismatch)
- R10 _execute_tool 捕获后回灌 conversation(结构化 dict 保留 error_code)
"""

from __future__ import annotations

import pytest

from agentkit.tools.base import Tool, ToolValidationError


class _StubTool(Tool):
    """测试用 stub,记录调用与参数。"""

    def __init__(self, *, schema=None, payload=None):
        super().__init__(name="stub", description="stub", input_schema=schema)
        self.calls: list[dict] = []
        self._payload = payload or {"ok": True}

    async def execute(self, **kwargs) -> dict:
        self.calls.append(kwargs)
        return self._payload


_SCHEMA = {
    "type": "object",
    "properties": {"count": {"type": "integer"}},
    "required": ["count"],
    "additionalProperties": False,
}


# ---- R8 happy path ----


async def test_schema_valid_passes_through_to_execute():
    tool = _StubTool(schema=_SCHEMA)
    result = await tool.safe_execute(count=5)
    assert result == {"ok": True}
    assert tool.calls == [{"count": 5}]


# ---- R9 backward compat: input_schema=None skips ----


async def test_none_schema_skips_validation():
    tool = _StubTool(schema=None)
    await tool.safe_execute(anything="ok", even_invalid_types=123)
    assert tool.calls == [{"anything": "ok", "even_invalid_types": 123}]


# ---- R9 type mismatch → tool_call_invalid ----


async def test_type_mismatch_raises_tool_call_invalid():
    tool = _StubTool(schema=_SCHEMA)
    with pytest.raises(ToolValidationError) as ei:
        await tool.safe_execute(count="abc")
    assert ei.value.error_code == "tool_call_invalid"
    assert ei.value.details["field"] == "count"
    assert tool.calls == []  # execute not called


# ---- R9 missing required → schema_mismatch ----


async def test_missing_required_raises_schema_mismatch():
    tool = _StubTool(schema=_SCHEMA)
    with pytest.raises(ToolValidationError) as ei:
        await tool.safe_execute()
    assert ei.value.error_code == "schema_mismatch"
    assert ei.value.details["validator"] == "required"
    assert tool.calls == []


# ---- R10 integration: _execute_tool catches and returns structured dict ----


async def test_execute_tool_catches_validation_error_returns_structured_dict():
    """ReActEngine._execute_tool 应优先捕获 ToolValidationError 并保留 error_code。"""
    from agentkit.core.react import ReActEngine

    tool = _StubTool(schema=_SCHEMA)
    engine = ReActEngine.__new__(ReActEngine)  # 绕过 __init__
    result = await engine._execute_tool("stub", {"count": "not-int"}, [tool])

    assert "error" in result
    assert result["error_code"] == "tool_call_invalid"
    assert "details" in result
    assert tool.calls == []  # execute 未执行


async def test_execute_tool_catches_missing_required_returns_schema_mismatch():
    from agentkit.core.react import ReActEngine

    tool = _StubTool(schema=_SCHEMA)
    engine = ReActEngine.__new__(ReActEngine)
    result = await engine._execute_tool("stub", {}, [tool])

    assert result["error_code"] == "schema_mismatch"
    assert result["details"]["validator"] == "required"


# ---- R10 self-check: structured error string survives into tool message ----


async def test_structured_error_dict_str_includes_error_code_for_llm_self_correction():
    """_build_tool_result_message 把 dict 转 str(content),LLM 看到文本含 error_code。"""
    from agentkit.core.react import ReActEngine

    tool = _StubTool(schema=_SCHEMA)
    engine = ReActEngine.__new__(ReActEngine)
    result = await engine._execute_tool("stub", {"count": "bad"}, [tool])
    msg = await engine._build_tool_result_message(
        tool_call_id="t1",
        result=result,
    )
    assert msg["role"] == "tool"
    assert msg["tool_call_id"] == "t1"
    assert "tool_call_invalid" in msg["content"]
