"""
Unit tests for OpenCodeStyleEventAdapter
========================================

覆盖点：
- 基本会话 start/finish 事件
- 步骤 start-step/finish-step 边界识别
- 模型 reasoning/text 流式拆分
- 工具 tool-input/tool-call/tool-result/tool-error 映射
- on_chain_error → error 事件
- 配置开关（enable_reasoning_events / enable_step_events）
"""

import pytest
from typing import Any, AsyncIterator, Dict, List

from langchain_agentx.agent import (
    OpenCodeStyleEventAdapter,
    OpenCodeEventType,
)


async def _aiter(events: List[Dict[str, Any]]) -> AsyncIterator[Dict[str, Any]]:
    """将列表包装为 AsyncIterator，方便测试。"""
    for e in events:
        yield e


class _Chunk:
    """简单的 chunk 模拟对象，带 reasoning / content 字段。"""

    def __init__(self, reasoning: str | None = None, content: str | None = None) -> None:
        self.reasoning = reasoning
        self.content = content


@pytest.mark.asyncio
async def test_session_start_and_finish():
    """测试会话级别 start / finish 映射。"""
    adapter = OpenCodeStyleEventAdapter()

    events = [
        {"event": "on_chain_start", "name": "MyAgent", "data": {}},
        {"event": "on_chain_end", "name": "MyAgent", "data": {}},
    ]

    out: List[Any] = []
    async for oc in adapter.adapt(_aiter(events)):
        out.append(oc)

    assert len(out) == 2
    assert out[0].event_type == OpenCodeEventType.START
    assert out[0].data["graph_name"] == "MyAgent"
    assert out[-1].event_type == OpenCodeEventType.FINISH
    assert out[-1].data["graph_name"] == "MyAgent"


@pytest.mark.asyncio
async def test_step_boundaries_with_default_nodes():
    """测试步骤边界识别（严格成对：下一轮开始/图结束时关闭上一轮）。"""
    adapter = OpenCodeStyleEventAdapter(graph_name="MyAgent")

    events = [
        {"event": "on_chain_start", "name": "MyAgent", "data": {}},
        {"event": "on_chain_start", "name": "model", "data": {}},
        {"event": "on_chain_end", "name": "model", "data": {}},
        {"event": "on_chain_start", "name": "loop_controller", "data": {}},
        {"event": "on_chain_end", "name": "loop_controller", "data": {}},
        {"event": "on_chain_end", "name": "MyAgent", "data": {}},
    ]

    types: List[OpenCodeEventType] = []
    async for oc in adapter.adapt(_aiter(events)):
        types.append(oc.event_type)

    assert OpenCodeEventType.START in types
    assert OpenCodeEventType.START_STEP in types
    assert OpenCodeEventType.FINISH_STEP in types
    assert OpenCodeEventType.FINISH in types


@pytest.mark.asyncio
async def test_steps_are_strictly_paired_and_ordered():
    """同一轮循环只允许 1 个 finish-step，且 finish-step 必须先于下一轮 start-step。"""
    adapter = OpenCodeStyleEventAdapter(graph_name="MyAgent")

    events = [
        {"event": "on_chain_start", "name": "MyAgent", "data": {}},
        # step 1
        {"event": "on_chain_start", "name": "model", "data": {}},
        {"event": "on_chain_end", "name": "loop_controller", "data": {}},  # 中间节点结束不应触发 finish-step
        {"event": "on_chain_end", "name": "prune_and_compress", "data": {}},  # 中间节点结束不应触发 finish-step
        # step 2 开始前应自动补齐 step 1 的 finish-step
        {"event": "on_chain_start", "name": "model", "data": {}},
        # 图结束时补齐最后一个 finish-step
        {"event": "on_chain_end", "name": "MyAgent", "data": {}},
    ]

    out: List[Any] = []
    async for oc in adapter.adapt(_aiter(events)):
        out.append(oc)

    start_steps = [e for e in out if e.event_type == OpenCodeEventType.START_STEP]
    finish_steps = [e for e in out if e.event_type == OpenCodeEventType.FINISH_STEP]

    assert [e.step_index for e in start_steps] == [1, 2]
    assert [e.step_index for e in finish_steps] == [1, 2]

    # 顺序：finish-step(1) 必须出现在 start-step(2) 之前
    idx_finish_1 = next(i for i, e in enumerate(out) if e.event_type == OpenCodeEventType.FINISH_STEP and e.step_index == 1)
    idx_start_2 = next(i for i, e in enumerate(out) if e.event_type == OpenCodeEventType.START_STEP and e.step_index == 2)
    assert idx_finish_1 < idx_start_2


@pytest.mark.asyncio
async def test_model_reasoning_and_text_flow():
    """测试 reasoning → text 流式拆分。"""
    adapter = OpenCodeStyleEventAdapter()

    events = [
        {"event": "on_chain_start", "name": "MyAgent", "data": {}},
        {"event": "on_chain_start", "name": "model", "data": {}},
        {
            "event": "on_chat_model_stream",
            "name": "model",
            "data": {"chunk": _Chunk(reasoning="我需要...", content=None)},
        },
        {
            "event": "on_chat_model_stream",
            "name": "model",
            "data": {"chunk": _Chunk(reasoning="调用搜索", content="调用搜索工具")},
        },
        {"event": "on_chat_model_end", "name": "model", "data": {}},
    ]

    types: List[OpenCodeEventType] = []
    payloads: List[Dict[str, Any]] = []
    async for oc in adapter.adapt(_aiter(events)):
        types.append(oc.event_type)
        payloads.append(oc.data)

    assert OpenCodeEventType.REASONING_START in types
    assert OpenCodeEventType.REASONING_DELTA in types
    assert OpenCodeEventType.REASONING_END in types
    assert OpenCodeEventType.TEXT_START in types
    assert OpenCodeEventType.TEXT_DELTA in types
    assert OpenCodeEventType.TEXT_END in types

    reasoning_texts = [d.get("text") for t, d in zip(types, payloads) if t == OpenCodeEventType.REASONING_DELTA]
    assert "我需要..." in reasoning_texts[0]
    assert "调用搜索" in reasoning_texts[1]


@pytest.mark.asyncio
async def test_disable_reasoning_events_only_text():
    """当关闭 reasoning 开关时，只输出 text 相关事件。"""
    adapter = OpenCodeStyleEventAdapter(config={"enable_reasoning_events": False})

    events = [
        {"event": "on_chain_start", "name": "MyAgent", "data": {}},
        {"event": "on_chain_start", "name": "model", "data": {}},
        {
            "event": "on_chat_model_stream",
            "name": "model",
            "data": {"chunk": _Chunk(reasoning="思考内容", content="正文内容")},
        },
        {"event": "on_chat_model_end", "name": "model", "data": {}},
    ]

    types: List[OpenCodeEventType] = []
    async for oc in adapter.adapt(_aiter(events)):
        types.append(oc.event_type)

    assert OpenCodeEventType.REASONING_START not in types
    assert OpenCodeEventType.REASONING_DELTA not in types
    assert OpenCodeEventType.REASONING_END not in types
    assert OpenCodeEventType.TEXT_START in types
    assert OpenCodeEventType.TEXT_DELTA in types
    assert OpenCodeEventType.TEXT_END in types


@pytest.mark.asyncio
async def test_tool_success_and_error_mapping():
    """测试工具成功和失败时的 tool-result / tool-error 映射。"""
    adapter = OpenCodeStyleEventAdapter()

    success_events = [
        {"event": "on_chain_start", "name": "MyAgent", "data": {}},
        {"event": "on_chain_start", "name": "model", "data": {}},
        {
            "event": "on_tool_start",
            "name": "search",
            "data": {"input": {"query": "天气"}},
        },
        {
            "event": "on_tool_end",
            "name": "search",
            "data": {"output": "晴天"},
        },
    ]

    out_success: List[Any] = []
    async for oc in adapter.adapt(_aiter(success_events)):
        out_success.append(oc)

    assert any(e.event_type == OpenCodeEventType.TOOL_INPUT for e in out_success)
    assert any(e.event_type == OpenCodeEventType.TOOL_CALL for e in out_success)
    tool_result = [e for e in out_success if e.event_type == OpenCodeEventType.TOOL_RESULT]
    assert len(tool_result) == 1
    assert tool_result[0].data["output"] == "晴天"

    error_events = [
        {
            "event": "on_tool_start",
            "name": "read_file",
            "data": {"input": {"path": "/root/secret"}},
        },
        {
            "event": "on_tool_end",
            "name": "read_file",
            "data": {"output": PermissionError("Access denied")},
        },
    ]

    out_error: List[Any] = []
    async for oc in adapter.adapt(_aiter(error_events)):
        out_error.append(oc)

    tool_error = [e for e in out_error if e.event_type == OpenCodeEventType.TOOL_ERROR]
    assert len(tool_error) == 1
    err = tool_error[0]
    assert err.data["tool_name"] == "read_file"
    assert err.data["error_type"] == "permission_denied"
    assert err.data["status"] == "blocked"


@pytest.mark.asyncio
async def test_chain_error_event_mapping():
    """测试 on_chain_error → error 事件。"""
    adapter = OpenCodeStyleEventAdapter()

    class DummyError(Exception):
        pass

    events = [
        {
            "event": "on_chain_error",
            "name": "model",
            "data": {"error": DummyError("something wrong")},
        }
    ]

    out: List[Any] = []
    async for oc in adapter.adapt(_aiter(events)):
        out.append(oc)

    assert len(out) == 1
    evt = out[0]
    assert evt.event_type == OpenCodeEventType.ERROR
    assert evt.data["error_type"] == "DummyError"
    assert evt.data["node_name"] == "model"


@pytest.mark.asyncio
async def test_tool_input_start_toggle():
    """测试 enable_tool_input_start 开关生效。"""
    adapter = OpenCodeStyleEventAdapter(config={"enable_tool_input_start": True})

    events = [
        {"event": "on_tool_start", "name": "search", "data": {"input": {"q": 1}}},
    ]

    types: List[OpenCodeEventType] = []
    async for oc in adapter.adapt(_aiter(events)):
        types.append(oc.event_type)

    assert OpenCodeEventType.TOOL_INPUT_START in types
    assert OpenCodeEventType.TOOL_INPUT in types

