import io
import importlib.util
import subprocess
import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path


SCRIPT = (
    Path(__file__).resolve().parents[3]
    / "skills"
    / "tsl-api-reference"
    / "scripts"
    / "lookup.py"
)
SKILL_MD = SCRIPT.parents[1] / "SKILL.md"
API_WORKFLOW = SCRIPT.parents[1] / "workflows" / "api-lookup.md"


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


class LookupTest(unittest.TestCase):
    def write_unified_api_fixture(self, skill_root):
        data_dir = skill_root / "data"
        codegen_dir = skill_root / "references" / "codegen"
        data_dir.mkdir()
        (codegen_dir / "project").mkdir(parents=True)

        header = [
            "name",
            "scope",
            "module",
            "signature",
            "page",
            "anchor",
            "tags",
            "summary",
            "kind",
            "binding",
            "visibility",
            "owner",
            "qualified_name",
        ]

        def row(
            name,
            signature,
            page,
            anchor,
            summary,
            kind,
            binding="",
            visibility="",
            owner="",
            qualified_name="",
        ):
            return [
                name,
                "project",
                "demo",
                signature,
                page,
                anchor,
                "",
                summary,
                kind,
                binding,
                visibility,
                owner,
                qualified_name or name,
            ]

        rows = [
            row(
                "DemoUnit",
                "DemoUnit",
                "project/demo_unit.md",
                "demounit",
                "接口。",
                "unit",
            ),
            row(
                "Open",
                "Open()",
                "project/demo_unit.md",
                "open",
                "打开。",
                "function",
                "unit",
                "public",
                "DemoUnit",
                "DemoUnit.Open",
            ),
            row(
                "Document",
                "Document",
                "project/demo_unit.md",
                "document",
                "文档。",
                "class",
                "unit",
                "public",
                "DemoUnit",
                "DemoUnit.Document",
            ),
            row(
                "Save",
                "Save(path)",
                "project/demo_unit.md",
                "savepath",
                "按路径保存。",
                "method",
                "instance",
                "public",
                "DemoUnit.Document",
                "DemoUnit.Document.Save",
            ),
            row(
                "Save",
                "Save(mode)",
                "project/demo_unit.md",
                "savemode",
                "按模式保存。",
                "method",
                "instance",
                "protected",
                "DemoUnit.Document",
                "DemoUnit.Document.Save",
            ),
            row(
                "Name",
                "Name",
                "project/demo_unit.md",
                "name",
                "文档名称。",
                "field",
                "instance",
                "public",
                "DemoUnit.Document",
                "DemoUnit.Document.Name",
            ),
            row(
                "Other",
                "Other",
                "project/demo_unit.md",
                "other",
                "其他对象。",
                "class",
                "unit",
                "public",
                "DemoUnit",
                "DemoUnit.Other",
            ),
            row(
                "Save",
                "Save()",
                "project/demo_unit.md",
                "save",
                "保存其他对象。",
                "method",
                "instance",
                "public",
                "DemoUnit.Other",
                "DemoUnit.Other.Save",
            ),
            row(
                "Widget",
                "Widget",
                "project/widget.md",
                "widget",
                "组件。",
                "class",
            ),
            row(
                "Create",
                "Create()",
                "project/widget.md",
                "create",
                "创建组件。",
                "method",
                "class",
                "public",
                "Widget",
                "Widget.Create",
            ),
            row(
                "Count",
                "Count",
                "project/widget.md",
                "count",
                "组件数量。",
                "field",
                "static",
                "protected",
                "Widget",
                "Widget.Count",
            ),
        ]
        index_text = "\n".join(
            ["\t".join(header), *("\t".join(values) for values in rows)]
        )
        tsv_path = data_dir / "function_index.tsv"
        tsv_path.write_text(index_text + "\n", encoding="utf-8")

        (codegen_dir / "project" / "demo_unit.md").write_text(
            "# Demo / Unit\n\n"
            "## `DemoUnit`\n\n"
            "声明：unit\n\n"
            "接口。\n\n"
            "### `Open()`\n\n"
            "声明：function\n\n"
            "打开。\n\n"
            "返回：Document\n\n"
            "### `Document`\n\n"
            "声明：class\n\n"
            "文档。\n\n"
            "#### `Save(path)`\n\n"
            "声明：function\n\n"
            "按路径保存。\n\n"
            "可见性：`public`\n\n"
            "#### `Save(mode)`\n\n"
            "声明：function\n\n"
            "按模式保存。\n\n"
            "可见性：`protected`\n\n"
            "#### `Name`\n\n"
            "声明：field\n\n"
            "文档名称。\n\n"
            "可见性：`public`\n\n"
            "类型：string\n\n"
            "### `Other`\n\n"
            "声明：class\n\n"
            "其他对象。\n\n"
            "#### `Save()`\n\n"
            "声明：function\n\n"
            "保存其他对象。\n\n"
            "可见性：`public`\n",
            encoding="utf-8",
        )
        (codegen_dir / "project" / "widget.md").write_text(
            "# Demo / Class\n\n"
            "## `Widget`\n\n"
            "声明：class\n\n"
            "组件。\n\n"
            "### `Create()`\n\n"
            "声明：class function\n\n"
            "创建组件。\n\n"
            "可见性：`public`\n\n"
            "### `Count`\n\n"
            "声明：static field\n\n"
            "组件数量。\n\n"
            "可见性：`protected`\n\n"
            "类型：integer\n",
            encoding="utf-8",
        )
        return tsv_path

    def write_mixed_api_fixture(self, skill_root):
        data_dir = skill_root / "data"
        codegen_dir = skill_root / "references" / "codegen" / "project"
        data_dir.mkdir()
        codegen_dir.mkdir(parents=True)
        header = [
            "name",
            "scope",
            "module",
            "signature",
            "page",
            "anchor",
            "tags",
            "summary",
            "kind",
            "binding",
            "visibility",
            "owner",
            "qualified_name",
        ]
        page = "project/mixed.md"
        rows = [
            [
                "Widget",
                "project",
                "mixed",
                "Widget",
                page,
                "widget",
                "",
                "组件。",
                "class",
                "",
                "",
                "",
                "Widget",
            ],
            [
                "Open",
                "project",
                "mixed",
                "Open()",
                page,
                "open",
                "",
                "打开。",
                "method",
                "instance",
                "public",
                "Widget",
                "Widget.Open",
            ],
            [
                "Parse",
                "project",
                "mixed",
                "Parse(text)",
                page,
                "parse",
                "",
                "解析。",
                "function",
                "",
                "",
                "",
                "Parse",
            ],
            [
                "Runtime",
                "project",
                "mixed",
                "Runtime",
                page,
                "runtime",
                "",
                "接口。",
                "unit",
                "",
                "",
                "",
                "Runtime",
            ],
            [
                "Document",
                "project",
                "mixed",
                "Document",
                page,
                "document",
                "",
                "文档。",
                "class",
                "unit",
                "public",
                "Runtime",
                "Runtime.Document",
            ],
            [
                "Save",
                "project",
                "mixed",
                "Save()",
                page,
                "save",
                "",
                "保存。",
                "method",
                "instance",
                "public",
                "Runtime.Document",
                "Runtime.Document.Save",
            ],
            [
                "Other",
                "project",
                "mixed",
                "Other",
                page,
                "other",
                "",
                "其他组件。",
                "class",
                "",
                "",
                "",
                "Other",
            ],
        ]
        tsv_path = data_dir / "function_index.tsv"
        tsv_path.write_text(
            "\n".join(
                ["\t".join(header), *("\t".join(row) for row in rows)]
            )
            + "\n",
            encoding="utf-8",
        )
        (codegen_dir / "mixed.md").write_text(
            "# Mixed\n\n"
            "## `Widget`\n\n声明：class\n\n组件。\n\n"
            "### `Open()`\n\n声明：function\n\n打开。\n\n可见性：public\n\n"
            "## `Parse(text)`\n\n声明：function\n\n解析。\n\n"
            "| 参数 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `text` | string | 文本 |\n\n"
            "返回：Widget\n\n"
            "### 示例\n\n"
            "```markdown\n## `NotBoundary`\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\n"
            "## `Other`\n\n声明：class\n\n其他组件。\n",
            encoding="utf-8",
        )
        return tsv_path

    def run_lookup(self, tsv_path, *args):
        return subprocess.run(
            [
                sys.executable,
                str(SCRIPT),
                *args,
                "--tsv",
                str(tsv_path),
            ],
            capture_output=True,
            text=True,
            encoding="utf-8",
            check=False,
        )

    def test_tsv_without_tags_is_rejected(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = Path(temp_dir) / "function_index.tsv"
            tsv_path.write_text(
                "name\tscope\tmodule\tsignature\tpage\tanchor\tsummary\n"
                "foo\tbuiltin\tmisc\tfoo()\tmisc.md\tfoo\tLegacy entry\n",
                encoding="utf-8",
            )

            result = subprocess.run(
                [
                    sys.executable,
                    str(SCRIPT),
                    "--kw",
                    "Legacy",
                    "--tsv",
                    str(tsv_path),
                ],
                capture_output=True,
                text=True,
                check=False,
            )

        self.assertEqual(1, result.returncode)
        self.assertEqual("", result.stdout)
        self.assertIn("missing required columns: tags", result.stderr)
        self.assertNotIn("Traceback", result.stderr)

    def test_missing_required_tsv_columns_are_rejected(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = Path(temp_dir) / "function_index.tsv"
            tsv_path.write_text("name\nfoo\n", encoding="utf-8")

            result = subprocess.run(
                [
                    sys.executable,
                    str(SCRIPT),
                    "--name",
                    "foo",
                    "--tsv",
                    str(tsv_path),
                ],
                capture_output=True,
                text=True,
                check=False,
            )

        self.assertEqual(1, result.returncode)
        self.assertIn("missing required columns", result.stderr)
        self.assertNotIn("Traceback", result.stderr)

    def test_tsv_row_width_is_validated(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = Path(temp_dir) / "function_index.tsv"
            tsv_path.write_text(
                "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
                "foo\tbuiltin\n",
                encoding="utf-8",
            )

            result = subprocess.run(
                [
                    sys.executable,
                    str(SCRIPT),
                    "--name",
                    "foo",
                    "--tsv",
                    str(tsv_path),
                ],
                capture_output=True,
                text=True,
                check=False,
            )

        self.assertEqual(1, result.returncode)
        self.assertIn("line 2 has 2 columns; expected 8", result.stderr)
        self.assertNotIn("Traceback", result.stderr)

    def test_missing_entry_body_is_rejected(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            skill_root = Path(temp_dir)
            data_dir = skill_root / "data"
            codegen_dir = skill_root / "references" / "codegen"
            data_dir.mkdir()
            codegen_dir.mkdir(parents=True)
            (data_dir / "function_index.tsv").write_text(
                "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
                "foo\tbuiltin\tmisc\tfoo()\tmissing.md\tfoo\t\tFallback only\n",
                encoding="utf-8",
            )

            result = subprocess.run(
                [
                    sys.executable,
                    str(SCRIPT),
                    "--name",
                    "foo",
                    "--tsv",
                    str(data_dir / "function_index.tsv"),
                ],
                capture_output=True,
                text=True,
                check=False,
            )

        self.assertEqual(1, result.returncode)
        self.assertEqual("", result.stdout)
        self.assertIn("entry body not found", result.stderr)
        self.assertIn("missing.md#foo", result.stderr)

    def test_empty_queries_are_rejected(self):
        for option in ("--name", "--kw"):
            with self.subTest(option=option):
                result = subprocess.run(
                    [sys.executable, str(SCRIPT), option, ""],
                    capture_output=True,
                    text=True,
                    check=False,
                )

                self.assertEqual(2, result.returncode)
                self.assertIn("must not be empty", result.stderr)

    def test_non_positive_limit_is_rejected(self):
        for limit in ("0", "-1"):
            with self.subTest(limit=limit):
                result = subprocess.run(
                    [
                        sys.executable,
                        str(SCRIPT),
                        "--kw",
                        "数组",
                        "--limit",
                        limit,
                    ],
                    capture_output=True,
                    text=True,
                    check=False,
                )

                self.assertEqual(2, result.returncode)
                self.assertIn("--limit must be >= 1", result.stderr)

    def test_help_derives_scopes_from_selected_index(self):
        result = subprocess.run(
            [sys.executable, str(SCRIPT), "--help"],
            capture_output=True,
            text=True,
            check=False,
        )

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("可用值从当前 function_index.tsv 读取", result.stdout)
        self.assertNotIn("deprecated", result.stdout)
        self.assertNotIn("--scope third", result.stdout)

    def test_exact_search_uses_anchor_for_duplicate_signatures(self):
        module = load_script()
        with tempfile.TemporaryDirectory() as temp_dir:
            skill_root = Path(temp_dir)
            data_dir = skill_root / "data"
            codegen_dir = skill_root / "references" / "codegen"
            data_dir.mkdir()
            codegen_dir.mkdir(parents=True)

            header = [
                "name",
                "scope",
                "module",
                "signature",
                "page",
                "anchor",
                "tags",
                "summary",
            ]
            rows = [
                [
                    "CAPA",
                    "builtin",
                    "language",
                    "CAPA()",
                    "objects.md",
                    "capa",
                    "",
                    "IMAP summary",
                ],
                [
                    "CAPA",
                    "builtin",
                    "language",
                    "CAPA()",
                    "objects.md",
                    "capa-1",
                    "",
                    "Pop3 summary",
                ],
            ]
            index_text = "\n".join(
                ["\t".join(header), *("\t".join(row) for row in rows)]
            )
            (data_dir / "function_index.tsv").write_text(
                index_text + "\n", encoding="utf-8"
            )
            (codegen_dir / "objects.md").write_text(
                "# Objects\n\n"
                "## `CAPA()`\n\n"
                "声明：function\n\n"
                "对象：IMAP\n\n"
                "## `CAPA()`\n\n"
                "声明：function\n\n"
                "对象：Pop3\n\n"
                "",
                encoding="utf-8",
            )

            stdout = io.StringIO()
            with redirect_stdout(stdout):
                status = module.main(
                    [
                        "--name",
                        "CAPA",
                        "--tsv",
                        str(data_dir / "function_index.tsv"),
                    ]
                )

        output = stdout.getvalue()
        self.assertEqual(0, status)
        self.assertEqual(1, output.count("对象：IMAP"))
        self.assertEqual(1, output.count("对象：Pop3"))
        self.assertIn("objects.md#capa -->", output)
        self.assertIn("objects.md#capa-1 -->", output)

    def test_exact_qualified_member_query_returns_only_member_overloads(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_unified_api_fixture(Path(temp_dir))

            result = self.run_lookup(
                tsv_path, "--name", "demounit.document.save"
            )

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertEqual(2, result.stdout.count("#### `Save("))
        self.assertNotIn("#### `Name`", result.stdout)
        self.assertNotIn("### `Other`", result.stdout)

    def test_exact_unit_class_query_returns_its_h4_subtree_only(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_unified_api_fixture(Path(temp_dir))

            result = self.run_lookup(
                tsv_path, "--name", "DemoUnit.Document"
            )

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("### `Document`", result.stdout)
        self.assertIn("#### `Save(path)`", result.stdout)
        self.assertIn("#### `Name`", result.stdout)
        self.assertNotIn("### `Other`", result.stdout)

    def test_exact_unit_query_returns_the_complete_unit_subtree(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_unified_api_fixture(Path(temp_dir))

            result = self.run_lookup(tsv_path, "--name", "DemoUnit")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("## `DemoUnit`", result.stdout)
        self.assertIn("### `Open()`", result.stdout)
        self.assertIn("### `Document`", result.stdout)
        self.assertIn("#### `Save(path)`", result.stdout)
        self.assertIn("### `Other`", result.stdout)
        self.assertNotIn("## `Widget`", result.stdout)

    def test_exact_class_query_returns_all_class_members(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_unified_api_fixture(Path(temp_dir))

            result = self.run_lookup(tsv_path, "--name", "widget")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("## `Widget`", result.stdout)
        self.assertIn("### `Create()`\n\n声明：class function", result.stdout)
        self.assertIn("### `Count`\n\n声明：static field", result.stdout)
        self.assertNotIn("## `DemoUnit`", result.stdout)

    def test_exact_class_in_mixed_page_stops_at_next_h2(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_mixed_api_fixture(Path(temp_dir))

            result = self.run_lookup(tsv_path, "--name", "Widget")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("### `Open()`", result.stdout)
        self.assertNotIn("## `Parse(text)`", result.stdout)

    def test_exact_function_in_mixed_page_keeps_h3_example_only(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_mixed_api_fixture(Path(temp_dir))

            result = self.run_lookup(tsv_path, "--name", "Parse")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("### 示例", result.stdout)
        self.assertIn("## `NotBoundary`", result.stdout)
        self.assertNotIn("## `Runtime`", result.stdout)

    def test_exact_unit_in_mixed_page_stops_before_following_class(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_mixed_api_fixture(Path(temp_dir))

            result = self.run_lookup(tsv_path, "--name", "Runtime")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertIn("#### `Save()`", result.stdout)
        self.assertNotIn("## `Other`", result.stdout)

    def test_unqualified_member_query_returns_all_owners(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_unified_api_fixture(Path(temp_dir))

            result = self.run_lookup(tsv_path, "--name", "Save")

        self.assertEqual(0, result.returncode, result.stderr)
        self.assertEqual(3, result.stdout.count("#### `Save("))
        self.assertIn("按路径保存。", result.stdout)
        self.assertIn("按模式保存。", result.stdout)
        self.assertIn("保存其他对象。", result.stdout)

    def test_keyword_search_includes_unified_fields_and_declaration_term(self):
        with tempfile.TemporaryDirectory() as temp_dir:
            tsv_path = self.write_unified_api_fixture(Path(temp_dir))

            qualified = self.run_lookup(
                tsv_path, "--kw", "Widget.Create"
            )
            visibility = self.run_lookup(
                tsv_path, "--kw", "protected", "method"
            )

        self.assertEqual(0, qualified.returncode, qualified.stderr)
        self.assertIn("Widget.Create\tclass function\tCreate()", qualified.stdout)
        self.assertNotIn("static function", qualified.stdout)
        self.assertEqual(0, visibility.returncode, visibility.stderr)
        self.assertIn("DemoUnit.Document.Save", visibility.stdout)

    def test_skill_documents_qualified_queries_and_legacy_index_compatibility(self):
        skill_text = SKILL_MD.read_text(encoding="utf-8")
        text = API_WORKFLOW.read_text(encoding="utf-8")

        self.assertIn("workflows/api-lookup.md", skill_text)
        self.assertIn("完全限定名称", text)
        self.assertIn("--scope module", text)
        self.assertIn("qualified_name", text)
        self.assertIn("owner、kind、", text)
        self.assertIn("binding 和 visibility", text)
        self.assertIn("class function", text)
        self.assertIn("8 列索引", text)
        self.assertIn("13 列索引", text)

    def test_skill_routes_workflows_and_keeps_stop_resident(self):
        skill_text = SKILL_MD.read_text(encoding="utf-8")
        skill_root = SKILL_MD.parent
        instruction_paths = sorted((skill_root / "workflows").glob("*.md"))

        for path in instruction_paths:
            self.assertIn(path.relative_to(skill_root).as_posix(), skill_text)
        self.assertIn("🔴 CHECKPOINT · 🛑 STOP", skill_text)
        self.assertIn("rc=1 不进入上述“API 缺失”分支", skill_text)
        self.assertIn("原样名称或 `qualified_name`", skill_text)
        self.assertIn("`profile_status=not_profiled`", skill_text)

    def test_skill_instruction_package_has_no_reverse_dependencies(self):
        skill_root = SKILL_MD.parent
        package_text = "\n".join(
            path.read_text(encoding="utf-8")
            for path in [SKILL_MD, *sorted((skill_root / "workflows").glob("*.md"))]
        )

        for forbidden in (
            "tsl-syntax-reference",
            "references/maintenance.md",
            "tools/tsl-codegen",
            "scripts/build_dictionary.py",
            "AGENTS.md",
            "CONTEXT.md",
            "docs/adr/",
        ):
            self.assertNotIn(forbidden, package_text)

    def test_repository_maintenance_doc_owns_api_data_maintenance(self):
        readme = (
            SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "MAINTENANCE.md"
        ).read_text(encoding="utf-8")
        for required in (
            "TSL API 数据维护",
            "class_lookup.py --check",
            "tools/tsl-codegen/scripts/build_index.py",
            "--skill-dir skills/tsl-api-reference",
            "tools/tsl-codegen/scripts/build_dictionary.py",
            "beautifulsoup4",
            "profile_status=not_profiled",
        ):
            self.assertIn(required, readme)

        user_readme = (
            SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "README.md"
        ).read_text(encoding="utf-8")
        for maintainer_only in (
            "class_lookup.py --check",
            "beautifulsoup4",
            "--source-root <exported-html-dir>",
        ):
            self.assertNotIn(maintainer_only, user_readme)
        self.assertIn("profile_status=not_profiled", user_readme)

    def test_public_cli_help_owns_status_and_exit_contracts(self):
        skill_root = SKILL_MD.parent
        expected = {
            "lookup.py": ("退出码：", "--name 无匹配", "page#anchor"),
            "class_lookup.py": ("输出状态：", "contract_status", "退出码："),
            "dictionary_lookup.py": ("status: ambiguous", "status: no_match", "退出码："),
        }

        for script_name, fragments in expected.items():
            with self.subTest(script=script_name):
                result = subprocess.run(
                    [
                        sys.executable,
                        str(skill_root / "scripts" / script_name),
                        "--help",
                    ],
                    capture_output=True,
                    text=True,
                    check=False,
                )
                self.assertEqual(0, result.returncode, result.stderr)
                for fragment in fragments:
                    self.assertIn(fragment, result.stdout)

    def test_keyword_search_includes_tags_and_summary(self):
        module = load_script()
        rows = [
            {
                "name": "demo",
                "signature": "demo()",
                "module": "base",
                "tags": "数组 列表",
                "summary": "返回示例值",
            }
        ]

        self.assertEqual(rows, module.search_keyword(rows, ["数组"]))
        self.assertEqual(rows, module.search_keyword(rows, ["返回示例"]))


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