"""
测试流式输出格式 & thought 清理 & run_shell 截断（覆盖近期改动的测试缺口）

覆盖：
1. thought 清理正则 — 剥离末尾 Action 声明行、压缩多余空行
2. thinking_prefix 清理 — 去除 thought 前的 "Thought:" / "THINKING:" 等前缀
3. run_shell command 截断 — heredoc/长命令行精简
4. seg_content JSON 事件格式 — thinking/thought/observation/tool_progress
"""

import sys
import re
import json
import tempfile
import io
from pathlib import Path
from unittest.mock import patch, MagicMock, AsyncMock

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest


# ============================================================================
#  0. 原始逻辑提取（与 main_agent.py / react_loop.py 保持一致的纯函数）
# ============================================================================

def _clean_thought_text(full_response):
    """剥离末尾的 Action 声明行 + 压缩多余空行（与 main_agent.py 逻辑一致）"""
    _clean = full_response
    _m = re.search(
        r'\n+Action:\s+[a-z][a-z0-9_-]*\s*(?:\nAction Input:\s*.*?)?\s*$',
        _clean,
    )
    if _m:
        _clean = _clean[:_m.start()].strip()
    _clean = re.sub(r'\n{3,}', '\n\n', _clean)
    return _clean


def _clean_thinking_prefix(text):
    """去除内容开头的 thinking 前缀（与 main_agent.py 一致）"""
    thinking_prefixes = [
        r'^Thought[:\s\n]*',
        r'^THINKING[:\s\n]*',
        r'^thinking[:\s\n]*',
        r'^reasoning[:\s\n]*',
    ]
    result = text.lstrip('\n')
    for pat in thinking_prefixes:
        result = re.sub(pat, '', result, flags=re.IGNORECASE).lstrip('\n')
    return result


def _simplify_run_shell_command(result_dict):
    """精简 run_shell 结果中的 command 字段（与 react_loop.py 一致）"""
    summary = dict(result_dict)
    cmd = summary.pop("command", "")
    first_line = cmd.split('\n')[0][:200] if cmd else ''
    if len(cmd) > len(first_line) or len(cmd) > 200:
        summary["command"] = first_line + ("..." if first_line and len(cmd) > len(first_line) else "")
    elif first_line:
        summary["command"] = first_line
    summary.pop("working_directory", None)
    return summary


def _encode_seg_content(seg, content):
    """编码 seg_content JSON 事件"""
    return json.dumps({"type": "seg_content", "seg": seg, "content": content})


def _encode_tool_progress(state, name, chars=0):
    """编码 tool_progress JSON 事件"""
    return json.dumps({"type": "tool_progress", "state": state, "name": name, "chars": chars})


# ============================================================================
#  1. thought 清理正则测试
# ============================================================================

class TestThoughtCleaning:
    """测试 thought 文本清理——剥离末尾 Action 声明"""

    def test_no_action_preserved(self):
        text = "Let's look at the project structure first."
        assert _clean_thought_text(text) == text

    def test_single_action_without_input_stripped(self):
        text = "Let's do it.\n\nAction: read_file"
        assert _clean_thought_text(text) == "Let's do it."

    def test_action_with_input_stripped(self):
        text = "Check the file.\nAction: read_file\nAction Input: {\"path\": \"test.py\"}"
        assert _clean_thought_text(text) == "Check the file."

    def test_action_only_line_stripped(self):
        text = "Action: run_shell"
        # 没有 \n+Action 前缀，单独一行不会被剥离（正则要求 \n+ 前置）
        assert _clean_thought_text(text) == "Action: run_shell"

    def test_text_after_action_not_preserved(self):
        text = "Do read\nAction: read_file\nMore text here"
        # "More text" 在 Action 之后但不是末尾，正则 $ 锚定只匹配末尾
        assert _clean_thought_text(text) == text

    def test_multiple_actions_only_last_stripped(self):
        text = "Step 1 done.\nAction: step1\nStep 2 done.\nAction: step2"
        assert _clean_thought_text(text) == "Step 1 done.\nAction: step1\nStep 2 done."

    def test_action_uppercase_tool_name_not_stripped(self):
        # 正则只匹配 [a-z] 开头的小写工具名
        text = "Done.\n\nAction: ReadFile"
        assert _clean_thought_text(text) == text

    def test_action_with_dashes_and_underscores_stripped(self):
        text = "Analyzing.\n\nAction: find_replace_items\nAction Input: {\"old\": \"foo\"}"
        assert _clean_thought_text(text) == "Analyzing."

    def test_multiple_newlines_compressed(self):
        text = "hello\n\n\n\n\n\n\nworld"
        assert _clean_thought_text(text) == "hello\n\nworld"

    def test_no_excessive_newlines_when_few(self):
        text = "line1\n\nline2"
        assert _clean_thought_text(text) == text

    def test_clean_keeps_content_after_strip_preserves_original(self):
        text = "Some reasoning text.\n\nAction: run_shell\nAction Input: {\"command\": \"ls\"}"
        result = _clean_thought_text(text)
        assert "Action:" not in result
        assert "Some reasoning text." == result

    def test_empty_string(self):
        assert _clean_thought_text("") == ""

    def test_action_with_unicode_in_input(self):
        text = "Done.\n\nAction: write_file\nAction Input: {\"content\": \"你好世界\"}"
        assert _clean_thought_text(text) == "Done."

    def test_action_with_leading_newlines(self):
        text = "\n\nSome text.\n\n\n\n\nAction: finalize_task"
        result = _clean_thought_text(text)
        assert "Action:" not in result
        assert result == "Some text."

    def test_trailing_whitespace_after_strip_removed(self):
        text = "Content here.  \n\nAction: run_shell  \n"
        assert _clean_thought_text(text) == "Content here."


# ============================================================================
#  2. thinking 前缀清理测试
# ============================================================================

class TestThinkingPrefixCleaning:
    """测试 thought 内容开头的 thinking 前缀移除"""

    def test_thought_colon_prefix(self):
        assert _clean_thinking_prefix("Thought: Let's analyze") == "Let's analyze"

    def test_thought_colon_space_prefix(self):
        assert _clean_thinking_prefix("Thought: analyze") == "analyze"

    def test_THINKING_uppercase(self):
        assert _clean_thinking_prefix("THINKING: We need to...") == "We need to..."

    def test_thinking_lowercase(self):
        assert _clean_thinking_prefix("thinking: maybe we should") == "maybe we should"

    def test_reasoning_prefix(self):
        assert _clean_thinking_prefix("reasoning: It appears that") == "It appears that"

    def test_no_prefix_preserved(self):
        assert _clean_thinking_prefix("Just a regular thought") == "Just a regular thought"

    def test_leading_newlines_handled(self):
        assert _clean_thinking_prefix("\n\n\nThought: clean this") == "clean this"

    def test_complete_word_thought_removed(self):
        """'Thoughtfully' 中的 'Thought' 会被正则匹配移除（无词边界限制）"""
        assert _clean_thinking_prefix("Thoughtfully consider") == "fully consider"

    def test_thought_no_content_after(self):
        assert _clean_thinking_prefix("Thought:") == ""

    def test_thought_newline_only_after(self):
        assert _clean_thinking_prefix("Thought:\n\n\n") == ""

    def test_case_insensitive_mixed(self):
        assert _clean_thinking_prefix("tHiNkInG: something") == "something"


# ============================================================================
#  3. run_shell command 截断测试
# ============================================================================

class TestRunShellCommandTruncation:
    """测试 run_shell 工具结果的 command 字段精简"""

    def test_single_short_command_preserved(self):
        result = {"command": "ls -la", "stdout": "total 100", "stderr": "", "working_directory": "/tmp"}
        out = _simplify_run_shell_command(result)
        assert out["command"] == "ls -la"
        assert out["stdout"] == "total 100"
        assert "working_directory" not in out

    def test_single_long_command_truncated(self):
        long_cmd = "echo " + "a" * 300
        result = {"command": long_cmd, "stdout": "a" * 300}
        out = _simplify_run_shell_command(result)
        expected = long_cmd[:200] + "..."
        assert out["command"] == expected

    def test_multiline_heredoc_truncated_to_first_line(self):
        cmd = """cat << 'EOF' > /tmp/script.py
import os
def main():
    print("hello")
EOF
"""
        result = {"command": cmd, "stdout": "success"}
        out = _simplify_run_shell_command(result)
        assert out["command"] == "cat << 'EOF' > /tmp/script.py..."

    def test_empty_command(self):
        """空 command 被 pop 后不写回，key 被移除"""
        result = {"command": "", "stdout": "noop"}
        out = _simplify_run_shell_command(result)
        assert "command" not in out

    def test_working_directory_stripped(self):
        result = {"command": "ls", "stdout": "ok", "working_directory": "/projects/foo"}
        out = _simplify_run_shell_command(result)
        assert "working_directory" not in out

    def test_all_other_fields_preserved(self):
        result = {
            "command": "git status",
            "stdout": "OK",
            "stderr": "",
            "exit_code": 0,
            "working_directory": "/tmp",
        }
        out = _simplify_run_shell_command(result)
        assert out["stdout"] == "OK"
        assert out["stderr"] == ""
        assert out["exit_code"] == 0
        assert "working_directory" not in out

    def test_200_char_boundary_exact(self):
        cmd_200 = "a" * 200
        result = {"command": cmd_200}
        out = _simplify_run_shell_command(result)
        assert out["command"] == cmd_200  # exactly 200 chars, not truncated

    def test_201_char_boundary_truncated(self):
        cmd_201 = "a" * 201
        result = {"command": cmd_201}
        out = _simplify_run_shell_command(result)
        assert out["command"] == cmd_201[:200] + "..."

    def test_first_line_at_200_char(self):
        first_line = "x" * 200
        rest = "y" * 50
        result = {"command": first_line + "\n" + rest}
        out = _simplify_run_shell_command(result)
        assert out["command"] == first_line + "..."

    def test_missing_command_key(self):
        """缺少 command key 时不影响其他字段"""
        result = {"stdout": "ok", "stderr": ""}
        out = _simplify_run_shell_command(result)
        assert "command" not in out
        assert out["stdout"] == "ok"


# ============================================================================
#  4. seg_content / tool_progress JSON 事件格式测试
# ============================================================================

class TestSegContentEvents:
    """测试流式输出的 JSON 事件格式"""

    def test_seg_content_thinking(self):
        event = _encode_seg_content("thinking", "I need to check the file.")
        parsed = json.loads(event)
        assert parsed["type"] == "seg_content"
        assert parsed["seg"] == "thinking"
        assert parsed["content"] == "I need to check the file."

    def test_seg_content_thought(self):
        event = _encode_seg_content("thought", "Let's use run_shell.")
        parsed = json.loads(event)
        assert parsed["type"] == "seg_content"
        assert parsed["seg"] == "thought"
        assert parsed["content"] == "Let's use run_shell."

    def test_seg_content_observation(self):
        event = _encode_seg_content("observation", "File contents:\nline1\nline2")
        parsed = json.loads(event)
        assert parsed["type"] == "seg_content"
        assert parsed["seg"] == "observation"
        assert "line1" in parsed["content"]

    def test_seg_content_thought_empty_for_tool_only(self):
        """没有 thought 文本时也发送空 seg_content，让前端知道段已完成"""
        event = _encode_seg_content("thought", "")
        parsed = json.loads(event)
        assert parsed["type"] == "seg_content"
        assert parsed["seg"] == "thought"
        assert parsed["content"] == ""

    def test_tool_progress_building(self):
        event = _encode_tool_progress("building", "read_file", 0)
        parsed = json.loads(event)
        assert parsed["type"] == "tool_progress"
        assert parsed["state"] == "building"
        assert parsed["name"] == "read_file"
        assert parsed["chars"] == 0

    def test_tool_progress_building_with_chars(self):
        event = _encode_tool_progress("building", "write_file", 1500)
        parsed = json.loads(event)
        assert parsed["type"] == "tool_progress"
        assert parsed["state"] == "building"
        assert parsed["name"] == "write_file"
        assert parsed["chars"] == 1500

    def test_tool_progress_done(self):
        event = _encode_tool_progress("done", "read_file")
        parsed = json.loads(event)
        assert parsed["type"] == "tool_progress"
        assert parsed["state"] == "done"
        assert parsed["name"] == "read_file"
        assert parsed["chars"] == 0

    def test_seg_content_unicode(self):
        content = "需要检查文件内容\n包括中文和 emoji 🚀"
        event = _encode_seg_content("thought", content)
        parsed = json.loads(event)
        assert parsed["content"] == content

    def test_seg_content_special_characters(self):
        content = '{"key": "value", "nested": {"a": 1}}'
        event = _encode_seg_content("observation", content)
        parsed = json.loads(event)
        assert parsed["content"] == content

    def test_seg_content_multiline(self):
        content = "line1\nline2\n\nline3"
        event = _encode_seg_content("observation", content)
        parsed = json.loads(event)
        assert parsed["content"] == content
        assert parsed["content"].count("\n") == 3


# ============================================================================
#  5. seg_content observation 事件
# ============================================================================

class TestObservationSegContent:
    """测试 observation 的 seg_content JSON 输出"""

    def test_observation_json_format(self):
        """验证 observation seg_content JSON 格式正确"""
        full_obs = "File content:\nline1\nline2\nline3"
        event = _encode_seg_content("observation", full_obs)
        parsed = json.loads(event)
        assert parsed["type"] == "seg_content"
        assert parsed["seg"] == "observation"
        assert parsed["content"] == full_obs

    def test_observation_matches_original(self):
        """验证 observation 内容与原始数据一致（不丢失信息）"""
        obs = "1000 lines of output\n" + "\n".join(f"line {i}" for i in range(1000))
        event = _encode_seg_content("observation", obs)
        parsed = json.loads(event)
        assert parsed["content"] == obs

    def test_observation_empty_content(self):
        event = _encode_seg_content("observation", "")
        parsed = json.loads(event)
        assert parsed["seg"] == "observation"
        assert parsed["content"] == ""

    def test_observation_with_json_like_content(self):
        """内容包含 JSON 样式的字符串不应干扰 event 解析"""
        content = '{"success": true, "stdout": "hello"}'
        event = _encode_seg_content("observation", content)
        parsed = json.loads(event)
        assert parsed["content"] == content
        assert parsed["type"] == "seg_content"


# ============================================================================
#  6. 综合场景 — 完整 streaming 输出组合
# ============================================================================

class TestStreamingOutputIntegration:
    """验证多段 streaming 事件的顺序和组合"""

    def test_complete_streaming_sequence(self):
        """模拟完整的流式输出: thinking → tool_progress → thought → observation → done"""
        events = []

        # 段1: thinking
        events.append(_encode_seg_content("thinking", "I need to read the file first."))

        # 段2: tool_progress building
        events.append(_encode_tool_progress("building", "read_file", 0))
        events.append(_encode_tool_progress("building", "read_file", 500))
        events.append(_encode_tool_progress("building", "read_file", 1000))

        # 段3: tool_progress done
        events.append(_encode_tool_progress("done", "read_file"))

        # 段4: thought (模型分析)
        events.append(_encode_seg_content("thought", "The file contains a Python script."))

        # 段5: observation (工具结果)
        events.append(_encode_seg_content("observation", "import os\nprint('hello')"))

        # 段6: tool_progress building → done (下一个工具)
        events.append(_encode_tool_progress("building", "finalize_task", 0))
        events.append(_encode_tool_progress("done", "finalize_task"))

        # 段7: 最终 thought
        events.append(_encode_seg_content("thought", "Task completed successfully."))

        parsed_events = [json.loads(e) for e in events]

        assert parsed_events[0]["type"] == "seg_content"
        assert parsed_events[0]["seg"] == "thinking"

        assert parsed_events[1]["type"] == "tool_progress"
        assert parsed_events[1]["state"] == "building"

        # events: [0]thinking, [1]building, [2]building, [3]building, [4]done, [5]thought, [6]observation, [7]building, [8]done, [9]thought
        assert parsed_events[5]["type"] == "seg_content"
        assert parsed_events[5]["seg"] == "thought"

        assert parsed_events[6]["type"] == "seg_content"
        assert parsed_events[6]["seg"] == "observation"

        assert "tool_progress" in {e["type"] for e in parsed_events}
        assert "seg_content" in {e["type"] for e in parsed_events}

    def test_tool_progress_all_states_exist(self):
        """tool_progress 只有 building 和 done 两个状态"""
        valid_states = {"building", "done"}
        ev1 = json.loads(_encode_tool_progress("building", "test", 500))
        ev2 = json.loads(_encode_tool_progress("done", "test"))
        assert ev1["state"] in valid_states
        assert ev2["state"] in valid_states
