import importlib.util
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path


SCRIPT = Path(__file__).parents[1] / "scripts" / "lint.py"
API_MARKDOWN = Path(__file__).parents[1] / "scripts" / "api_markdown.py"
VALID_PAGE = (
    "# 项目 / 示例\n\n"
    "## `demo()`\n\n"
    "声明：function\n\n"
    "示例函数\n\n"
    "返回：nil\n"
)


def load_api_markdown():
    spec = importlib.util.spec_from_file_location("tsl_api_markdown", API_MARKDOWN)
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


class DocLintCliTest(unittest.TestCase):
    def setUp(self):
        self.temp_dir = tempfile.TemporaryDirectory()
        self.root = Path(self.temp_dir.name)
        self.page = self.root / "base" / "demo.md"
        self.page.parent.mkdir()
        self.page.write_text(VALID_PAGE, encoding="utf-8")

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

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

    def test_accepts_one_markdown_file(self):
        result = self.run_cli("--file", self.page)
        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertIn("1 files", result.stderr)

    def test_accepts_one_directory(self):
        result = self.run_cli("--dir", self.root)
        self.assertEqual(result.returncode, 0, result.stderr)
        self.assertIn("1 files", result.stderr)

    def test_accepts_mixed_typed_h2_declarations(self):
        self.page.write_text(
            "# 示例 / 混合\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "组件。\n\n"
            "### `Open()`\n\n"
            "声明：function\n\n"
            "打开。\n\n"
            "可见性：`public`\n\n"
            "## `Parse()`\n\n"
            "声明：function\n\n"
            "解析。\n\n"
            "返回：Widget\n\n"
            "### 示例\n\n"
            "```tsl\nreturn Parse();\n```\n\n"
            "## `Runtime`\n\n"
            "声明：unit\n\n"
            "运行时接口。\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档。\n\n"
            "#### `Save()`\n\n"
            "声明：function\n\n"
            "保存。\n\n"
            "可见性：`public`\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page, "--strict")

        self.assertEqual(0, result.returncode, result.stdout + result.stderr)

    def test_declaration_must_use_exact_canonical_line(self):
        self.page.write_text(
            "# 示例 / 函数\n\n"
            "## `Open()`\n\n"
            "声明:Function\n\n"
            "打开。\n\n"
            "返回：nil\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[heading]", result.stdout)
        self.assertIn("声明：function", result.stdout)

    def test_business_type_metadata_is_not_a_declaration_marker(self):
        module = load_api_markdown()
        lines = [
            "## `Account`",
            "",
            "声明：function",
            "",
            "远程服务器账户。",
            "",
            "类型：读写",
            "",
            "返回：nil",
        ]

        entries = list(module.iter_api_entries(lines))

        self.assertEqual(1, len(entries))
        self.assertTrue(entries[0].heading.valid)
        self.assertEqual("function", entries[0].heading.root_kind)

        self.page.write_text(
            "# 示例 / 遗留属性\n\n" + "\n".join(lines) + "\n",
            encoding="utf-8",
        )
        result = self.run_cli("--file", self.page, "--strict")
        self.assertEqual(0, result.returncode, result.stdout + result.stderr)

    def test_declaration_must_be_first_non_empty_line_after_heading(self):
        self.page.write_text(
            "# 示例 / 函数\n\n"
            "## `Open()`\n\n"
            "打开。\n\n"
            "声明：function\n\n"
            "返回：nil\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[heading]", result.stdout)
        self.assertIn("第一条非空正文", result.stdout)

    def test_tags_must_follow_description(self):
        self.page.write_text(
            "# 示例 / 函数\n\n"
            "## `Open()`\n\n"
            "声明：function\n\n"
            "<!-- tags: 示例 -->\n\n"
            "打开。\n\n"
            "返回：nil\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[description]", result.stdout)

    def test_property_type_is_optional_but_access_is_required(self):
        page_without_type = (
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Title`\n\n"
            "声明：property\n\n"
            "组件标题。\n\n"
            "可见性：`public`\n\n"
            "访问：read\n"
        )
        self.page.write_text(page_without_type, encoding="utf-8")

        valid = self.run_cli("--file", self.page, "--strict")

        self.assertEqual(0, valid.returncode, valid.stdout + valid.stderr)

        self.page.write_text(
            page_without_type.replace("\n访问：read\n", "\n"),
            encoding="utf-8",
        )
        invalid = self.run_cli("--file", self.page)

        self.assertEqual(1, invalid.returncode)
        self.assertIn("[access]", invalid.stdout)

    def test_accepts_class_page_with_optional_method_return(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Close()`\n\n"
            "声明：function\n\n"
            "关闭组件。\n\n"
            "可见性：`public`\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page, "--strict")

        self.assertEqual(0, result.returncode, result.stdout + result.stderr)

    def test_class_member_requires_visibility(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Count`\n\n"
            "声明：field\n\n"
            "组件数量。\n\n"
            "类型：integer\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[visibility]", result.stdout)

    def test_parameterized_property_requires_the_fixed_parameter_table(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Items(index)`\n\n"
            "声明：property\n\n"
            "按索引读取组件。\n\n"
            "可见性：`public`\n\n"
            "类型：Widget\n\n"
            "访问：read\n\n"
            "| 名称 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `index` | integer | 索引 |\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[param-header]", result.stdout)

    def test_nullary_property_rejects_a_parameter_table(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Title`\n\n"
            "声明：property\n\n"
            "组件标题。\n\n"
            "可见性：`public`\n\n"
            "类型：string\n\n"
            "访问：read\n\n"
            "| 参数 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `unused` | integer | 不应存在 |\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[param-table]", result.stdout)

    def test_class_method_allows_h4_value_and_example_subheadings(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Open(mode)`\n\n"
            "声明：function\n\n"
            "打开组件。\n\n"
            "可见性：`public`\n\n"
            "| 参数 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `mode` | integer | 打开模式 |\n\n"
            "#### `mode` 取值\n\n"
            "- `0` — 默认模式\n\n"
            "#### 示例\n\n"
            "```tsl\nreturn Open(0);\n```\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page, "--strict")

        self.assertEqual(0, result.returncode, result.stdout + result.stderr)

    def test_property_rejects_example_subheading(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Title`\n\n"
            "声明：property\n\n"
            "组件标题。\n\n"
            "可见性：`public`\n\n"
            "类型：string\n\n"
            "访问：read\n\n"
            "#### 示例\n\n"
            "```tsl\nreturn Widget.Title;\n```\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[subheading]", result.stdout)
        self.assertIn("property", result.stdout)

    def test_accepts_unit_page_with_nested_class_member(self):
        self.page.write_text(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供接口。\n\n"
            "### `DefaultSize`\n\n"
            "声明：const\n\n"
            "默认大小。\n\n"
            "值：`100`\n\n"
            "### `Open()`\n\n"
            "声明：function\n\n"
            "打开。\n\n"
            "返回：Document\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档对象。\n\n"
            "#### `Save()`\n\n"
            "声明：function\n\n"
            "保存。\n\n"
            "可见性：`protected`\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page, "--strict")

        self.assertEqual(0, result.returncode, result.stdout + result.stderr)

    def test_unit_class_method_allows_h5_value_and_example_subheadings(self):
        self.page.write_text(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供接口。\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档对象。\n\n"
            "#### `Save(mode)`\n\n"
            "声明：function\n\n"
            "保存。\n\n"
            "可见性：`public`\n\n"
            "| 参数 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `mode` | integer | 保存模式 |\n\n"
            "##### `mode` 取值\n\n"
            "- `0` — 默认模式\n\n"
            "##### 示例\n\n"
            "```tsl\nreturn Save(0);\n```\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page, "--strict")

        self.assertEqual(0, result.returncode, result.stdout + result.stderr)

    def test_unit_function_requires_return(self):
        self.page.write_text(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供接口。\n\n"
            "### `Open()`\n\n"
            "声明：function\n\n"
            "打开。\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[return]", result.stdout)

    def test_unit_function_rejects_an_empty_return_type(self):
        self.page.write_text(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供接口。\n\n"
            "### `Open()`\n\n"
            "声明：function\n\n"
            "打开。\n\n"
            "返回：\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[return]", result.stdout)

    def test_empty_type_and_value_lines_are_rejected(self):
        cases = {
            "property": (
                "# 示例 / 类\n\n## `Widget`\n\n声明：class\n\n组件。\n\n"
                "### `Title`\n\n声明：property\n\n标题。\n\n可见性：`public`\n\n"
                "类型：\n\n访问：read\n",
                "[type]",
            ),
            "field": (
                "# 示例 / 类\n\n## `Widget`\n\n声明：class\n\n组件。\n\n"
                "### `Count`\n\n声明：field\n\n数量。\n\n可见性：`public`\n\n类型：\n",
                "[type]",
            ),
            "variable": (
                "# 示例 / Unit\n\n## `DemoUnit`\n\n声明：unit\n\n接口。\n\n"
                "### `Current`\n\n声明：var\n\n当前值。\n\n类型：\n",
                "[type]",
            ),
            "constant": (
                "# 示例 / Unit\n\n## `DemoUnit`\n\n声明：unit\n\n接口。\n\n"
                "### `Default`\n\n声明：const\n\n默认值。\n\n值：\n",
                "[value]",
            ),
        }
        for name, (text, expected) in cases.items():
            with self.subTest(name=name):
                self.page.write_text(text, encoding="utf-8")

                result = self.run_cli("--file", self.page)

                self.assertEqual(1, result.returncode)
                self.assertIn(expected, result.stdout)

    def test_class_parent_metadata_does_not_count_as_description(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "父类：`BaseWidget`\n\n"
            "",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[description]", result.stdout)

    def test_class_parameter_rows_must_match_signature(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Open(mode)`\n\n"
            "声明：function\n\n"
            "打开组件。\n\n"
            "可见性：`public`\n\n"
            "| 参数 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `other` | integer | 错误参数 |\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[param-names]", result.stdout)

    def test_class_parameter_rows_require_type_and_description(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Open(mode)`\n\n"
            "声明：function\n\n"
            "打开组件。\n\n"
            "可见性：`public`\n\n"
            "| 参数 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `mode` |  |  |\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[param-row]", result.stdout)

    def test_unit_class_member_requires_visibility(self):
        self.page.write_text(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供接口。\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档对象。\n\n"
            "#### `Name`\n\n"
            "声明：field\n\n"
            "文档名称。\n\n"
            "类型：string\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[visibility]", result.stdout)

    def test_class_and_unit_member_data_lines_are_required(self):
        cases = {
            "property": (
                "# 示例 / 类\n\n## `Widget`\n\n声明：class\n\n组件。\n\n"
                "### `Title`\n\n声明：property\n\n标题。\n\n可见性：`public`\n",
                "[access]",
            ),
            "field": (
                "# 示例 / 类\n\n## `Widget`\n\n声明：class\n\n组件。\n\n"
                "### `Count`\n\n声明：field\n\n数量。\n\n可见性：`public`\n",
                "[type]",
            ),
            "variable": (
                "# 示例 / Unit\n\n## `DemoUnit`\n\n声明：unit\n\n接口。\n\n"
                "### `Current`\n\n声明：var\n\n当前值。\n",
                "[type]",
            ),
            "constant": (
                "# 示例 / Unit\n\n## `DemoUnit`\n\n声明：unit\n\n接口。\n\n"
                "### `Default`\n\n声明：const\n\n默认值。\n",
                "[value]",
            ),
        }
        for name, (text, expected) in cases.items():
            with self.subTest(name=name):
                self.page.write_text(text, encoding="utf-8")

                result = self.run_cli("--file", self.page)

                self.assertEqual(1, result.returncode)
                self.assertIn(expected, result.stdout)

    def test_class_legacy_typed_api_heading_is_rejected(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### function `Open()`\n\n"
            "打开组件。\n\n"
            "可见性：`public`\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[heading]", result.stdout)
        self.assertIn("标题只写名称或调用签名", result.stdout)

    def test_unit_legacy_typed_api_heading_is_rejected(self):
        self.page.write_text(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供接口。\n\n"
            "### function `Open()`\n\n"
            "打开。\n\n"
            "返回：Document\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[heading]", result.stdout)
        self.assertIn("标题只写名称或调用签名", result.stdout)

    def test_class_member_at_h4_is_rejected(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "#### `Open()`\n\n"
            "声明：function\n\n"
            "打开组件。\n\n"
            "可见性：`public`\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[heading]", result.stdout)
        self.assertIn("H3", result.stdout)

    def test_unit_class_member_at_h5_is_rejected(self):
        self.page.write_text(
            "# 示例 / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "提供接口。\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档对象。\n\n"
            "##### `Save()`\n\n"
            "声明：function\n\n"
            "保存。\n\n"
            "可见性：`public`\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("[heading]", result.stdout)
        self.assertIn("H4", result.stdout)

    def test_rejects_static_function_and_private_api(self):
        self.page.write_text(
            "# 示例 / 类\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "表示组件。\n\n"
            "### `Create()`\n\n"
            "声明：static function\n\n"
            "创建。\n\n"
            "可见性：`private`\n",
            encoding="utf-8",
        )

        result = self.run_cli("--file", self.page)

        self.assertEqual(1, result.returncode)
        self.assertIn("static function", result.stdout)
        self.assertIn("private", result.stdout)

    def test_mixed_h2_declarations_reset_nested_context(self):
        module = load_api_markdown()
        lines = [
            "# Demo",
            "",
            "## `Widget`",
            "",
            "声明：class",
            "",
            "组件。",
            "",
            "### `Open()`",
            "",
            "声明：function",
            "",
            "打开。",
            "",
            "可见性：public",
            "",
            "## `Parse(text)`",
            "",
            "声明：function",
            "",
            "解析。",
            "",
            "返回：Widget",
            "",
            "## `DemoUnit`",
            "",
            "声明：unit",
            "",
            "接口。",
            "",
            "### `Document`",
            "",
            "声明：class",
            "",
            "文档。",
            "",
            "#### `Save()`",
            "",
            "声明：function",
            "",
            "保存。",
            "",
            "可见性：public",
        ]

        headings = [entry.heading for entry in module.iter_api_entries(lines)]

        self.assertEqual(
            [
                (2, "class", "class", "Widget"),
                (3, "method", "class", "Open"),
                (2, "function", "function", "Parse"),
                (2, "unit", "unit", "DemoUnit"),
                (3, "class", "unit", "Document"),
                (4, "method", "unit", "Save"),
            ],
            [
                (item.level, item.kind, item.root_kind, item.name)
                for item in headings
            ],
        )

    def test_missing_duplicate_and_unknown_declarations_are_errors(self):
        module = load_api_markdown()
        cases = {
            "missing": (
                ["## `Open()`", "", "打开。", "", "返回：nil"],
                "第一条非空正文",
            ),
            "duplicate": (
                [
                    "## `Widget`",
                    "",
                    "声明：class",
                    "",
                    "组件。",
                    "",
                    "声明：unit",
                ],
                "必须且只能包含一行",
            ),
            "unknown": (
                ["## `Runtime`", "", "声明：module", "", "接口。"],
                "必须是 function、class 或 unit",
            ),
        }
        for name, (lines, expected) in cases.items():
            with self.subTest(name=name):
                headings = [
                    entry.heading for entry in module.iter_api_entries(lines)
                ]

                self.assertEqual(1, len(headings))
                self.assertFalse(headings[0].valid)
                self.assertIn(expected, headings[0].error)

    def test_plain_h2_ends_api_body_and_resets_nested_context(self):
        module = load_api_markdown()
        lines = [
            "# Demo",
            "",
            "## `Parse()`",
            "",
            "声明：function",
            "",
            "解析。",
            "",
            "返回：nil",
            "",
            "## 其他说明",
            "",
            "### function `NotAMember()`",
        ]

        entries = list(module.iter_api_entries(lines))

        self.assertEqual(["Parse"], [entry.heading.name for entry in entries])
        self.assertEqual(10, entries[0].end)

    def test_parameter_value_and_example_headings_are_not_api_entries(self):
        module = load_api_markdown()
        lines = [
            "# Demo",
            "",
            "## `Widget`",
            "",
            "声明：class",
            "",
            "表示组件。",
            "",
            "### `Open(mode)`",
            "",
            "声明：function",
            "",
            "打开。",
            "",
            "可见性：`public`",
            "",
            "#### `mode` 取值",
            "",
            "#### 示例",
            "",
            "## `Parse(mode)`",
            "",
            "声明：function",
            "",
            "解析。",
            "",
            "| 参数 | 类型 | 说明 |",
            "| --- | --- | --- |",
            "| `mode` | integer | 模式 |",
            "",
            "mode 取值",
            "",
            "### 示例",
            "",
            "## `DemoUnit`",
            "",
            "声明：unit",
            "",
            "接口。",
            "",
            "### `Load(mode)`",
            "",
            "声明：function",
            "",
            "加载。",
            "",
            "#### `mode` 取值",
            "",
            "#### 示例",
        ]

        entries = list(module.iter_api_entries(lines))

        self.assertEqual(
            ["Widget", "Open", "Parse", "DemoUnit", "Load"],
            [entry.heading.name for entry in entries],
        )


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