# tests/test_regression_comprehensive.py
"""
全面回归测试：覆盖本次所有改动的边界情况和潜在副作用

改动清单：
1. todo_manager.py: todo_id、去掉记录section、add_execution_record 废弃
2. todo_tools.py: 返回 todo_id、mark_todo_completed 支持 todo_id、add_execution_record 废弃
3. tool_schemas.py: schema 更新（todo_id、stdin_input、废弃 execution_record）
4. react_loop.py: 去掉 _update_todo_from_thought 调用、更新 system prompt
5. prompts.py: 工具列表更新
6. web_tools.py: cleanup 加 sleep、__del__ 改空
7. main_agent.py: __del__ 改空
8. atomic_tools.py: run_shell 加 stdin_input、默认 DEVNULL
"""

import asyncio
import pytest
import sys
import os
from pathlib import Path

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

from aacode.utils.todo_manager import TodoManager, get_todo_manager, _todo_managers
from aacode.tools.todo_tools import TodoTools
from aacode.tools.atomic_tools import AtomicTools


# ==================== Fixtures ====================

class MockSafetyGuard:
    def check_command(self, command):
        return {"allowed": True}
    def is_safe_path(self, path):
        return True


@pytest.fixture
def tmp_project(tmp_path):
    project_dir = tmp_path / "test_project"
    project_dir.mkdir()
    return project_dir


@pytest.fixture
def todo_manager(tmp_project):
    _todo_managers.clear()
    return TodoManager(tmp_project)


@pytest.fixture
def todo_tools(tmp_project):
    _todo_managers.clear()
    return TodoTools(tmp_project)


@pytest.fixture
def shell_tools(tmp_project):
    return AtomicTools(tmp_project, MockSafetyGuard())


# ==================== 1. Todo: 老格式文件兼容 ====================

class TestOldFormatCompat:
    """确保老格式 todo 文件（有 ## 记录、无 [#tN]）不会崩溃"""

    @pytest.mark.asyncio
    async def test_old_format_with_record_section(self, tmp_project):
        """老文件有 ## 记录 section，add_todo_item 仍能工作"""
        _todo_managers.clear()
        manager = TodoManager(tmp_project)
        todo_dir = tmp_project / ".aacode" / "todos"
        todo_dir.mkdir(parents=True, exist_ok=True)
        old_file = todo_dir / "old.md"
        old_file.write_text(
            "# old - 待办清单\n\n## 待办\n\n## 已完成\n\n## 记录\n- 创建\n",
            encoding="utf-8",
        )
        manager.current_todo_file = old_file

        # 添加新事项（应该能正常工作，ID 从 t1 开始）
        todo_id = await manager.add_todo_item("新任务")
        assert todo_id == "t1"

        content = old_file.read_text(encoding="utf-8")
        assert "[#t1]" in content
        assert "新任务" in content

    @pytest.mark.asyncio
    async def test_old_format_mark_completed_by_text(self, tmp_project):
        """老文件无 [#tN] 标记，文本匹配仍能工作"""
        _todo_managers.clear()
        manager = TodoManager(tmp_project)
        todo_dir = tmp_project / ".aacode" / "todos"
        todo_dir.mkdir(parents=True, exist_ok=True)
        old_file = todo_dir / "old.md"
        old_file.write_text(
            "# old - 待办清单\n\n## 待办\n- [ ] 🔴 **任务**: 修复登录bug\n\n## 已完成\n",
            encoding="utf-8",
        )
        manager.current_todo_file = old_file

        result = await manager.mark_todo_completed(item_pattern="登录bug")
        assert result is True

    @pytest.mark.asyncio
    async def test_old_format_add_execution_record_noop(self, tmp_project):
        """老文件调用 add_execution_record 不崩溃"""
        _todo_managers.clear()
        manager = TodoManager(tmp_project)
        todo_dir = tmp_project / ".aacode" / "todos"
        todo_dir.mkdir(parents=True, exist_ok=True)
        old_file = todo_dir / "old.md"
        old_file.write_text(
            "# old - 待办清单\n\n## 待办\n\n## 已完成\n\n## 记录\n",
            encoding="utf-8",
        )
        manager.current_todo_file = old_file

        result = await manager.add_execution_record("测试记录")
        assert result is True
        # 不应写入任何内容（noop）
        content = old_file.read_text(encoding="utf-8")
        assert "测试记录" not in content


# ==================== 2. Todo: 边界情况 ====================

class TestTodoEdgeCases:
    """todo 系统边界情况"""

    @pytest.mark.asyncio
    async def test_mark_completed_with_both_id_and_pattern(self, todo_manager):
        """同时传 todo_id 和 item_pattern，todo_id 优先"""
        await todo_manager.create_todo_list("测试")
        id1 = await todo_manager.add_todo_item("任务A")
        await todo_manager.add_todo_item("任务B")

        # todo_id 指向 A，pattern 指向 B → 应该标记 A
        result = await todo_manager.mark_todo_completed(
            item_pattern="任务B", todo_id=id1
        )
        assert result is True

        content = todo_manager.current_todo_file.read_text(encoding="utf-8")
        # A 应该被标记完成
        lines = content.split("\n")
        a_lines = [l for l in lines if "任务A" in l and l.strip().startswith("- [")]
        assert any("- [x]" in l for l in a_lines)
        # B 应该还是未完成
        b_lines = [l for l in lines if "任务B" in l and l.strip().startswith("- [")]
        assert any("- [ ]" in l for l in b_lines)

    @pytest.mark.asyncio
    async def test_mark_completed_wrong_id_falls_through_to_pattern(self, todo_manager):
        """todo_id 不存在时，fallback 到 pattern 匹配"""
        await todo_manager.create_todo_list("测试")
        await todo_manager.add_todo_item("修复bug")

        result = await todo_manager.mark_todo_completed(
            item_pattern="修复bug", todo_id="t999"
        )
        # t999 不存在，但 pattern "修复bug" 能匹配到
        assert result is True

    @pytest.mark.asyncio
    async def test_special_chars_in_description(self, todo_manager):
        """描述中包含特殊字符"""
        await todo_manager.create_todo_list("测试")
        todo_id = await todo_manager.add_todo_item("打印'Hello, World!'")
        assert todo_id is not None

        result = await todo_manager.mark_todo_completed(todo_id=todo_id)
        assert result is True

    @pytest.mark.asyncio
    async def test_many_items_counter_integrity(self, todo_manager):
        """大量事项时 ID 计数器完整性"""
        await todo_manager.create_todo_list("测试")
        ids = []
        for i in range(20):
            tid = await todo_manager.add_todo_item(f"任务{i}")
            ids.append(tid)

        assert ids[0] == "t1"
        assert ids[19] == "t20"

        # 标记中间的几个完成
        await todo_manager.mark_todo_completed(todo_id="t5")
        await todo_manager.mark_todo_completed(todo_id="t10")
        await todo_manager.mark_todo_completed(todo_id="t15")

        summary = await todo_manager.get_todo_summary()
        assert summary["total_todos"] == 20
        assert summary["completed_todos"] == 3
        assert summary["pending_todos"] == 17

    @pytest.mark.asyncio
    async def test_new_file_has_no_placeholder_text(self, todo_manager):
        """新文件不应有 （暂无） 占位符"""
        await todo_manager.create_todo_list("测试")
        content = todo_manager.current_todo_file.read_text(encoding="utf-8")
        assert "（暂无）" not in content
        assert "自动维护" not in content


# ==================== 3. TodoTools: 参数别名兼容 ====================

class TestTodoToolsAliases:
    """模型可能用各种参数名，确保都能工作"""

    @pytest.mark.asyncio
    async def test_add_via_item_alias(self, todo_tools, tmp_project):
        _todo_managers.clear()
        m = get_todo_manager(tmp_project)
        await m.create_todo_list("测试")
        result = await todo_tools.add_todo_item(item="写代码")
        assert result["success"] is True
        assert result["todo_id"] is not None

    @pytest.mark.asyncio
    async def test_complete_via_title_alias(self, todo_tools, tmp_project):
        _todo_managers.clear()
        m = get_todo_manager(tmp_project)
        await m.create_todo_list("测试")
        r = await todo_tools.add_todo_item(description="写代码")
        # 模型用 title 参数
        result = await todo_tools.mark_todo_completed(title="写代码")
        assert result["success"] is True

    @pytest.mark.asyncio
    async def test_complete_via_id_in_kwargs(self, todo_tools, tmp_project):
        """模型可能把 todo_id 放在 kwargs 里"""
        _todo_managers.clear()
        m = get_todo_manager(tmp_project)
        await m.create_todo_list("测试")
        r = await todo_tools.add_todo_item(description="写代码")
        # 通过 kwargs 传 todo_id（normalize_params 会处理 id → todo_id）
        result = await todo_tools.mark_todo_completed(todo_id=r["todo_id"])
        assert result["success"] is True


# ==================== 4. run_shell: DEVNULL 副作用检查 ====================

class TestRunShellDevnullSideEffects:
    """确保 DEVNULL 不会破坏正常命令"""

    @pytest.mark.asyncio
    async def test_pipe_commands_work(self, shell_tools):
        """管道命令不受 DEVNULL 影响"""
        result = await shell_tools.run_shell(command='echo "hello world" | grep hello')
        assert result["success"] is True
        assert "hello" in result["stdout"]

    @pytest.mark.asyncio
    async def test_redirect_input_still_works(self, shell_tools, tmp_project):
        """重定向输入仍然有效（< file）"""
        input_file = tmp_project / "input.txt"
        input_file.write_text("test content\n")
        result = await shell_tools.run_shell(command=f"cat < {input_file}")
        assert result["success"] is True
        assert "test content" in result["stdout"]

    @pytest.mark.asyncio
    async def test_heredoc_works(self, shell_tools):
        """heredoc 仍然有效"""
        result = await shell_tools.run_shell(
            command='cat << EOF\nhello heredoc\nEOF'
        )
        assert result["success"] is True
        assert "hello heredoc" in result["stdout"]

    @pytest.mark.asyncio
    async def test_python_c_works(self, shell_tools):
        """python -c 不受影响"""
        result = await shell_tools.run_shell(
            command='python3 -c "print(42)"'
        )
        assert result["success"] is True
        assert "42" in result["stdout"]

    @pytest.mark.asyncio
    async def test_git_commands_work(self, shell_tools):
        """git 命令不受影响（git 不读 stdin）"""
        result = await shell_tools.run_shell(command="git --version")
        assert result["success"] is True
        assert "git version" in result["stdout"]

    @pytest.mark.asyncio
    async def test_ls_works(self, shell_tools):
        """基本 ls 命令"""
        result = await shell_tools.run_shell(command="ls")
        assert result["success"] is True

    @pytest.mark.asyncio
    async def test_echo_works(self, shell_tools):
        """echo 命令"""
        result = await shell_tools.run_shell(command='echo "test123"')
        assert result["success"] is True
        assert "test123" in result["stdout"]


# ==================== 5. run_shell: stdin_input 边界情况 ====================

class TestStdinInputEdgeCases:
    """stdin_input 的各种边界情况"""

    @pytest.mark.asyncio
    async def test_stdin_with_unicode(self, shell_tools, tmp_project):
        """Unicode 输入"""
        script = tmp_project / "unicode_input.py"
        script.write_text(
            'name = input()\nprint(f"你好, {name}!")\n',
            encoding="utf-8",
        )
        result = await shell_tools.run_shell(
            command=f"python3 {script}",
            stdin_input="世界\n",
        )
        assert result["success"] is True
        assert "你好, 世界!" in result["stdout"]

    @pytest.mark.asyncio
    async def test_stdin_with_newline_only(self, shell_tools, tmp_project):
        """只传换行符（模拟按回车）"""
        script = tmp_project / "enter.py"
        script.write_text(
            'input("Press enter...")\nprint("done")\n',
            encoding="utf-8",
        )
        result = await shell_tools.run_shell(
            command=f"python3 {script}",
            stdin_input="\n",
        )
        assert result["success"] is True
        assert "done" in result["stdout"]

    @pytest.mark.asyncio
    async def test_stdin_insufficient_input(self, shell_tools, tmp_project):
        """输入不够（程序需要 2 次 input，只给 1 次）"""
        script = tmp_project / "two_inputs.py"
        script.write_text(
            'a = input()\nb = input()\nprint(a, b)\n',
            encoding="utf-8",
        )
        result = await shell_tools.run_shell(
            command=f"python3 {script}",
            stdin_input="only_one\n",
        )
        assert result["success"] is True
        # 第二个 input() 会 EOF → 非零退出码
        assert result["returncode"] != 0

    @pytest.mark.asyncio
    async def test_stdin_excess_input(self, shell_tools, tmp_project):
        """输入过多（程序只需要 1 次 input，给了 3 次）"""
        script = tmp_project / "one_input.py"
        script.write_text(
            'x = input()\nprint(f"got: {x}")\n',
            encoding="utf-8",
        )
        result = await shell_tools.run_shell(
            command=f"python3 {script}",
            stdin_input="first\nsecond\nthird\n",
        )
        assert result["success"] is True
        assert result["returncode"] == 0
        assert "got: first" in result["stdout"]

    @pytest.mark.asyncio
    async def test_stdin_with_timeout(self, shell_tools, tmp_project):
        """stdin_input + timeout 组合"""
        script = tmp_project / "slow.py"
        script.write_text(
            'import time\nx = input()\ntime.sleep(0.1)\nprint(f"got: {x}")\n',
            encoding="utf-8",
        )
        result = await shell_tools.run_shell(
            command=f"python3 {script}",
            stdin_input="hello\n",
            timeout=10,
        )
        assert result["success"] is True
        assert "got: hello" in result["stdout"]


# ==================== 6. Schema 验证 ====================

class TestSchemaIntegrity:
    """确保 schema 定义正确，不会导致参数验证失败"""

    def test_todo_schemas_load(self):
        """todo 相关 schema 能正常加载"""
        from aacode.utils.tool_schemas import (
            ADD_TODO_ITEM_SCHEMA,
            MARK_TODO_COMPLETED_SCHEMA,
            ADD_EXECUTION_RECORD_SCHEMA,
        )
        assert ADD_TODO_ITEM_SCHEMA.name == "add_todo_item"
        assert MARK_TODO_COMPLETED_SCHEMA.name == "mark_todo_completed"
        assert ADD_EXECUTION_RECORD_SCHEMA.name == "add_execution_record"

    def test_mark_completed_schema_has_todo_id(self):
        """mark_todo_completed schema 有 todo_id 参数"""
        from aacode.utils.tool_schemas import MARK_TODO_COMPLETED_SCHEMA
        param_names = [p.name for p in MARK_TODO_COMPLETED_SCHEMA.parameters]
        assert "todo_id" in param_names
        assert "item_pattern" in param_names

    def test_mark_completed_both_optional(self):
        """todo_id 和 item_pattern 都是可选的"""
        from aacode.utils.tool_schemas import MARK_TODO_COMPLETED_SCHEMA
        for p in MARK_TODO_COMPLETED_SCHEMA.parameters:
            assert p.required is False

    def test_run_shell_schema_has_stdin_input(self):
        """run_shell schema 有 stdin_input 参数"""
        from aacode.utils.tool_schemas import RUN_SHELL_SCHEMA
        param_names = [p.name for p in RUN_SHELL_SCHEMA.parameters]
        assert "stdin_input" in param_names
        assert "command" in param_names
        assert "timeout" in param_names

    def test_run_shell_stdin_input_optional(self):
        """stdin_input 是可选的"""
        from aacode.utils.tool_schemas import RUN_SHELL_SCHEMA
        stdin_param = next(p for p in RUN_SHELL_SCHEMA.parameters if p.name == "stdin_input")
        assert stdin_param.required is False

    def test_add_todo_schema_mentions_todo_id_in_returns(self):
        """add_todo_item 的 returns 描述提到 todo_id"""
        from aacode.utils.tool_schemas import ADD_TODO_ITEM_SCHEMA
        assert "todo_id" in ADD_TODO_ITEM_SCHEMA.returns

    def test_schema_normalize_params_stdin(self):
        """stdin_input 的别名 'input' 和 'stdin' 能正确映射"""
        from aacode.utils.tool_schemas import RUN_SHELL_SCHEMA
        # 模型可能用 "input" 作为参数名
        normalized = RUN_SHELL_SCHEMA.normalize_params({"command": "ls", "input": "hello\n"})
        assert "stdin_input" in normalized
        assert normalized["stdin_input"] == "hello\n"

    def test_schema_normalize_params_todo_id_alias(self):
        """mark_todo_completed 的 'id' 别名能正确映射"""
        from aacode.utils.tool_schemas import MARK_TODO_COMPLETED_SCHEMA
        normalized = MARK_TODO_COMPLETED_SCHEMA.normalize_params({"id": "t1"})
        assert "todo_id" in normalized
        assert normalized["todo_id"] == "t1"


# ==================== 7. Web Tools Cleanup ====================

class TestWebToolsCleanup:
    """web_tools cleanup 不崩溃"""

    @pytest.mark.asyncio
    async def test_cleanup_without_session(self):
        """没有 session 时 cleanup 不崩溃"""
        from aacode.tools.web_tools import WebTools
        wt = WebTools(Path("/tmp"), MockSafetyGuard())
        await wt.cleanup()  # 不应抛异常

    @pytest.mark.asyncio
    async def test_cleanup_twice(self):
        """连续调用两次 cleanup 不崩溃"""
        from aacode.tools.web_tools import WebTools
        wt = WebTools(Path("/tmp"), MockSafetyGuard())
        await wt.cleanup()
        await wt.cleanup()  # 第二次也不应抛异常

    def test_del_does_not_raise(self):
        """__del__ 不抛异常"""
        from aacode.tools.web_tools import WebTools
        wt = WebTools(Path("/tmp"), MockSafetyGuard())
        wt.__del__()  # 应该是 pass，不抛异常


# ==================== 8. Prompts 完整性 ====================

class TestPromptsIntegrity:
    """确保 prompts.py 的改动不破坏格式"""

    def test_prompts_import(self):
        """prompts 模块能正常导入"""
        from aacode.core.prompts import SYSTEM_PROMPT_FOR_MAIN_AGENT
        assert isinstance(SYSTEM_PROMPT_FOR_MAIN_AGENT, str)
        assert len(SYSTEM_PROMPT_FOR_MAIN_AGENT) > 100

    def test_prompts_has_todo_id_guidance(self):
        """system prompt 包含 todo_id 使用指引"""
        from aacode.core.prompts import SYSTEM_PROMPT_FOR_MAIN_AGENT
        assert "todo_id" in SYSTEM_PROMPT_FOR_MAIN_AGENT
        assert "mark_todo_completed" in SYSTEM_PROMPT_FOR_MAIN_AGENT

    def test_prompts_no_execution_record_in_tool_list(self):
        """工具列表中不再有 add_execution_record"""
        from aacode.core.prompts import SYSTEM_PROMPT_FOR_MAIN_AGENT
        # 在工具列表区域不应出现 add_execution_record
        tool_section_start = SYSTEM_PROMPT_FOR_MAIN_AGENT.find("To-Do List工具")
        tool_section_end = SYSTEM_PROMPT_FOR_MAIN_AGENT.find("Skills", tool_section_start)
        tool_section = SYSTEM_PROMPT_FOR_MAIN_AGENT[tool_section_start:tool_section_end]
        assert "add_execution_record" not in tool_section
