# -*- coding: utf-8 -*-
"""
回归测试：tool_calls 流式生成期间必须显示 spinner，避免终端看似卡死。
"""
import io
import re
import sys
import time
from unittest.mock import patch, MagicMock
import pytest

import abyss.main as main_mod


def _render_terminal(raw_text):
    """模拟终端渲染，返回可见行列表（处理 \\r \\n \\x1b[K ANSI SGR）。"""
    text = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', raw_text)
    lines = [""]
    cur = 0
    pos = 0
    i = 0
    while i < len(text):
        ch = text[i]
        if ch == '\r':
            pos = 0
            i += 1
        elif ch == '\n':
            lines.append("")
            cur += 1
            pos = 0
            i += 1
        else:
            line = lines[cur]
            if pos < len(line):
                lines[cur] = line[:pos] + ch + line[pos+1:]
            else:
                lines[cur] = line + ' ' * (pos - len(line)) + ch
            pos += 1
            i += 1
    return lines


def _make_tc_delta(index=0, name=None, args=""):
    """创建一个 tool_calls delta chunk 片段。"""
    return {"index": index, "id": f"call_{index}", "function": {"name": name, "arguments": args}}


def _run_consume_stream(chunks, show_reasoning=True):
    """运行 _consume_stream 并捕获 stdout，返回 (raw_output, visible_lines, result)。"""
    buf = io.StringIO()
    old_stdout = sys.stdout
    sys.stdout = buf
    spinner = main_mod.Spinner("思考中")
    try:
        with patch.object(main_mod, '_ESC', None), \
             patch.object(main_mod.logger, 'api_first_token', MagicMock()), \
             patch.object(main_mod.logger, 'api_response', MagicMock()), \
             patch.object(main_mod.logger, 'stream_reasoning_start', MagicMock()), \
             patch.object(main_mod.logger, 'stream_content_start', MagicMock()), \
             patch.object(main_mod.logger, 'spinner_start', MagicMock()), \
             patch.object(main_mod.logger, 'spinner_stop', MagicMock()), \
             patch('time.sleep', return_value=None):
            spinner.start()
            result = main_mod._consume_stream(iter(chunks), spinner, show_reasoning)
    finally:
        spinner.stop()
        sys.stdout = old_stdout

    raw = buf.getvalue()
    return raw, _render_terminal(raw), result


class TestToolCallsSpinner:
    """验证 tool_calls 流式生成期间显示「生成中」spinner。"""

    def test_spinner_shows_during_tool_calls_after_content(self):
        """先输出 content，再输出 tool_calls → 生成中 spinner 必须出现。"""
        chunks = [
            {"reasoning_content": None, "content": "我来帮你修改文件。", "tool_calls": None},
            {"reasoning_content": None, "content": None, "tool_calls": [_make_tc_delta(name="file_write")]},
            {"reasoning_content": None, "content": None, "tool_calls": [{"index": 0, "function": {"arguments": "{}"}}]},
        ]
        raw, lines, result = _run_consume_stream(chunks)
        content, reasoning, tool_calls, _usage = result

        assert len(tool_calls) > 0, "应该收集到 tool_calls"
        assert "生成中" in raw, (
            f"tool_calls 生成期间应显示「生成中」spinner！\n原始输出: {raw!r}"
        )

    def test_spinner_shows_during_tool_calls_after_reasoning(self):
        """先输出 reasoning，再输出 tool_calls（无 content）→ 生成中 spinner 必须出现。"""
        chunks = [
            {"reasoning_content": "用户要求修改，我需要先读取文件。", "content": None, "tool_calls": None},
            {"reasoning_content": None, "content": None, "tool_calls": [_make_tc_delta(name="file_write")]},
        ]
        raw, lines, result = _run_consume_stream(chunks)
        content, reasoning, tool_calls, _usage = result

        assert len(reasoning) > 0, "应该有 reasoning"
        assert len(tool_calls) > 0, "应该收集到 tool_calls"
        assert "生成中" in raw, (
            f"reasoning 后直接 tool_calls 时应显示「生成中」spinner！\n原始输出: {raw!r}"
        )

    def test_spinner_shows_when_tool_calls_first(self):
        """无 reasoning 无 content，直接 tool_calls → 生成中 spinner 必须出现。"""
        chunks = [
            {"reasoning_content": None, "content": None, "tool_calls": [_make_tc_delta(name="shell_exec")]},
        ]
        raw, lines, result = _run_consume_stream(chunks, show_reasoning=False)
        content, reasoning, tool_calls, _usage = result

        assert len(tool_calls) > 0, "应该收集到 tool_calls"
        assert "生成中" in raw, (
            f"首个输出为 tool_calls 时应显示「生成中」spinner！\n原始输出: {raw!r}"
        )

    def test_no_extra_spinner_for_pure_content(self):
        """纯 content 回复（无 tool_calls）→ 不应出现「生成中」spinner。"""
        chunks = [
            {"reasoning_content": None, "content": "这是纯文本回复，没有工具调用。", "tool_calls": None},
        ]
        raw, lines, result = _run_consume_stream(chunks)
        content, reasoning, tool_calls, _usage = result

        assert len(content) > 0, "应该有 content"
        assert len(tool_calls) == 0, "不应有 tool_calls"
        assert "生成中" not in raw, (
            f"纯 content 回复不应出现「生成中」spinner！\n原始输出: {raw!r}"
        )

    def test_collects_all_tool_call_delta_chunks(self):
        """多个 tool_calls delta chunk 应被正确收集。"""
        chunks = [
            {"reasoning_content": None, "content": "开始写文件。", "tool_calls": None},
            {"reasoning_content": None, "content": None, "tool_calls": [_make_tc_delta(name="file_write")]},
            {"reasoning_content": None, "content": None, "tool_calls": [{"index": 0, "function": {"arguments": "{}"}}]},
        ]
        raw, lines, result = _run_consume_stream(chunks)
        content, reasoning, tool_calls, _usage = result

        assert len(tool_calls) == 2, f"应收集2个 tool_calls delta，实际 {len(tool_calls)}"
        assert "生成中" in raw, "生成中 spinner 应出现"
