# -*- coding: utf-8 -*-
"""
回归测试：文件操作 spinner 完成后，动作行文字（写入文件/读取文件/编辑文件）必须保留可见。

Bug: spinner 用 \\r 覆盖同行，stop() 用 \\r\\x1b[K 清行，导致 _show_tool_action
提前打印的动作行文字被清除。修复后：文件操作跳过预打动作行，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;]*m', '', 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
        elif ch == '\x1b':
            m = re.match(r'\x1b\[([0-9;]*)([a-zA-Z])', text[i:])
            if m:
                letter = m.group(2)
                params = m.group(1)
                if letter == 'K':
                    param = int(params) if params else 0
                    if param == 0:
                        lines[cur] = lines[cur][:pos]
                    elif param == 2:
                        lines[cur] = ""
                        pos = 0
                i += len(m.group(0))
            else:
                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_mock_tool(result=None):
    """创建一个快速执行的 mock 工具，返回 result（默认成功空结果）。"""
    tool = MagicMock()
    tool.execute = MagicMock(return_value=result or {"success": True})
    return tool


def _run_execute_tool(tool_call, tool_map, batch_index=1, batch_total=1):
    """运行 execute_tool 并捕获 stdout，返回 (raw_output, visible_lines)。"""
    mock_registry = MagicMock()
    mock_registry.get = lambda name: tool_map.get(name, _make_mock_tool())
    mock_registry.__contains__ = lambda self, name: name in tool_map or True

    buf = io.StringIO()
    old_stdout = sys.stdout
    sys.stdout = buf
    try:
        with patch.object(main_mod, '_HOST', None), \
             patch.object(main_mod, '_ESC', None), \
             patch.object(main_mod, 'REGISTRY', mock_registry), \
             patch.object(main_mod.logger, 'tool_start', MagicMock()), \
             patch.object(main_mod.logger, 'tool_end', MagicMock()), \
             patch.object(main_mod.logger, 'spinner_start', MagicMock()), \
             patch.object(main_mod.logger, 'spinner_stop', MagicMock()), \
             patch('time.sleep', return_value=None):
            main_mod.execute_tool(tool_call, batch_index=batch_index, batch_total=batch_total)
            time.sleep(0.15)
    finally:
        sys.stdout = old_stdout

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


class TestFileSpinnerActionText:
    """验证文件操作完成后动作行文字保留可见。"""

    @pytest.mark.parametrize("tool_name,action_text", [
        ("file_read", "读取文件"),
        ("file_write", "写入文件"),
        ("file_edit", "编辑文件"),
    ])
    def test_file_tool_action_text_visible_after_spinner(self, tool_name, action_text):
        """文件操作（读/写/编）完成后，动作行文字必须在终端可见。"""
        tool_call = {
            "function": {
                "name": tool_name,
                "arguments": '{"path": "D:\\\\test.txt"}',
            }
        }
        mock_tool = _make_mock_tool()
        raw, lines = _run_execute_tool(tool_call, {tool_name: mock_tool})
        visible = '\n'.join(lines)

        assert action_text in visible, (
            f"文件操作 {tool_name} 的动作行「{action_text}」不可见！\n"
            f"可见行: {lines}\n原始输出: {raw!r}"
        )
        assert "完成" in visible, (
            f"文件操作 {tool_name} 「完成」不可见！\n可见行: {lines}"
        )

    def test_non_file_tool_action_text_still_visible(self):
        """非文件工具动作行仍应可见——回归保护。"""
        tool_call = {
            "function": {
                "name": "web_search",
                "arguments": '{"query": "test"}',
            }
        }
        mock_tool = _make_mock_tool(result={"success": True, "results": []})
        raw, lines = _run_execute_tool(tool_call, {"web_search": mock_tool})
        visible = '\n'.join(lines)

        assert "搜索" in visible, (
            f"web_search 动作行「搜索」不可见！\n可见行: {lines}\n原始输出: {raw!r}"
        )
