# -*- coding: utf-8 -*-
"""
回归测试：所有 LLM-facing 工具的参数类型守卫。

验证每个工具在 LLM 把参数序列化为 JSON 字符串时不崩溃、不静默损坏数据。
"""
import json
import os
import tempfile
from unittest.mock import patch, MagicMock

import pytest

from abyss.tools.file_read import FileReadTool
from abyss.tools.file_write import FileWriteTool
from abyss.tools.knowledge_save import KnowledgeSaveTool
from abyss.tools.shell_exec import ShellExecTool
from abyss.tools.spawn_subagents import SpawnSubagentsTool
from abyss.tools.web_fetch import WebFetchTool


# ---------- spawn_subagents ----------

class TestSpawnSubagentsTasksCoercion:
    """验证 tasks 参数被序列化为 JSON 字符串时不崩溃。"""

    def _tool(self):
        tool = SpawnSubagentsTool(cfg=MagicMock(), working_dir=".")
        # 不实际启动 runner
        tool._runner = MagicMock()
        tool._runner.run_parallel.return_value = ["ok"]
        return tool

    def test_tasks_as_json_string(self):
        """LLM 传 tasks='[{"task":"x"}]' 应正常工作。"""
        tool = self._tool()
        tasks_str = json.dumps([{"task": "do something"}])

        out = tool.execute(tasks=tasks_str)

        assert out["success"] is True
        tool._runner.run_parallel.assert_called_once()
        called_tasks = tool._runner.run_parallel.call_args[0][0]
        assert called_tasks == [{"task": "do something"}]

    def test_tasks_as_invalid_string_returns_empty_error(self):
        tool = self._tool()

        out = tool.execute(tasks="not json")

        assert out["success"] is False
        assert "空" in out["error"]

    def test_tasks_as_none_returns_error(self):
        tool = self._tool()

        out = tool.execute(tasks=None)

        assert out["success"] is False


# ---------- knowledge_save ----------

class TestKnowledgeSaveTagsCoercion:
    """验证 tags 参数被序列化为 JSON 字符串时不污染 frontmatter。"""

    def _tool(self, tmp_path):
        return KnowledgeSaveTool(working_dir=str(tmp_path))

    def test_tags_as_json_string_written_correctly(self, tmp_path):
        """LLM 传 tags='["a","b"]' 应正确写入 frontmatter，而非迭代字符。"""
        tool = self._tool(tmp_path)
        tags_str = json.dumps(["python", "bugfix"])

        out = tool.execute(
            name="test-knowledge",
            level="project",
            description="测试 tags 字符串",
            content="正文",
            category="experience",
            tags=tags_str,
        )

        assert out["success"] is True
        content = open(out["path"], "r", encoding="utf-8").read()
        # frontmatter 应正确写入两个 tag，而非字符迭代
        assert "python" in content
        assert "bugfix" in content
        # 不应出现 [, ], " 等乱码字符混入
        frontmatter_line = [l for l in content.split("\n") if l.startswith("tags:")][0]
        assert "p" in frontmatter_line  # python
        # 不应该是字符迭代：[, ", p, ", ...
        assert not frontmatter_line.startswith("tags: [p, \"") or "y" in frontmatter_line

    def test_tags_as_invalid_string_returns_empty(self, tmp_path):
        tool = self._tool(tmp_path)

        out = tool.execute(
            name="test-knowledge-2",
            level="project",
            description="测试",
            content="正文",
            tags="not json",
        )

        assert out["success"] is True
        content = open(out["path"], "r", encoding="utf-8").read()
        frontmatter_line = [l for l in content.split("\n") if l.startswith("tags:")][0]
        assert frontmatter_line.strip() == "tags: []"


# ---------- file_read ----------

class TestFileReadOffsetLimitCoercion:
    """验证 offset/limit 被传为字符串时不抛 TypeError。"""

    def test_offset_as_string(self, tmp_path):
        f = tmp_path / "sample.txt"
        f.write_text("line1\nline2\nline3\n", encoding="utf-8")
        tool = FileReadTool()

        out = tool.execute(path=str(f), offset="2", limit="1")

        assert out["success"] is True
        assert "line2" in out["content"]
        assert "line1" not in out["content"]
        assert "line3" not in out["content"]

    def test_offset_as_invalid_string_uses_default(self, tmp_path):
        f = tmp_path / "sample.txt"
        f.write_text("line1\nline2\n", encoding="utf-8")
        tool = FileReadTool()

        # 非法字符串 → 默认值 1
        out = tool.execute(path=str(f), offset="abc")

        assert out["success"] is True
        assert "line1" in out["content"]


# ---------- file_write ----------

class TestFileWriteAppendCoercion:
    """验证 append='false' 字符串不被误判为 truthy。"""

    def test_append_string_false_does_not_append(self, tmp_path):
        f = tmp_path / "out.txt"
        f.write_text("original\n", encoding="utf-8")
        tool = FileWriteTool()

        # LLM 传 "false" 字符串 — 若直接 `if append` 会当 True
        out = tool.execute(
            path=str(f),
            content="new content",
            append="false",
        )

        assert out["success"] is True
        assert out["mode"] == "write"
        content = f.read_text(encoding="utf-8")
        assert "new content" in content
        # 不应保留原内容
        assert "original" not in content

    def test_append_string_true_appends(self, tmp_path):
        f = tmp_path / "out.txt"
        f.write_text("original\n", encoding="utf-8")
        tool = FileWriteTool()

        out = tool.execute(
            path=str(f),
            content="appended",
            append="true",
        )

        assert out["success"] is True
        assert out["mode"] == "append"
        content = f.read_text(encoding="utf-8")
        assert "original" in content
        assert "appended" in content


# ---------- shell_exec ----------

class TestShellExecTimeoutCoercion:
    """验证 timeout 被传为字符串时不抛 TypeError。"""

    def test_timeout_as_string_executes(self):
        tool = ShellExecTool()

        # 短超时字符串 — 验证 _run_once 内 proc.wait(timeout=timeout) 不抛 TypeError
        out = tool.execute(command="echo hello", timeout="10")

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


# ---------- web_fetch ----------

class TestWebFetchMaxCharsCoercion:
    """验证 max_chars 被传为字符串时不抛 TypeError。"""

    def test_max_chars_as_string_truncates(self):
        tool = WebFetchTool()

        # mock httpx 避免真实网络
        mock_response = MagicMock()
        mock_response.text = "<html>" + ("x" * 1000) + "</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="50")

        assert out["success"] is True
        assert len(out["content"]) <= 100  # 50 + 截断后缀
        assert "截断" in out["content"]
