# -*- coding: utf-8 -*-
"""
ansi_menu CJK 显示宽度 bug 回归测试。
复现：输入超过一行 CJK 内容时，每多打一字就会复制前面的字一次。
根因：visual line 计算用 len(buf) 字符数而非显示列宽。
"""
import os
import io
from unittest.mock import patch

from abyss.ansi_menu import _clear_visual_lines


class TestDisplayWidthBug:
    """CJK 字符显示宽度导致的视觉行计算错误。"""

    def _display_width(self, s: str) -> int:
        """计算字符串在终端中的显示列宽（CJK 字符占 2 列）。"""
        w = 0
        for ch in s:
            if '\u4e00' <= ch <= '\u9fff' or '\u3000' <= ch <= '\u303f' or '\uff00' <= ch <= '\uffef':
                w += 2
            else:
                w += 1
        return w

    def test_visual_lines_for_cjk_exceeds_one(self):
        """45 个中文字符 = 90 列宽，在 80 列终端应占 2 行，而非 len()=1 行。"""
        cjk_text = "你好" * 23  # 46 个中文字符 = 92 列宽
        char_count = len(cjk_text)
        display_w = self._display_width(cjk_text)
        assert display_w == 92, f"预期 92 列宽，实际 {display_w}"
        # 旧代码的算法：(char_count + 79) // 80
        old_visuals = max(1, (char_count + 79) // 80)
        assert old_visuals == 1, "旧算法错误地认为只有 1 行"
        # 正确的算法应基于显示宽度
        correct_visuals = max(1, (display_w + 79) // 80)
        assert correct_visuals == 2, f"正确算法应为 2 行，实际 {correct_visuals}"

    def test_cjk_string_wraps_at_terminal_boundary(self):
        """40 个中文字符（80 列）恰好填满一行，第 41 个字符应换行。"""
        text_40 = "测" * 40  # 80 列 = 恰好一行
        assert self._display_width(text_40) == 80
        text_41 = "测" * 41  # 82 列 = 两行
        assert self._display_width(text_41) == 82
        # 正确视觉行数
        assert max(1, (self._display_width(text_40) + 79) // 80) == 1
        assert max(1, (self._display_width(text_41) + 79) // 80) == 2

    def test_mixed_cjk_ascii_width(self):
        """混合 CJK 和 ASCII 的显示宽度计算。"""
        text = "> 静安寺从来就擦v"  # prompt(2) + 7 CJK(14) + 1 ASCII(1) = 17 列
        w = self._display_width(text)
        assert w == 17, f"预期 17 列宽，实际 {w}"

    @patch("os.get_terminal_size")
    def test_clear_visual_lines_with_cjk(self, mock_term_size):
        """_clear_visual_lines 在 CJK 内容下正确清除多行。"""
        mock_term_size.return_value = os.terminal_size((80, 24))
        # 23 个中文字符 (46 列) 在 80 列终端只占 1 行 → 没问题
        buf = "测" * 23
        captured = io.StringIO()
        with patch("sys.stdout", captured):
            _clear_visual_lines(buf)
        output = captured.getvalue()
        # 应包含 \r\x1b[K（单行清除）
        assert '\r\x1b[K' in output

    @patch("os.get_terminal_size")
    def test_clear_visual_lines_cjk_multiline_broken(self, mock_term_size):
        """
        复现 bug：CJK 内容超过终端宽度时，用 len() 算视觉行，
        清除的视觉行数不足 → 旧内容残留。
        """
        mock_term_size.return_value = os.terminal_size((80, 24))
        # 45 个中文字符 = 90 列 → 需要 2 视觉行，但 len()=45 → 旧算法认为是 1 行
        buf = "测" * 45
        captured = io.StringIO()
        with patch("sys.stdout", captured):
            _clear_visual_lines(buf)
        output = captured.getvalue()
        # 修复后：45 CJK 字符 = 90 列 → 应清除 2 视觉行
        lines_cleared = output.count('\x1b[K')
        assert lines_cleared == 2, f"修复后清除了 {lines_cleared} 行，预期 2 行"

    @patch("os.get_terminal_size")
    def test_redraw_simulates_cjk_typing_duplication(self, mock_term_size):
        """
        模拟逐字输入 CJK 内容，检测输出中是否出现重复的 prompt。
        每次 redraw 输出中 prompt 量不应超过 1（只有当前行）。
        """
        mock_term_size.return_value = os.terminal_size((80, 24))
        # 模拟打字过程：每打一个 CJK 字符，调用 _clear_visual_lines + 写 buf
        prompt = "> "
        buf_chars = []
        for i, ch in enumerate("测" * 50):
            buf_chars.append(ch)
            buf_str = prompt + ''.join(buf_chars)
            char_len = len(buf_str)
            display_w = self._display_width(buf_str)
            visual_lines = max(1, (display_w + 79) // 80)
            # 旧算法
            old_visuals = max(1, (char_len + 79) // 80)

            if display_w > 80:
                # 已超过一行，检查旧算法是否漏算
                assert old_visuals < visual_lines or old_visuals == visual_lines, \
                    f"第{i+1}个字符后：旧算法 {old_visuals} 行 vs 正确 {visual_lines} 行"
                if old_visuals < visual_lines:
                    # bug 触发：旧算法少算了视觉行
                    return  # 成功复现
        # 如果没有触发，说明测试用例不够长
        # 50 个 CJK = 100 列，一定会触发
        assert False, "应触发 bug 但未触发"
