import importlib.util
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

try:
    import yaml
except ImportError:
    yaml = None

SCRIPT = Path(__file__).parents[1] / "scripts" / "generate.py"
REPO_ROOT = Path(__file__).resolve().parents[3]


def load_script():
    spec = importlib.util.spec_from_file_location("tsl_codegen_generate", SCRIPT)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module


class DocGenCliTest(unittest.TestCase):
    def setUp(self):
        self.temp_dir = tempfile.TemporaryDirectory()
        self.root = Path(self.temp_dir.name)
        self.input = self.root / "entry.json"
        self.input.write_text(
            json.dumps(
                {
                    "module": "项目 / 示例",
                    "path": "base/my_functions",
                    "declarations": [
                        {
                            "kind": "function",
                            "name": "demo",
                            "signature": "demo()",
                            "desc": "示例函数。",
                            "returns": "nil",
                        }
                    ],
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )

    def tearDown(self):
        self.temp_dir.cleanup()

    def run_cli(self, *args, env=None):
        return subprocess.run(
            [sys.executable, str(SCRIPT), "--file", str(self.input), *args],
            capture_output=True,
            text=True,
            encoding="utf-8",
            cwd=self.root,
            env=env,
        )

    def run_raw_cli(self, *args, env=None):
        return subprocess.run(
            [sys.executable, str(SCRIPT), *map(str, args)],
            capture_output=True,
            text=True,
            encoding="utf-8",
            cwd=self.root,
            env=env,
        )

    def write_input(self, data):
        self.input.write_text(
            json.dumps(data, ensure_ascii=False),
            encoding="utf-8",
        )

    def write_recording(self, path, relative, module="项目 / 批量"):
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(
            json.dumps(
                {
                    "module": module,
                    "path": relative,
                    "declarations": [
                        {
                            "kind": "function",
                            "name": "demo",
                            "signature": "demo()",
                            "desc": "示例函数。",
                            "returns": "nil",
                        }
                    ],
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )
        return path

    def generated(self, relative, scope="project"):
        return (
            self.root
            / "skills"
            / "tsl-api-reference"
            / "references"
            / "codegen"
            / scope
            / f"{relative}.md"
        )

    def write_class_member_input(self, member, relative):
        self.write_input(
            {
                "module": "示例 / 类",
                "path": relative,
                "declarations": [
                    {
                        "kind": "class",
                        "name": "StrictClass",
                        "desc": "严格类。",
                        "members": [member],
                    }
                ],
            }
        )

    def assert_rejected_without_overwrite(self, relative, expected):
        output = self.generated(relative)
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text("原内容\n", encoding="utf-8")

        result = self.run_cli()

        self.assertEqual(1, result.returncode)
        self.assertIn(expected, result.stderr)
        self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))

    def test_default_scope_writes_configured_path_under_project(self):
        result = self.run_cli()
        output = (
            self.root
            / "skills"
            / "tsl-api-reference"
            / "references"
            / "codegen"
            / "project"
            / "base"
            / "my_functions.md"
        )
        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertTrue(output.is_file())
        self.assertTrue(
            output.read_text(encoding="utf-8").startswith("# 项目 / 示例\n")
        )

    def test_file_option_generates_one_recording(self):
        result = self.run_raw_cli("--file", self.input)

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertTrue(self.generated("base/my_functions").is_file())

    def test_positional_input_is_rejected(self):
        result = self.run_raw_cli(self.input)

        self.assertEqual(2, result.returncode)
        self.assertIn(f"无法识别的参数： {self.input}", result.stderr)
        self.assertFalse(self.generated("base/my_functions").exists())

    def test_help_only_documents_named_input_options(self):
        result = self.run_raw_cli("--help")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("--file INPUT_FILE", result.stdout)
        self.assertIn("--dir INPUT_DIR", result.stdout)
        self.assertNotIn("positional arguments:", result.stdout)
        self.assertNotIn("已废弃", result.stdout)

    def test_invalid_format_error_is_fully_chinese(self):
        result = self.run_raw_cli("--file", self.input, "--format", "xml")

        self.assertEqual(2, result.returncode)
        self.assertIn(
            "参数 --format: 取值无效：'xml'（可选值：'json', 'yaml'）",
            result.stderr,
        )
        self.assertNotIn("choose from", result.stderr)

    def test_exactly_one_input_mode_is_required(self):
        cases = (
            (),
            ("--file", self.input, "--dir", self.root),
        )
        for args in cases:
            with self.subTest(args=args):
                result = self.run_raw_cli(*args)

                self.assertEqual(2, result.returncode)
                self.assertIn(
                    "必须且只能指定一种输入方式：--file 或 --dir",
                    result.stderr,
                )

    def test_dir_processes_only_direct_json_files_in_name_order(self):
        input_dir = self.root / "recordings"
        second = self.write_recording(input_dir / "b.json", "base/b")
        first = self.write_recording(input_dir / "a.json", "base/a")
        self.write_recording(input_dir / "nested" / "c.json", "base/c")
        (input_dir / "notes.txt").write_text("ignore", encoding="utf-8")

        result = self.run_raw_cli("--dir", input_dir, "--format", "json")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertTrue(self.generated("base/a").is_file())
        self.assertTrue(self.generated("base/b").is_file())
        self.assertFalse(self.generated("base/c").exists())
        self.assertLess(
            result.stderr.index(str(first)), result.stderr.index(str(second))
        )

    def test_dir_format_filters_out_other_supported_extensions(self):
        input_dir = self.root / "recordings"
        self.write_recording(input_dir / "only.json", "base/only")
        (input_dir / "ignored.yaml").write_text(": invalid", encoding="utf-8")

        result = self.run_raw_cli("--dir", input_dir, "--format", "json")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertTrue(self.generated("base/only").is_file())
        self.assertNotIn("ignored.yaml", result.stderr)

    @unittest.skipUnless(yaml is not None, "pyyaml is not installed")
    def test_dir_without_format_processes_json_yaml_and_yml(self):
        input_dir = self.root / "recordings"
        self.write_recording(input_dir / "first.json", "base/first")
        for name, relative in (
            ("second.yaml", "base/second"),
            ("third.yml", "base/third"),
        ):
            data = {
                "module": "项目 / 批量",
                "path": relative,
                "declarations": [
                    {
                        "kind": "function",
                        "name": "demo",
                        "signature": "demo()",
                        "desc": "示例函数。",
                        "returns": "nil",
                    }
                ],
            }
            (input_dir / name).write_text(
                yaml.safe_dump(data, allow_unicode=True, sort_keys=False),
                encoding="utf-8",
            )

        result = self.run_raw_cli("--dir", input_dir)

        self.assertEqual(0, result.returncode, result.stderr)
        for relative in ("base/first", "base/second", "base/third"):
            self.assertTrue(self.generated(relative).is_file())

    def test_dir_without_matching_files_reports_directory_and_extensions(self):
        input_dir = self.root / "recordings"
        input_dir.mkdir()
        (input_dir / "notes.txt").write_text("ignore", encoding="utf-8")
        self.write_recording(input_dir / "nested" / "hidden.json", "base/hidden")

        result = self.run_raw_cli("--dir", input_dir)

        self.assertEqual(1, result.returncode)
        self.assertIn(str(input_dir), result.stderr)
        self.assertIn("未找到符合条件的直属录入文件", result.stderr)
        self.assertIn(".json、.yaml、.yml", result.stderr)

    def test_dir_scan_error_reports_directory_and_specific_reason(self):
        module = load_script()
        input_dir = self.root / "recordings"
        input_dir.mkdir()

        with mock.patch.object(
            module.Path, "iterdir", side_effect=PermissionError("拒绝访问")
        ):
            with self.assertRaises(module.GenerationError) as caught:
                module.gather_directory_inputs(input_dir, None)

        message = str(caught.exception)
        self.assertIn(f"读取输入目录失败：{input_dir}", message)
        self.assertIn("拒绝访问", message)

    def test_dir_collects_all_file_errors_before_writing_any_markdown(self):
        input_dir = self.root / "recordings"
        self.write_recording(input_dir / "a-valid.json", "base/valid")
        (input_dir / "b-invalid-json.json").write_text("{", encoding="utf-8")
        (input_dir / "c-invalid-data.json").write_text(
            json.dumps(
                {
                    "module": "项目 / 错误",
                    "path": "base/invalid_data",
                    "declarations": [],
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )
        existing = self.generated("base/valid")
        existing.parent.mkdir(parents=True, exist_ok=True)
        existing.write_text("原内容\n", encoding="utf-8")

        result = self.run_raw_cli("--dir", input_dir, "--format", "json")

        self.assertEqual(1, result.returncode)
        self.assertIn(str(input_dir / "b-invalid-json.json"), result.stderr)
        self.assertIn("JSON 格式错误", result.stderr)
        self.assertIn(str(input_dir / "c-invalid-data.json"), result.stderr)
        self.assertIn("录入数据校验失败", result.stderr)
        self.assertIn("declarations 必须是非空列表", result.stderr)
        self.assertIn("批量生成已中止", result.stderr)
        self.assertIn("未写入任何 Markdown 文件", result.stderr)
        self.assertEqual("原内容\n", existing.read_text(encoding="utf-8"))

    def test_dir_rejects_output_collisions_before_writing(self):
        input_dir = self.root / "recordings"
        first = self.write_recording(input_dir / "a.json", "base/collision")
        second = self.write_recording(input_dir / "b.json", "base/collision")
        output = self.generated("base/collision")

        result = self.run_raw_cli("--dir", input_dir, "--format", "json")

        self.assertEqual(1, result.returncode)
        self.assertIn("输出目标冲突", result.stderr)
        self.assertIn(str(first), result.stderr)
        self.assertIn(str(second), result.stderr)
        self.assertIn(str(output), result.stderr)
        self.assertIn("未写入任何 Markdown 文件", result.stderr)
        self.assertFalse(output.exists())

    def test_single_file_json_error_reports_file_and_position_in_chinese(self):
        self.input.write_text("{", encoding="utf-8")

        result = self.run_raw_cli("--file", self.input)

        self.assertEqual(1, result.returncode)
        self.assertIn(f"错误：{self.input}：JSON 格式错误", result.stderr)
        self.assertIn("第 1 行", result.stderr)
        self.assertIn("第 2 列", result.stderr)

    def test_windows_path_separator_writes_nested_markdown(self):
        data = json.loads(self.input.read_text(encoding="utf-8"))
        data["path"] = r"base\windows_example"
        self.write_input(data)

        result = self.run_raw_cli("--file", self.input)

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertTrue(self.generated("base/windows_example").is_file())

    def test_invalid_paths_report_input_file_and_specific_reason(self):
        cases = (
            (r"C:\base\example", "不允许 Windows 盘符路径"),
            (r"\\server\share\example", "不允许 UNC 路径"),
            (r"\base\example", "不能以斜杠或反斜杠开头"),
            (r"base\..\example", "不允许跳转到父目录"),
            (r"base\example.md", "不能包含 .md 等文件扩展名"),
            (".", "必须指向具体文档"),
        )
        for path, reason in cases:
            with self.subTest(path=path):
                data = json.loads(self.input.read_text(encoding="utf-8"))
                data["path"] = path
                self.write_input(data)

                result = self.run_raw_cli("--file", self.input)

                self.assertEqual(1, result.returncode, result.stderr)
                self.assertIn(
                    f"错误：{self.input}：输出路径无效：",
                    result.stderr,
                )
                self.assertIn(reason, result.stderr)
                self.assertNotIn("Traceback", result.stderr)

    def test_custom_scope_changes_first_destination_directory(self):
        result = self.run_cli("--scope", "my-project")
        output = (
            self.root
            / "skills"
            / "tsl-api-reference"
            / "references"
            / "codegen"
            / "my-project"
            / "base"
            / "my_functions.md"
        )
        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertTrue(output.is_file())

    def test_root_writes_under_explicit_project_root(self):
        output_root = self.root / "explicit-root"

        result = self.run_cli("--root", str(output_root))

        output = (
            output_root
            / "skills"
            / "tsl-api-reference"
            / "references"
            / "codegen"
            / "project"
            / "base"
            / "my_functions.md"
        )
        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertTrue(output.is_file())
        self.assertFalse(self.generated("base/my_functions").exists())

    def test_relative_root_is_resolved_from_current_working_directory(self):
        result = self.run_cli("--root", "explicit-root")

        output = (
            self.root
            / "explicit-root"
            / "skills"
            / "tsl-api-reference"
            / "references"
            / "codegen"
            / "project"
            / "base"
            / "my_functions.md"
        )
        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertTrue(output.is_file())

    def test_help_documents_root_option(self):
        result = self.run_raw_cli("--help")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("--root ROOT_DIR", result.stdout)
        self.assertIn("包含 skills 目录的项目根目录", result.stdout)

    def test_output_option_is_rejected(self):
        result = self.run_cli("--output", str(self.root / "out.md"))
        self.assertNotEqual(result.returncode, 0)
        self.assertIn("无法识别的参数： --output", result.stderr)

    def test_generated_markdown_is_formatted_by_repo_prettier(self):
        self.input.write_text(
            json.dumps(
                {
                    "module": "项目 / 示例",
                    "path": "base/formatted",
                    "declarations": [
                        {
                            "kind": "function",
                            "name": "demo",
                            "signature": "demo(short, long_name)",
                            "desc": "示例函数。",
                            "params": [
                                {
                                    "name": "short",
                                    "type": "integer",
                                    "desc": "短说明",
                                },
                                {
                                    "name": "long_name",
                                    "type": "very_long_type",
                                    "desc": "这是较长的说明",
                                },
                            ],
                            "returns": "integer",
                        }
                    ],
                },
                ensure_ascii=False,
            ),
            encoding="utf-8",
        )
        output = (
            self.root
            / "skills"
            / "tsl-api-reference"
            / "references"
            / "codegen"
            / "project"
            / "base"
            / "formatted.md"
        )

        result = self.run_cli()
        self.assertEqual(0, result.returncode, result.stderr)
        generated = output.read_text(encoding="utf-8")
        prettier = subprocess.run(
            [
                shutil.which("npx"),
                "--no-install",
                "prettier",
                "--config",
                str(REPO_ROOT / ".prettierrc.json"),
                "--parser",
                "markdown",
            ],
            input=generated,
            capture_output=True,
            text=True,
            encoding="utf-8",
            cwd=REPO_ROOT,
        )

        self.assertEqual(0, prettier.returncode, prettier.stderr)
        self.assertEqual(prettier.stdout, generated)

    def test_missing_prettier_fails_without_writing_markdown(self):
        output = (
            self.root
            / "skills"
            / "tsl-api-reference"
            / "references"
            / "codegen"
            / "project"
            / "base"
            / "my_functions.md"
        )
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text("原内容\n", encoding="utf-8")
        env = os.environ.copy()
        env["PATH"] = ""

        result = self.run_cli(env=env)

        self.assertEqual(1, result.returncode)
        self.assertIn("未找到 Prettier", result.stderr)
        self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))

    def test_prettier_process_failure_preserves_existing_markdown(self):
        output = self.generated("base/my_functions")
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_text("原内容\n", encoding="utf-8")
        bin_dir = self.root / "bin"
        bin_dir.mkdir()
        if os.name == "nt":
            fake_npx = bin_dir / "npx.cmd"
            fake_npx.write_text(
                "@echo off\r\necho formatter-failed 1>&2\r\nexit /b 9\r\n",
                encoding="utf-8",
            )
        else:
            fake_npx = bin_dir / "npx"
            fake_npx.write_text(
                "#!/bin/sh\necho formatter-failed >&2\nexit 9\n",
                encoding="utf-8",
            )
            fake_npx.chmod(0o755)
        env = os.environ.copy()
        env["PATH"] = str(bin_dir)

        result = self.run_cli(env=env)

        self.assertEqual(1, result.returncode)
        self.assertIn("Prettier 格式化失败", result.stderr)
        self.assertIn("formatter-failed", result.stderr)
        self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))

    def test_mixed_declarations_render_typed_h2_in_input_order(self):
        self.write_input(
            {
                "module": "示例 / 混合",
                "path": "base/mixed",
                "declarations": [
                    {
                        "kind": "class",
                        "name": "Widget",
                        "desc": "组件。",
                        "members": [],
                    },
                    {
                        "kind": "function",
                        "name": "OpenWidget",
                        "signature": "OpenWidget()",
                        "desc": "打开组件。",
                        "returns": "Widget",
                    },
                    {
                        "kind": "unit",
                        "name": "WidgetRuntime",
                        "desc": "运行时接口。",
                        "members": [],
                    },
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/mixed").read_text(encoding="utf-8")
        markers = [
            "## `Widget`\n\n声明：class\n\n组件。",
            "## `OpenWidget()`\n\n声明：function\n\n打开组件。",
            "## `WidgetRuntime`\n\n声明：unit\n\n运行时接口。",
        ]
        positions = [text.index(marker) for marker in markers]
        self.assertEqual(sorted(positions), positions)

    def test_class_page_renders_binding_visibility_and_member_order(self):
        self.write_input(
            {
                "module": "示例 / 类",
                "path": "base/widget",
                "declarations": [
                    {
                        "kind": "class",
                        "name": "Widget",
                        "desc": "表示组件。",
                        "tags": ["组件", "示例"],
                        "bases": ["BaseWidget"],
                        "members": [
                            {
                                "kind": "method",
                                "name": "Close",
                                "visibility": "public",
                                "binding": "instance",
                                "signature": "Close()",
                                "desc": "关闭组件。",
                            },
                            {
                                "kind": "method",
                                "name": "Create",
                                "visibility": "protected",
                                "binding": "class",
                                "signature": "Create(name)",
                                "desc": "创建组件。",
                                "params": [
                                    {
                                        "name": "name",
                                        "type": "string",
                                        "desc": "组件名称",
                                    }
                                ],
                                "returns": "Widget",
                                "modifiers": ["overload"],
                            },
                            {
                                "kind": "property",
                                "name": "Title",
                                "visibility": "public",
                                "desc": "组件标题。",
                                "type": "string",
                                "access": "readwrite",
                            },
                            {
                                "kind": "field",
                                "name": "Count",
                                "visibility": "protected",
                                "desc": "组件数量。",
                                "type": "integer",
                                "static": True,
                            },
                            {
                                "kind": "constant",
                                "name": "DefaultName",
                                "visibility": "public",
                                "desc": "默认名称。",
                                "value": "'widget'",
                            },
                        ],
                    }
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/widget").read_text(encoding="utf-8")
        expected = [
            "## `Widget`",
            "声明：class",
            "父类：`BaseWidget`",
            "### `Close()`\n\n声明：function",
            "### `Create(name)`\n\n声明：class function",
            "### `Title`\n\n声明：property",
            "### `Count`\n\n声明：static field",
            "### `DefaultName`\n\n声明：const",
        ]
        positions = [text.index(fragment) for fragment in expected]
        self.assertEqual(sorted(positions), positions)
        self.assertIn("可见性：`protected`", text)
        self.assertIn("修饰符：`overload`", text)
        self.assertIn("访问：read / write", text)
        self.assertNotIn("static function", text)
        self.assertEqual(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "<!-- tags: 组件 示例 -->\n\n"
            "父类：`BaseWidget`\n\n"
            "### `Close()`\n\n"
            "声明：function\n\n"
            "关闭组件。\n\n"
            "可见性：`public`\n\n"
            "### `Create(name)`\n\n"
            "声明：class function\n\n"
            "创建组件。\n\n"
            "可见性：`protected`\n\n"
            "修饰符：`overload`\n\n"
            "| 参数   | 类型   | 说明     |\n"
            "| ------ | ------ | -------- |\n"
            "| `name` | string | 组件名称 |\n\n"
            "返回：Widget\n\n"
            "### `Title`\n\n"
            "声明：property\n\n"
            "组件标题。\n\n"
            "可见性：`public`\n\n"
            "类型：string\n\n"
            "访问：read / write\n\n"
            "### `Count`\n\n"
            "声明：static field\n\n"
            "组件数量。\n\n"
            "可见性：`protected`\n\n"
            "类型：integer\n\n"
            "### `DefaultName`\n\n"
            "声明：const\n\n"
            "默认名称。\n\n"
            "可见性：`public`\n\n"
            "值：`'widget'`\n",
            text,
        )

    def test_unit_page_renders_interface_class_members_at_h4(self):
        self.write_input(
            {
                "module": "示例 / Unit",
                "path": "base/demo_unit",
                "declarations": [
                    {
                        "kind": "unit",
                        "name": "DemoUnit",
                        "desc": "提供文档能力。",
                        "members": [
                            {
                                "kind": "constant",
                                "name": "DefaultSize",
                                "desc": "默认大小。",
                                "value": 100,
                            },
                            {
                                "kind": "variable",
                                "name": "CurrentDocument",
                                "desc": "当前文档。",
                                "type": "Document",
                            },
                            {
                                "kind": "function",
                                "name": "OpenDocument",
                                "signature": "OpenDocument(path)",
                                "desc": "打开文档。",
                                "params": [
                                    {
                                        "name": "path",
                                        "type": "string",
                                        "desc": "文档路径",
                                    }
                                ],
                                "returns": "Document",
                            },
                            {
                                "kind": "class",
                                "name": "Document",
                                "desc": "文档对象。",
                                "bases": ["BaseDocument"],
                                "members": [
                                    {
                                        "kind": "method",
                                        "name": "Save",
                                        "visibility": "public",
                                        "binding": "instance",
                                        "signature": "Save()",
                                        "desc": "保存文档。",
                                        "returns": "boolean",
                                    }
                                ],
                            },
                        ],
                    }
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/demo_unit").read_text(encoding="utf-8")
        self.assertIn("## `DemoUnit`", text)
        self.assertIn("声明：unit", text)
        self.assertIn("### `DefaultSize`\n\n声明：const", text)
        self.assertIn("值：`100`", text)
        self.assertIn("### `CurrentDocument`\n\n声明：var", text)
        self.assertIn("### `OpenDocument(path)`\n\n声明：function", text)
        self.assertIn("### `Document`\n\n声明：class", text)
        self.assertIn("父类：`BaseDocument`", text)
        self.assertIn("#### `Save()`\n\n声明：function", text)
        self.assertNotIn("可见性：`public`\n\n### `Document`", text)
        self.assertEqual(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供文档能力。\n\n"
            "### `DefaultSize`\n\n"
            "声明：const\n\n"
            "默认大小。\n\n"
            "值：`100`\n\n"
            "### `CurrentDocument`\n\n"
            "声明：var\n\n"
            "当前文档。\n\n"
            "类型：Document\n\n"
            "### `OpenDocument(path)`\n\n"
            "声明：function\n\n"
            "打开文档。\n\n"
            "| 参数   | 类型   | 说明     |\n"
            "| ------ | ------ | -------- |\n"
            "| `path` | string | 文档路径 |\n\n"
            "返回：Document\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档对象。\n\n"
            "父类：`BaseDocument`\n\n"
            "#### `Save()`\n\n"
            "声明：function\n\n"
            "保存文档。\n\n"
            "可见性：`public`\n\n"
            "返回：boolean\n",
            text,
        )

    def test_class_method_empty_returns_is_treated_as_omitted(self):
        self.write_input(
            {
                "module": "示例 / 类",
                "path": "base/no_return",
                "declarations": [
                    {
                        "kind": "class",
                        "name": "NoReturn",
                        "desc": "无返回类。",
                        "members": [
                            {
                                "kind": "method",
                                "name": "Close",
                                "visibility": "public",
                                "binding": "instance",
                                "signature": "Close()",
                                "desc": "关闭。",
                                "returns": "",
                            }
                        ],
                    }
                ],
            }
        )
        class_result = self.run_cli()
        self.assertEqual(0, class_result.returncode, class_result.stderr)
        class_text = self.generated("base/no_return").read_text(encoding="utf-8")
        self.assertNotIn("返回：", class_text)

    def test_unit_function_requires_returns(self):
        self.write_input(
            {
                "module": "示例 / Unit",
                "path": "base/missing_return",
                "declarations": [
                    {
                        "kind": "unit",
                        "name": "MissingReturn",
                        "desc": "缺少返回。",
                        "members": [
                            {
                                "kind": "function",
                                "name": "Open",
                                "signature": "Open()",
                                "desc": "打开。",
                            }
                        ],
                    }
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(1, result.returncode)
        self.assertIn("缺少 returns", result.stderr)

    def test_examples_list_renders_independent_fences_and_output_comments(self):
        self.write_input(
            {
                "module": "项目 / 示例",
                "path": "base/examples",
                "declarations": [
                    {
                        "kind": "function",
                        "name": "demo",
                        "signature": "demo()",
                        "desc": "示例函数。",
                        "returns": "string",
                        "examples": [
                            {
                                "desc": "单行输出",
                                "code": "return demo();",
                                "output": "ok",
                            },
                            {
                                "desc": "多行输出",
                                "code": "return demo();",
                                "output": "first\nsecond",
                            },
                        ],
                    }
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/examples").read_text(encoding="utf-8")
        self.assertEqual(2, text.count("```tsl"))
        self.assertIn("范例01：单行输出", text)
        self.assertIn("// 输出：ok", text)
        self.assertIn("范例02：多行输出", text)
        self.assertIn("// 输出：\n// first\n// second", text)

    def test_singular_example_is_rejected_without_overwrite(self):
        self.write_input(
            {
                "module": "项目 / 旧示例格式",
                "path": "base/singular_example",
                "declarations": [
                    {
                        "kind": "function",
                        "name": "demo",
                        "signature": "demo()",
                        "desc": "示例函数。",
                        "returns": "nil",
                        "example": "return demo();",
                    }
                ],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/singular_example", "存在未知字段：example"
        )

    def test_singular_method_example_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "method",
                "name": "Run",
                "visibility": "public",
                "binding": "instance",
                "signature": "Run()",
                "desc": "运行。",
                "example": "return self.Run();",
            },
            "base/singular_method_example",
        )

        self.assert_rejected_without_overwrite(
            "base/singular_method_example", "存在未知字段：example"
        )

    def test_top_level_enum_sections_are_plain_text_not_emphasis_headings(self):
        self.write_input(
            {
                "module": "项目 / 枚举",
                "path": "base/enum_values",
                "declarations": [
                    {
                        "kind": "function",
                        "name": "demo",
                        "signature": "demo(mode)",
                        "desc": "按模式返回结果。",
                        "params": [
                            {
                                "name": "mode",
                                "type": "integer",
                                "desc": "处理模式",
                                "values": [
                                    {"value": 0, "desc": "原样返回"},
                                    {"value": 1, "desc": "转换后返回"},
                                ],
                            }
                        ],
                        "returns": "integer",
                    }
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/enum_values").read_text(encoding="utf-8")
        self.assertIn("\nmode 取值\n", text)
        self.assertNotIn("**mode 取值**", text)

    def test_atomic_write_failure_preserves_existing_output_and_cleans_temp(self):
        module = load_script()
        output = self.root / "existing.md"
        output.write_text("原内容\n", encoding="utf-8")

        with mock.patch.object(
            module.os, "replace", side_effect=OSError("replace failed")
        ):
            with self.assertRaisesRegex(OSError, "replace failed"):
                module.atomic_write(output, "新内容\n")

        self.assertEqual("原内容\n", output.read_text(encoding="utf-8"))
        self.assertFalse(any(path.suffix == ".tmp" for path in self.root.iterdir()))

    def test_legacy_root_keys_are_rejected_without_overwrite(self):
        legacy_values = {
            "functions": [{"signature": "Old()", "desc": "旧函数。", "returns": "nil"}],
            "class": {"name": "Old", "desc": "旧类。", "members": []},
            "unit": {"name": "Old", "desc": "旧接口。", "members": []},
        }
        for key, value in legacy_values.items():
            with self.subTest(key=key):
                relative = f"base/legacy_{key}"
                self.write_input(
                    {
                        "module": "示例 / 旧录入",
                        "path": relative,
                        key: value,
                    }
                )

                self.assert_rejected_without_overwrite(relative, f"存在未知字段：{key}")

    def test_unknown_class_member_kind_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "event",
                "name": "Changed",
                "visibility": "public",
                "desc": "发生变化。",
            },
            "base/unknown_kind",
        )

        self.assert_rejected_without_overwrite("base/unknown_kind", "未知 kind：event")

    def test_method_static_field_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "method",
                "name": "Bad",
                "visibility": "public",
                "binding": "instance",
                "signature": "Bad()",
                "desc": "非法方法。",
                "static": True,
            },
            "base/method_static",
        )

        self.assert_rejected_without_overwrite(
            "base/method_static", "存在未知字段：static"
        )

    def test_private_class_member_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "field",
                "name": "hidden_",
                "visibility": "private",
                "desc": "私有字段。",
                "type": "string",
            },
            "base/private_member",
        )

        self.assert_rejected_without_overwrite(
            "base/private_member", "visibility 只能是 public 或 protected"
        )

    def test_property_empty_type_is_treated_as_omitted(self):
        self.write_class_member_input(
            {
                "kind": "property",
                "name": "Title",
                "visibility": "public",
                "desc": "标题。",
                "type": "",
                "access": "read",
            },
            "base/property_type",
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/property_type").read_text(encoding="utf-8")
        self.assertIn("### `Title`\n\n声明：property", text)
        self.assertIn("访问：read", text)
        self.assertNotIn("类型：", text)

    def test_constant_empty_type_is_treated_as_omitted(self):
        self.write_class_member_input(
            {
                "kind": "constant",
                "name": "DefaultSize",
                "visibility": "public",
                "desc": "默认大小。",
                "type": "",
                "value": 100,
            },
            "base/constant_type",
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/constant_type").read_text(encoding="utf-8")
        self.assertIn("值：`100`", text)
        self.assertNotIn("类型：", text)

    def test_field_missing_type_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "field",
                "name": "Count",
                "visibility": "public",
                "desc": "数量。",
            },
            "base/field_type",
        )

        self.assert_rejected_without_overwrite(
            "base/field_type", "members[0]: type：不能为空"
        )

    def test_unit_variable_missing_type_is_rejected_without_overwrite(self):
        self.write_input(
            {
                "module": "示例 / Unit",
                "path": "base/variable_type",
                "declarations": [
                    {
                        "kind": "unit",
                        "name": "StrictUnit",
                        "desc": "严格接口。",
                        "members": [
                            {
                                "kind": "variable",
                                "name": "Current",
                                "desc": "当前值。",
                            }
                        ],
                    }
                ],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/variable_type", "members[0]: type：不能为空"
        )

    def test_invalid_method_binding_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "method",
                "name": "Bad",
                "visibility": "public",
                "binding": "static",
                "signature": "Bad()",
                "desc": "非法绑定。",
            },
            "base/invalid_binding",
        )

        self.assert_rejected_without_overwrite("base/invalid_binding", "binding 无效")

    def test_invalid_property_access_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "property",
                "name": "Title",
                "visibility": "public",
                "desc": "标题。",
                "type": "string",
                "access": "readonly",
            },
            "base/invalid_access",
        )

        self.assert_rejected_without_overwrite("base/invalid_access", "access 无效")

    def test_invalid_method_modifier_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "method",
                "name": "Bad",
                "visibility": "public",
                "binding": "instance",
                "signature": "Bad()",
                "desc": "非法修饰符。",
                "modifiers": ["final"],
            },
            "base/invalid_modifier",
        )

        self.assert_rejected_without_overwrite("base/invalid_modifier", "无效 modifier")

    def test_property_examples_are_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "property",
                "name": "Title",
                "visibility": "public",
                "desc": "标题。",
                "type": "string",
                "access": "read",
                "examples": [
                    {
                        "desc": "读取标题",
                        "code": "return Widget.Title;",
                    }
                ],
            },
            "base/property_examples",
        )

        self.assert_rejected_without_overwrite(
            "base/property_examples", "存在未知字段：examples"
        )

    def test_unit_rejects_implementation_data_without_overwrite(self):
        self.write_input(
            {
                "module": "示例 / Unit",
                "path": "base/unit_implementation",
                "declarations": [
                    {
                        "kind": "unit",
                        "name": "StrictUnit",
                        "desc": "严格接口。",
                        "members": [],
                        "implementation": ["hidden_"],
                    }
                ],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/unit_implementation", "存在未知字段：implementation"
        )

    def test_empty_declarations_are_rejected_without_overwrite(self):
        self.write_input(
            {
                "module": "示例 / 空页",
                "path": "base/missing_branch",
                "declarations": [],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/missing_branch", "declarations 必须是非空列表"
        )

    def test_unknown_declaration_kind_is_rejected_without_overwrite(self):
        self.write_input(
            {
                "module": "示例 / 未知声明",
                "path": "base/unknown_declaration",
                "declarations": [{"kind": "procedure"}],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/unknown_declaration", "未知 kind：procedure"
        )

    def test_top_level_function_requires_name(self):
        self.write_input(
            {
                "module": "示例 / 函数",
                "path": "base/missing_function_name",
                "declarations": [
                    {
                        "kind": "function",
                        "signature": "Open()",
                        "desc": "打开。",
                        "returns": "nil",
                    }
                ],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/missing_function_name", "declarations[0]: name"
        )

    def test_top_level_function_name_must_match_signature(self):
        self.write_input(
            {
                "module": "示例 / 函数",
                "path": "base/function_name_mismatch",
                "declarations": [
                    {
                        "kind": "function",
                        "name": "Open",
                        "signature": "Close()",
                        "desc": "关闭。",
                        "returns": "nil",
                    }
                ],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/function_name_mismatch", "name 与 signature 中的名称不一致"
        )

    def test_duplicate_function_signature_is_rejected(self):
        self.write_input(
            {
                "module": "示例 / 重复函数",
                "path": "base/duplicate_function",
                "declarations": [
                    {
                        "kind": "function",
                        "name": "Open",
                        "signature": "Open()",
                        "desc": "打开。",
                        "returns": "nil",
                    },
                    {
                        "kind": "function",
                        "name": "open",
                        "signature": "open()",
                        "desc": "再次打开。",
                        "returns": "nil",
                    },
                ],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/duplicate_function", "function signature 重复"
        )

    def test_duplicate_class_and_unit_names_are_rejected(self):
        cases = {
            "class": [
                {
                    "kind": "class",
                    "name": "Widget",
                    "desc": "组件。",
                    "members": [],
                },
                {
                    "kind": "class",
                    "name": "widget",
                    "desc": "另一组件。",
                    "members": [],
                },
            ],
            "unit": [
                {
                    "kind": "unit",
                    "name": "Runtime",
                    "desc": "运行时。",
                    "members": [],
                },
                {
                    "kind": "unit",
                    "name": "runtime",
                    "desc": "另一运行时。",
                    "members": [],
                },
            ],
        }
        for kind, declarations in cases.items():
            with self.subTest(kind=kind):
                relative = f"base/duplicate_{kind}"
                self.write_input(
                    {
                        "module": "示例 / 重复声明",
                        "path": relative,
                        "declarations": declarations,
                    }
                )

                self.assert_rejected_without_overwrite(relative, f"{kind} 名称重复")

    def test_function_overloads_and_cross_kind_same_name_are_allowed(self):
        self.write_input(
            {
                "module": "示例 / 重载",
                "path": "base/overloads",
                "declarations": [
                    {
                        "kind": "function",
                        "name": "Open",
                        "signature": "Open(path)",
                        "desc": "按路径打开。",
                        "params": [{"name": "path", "type": "string", "desc": "路径"}],
                        "returns": "nil",
                    },
                    {
                        "kind": "function",
                        "name": "Open",
                        "signature": "Open(mode)",
                        "desc": "按模式打开。",
                        "params": [{"name": "mode", "type": "integer", "desc": "模式"}],
                        "returns": "nil",
                    },
                    {
                        "kind": "class",
                        "name": "Open",
                        "desc": "打开器。",
                        "members": [],
                    },
                    {
                        "kind": "unit",
                        "name": "Open",
                        "desc": "打开接口。",
                        "members": [],
                    },
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/overloads").read_text(encoding="utf-8")
        markers = [
            "## `Open(path)`",
            "## `Open(mode)`",
            "## `Open`",
            "## `Open`",
        ]
        positions = []
        start = 0
        for marker in markers:
            position = text.index(marker, start)
            positions.append(position)
            start = position + len(marker)
        self.assertEqual(sorted(positions), positions)
        self.assertEqual(2, text.count("声明：function"))
        self.assertEqual(1, text.count("声明：class"))
        self.assertEqual(1, text.count("声明：unit"))

    def test_constant_missing_value_is_rejected_without_overwrite(self):
        self.write_class_member_input(
            {
                "kind": "constant",
                "name": "Missing",
                "visibility": "public",
                "desc": "缺少值。",
            },
            "base/missing_value",
        )

        self.assert_rejected_without_overwrite("base/missing_value", "缺少 value")

    def test_class_missing_description_is_rejected_without_overwrite(self):
        self.write_input(
            {
                "module": "示例 / 类",
                "path": "base/missing_class_desc",
                "declarations": [
                    {
                        "kind": "class",
                        "name": "MissingDescription",
                        "desc": "",
                        "members": [],
                    }
                ],
            }
        )

        self.assert_rejected_without_overwrite(
            "base/missing_class_desc",
            "declarations[0]: desc：不能为空",
        )

    def test_method_name_and_parameter_order_must_match_signature(self):
        cases = {
            "name": {
                "kind": "method",
                "name": "Expected",
                "visibility": "public",
                "binding": "instance",
                "signature": "Actual()",
                "desc": "名称不一致。",
            },
            "parameter_order": {
                "kind": "method",
                "name": "Open",
                "visibility": "public",
                "binding": "instance",
                "signature": "Open(first, second)",
                "desc": "参数顺序不一致。",
                "params": [
                    {"name": "second", "type": "integer", "desc": "第二项"},
                    {"name": "first", "type": "integer", "desc": "第一项"},
                ],
            },
        }
        for name, member in cases.items():
            with self.subTest(name=name):
                relative = f"base/mismatch_{name}"
                self.write_class_member_input(member, relative)

                expected = "名称不一致" if name == "name" else "参数顺序一致"
                self.assert_rejected_without_overwrite(relative, expected)

    def test_instance_field_and_static_constant_render_distinct_headings(self):
        self.write_input(
            {
                "module": "示例 / 类",
                "path": "base/bindings",
                "declarations": [
                    {
                        "kind": "class",
                        "name": "Bindings",
                        "desc": "绑定示例。",
                        "members": [
                            {
                                "kind": "field",
                                "name": "Name",
                                "visibility": "public",
                                "desc": "名称。",
                                "type": "string",
                            },
                            {
                                "kind": "constant",
                                "name": "Maximum",
                                "visibility": "protected",
                                "desc": "最大值。",
                                "value": 10,
                                "static": True,
                            },
                        ],
                    }
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/bindings").read_text(encoding="utf-8")
        self.assertIn("### `Name`\n\n声明：field", text)
        self.assertIn("### `Maximum`\n\n声明：static const", text)

    def test_constants_accept_zero_and_false_values(self):
        self.write_input(
            {
                "module": "示例 / Unit",
                "path": "base/constants",
                "declarations": [
                    {
                        "kind": "unit",
                        "name": "Constants",
                        "desc": "常量接口。",
                        "members": [
                            {
                                "kind": "constant",
                                "name": "Zero",
                                "desc": "零。",
                                "value": 0,
                            },
                            {
                                "kind": "constant",
                                "name": "Disabled",
                                "desc": "关闭。",
                                "value": False,
                            },
                        ],
                    }
                ],
            }
        )

        result = self.run_cli()

        self.assertEqual(0, result.returncode, result.stderr)
        text = self.generated("base/constants").read_text(encoding="utf-8")
        self.assertIn("值：`0`", text)
        self.assertIn("值：`false`", text)


if __name__ == "__main__":
    unittest.main()
