"""子智能体 Python 重写回归测试（mock 模型）。"""

from __future__ import annotations

import asyncio
import json
import os
import shutil
import tempfile
import time
from pathlib import Path
from typing import Any, Dict, List

import sys
# 测试脚本在 test/ 目录下，需要把项目根目录加入路径
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

# 把子智能体运行态数据重定向到临时目录，避免写入受保护的 ~/.agents 路径
_test_tmp = Path(tempfile.mkdtemp(prefix="sub_agent_cfg_"))
os.environ["SUB_AGENT_TASKS_BASE_DIR"] = str(_test_tmp / "tasks")
os.environ["SUB_AGENT_STATE_FILE"] = str(_test_tmp / "state.json")
os.environ["SUB_AGENT_PROJECT_RESULTS_DIR"] = str(_test_tmp / "results")

from modules.sub_agent import SubAgentManager
from utils.api_client import DeepSeekClient


class FakeTerminal:
    """模拟 WebTerminal，只实现 handle_tool_call。"""

    def __init__(self, project_path: Path):
        self.project_path = project_path

    async def handle_tool_call(self, name: str, arguments: Dict[str, Any]) -> str:
        if name == "write_file":
            path = self.project_path / arguments["file_path"]
            path.parent.mkdir(parents=True, exist_ok=True)
            mode = "a" if arguments.get("append") else "w"
            with open(path, mode, encoding="utf-8") as f:
                f.write(arguments["content"])
            return json.dumps({"success": True, "message": "写入成功", "path": str(path)})

        if name == "edit_file":
            path = self.project_path / arguments["file_path"]
            content = path.read_text(encoding="utf-8")
            count = 0
            for repl in arguments.get("replacements", []):
                old = repl["old_string"]
                new = repl["new_string"]
                if repl.get("replace_all"):
                    n = content.count(old)
                    content = content.replace(old, new)
                    count += n
                else:
                    content = content.replace(old, new, 1)
                    count += 1
            path.write_text(content, encoding="utf-8")
            return json.dumps({"success": True, "path": str(path), "replacements": count})

        if name == "read_file":
            path = self.project_path / arguments["path"]
            if not path.exists():
                return json.dumps({"success": False, "error": f"文件不存在: {path}"})
            text = path.read_text(encoding="utf-8")
            return json.dumps({"success": True, "type": "read", "content": text})

        if name == "run_command":
            import subprocess
            cmd = arguments["command"]
            timeout = arguments.get("timeout", 30)
            cwd = arguments.get("working_dir")
            if cwd:
                cwd = self.project_path / cwd
            # mock 中不真正执行长时间 sleep，避免阻塞事件循环
            if cmd.strip().startswith("sleep "):
                return json.dumps({"success": True, "output": f"mock: {cmd}"})
            try:
                proc = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True, timeout=timeout)
                output = (proc.stdout or "") + (proc.stderr or "")
                return json.dumps({"success": proc.returncode == 0, "output": output})
            except Exception as exc:
                return json.dumps({"success": False, "output": str(exc)})

        return json.dumps({"success": False, "error": f"未实现的工具: {name}"})


class FakeModel:
    """按脚本驱动 DeepSeekClient.chat 的返回。"""

    def __init__(self, responses: List[List[Dict[str, Any]]]):
        self.responses = responses
        self.call_index = 0

    async def chat(self, messages, *, tools=None, stream=True, **kwargs):
        if self.call_index >= len(self.responses):
            chunks = [{"choices": [{"delta": {"content": ""}}]}]
        else:
            chunks = self.responses[self.call_index]
        self.call_index += 1
        for chunk in chunks:
            yield chunk


def make_tool_chunk(tool_calls: List[Dict[str, Any]]) -> Dict[str, Any]:
    return {
        "choices": [{
            "delta": {
                "tool_calls": [
                    {"index": i, "id": f"call_{i}", "function": {"name": tc["name"], "arguments": tc["args"]}}
                    for i, tc in enumerate(tool_calls)
                ]
            }
        }]
    }


def make_finish_chunk() -> Dict[str, Any]:
    return {
        "choices": [{"delta": {"content": ""}}]
    }


def install_fake_model(responses: List[List[Dict[str, Any]]]):
    fake = FakeModel(responses)
    original = DeepSeekClient.chat
    DeepSeekClient.chat = fake.chat
    return fake, original


def restore_model(original):
    DeepSeekClient.chat = original


def test_write_file_and_edit():
    """测试 write_file 与 edit_file（多处替换 + replace_all）。"""
    tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
    try:
        manager = SubAgentManager(str(tmp), str(tmp / "data"))
        terminal = FakeTerminal(tmp)
        manager.set_terminal(terminal)

        # 模型调用序列：先 write_file，再 edit_file 多处替换，最后 finish
        responses = [
            [make_tool_chunk([{
                "name": "write_file",
                "args": json.dumps({"file_path": "demo.txt", "content": "aaa bbb aaa ccc\n"})
            }])],
            [make_tool_chunk([{
                "name": "edit_file",
                "args": json.dumps({
                    "file_path": "demo.txt",
                    "replacements": [
                        {"old_string": "bbb", "new_string": "BBB"},
                        {"old_string": "aaa", "new_string": "AAA", "replace_all": True},
                    ]
                })
            }])],
            [make_tool_chunk([{
                "name": "finish_task",
                "args": json.dumps({"success": True, "summary": "已完成写入与编辑测试"})
            }])],
        ]
        _, original = install_fake_model(responses)
        try:
            result = manager.create_sub_agent(
                agent_id=1,
                summary="写入编辑测试",
                task="测试 write_file 与 edit_file",
                deliverables_dir="deliverables",
                timeout_seconds=30,
                conversation_id="conv-test-1",
                thinking_mode="fast",
            )
            assert result["success"], result
            task_id = result["task_id"]
            final = manager.wait_for_completion(task_id=task_id, timeout_seconds=10)
            assert final["status"] == "completed", final

            content = (tmp / "demo.txt").read_text(encoding="utf-8")
            assert content == "AAA BBB AAA ccc\n", f"内容不符: {content!r}"
            assert (tmp / "deliverables").exists()
        finally:
            restore_model(original)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def test_search_workspace():
    """测试 search_workspace 走 run_command rg 回退。"""
    tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
    try:
        manager = SubAgentManager(str(tmp), str(tmp / "data"))
        terminal = FakeTerminal(tmp)
        manager.set_terminal(terminal)

        (tmp / "findme.txt").write_text("hello world unique string", encoding="utf-8")

        async def fake_search(project_path, terminal_, arguments):
            query = arguments["query"]
            if arguments.get("mode") == "file":
                matches = [p.name for p in project_path.rglob("*") if p.is_file() and query in p.name]
                return {"success": True, "mode": "file", "matches": matches}
            matches = []
            for p in project_path.rglob("*.txt"):
                text = p.read_text(encoding="utf-8")
                if query in text:
                    matches.append({"file": str(p.relative_to(project_path)), "matches": [{"line": 1, "snippet": text}]})
            return {"success": True, "mode": "content", "results": matches}

        import modules.sub_agent.tools as sat
        original_search = sat.handle_search_workspace
        sat.handle_search_workspace = fake_search

        responses = [
            [make_tool_chunk([{
                "name": "search_workspace",
                "args": json.dumps({"mode": "content", "query": "unique string"})
            }])],
            [make_tool_chunk([{
                "name": "finish_task",
                "args": json.dumps({"success": True, "summary": "搜索完成"})
            }])],
        ]
        _, original_model = install_fake_model(responses)
        try:
            result = manager.create_sub_agent(
                agent_id=2,
                summary="搜索测试",
                task="测试 search_workspace",
                deliverables_dir="deliverables2",
                timeout_seconds=30,
                conversation_id="conv-test-2",
                thinking_mode="fast",
            )
            assert result["success"], result
            final = manager.wait_for_completion(task_id=result["task_id"], timeout_seconds=10)
            assert final["status"] == "completed", final
        finally:
            restore_model(original_model)
            sat.handle_search_workspace = original_search
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def test_read_mediafile():
    """测试 read_mediafile 返回 base64。"""
    tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
    try:
        manager = SubAgentManager(str(tmp), str(tmp / "data"))
        terminal = FakeTerminal(tmp)
        manager.set_terminal(terminal)

        # 创建一个最小 PNG：1x1 透明像素
        png_bytes = bytes.fromhex(
            "89504e470d0a1a0a0000000d49484452"
            "000000010000000108060000001f15c4"
            "890000000d4944415478da6300180000"
            "03010005fe027f0000000049454e44ae"
            "426082"
        )
        (tmp / "pixel.png").write_bytes(png_bytes)

        responses = [
            [make_tool_chunk([{
                "name": "read_mediafile",
                "args": json.dumps({"path": "pixel.png"})
            }])],
            [make_tool_chunk([{
                "name": "finish_task",
                "args": json.dumps({"success": True, "summary": "媒体读取完成"})
            }])],
        ]
        _, original = install_fake_model(responses)
        try:
            result = manager.create_sub_agent(
                agent_id=3,
                summary="媒体测试",
                task="测试 read_mediafile",
                deliverables_dir="deliverables3",
                timeout_seconds=30,
                conversation_id="conv-test-3",
                thinking_mode="fast",
            )
            assert result["success"], result
            final = manager.wait_for_completion(task_id=result["task_id"], timeout_seconds=10)
            assert final["status"] == "completed", final
        finally:
            restore_model(original)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def test_terminate():
    """测试手动终止子智能体。"""
    tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
    try:
        manager = SubAgentManager(str(tmp), str(tmp / "data"))
        terminal = FakeTerminal(tmp)
        manager.set_terminal(terminal)

        # 模型先调用一次 run_command，之后返回空内容，使任务持续运行
        responses = [
            [make_tool_chunk([{
                "name": "run_command",
                "args": json.dumps({"command": "sleep 1000", "timeout": 1200})
            }])],
            [make_finish_chunk()],
            [make_finish_chunk()],
            [make_finish_chunk()],
            [make_finish_chunk()],
        ]
        fake, original = install_fake_model(responses)
        try:
            result = manager.create_sub_agent(
                agent_id=4,
                summary="终止测试",
                task="测试终止功能",
                deliverables_dir="deliverables4",
                timeout_seconds=30,
                conversation_id="conv-test-4",
                thinking_mode="fast",
                run_in_background=True,
            )
            assert result["success"], result
            time.sleep(0.5)
            term_result = manager.terminate_sub_agent(task_id=result["task_id"])
            assert term_result["success"], term_result
            # 通过 task_id 查询最终状态
            task = manager.lookup_task(task_id=result["task_id"])
            assert task is not None
            assert task["status"] == "terminated"
        finally:
            restore_model(original)
    finally:
        shutil.rmtree(tmp, ignore_errors=True)


def run_all():
    print("=" * 60)
    print("SubAgent Python rewrite regression tests")
    print("=" * 60)
    tests = [
        ("write_file + edit_file", test_write_file_and_edit),
        ("search_workspace", test_search_workspace),
        ("read_mediafile", test_read_mediafile),
        ("terminate", test_terminate),
    ]
    passed = 0
    failed = 0
    for name, fn in tests:
        print(f"\nRunning: {name} ...")
        try:
            fn()
            print(f"  ✅ PASSED: {name}")
            passed += 1
        except Exception as exc:
            print(f"  ❌ FAILED: {name} -> {exc}")
            import traceback
            traceback.print_exc()
            failed += 1
    print("\n" + "=" * 60)
    print(f"RESULT: {passed} passed, {failed} failed")
    print("=" * 60)
    return failed == 0


if __name__ == "__main__":
    ok = run_all()
    sys.exit(0 if ok else 1)
