# -*- coding: utf-8 -*-
"""
工具集成测试套件：端到端调用每个工具，验证实际功能正常。

与单元测试不同，这里调用真实的文件系统、真实的命令执行、真实的子进程，
验证工具在真实环境下的行为（不只是 mock）。

覆盖 9 个工具：
1. file_write 写文件 → file_read 读回校验
2. file_edit 编辑文件 → file_read 校验修改
3. shell_exec 执行命令 → 校验 stdout
4. todo_write 写任务 → 验证输出
5. knowledge_save 保存 → knowledge_fetch 读取
6. web_search / web_fetch 用 mock 验证网络层（无真实网络）
7. spawn_subagents 用 mock 验证任务分发
"""
import json
import os
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from abyss.tools.file_edit import FileEditTool
from abyss.tools.file_read import FileReadTool
from abyss.tools.file_write import FileWriteTool
from abyss.tools.knowledge_fetch import KnowledgeFetchTool
from abyss.tools.knowledge_save import KnowledgeSaveTool
from abyss.tools.shell_exec import ShellExecTool
from abyss.tools.spawn_subagents import SpawnSubagentsTool
from abyss.tools.todo_write import TodoWriteTool
from abyss.tools.web_fetch import WebFetchTool
from abyss.tools.web_search import WebSearchTool


# ========== 1. 文件 I/O 工具链 ==========

class TestFileWriteReadRoundTrip:
    """文件写入 → 读取 → 验证内容一致。"""

    def test_write_then_read_returns_same_content(self, tmp_path):
        """写入文件后读取，内容应完全一致。"""
        f = tmp_path / "round.txt"
        content = "第一行\n第二行\n第三行\n"
        writer = FileWriteTool()
        reader = FileReadTool()

        w_out = writer.execute(path=str(f), content=content)
        r_out = reader.execute(path=str(f))

        assert w_out["success"] is True
        assert r_out["success"] is True
        assert r_out["content"] == content

    def test_write_append_mode(self, tmp_path):
        """追加模式应保留原内容。"""
        f = tmp_path / "append.txt"
        writer = FileWriteTool()

        writer.execute(path=str(f), content="原内容\n", append=False)
        writer.execute(path=str(f), content="追加内容\n", append=True)

        content = f.read_text(encoding="utf-8")
        assert "原内容" in content
        assert "追加内容" in content

    def test_read_with_offset_limit(self, tmp_path):
        """offset/limit 应正确切片。"""
        f = tmp_path / "lines.txt"
        f.write_text("L1\nL2\nL3\nL4\nL5\n", encoding="utf-8")
        reader = FileReadTool()

        out = reader.execute(path=str(f), offset=2, limit=2)

        assert out["success"] is True
        assert "L2" in out["content"]
        assert "L3" in out["content"]
        assert "L1" not in out["content"]
        assert "L4" not in out["content"]


class TestFileEditTool:
    """文件编辑工具集成测试。"""

    def test_edit_replaces_old_string(self, tmp_path):
        """old_str → new_str 替换。"""
        f = tmp_path / "edit.txt"
        f.write_text("hello world\n", encoding="utf-8")
        editor = FileEditTool()

        out = editor.execute(
            path=str(f),
            old_str="hello",
            new_str="goodbye",
        )

        assert out["success"] is True
        assert f.read_text(encoding="utf-8") == "goodbye world\n"

    def test_edit_missing_old_string_fails(self, tmp_path):
        """old_str 不存在应失败。"""
        f = tmp_path / "edit.txt"
        f.write_text("hello world\n", encoding="utf-8")
        editor = FileEditTool()

        out = editor.execute(
            path=str(f),
            old_str="nonexistent",
            new_str="whatever",
        )

        assert out["success"] is False


# ========== 2. Shell 工具 ==========

class TestShellExecTool:
    """Shell 执行工具集成测试。"""

    def test_echo_command(self):
        """echo 命令应返回 stdout。"""
        tool = ShellExecTool()

        out = tool.execute(command="echo hello_world_12345")

        assert out["success"] is True
        assert "hello_world_12345" in out["stdout"]

    def test_command_failure(self):
        """失败命令应返回 success=False。"""
        tool = ShellExecTool()

        out = tool.execute(command="exit 1")

        assert out["success"] is False

    def test_unicode_output(self):
        """中文字符应正确处理。"""
        tool = ShellExecTool()

        # PowerShell echo 中文
        out = tool.execute(command="Write-Output '你好世界'")

        assert out["success"] is True
        # 不同 shell 编码可能有差异，至少不应崩溃


# ========== 3. Todo 工具 ==========

class TestTodoWriteTool:
    """Todo 工具集成测试。"""

    def test_write_todos_renders_output(self, tmp_path, capsys):
        """写入 todo 应产生终端输出。"""
        tool = TodoWriteTool()

        out = tool.execute(todos=[
            {"id": "1", "content": "任务A", "status": "pending", "priority": "high"},
            {"id": "2", "content": "任务B", "status": "in_progress", "priority": "medium"},
        ], merge=False)

        assert out["success"] is True

    def test_empty_todos_fails_gracefully(self):
        """空 todo 列表应失败但不崩溃。"""
        tool = TodoWriteTool()

        out = tool.execute(todos=[], merge=False)

        assert out["success"] is False

    def test_json_string_todos(self):
        """JSON 字符串形式的 todos 应被解析。"""
        tool = TodoWriteTool()
        todos_str = json.dumps([
            {"id": "1", "content": "任务", "status": "pending", "priority": "high"},
        ])

        out = tool.execute(todos=todos_str, merge=False)

        assert out["success"] is True


# ========== 4. 知识库工具 ==========

class TestKnowledgeSaveFetch:
    """知识库保存 + 读取集成测试。"""

    def test_save_then_fetch(self, tmp_path, monkeypatch):
        """保存的知识应能被读回。"""
        monkeypatch.chdir(tmp_path)
        save_tool = KnowledgeSaveTool(working_dir=str(tmp_path))

        # 保存前 mock host（避免依赖全局 _HOST）
        save_tool._host = MagicMock()
        save_tool._host._knowledge_curator = MagicMock()
        save_tool._host._knowledge_curator.update_index = MagicMock()

        save_out = save_tool.execute(
            name="test-knowledge-001",
            level="project",
            description="测试知识",
            content="这是知识正文",
            category="experience",
            tags=["test", "验证"],
        )

        assert save_out["success"] is True
        assert "path" in save_out

        # 文件应实际写入
        saved_file = Path(save_out["path"])
        assert saved_file.exists()

        # 内容应包含正文
        content = saved_file.read_text(encoding="utf-8")
        assert "这是知识正文" in content
        assert "test" in content
        assert "验证" in content


# ========== 5. Web 工具（mock 网络层）==========

class TestWebToolsWithMock:
    """Web 工具集成测试（mock 网络，无真实请求）。"""

    def test_web_fetch_with_mock_httpx(self):
        """web_fetch 应正确解析 HTML。"""
        tool = WebFetchTool()

        mock_response = MagicMock()
        mock_response.text = "<html><body><h1>标题</h1><p>正文内容</p></body></html>"
        mock_response.url = "http://example.com"
        mock_response.status_code = 200
        mock_response.raise_for_status.return_value = None

        mock_client = MagicMock()
        mock_client.__enter__.return_value = mock_client
        mock_client.__exit__.return_value = None
        mock_client.get.return_value = mock_response

        with patch("httpx.Client", return_value=mock_client):
            out = tool.execute(url="http://example.com", max_chars=1000)

        assert out["success"] is True
        assert "标题" in out["content"]
        assert "正文内容" in out["content"]


# ========== 6. Subagent 工具 ==========

class TestSpawnSubagentsTool:
    """子 Agent 编排工具集成测试。"""

    def test_spawn_with_mock_runner(self):
        """spawn_subagents 应正确分发任务（mock runner）。"""
        cfg = MagicMock()
        tool = SpawnSubagentsTool(cfg=cfg, working_dir=".")
        tool._runner = MagicMock()
        tool._runner.run_parallel.return_value = [
            "子任务 1 结果",
            "子任务 2 结果",
        ]

        out = tool.execute(tasks=[
            {"task": "做 A"},
            {"task": "做 B"},
        ])

        assert out["success"] is True
        tool._runner.run_parallel.assert_called_once()
        # 返回结构是 results 列表 + summary 字符串
        assert "子任务 1 结果" in out["results"][0]
        assert "子任务 2 结果" in out["results"][1]
        assert "共 2 个子任务" in out["summary"]

    def test_spawn_empty_tasks_fails(self):
        """空任务列表应失败。"""
        cfg = MagicMock()
        tool = SpawnSubagentsTool(cfg=cfg, working_dir=".")
        tool._runner = MagicMock()

        out = tool.execute(tasks=[])

        assert out["success"] is False


# ========== 7. 工具注册完整性 ==========

class TestToolRegistryIntegrity:
    """验证所有工具都正确注册到 REGISTRY。"""

    def test_all_builtin_tools_registered(self):
        """默认 7 个内置工具在模块加载时注册（不含运行时依赖 cfg/host 的 3 个）。"""
        import abyss.main as main_mod

        names = set(main_mod.REGISTRY._tools.keys())
        # 这 7 个是模块级注册的，不依赖 cfg/host
        expected_module_level = {
            "file_read", "file_write", "file_edit",
            "shell_exec",
            "todo_write",
            "web_search", "web_fetch",
        }
        missing = expected_module_level - names
        assert not missing, f"未注册的工具: {missing}"

    def test_all_tools_have_schema(self):
        """每个工具都应能产出 OpenAI schema（嵌套在 function 字段下）。"""
        import abyss.main as main_mod

        schemas = main_mod.REGISTRY.to_openai_schemas()

        # OpenAI schema 结构: {"type":"function", "function": {"name":..., "parameters":...}}
        assert len(schemas) >= 7
        for s in schemas:
            assert s.get("type") == "function"
            fn = s.get("function", {})
            assert "name" in fn
            assert "parameters" in fn
            assert fn["name"], "工具名不能为空"

    def test_runtime_tools_can_register(self):
        """验证 3 个运行时工具（spawn_subagents + knowledge_*）可以单独注册。"""
        import abyss.main as main_mod
        from abyss.tools.spawn_subagents import SpawnSubagentsTool
        from abyss.tools.knowledge_save import KnowledgeSaveTool
        from abyss.tools.knowledge_fetch import KnowledgeFetchTool
        from unittest.mock import MagicMock

        reg = main_mod.REGISTRY
        before = len(reg._tools)

        # 注册这 3 个工具（模拟 run_with_pt 启动流程）
        reg.register(
            SpawnSubagentsTool(cfg=MagicMock(), working_dir="."),
            source="test",
        )
        reg.register(
            KnowledgeFetchTool(host=MagicMock()),
            source="test",
        )
        reg.register(
            KnowledgeSaveTool(working_dir="."),
            source="test",
        )

        # 应能成功注册
        after_names = set(reg._tools.keys())
        assert "spawn_subagents" in after_names
        assert "knowledge_save" in after_names
        assert "knowledge_fetch" in after_names


# ========== 8. 端到端：完整工作流 ==========

class TestEndToEndWorkflow:
    """模拟用户真实工作流：写文件→读文件→编辑→执行 shell→保存知识。"""

    def test_full_workflow(self, tmp_path, monkeypatch):
        """完整工作流应每步都成功。"""
        monkeypatch.chdir(tmp_path)

        # Step 1: 写文件
        f = tmp_path / "workflow.txt"
        FileWriteTool().execute(path=str(f), content="原始内容\n")

        # Step 2: 读文件验证
        r = FileReadTool().execute(path=str(f))
        assert r["success"]
        assert "原始内容" in r["content"]

        # Step 3: 编辑文件
        FileEditTool().execute(
            path=str(f),
            old_str="原始",
            new_str="修改后",
        )

        # Step 4: 读回验证编辑
        r = FileReadTool().execute(path=str(f))
        assert "修改后内容" in r["content"]

        # Step 5: shell 执行
        s = ShellExecTool().execute(command="echo workflow_done")
        assert s["success"]

        # Step 6: 保存知识
        save_tool = KnowledgeSaveTool(working_dir=str(tmp_path))
        save_tool._host = MagicMock()
        save_tool._host._knowledge_curator = MagicMock()
        k = save_tool.execute(
            name="workflow-knowledge",
            level="project",
            description="工作流测试",
            content="完成端到端测试",
        )
        assert k["success"]

    def test_todo_workflow_with_json_string(self):
        """Todo 工作流：JSON 字符串 → 显示。"""
        tool = TodoWriteTool()
        todos_str = json.dumps([
            {"id": "1", "content": "第一步", "status": "completed", "priority": "high"},
            {"id": "2", "content": "第二步", "status": "in_progress", "priority": "medium"},
            {"id": "3", "content": "第三步", "status": "pending", "priority": "low"},
        ], ensure_ascii=False)

        out = tool.execute(todos=todos_str, merge=False)

        assert out["success"] is True
