import importlib.util
import io
import tempfile
import unittest
from contextlib import redirect_stderr, redirect_stdout
from pathlib import Path


SCRIPT_PATH = (
    Path(__file__).resolve().parents[1]
    / "scripts"
    / "build_index.py"
)


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


class FunctionIndexTest(unittest.TestCase):
    def setUp(self):
        self.temp_dir = tempfile.TemporaryDirectory()
        self.skill_dir = Path(self.temp_dir.name) / "tsl-api-reference"
        self.codegen_root = self.skill_dir / "references" / "codegen"
        self.data_dir = self.skill_dir / "data"
        leaf = self.codegen_root / "builtin" / "array.md"
        leaf.parent.mkdir(parents=True)
        leaf.write_text(
            "# Builtin - 数组\n\n"
            "## `demo()`\n\n"
            "声明：function\n\n"
            "返回示例值。\n\n"
            "<!-- tags: 数组 列表 -->\n\n"
            "返回：integer\n",
            encoding="utf-8",
        )
        self.module = load_script()

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

    def run_main(self, *args):
        stdout = io.StringIO()
        stderr = io.StringIO()
        with redirect_stdout(stdout), redirect_stderr(stderr):
            result = self.module.main(["--skill-dir", str(self.skill_dir), *args])
        return result, stdout.getvalue(), stderr.getvalue()

    def write_page(self, relative_path, text):
        page = self.codegen_root / relative_path
        page.parent.mkdir(parents=True, exist_ok=True)
        page.write_text(text, encoding="utf-8")
        return page

    def test_rebuild_writes_only_tsv(self):
        result, _, _ = self.run_main()

        self.assertEqual(0, result)
        self.assertTrue((self.data_dir / "function_index.tsv").is_file())
        self.assertEqual([], list(self.codegen_root.rglob("index.md")))

    def test_tags_and_summary_are_stored_separately(self):
        rows = self.module.build_rows(self.codegen_root)
        row = dict(zip(self.module.HEADER, rows[0]))

        self.assertEqual("数组 列表", row["tags"])
        self.assertEqual("返回示例值。", row["summary"])

    def test_root_page_uses_file_stem_for_scope_and_module(self):
        self.write_page(
            "deprecated.md",
            "# Deprecated\n\n"
            "## `legacy()`\n\n"
            "声明：function\n\n"
            "已废弃。\n",
        )

        rows = {
            row[0]: dict(zip(self.module.HEADER, row))
            for row in self.module.build_rows(self.codegen_root)
        }

        self.assertEqual("deprecated", rows["legacy"]["scope"])
        self.assertEqual("deprecated", rows["legacy"]["module"])
        self.assertEqual("deprecated.md", rows["legacy"]["page"])

    def test_declaration_does_not_become_a_missing_description_summary(self):
        self.write_page(
            "project/missing_description.md",
            "# Missing\n\n"
            "## `Missing()`\n\n"
            "声明：function\n\n"
            "返回：nil\n",
        )

        rows = {
            row[0]: dict(zip(self.module.HEADER, row))
            for row in self.module.build_rows(self.codegen_root)
        }

        self.assertEqual("", rows["Missing"]["summary"])

    def test_legacy_function_anchor_and_first_eight_columns_are_unchanged(self):
        rows = self.module.build_rows(self.codegen_root)
        row = dict(zip(self.module.HEADER, rows[0]))

        self.assertEqual(
            [
                "name",
                "scope",
                "module",
                "signature",
                "page",
                "anchor",
                "tags",
                "summary",
                "kind",
                "binding",
                "visibility",
                "owner",
                "qualified_name",
            ],
            self.module.HEADER,
        )
        self.assertEqual(
            [
                "demo",
                "builtin",
                "array",
                "demo()",
                "builtin/array.md",
                "demo",
                "数组 列表",
                "返回示例值。",
            ],
            rows[0][:8],
        )
        self.assertEqual("function", row["kind"])
        self.assertEqual("", row["binding"])
        self.assertEqual("", row["visibility"])
        self.assertEqual("", row["owner"])
        self.assertEqual("demo", row["qualified_name"])

    def test_mixed_page_resets_owner_for_every_h2(self):
        self.write_page(
            "project/mixed/api.md",
            "# Mixed\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"
            "## `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",
        )

        rows = {
            (row[0], row[3]): dict(zip(self.module.HEADER, row))
            for row in self.module.build_rows(self.codegen_root)
        }

        self.assertEqual("", rows[("Widget", "Widget")]["owner"])
        self.assertEqual("Widget", rows[("Open", "Open()")]["owner"])
        self.assertEqual("", rows[("Parse", "Parse()")]["owner"])
        self.assertEqual("", rows[("Runtime", "Runtime")]["owner"])
        self.assertEqual("Runtime", rows[("Document", "Document")]["owner"])
        self.assertEqual(
            "Runtime.Document", rows[("Save", "Save()")]["owner"]
        )

    def test_class_page_maps_members_to_unified_index_columns(self):
        self.write_page(
            "project/widgets/widget.md",
            "# Project / Widgets\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "组件。\n\n"
            "<!-- tags: 组件 -->\n\n"
            "### `Open(path)`\n\n"
            "声明：function\n\n"
            "打开组件。\n\n"
            "可见性：`public`\n\n"
            "### `Create()`\n\n"
            "声明：class function\n\n"
            "创建组件。\n\n"
            "可见性：`protected`\n\n"
            "### `Count`\n\n"
            "声明：static field\n\n"
            "组件数量。\n\n"
            "可见性：`protected`\n\n"
            "类型：integer\n\n"
            "### `Items(index)`\n\n"
            "声明：property\n\n"
            "按索引读取组件。\n\n"
            "可见性：`public`\n\n"
            "类型：Widget\n\n"
            "访问：read\n\n"
            "### `DefaultName`\n\n"
            "声明：const\n\n"
            "默认名称。\n\n"
            "可见性：`public`\n\n"
            "值：`'widget'`\n\n"
            "### `Maximum`\n\n"
            "声明：static const\n\n"
            "最大数量。\n\n"
            "可见性：`protected`\n\n"
            "值：`100`\n",
        )

        rows = {
            row[self.module.HEADER.index("qualified_name")]: dict(
                zip(self.module.HEADER, row)
            )
            for row in self.module.build_rows(self.codegen_root)
        }

        self.assertEqual(
            {
                "name": "Widget",
                "scope": "project",
                "module": "widgets",
                "signature": "Widget",
                "page": "project/widgets/widget.md",
                "anchor": "widget",
                "tags": "组件",
                "summary": "组件。",
                "kind": "class",
                "binding": "",
                "visibility": "",
                "owner": "",
                "qualified_name": "Widget",
            },
            rows["Widget"],
        )
        self.assertEqual("method", rows["Widget.Open"]["kind"])
        self.assertEqual("instance", rows["Widget.Open"]["binding"])
        self.assertEqual("public", rows["Widget.Open"]["visibility"])
        self.assertEqual("Widget", rows["Widget.Open"]["owner"])
        self.assertEqual("openpath", rows["Widget.Open"]["anchor"])
        self.assertEqual("class", rows["Widget.Create"]["binding"])
        self.assertEqual("create", rows["Widget.Create"]["anchor"])
        self.assertEqual("field", rows["Widget.Count"]["kind"])
        self.assertEqual("static", rows["Widget.Count"]["binding"])
        self.assertEqual("count", rows["Widget.Count"]["anchor"])
        self.assertEqual("property", rows["Widget.Items"]["kind"])
        self.assertEqual("instance", rows["Widget.Items"]["binding"])
        self.assertEqual("Items(index)", rows["Widget.Items"]["signature"])
        self.assertEqual("constant", rows["Widget.DefaultName"]["kind"])
        self.assertEqual("instance", rows["Widget.DefaultName"]["binding"])
        self.assertEqual("constant", rows["Widget.Maximum"]["kind"])
        self.assertEqual("static", rows["Widget.Maximum"]["binding"])
        self.assertEqual("maximum", rows["Widget.Maximum"]["anchor"])

    def test_unit_page_derives_nested_owner_and_public_interface_visibility(self):
        self.write_page(
            "dotnet/runtime/demo_unit.md",
            "# Dotnet / Runtime\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "接口。\n\n"
            "### `Open()`\n\n"
            "声明：function\n\n"
            "打开。\n\n"
            "返回：Document\n\n"
            "### `Current`\n\n"
            "声明：var\n\n"
            "当前文档。\n\n"
            "类型：Document\n\n"
            "### `DefaultName`\n\n"
            "声明：const\n\n"
            "默认名称。\n\n"
            "值：`'demo'`\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档对象。\n\n"
            "#### `Save()`\n\n"
            "声明：function\n\n"
            "保存。\n\n"
            "可见性：`protected`\n",
        )

        rows = {
            row[self.module.HEADER.index("qualified_name")]: dict(
                zip(self.module.HEADER, row)
            )
            for row in self.module.build_rows(self.codegen_root)
        }

        self.assertEqual("unit", rows["DemoUnit"]["kind"])
        self.assertEqual("", rows["DemoUnit"]["owner"])
        self.assertEqual("unit", rows["DemoUnit.Open"]["binding"])
        self.assertEqual("public", rows["DemoUnit.Open"]["visibility"])
        self.assertEqual("DemoUnit", rows["DemoUnit.Open"]["owner"])
        self.assertEqual("variable", rows["DemoUnit.Current"]["kind"])
        self.assertEqual("constant", rows["DemoUnit.DefaultName"]["kind"])
        self.assertEqual("unit", rows["DemoUnit.DefaultName"]["binding"])
        self.assertEqual("public", rows["DemoUnit.DefaultName"]["visibility"])
        self.assertEqual("unit", rows["DemoUnit.Document"]["binding"])
        self.assertEqual("public", rows["DemoUnit.Document"]["visibility"])
        self.assertEqual("DemoUnit.Document", rows["DemoUnit.Document.Save"]["owner"])
        self.assertEqual("method", rows["DemoUnit.Document.Save"]["kind"])
        self.assertEqual("instance", rows["DemoUnit.Document.Save"]["binding"])
        self.assertEqual("protected", rows["DemoUnit.Document.Save"]["visibility"])

    def test_class_overloads_share_qualified_name_but_have_distinct_anchors(self):
        self.write_page(
            "project/widgets/overloads.md",
            "# Project / Widgets\n\n"
            "## `Overloads`\n\n"
            "声明：class\n\n"
            "重载示例。\n\n"
            "### `Open(path)`\n\n"
            "声明：function\n\n"
            "按路径打开。\n\n"
            "可见性：`public`\n\n"
            "### `Open(mode, flags)`\n\n"
            "声明：function\n\n"
            "按模式打开。\n\n"
            "可见性：`public`\n",
        )

        rows = [
            dict(zip(self.module.HEADER, row))
            for row in self.module.build_rows(self.codegen_root)
            if row[0] == "Open"
        ]

        self.assertEqual(2, len(rows))
        self.assertEqual({"Overloads.Open"}, {row["qualified_name"] for row in rows})
        self.assertEqual(
            {"openpath", "openmodeflags"},
            {row["anchor"] for row in rows},
        )
        self.assertEqual(
            2,
            len({f"{row['page']}#{row['anchor']}" for row in rows}),
        )

    def test_top_level_overloads_and_cross_kind_names_get_unique_anchors(self):
        page = "project/mixed/overloads.md"
        self.write_page(
            page,
            "# Project / Mixed\n\n"
            "## `Open(path)`\n\n声明：function\n\n按路径打开。\n\n"
            "返回：nil\n\n"
            "## `Open(mode)`\n\n声明：function\n\n按模式打开。\n\n"
            "返回：nil\n\n"
            "## `Open`\n\n声明：class\n\n打开器。\n\n"
            "## `Open`\n\n声明：unit\n\n打开接口。\n",
        )

        rows = [
            dict(zip(self.module.HEADER, row))
            for row in self.module.build_rows(self.codegen_root)
            if row[4] == page
        ]
        by_identity = {
            (row["kind"], row["signature"]): row for row in rows
        }

        self.assertEqual(4, len(rows))
        self.assertEqual({""}, {row["owner"] for row in rows})
        self.assertEqual({"Open"}, {row["qualified_name"] for row in rows})
        self.assertEqual(
            "open", by_identity[("function", "Open(path)")]["anchor"]
        )
        self.assertEqual(
            "open-1", by_identity[("function", "Open(mode)")]["anchor"]
        )
        self.assertEqual("open-2", by_identity[("class", "Open")]["anchor"])
        self.assertEqual("open-3", by_identity[("unit", "Open")]["anchor"])
        self.assertEqual(
            4, len({f"{row['page']}#{row['anchor']}" for row in rows})
        )

    def test_check_does_not_require_index_pages(self):
        self.data_dir.mkdir(parents=True)
        rows = self.module.build_rows(self.codegen_root)
        (self.data_dir / "function_index.tsv").write_text(
            self.module.render_tsv(rows),
            encoding="utf-8",
            newline="\n",
        )

        result, stdout, _ = self.run_main("--check")

        self.assertEqual(0, result)
        self.assertIn("matches md tree", stdout)


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