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

try:
    import yaml
except ImportError:
    yaml = None

BS4_AVAILABLE = importlib.util.find_spec("bs4") is not None


TOOL_ROOT = Path(__file__).resolve().parents[1]
REPO_ROOT = TOOL_ROOT.parents[1]
CONVERT = TOOL_ROOT / "scripts" / "convert_tsf.py"
GENERATE = TOOL_ROOT / "scripts" / "generate.py"
LINT = TOOL_ROOT / "scripts" / "lint.py"
BUILD_INDEX = TOOL_ROOT / "scripts" / "build_index.py"
BUILD_DICTIONARY = TOOL_ROOT / "scripts" / "build_dictionary.py"
LOOKUP = REPO_ROOT / "skills" / "tsl-api-reference" / "scripts" / "lookup.py"
STANDARD = TOOL_ROOT / "STANDARD.md"
README = TOOL_ROOT / "README.md"
EXAMPLE_JSON = TOOL_ROOT / "examples" / "example.json"
EXAMPLE_YAML = TOOL_ROOT / "examples" / "example.yaml"
SKILL = REPO_ROOT / "skills" / "tsl-api-reference" / "SKILL.md"
API_WORKFLOW = (
    REPO_ROOT / "skills" / "tsl-api-reference" / "workflows" / "api-lookup.md"
)


def load_build_dictionary():
    spec = importlib.util.spec_from_file_location(
        "tsl_codegen_build_dictionary", BUILD_DICTIONARY
    )
    if spec is None or spec.loader is None:
        raise RuntimeError(f"cannot load {BUILD_DICTIONARY}")
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


class UnifiedPipelineTest(unittest.TestCase):
    def setUp(self):
        self.temp_dir = tempfile.TemporaryDirectory()
        self.root = Path(self.temp_dir.name)
        self.skill_dir = self.root / "skills" / "tsl-api-reference"

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

    @unittest.skipUnless(BS4_AVAILABLE, "beautifulsoup4 is not installed")
    def test_dictionary_notes_escape_source_numbering(self):
        module = load_build_dictionary()
        page = module.DictionaryPage(
            kind="table",
            scope="macro",
            title="示例",
            source_path=("宏观",),
            source_file="example.html",
            table=module.DictionaryTable(
                name="示例",
                table_id="1",
                extract_method="",
                access_code="",
                update_info=("1、更新频率：月度",),
            ),
            fields=(),
            notes=("1. 第一条说明", "2. 第二条说明"),
            examples=(),
        )

        rendered = module.render_dictionary_page(page)

        self.assertIn("- 1\\. 第一条说明", rendered)
        self.assertIn("- 2\\. 第二条说明", rendered)
        self.assertIn("- 1、更新频率：月度", rendered)

    def run_command(self, *args, cwd=None):
        return subprocess.run(
            [sys.executable, *map(str, args)],
            cwd=cwd or REPO_ROOT,
            capture_output=True,
            text=True,
            encoding="utf-8",
            check=False,
        )

    def run_pipeline(self, sources, queries):
        tsf_paths = []
        for name, source in sources:
            tsf_path = self.root / f"{name}.tsf"
            tsf_path.write_text(textwrap.dedent(source).lstrip(), encoding="utf-8")
            tsf_paths.append(tsf_path)

        page_path = "base/mixed"
        recordings = {}
        recording_paths = {}
        for output_format in ("json", "yaml"):
            recording = self.root / f"mixed.{output_format}"
            convert = self.run_command(
                CONVERT,
                *tsf_paths,
                "--format",
                output_format,
                "--module",
                "示例 / 混合 API",
                "--path",
                page_path,
                "--output",
                recording,
            )
            self.assertEqual(0, convert.returncode, convert.stderr)
            recording_paths[output_format] = recording
            text = recording.read_text(encoding="utf-8")
            recordings[output_format] = (
                json.loads(text) if output_format == "json" else yaml.safe_load(text)
            )
        self.assertEqual(recordings["json"], recordings["yaml"])

        markdown = (
            self.skill_dir / "references" / "codegen" / "project" / f"{page_path}.md"
        )
        generated_markdown = {}
        for output_format, recording in recording_paths.items():
            generate = self.run_command(GENERATE, "--file", recording, cwd=self.root)
            self.assertEqual(0, generate.returncode, generate.stderr)
            self.assertTrue(markdown.is_file())
            generated_markdown[output_format] = markdown.read_text(encoding="utf-8")
            lint = self.run_command(LINT, "--file", markdown, "--strict")
            self.assertEqual(0, lint.returncode, lint.stdout + lint.stderr)
        self.assertEqual(generated_markdown["json"], generated_markdown["yaml"])

        build = self.run_command(BUILD_INDEX, "--skill-dir", self.skill_dir)
        self.assertEqual(0, build.returncode, build.stderr)
        check = self.run_command(BUILD_INDEX, "--skill-dir", self.skill_dir, "--check")
        self.assertEqual(0, check.returncode, check.stderr)

        lookup_outputs = {}
        for query in queries:
            lookup = self.run_command(
                LOOKUP,
                "--name",
                query,
                "--tsv",
                self.skill_dir / "data" / "function_index.tsv",
            )
            self.assertEqual(0, lookup.returncode, lookup.stderr)
            lookup_outputs[query] = lookup.stdout
        return (
            recordings["json"],
            generated_markdown["json"],
            lookup_outputs,
        )

    @unittest.skipUnless(yaml is not None, "pyyaml is not installed")
    def test_mixed_pipeline_convert_generate_lint_index_lookup(self):
        recording, markdown, lookups = self.run_pipeline(
            sources=[
                (
                    "Widget",
                    """
                    type Widget = class
                    /// 组件
                    public
                        /// 重命名组件
                        /// @param: name 组件名称
                        function Rename(name: string): string;
                        /// 创建组件
                        class function Create(): Widget;
                        /// 组件标题
                        property Title: string read title_ write title_;
                        /// 原始标题
                        property RawTitle read title_;
                        /// 标题存储
                        title_: string;
                        /// 组件数量
                        static Count: integer;
                        /// 默认类型
                        const DefaultKind = 1;
                        /// 最大数量
                        static const Maximum = 100;
                    end;
                    """,
                ),
                (
                    "OpenWidget",
                    """
                    function OpenWidget(path: string): Widget;
                    begin
                        /// 打开组件
                        /// @param: path 组件路径
                        return nil;
                    end;
                    """,
                ),
                (
                    "DemoUnit",
                    """
                    unit DemoUnit;
                    /// 接口
                    interface
                    /// 默认大小
                    const DefaultSize = 100;
                    /// 当前文档
                    var CurrentDocument: Document;
                    /// 打开文档
                    /// @param: path 文档路径
                    function OpenDocument(path: string): Document;
                    type Document = class
                    /// 文档
                    public
                        /// 保存
                        function Save(): boolean;
                        /// 创建文档
                        class function Create(): Document;
                        /// 文档标题
                        property Title: string read title_ write title_;
                    private
                        title_: string;
                    end;
                    implementation
                    end.
                    """,
                ),
            ],
            queries=("Widget", "OpenWidget", "DemoUnit.Document"),
        )

        self.assertEqual(
            ["class", "function", "unit"],
            [item["kind"] for item in recording["declarations"]],
        )
        widget_members = recording["declarations"][0]["members"]
        raw_title = next(item for item in widget_members if item["name"] == "RawTitle")
        self.assertEqual("", raw_title["type"])
        self.assertEqual([], raw_title["params"])
        self.assertIn("## `Widget`\n\n声明：class", markdown)
        self.assertIn("## `OpenWidget(path)`\n\n声明：function", markdown)
        self.assertIn("## `DemoUnit`\n\n声明：unit", markdown)
        self.assertIn("### `Create()`\n\n声明：class function", lookups["Widget"])
        self.assertIn("### `Rename(name)`\n\n声明：function", lookups["Widget"])
        self.assertNotIn("## `OpenWidget()`", lookups["Widget"])
        self.assertIn("声明：function", lookups["OpenWidget"])
        self.assertNotIn("## `DemoUnit`", lookups["OpenWidget"])
        self.assertIn("#### `Save()`\n\n声明：function", lookups["DemoUnit.Document"])
        self.assertIn(
            "#### `Create()`\n\n声明：class function",
            lookups["DemoUnit.Document"],
        )

    def test_standard_and_readme_describe_unified_declarations(self):
        standard = STANDARD.read_text(encoding="utf-8")
        readme = README.read_text(encoding="utf-8")
        skill = SKILL.read_text(encoding="utf-8")
        api_workflow = API_WORKFLOW.read_text(encoding="utf-8")

        self.assertTrue(standard.startswith("# TSL API 文档标准\n"))
        self.assertNotIn("根字段 `functions`、`class`、`unit`", standard)
        self.assertNotIn("每个 markdown 叶子页只记录一种页面类型", standard)
        for fragment in (
            "declarations",
            "声明：function",
            "声明：class",
            "声明：unit",
            "顶级 function",
            "顶级 class",
            "顶级 unit",
            "综合示例",
        ):
            self.assertIn(fragment, standard)
        for heading in (
            "### function\n",
            "### class\n",
            "### unit\n",
            "### 综合示例\n",
        ):
            self.assertIn(heading, standard)
        for heading in (
            "### 顶级 function\n",
            "### 顶级 class\n",
            "### 顶级 unit\n",
            "### 混合页面示例\n",
        ):
            self.assertNotIn(heading, standard)
        for fragment in (
            "declarations",
            "混合输入",
            "OpenXmlAttribute.tsf",
            "OpenXmlRuntime.tsf",
            "13 列",
            "property 类型可选",
        ):
            self.assertIn(fragment, readme)
        self.assertIn("## `OpenXmlAttribute`\n\n声明：class", readme)
        self.assertIn(
            "### `CreateVirtual(position, row_index, _story)`\n\n"
            "声明：class function",
            readme,
        )
        self.assertNotIn("property/field/variable 缺少类型", readme)
        self.assertIn(
            "generate.py --file tmp/my-api.yaml",
            readme,
        )
        self.assertIn(
            "generate.py --file tmp/my-api.json",
            readme,
        )
        self.assertIn(
            "generate.py --dir tmp/api-recordings",
            readme,
        )
        self.assertIn("只处理目录中的直属文件", readme)
        self.assertNotIn("generate.py tmp/my-api.yaml", readme)
        self.assertNotIn("generate.py tmp/my-api.json", readme)
        self.assertNotIn("已废弃，请使用 --file", readme)
        self.assertIn("workflows/api-lookup.md", skill)
        self.assertIn("混合页面", api_workflow)
        self.assertIn("page#anchor", api_workflow)

    def test_standard_class_methods_reuse_top_level_function_structure(self):
        standard = STANDARD.read_text(encoding="utf-8")
        class_section = standard.split("### class\n", 1)[1].split("### unit\n", 1)[0]
        normalized_class = " ".join(class_section.split())
        normalized_standard = " ".join(standard.split())

        self.assertIn("正文结构与顶级 function 相同", normalized_class)
        self.assertIn("实例 function 和 class function", normalized_class)
        self.assertIn("标题及其子标题整体下移一级", normalized_class)
        self.assertEqual(
            2,
            class_section.count("<!-- tags: OpenXml 属性 创建 -->"),
        )
        self.assertIn(
            "所有 class member 都可以包含可选的 `tags`",
            normalized_standard,
        )
        self.assertIn(
            "### `CreateVirtual(position, row_index, _story)`",
            class_section,
        )
        self.assertIn("声明：class function", class_section)
        self.assertIn("返回：ParagraphSegment", class_section)

    @unittest.skipUnless(yaml is not None, "pyyaml is not installed")
    def test_json_and_yaml_examples_use_the_same_complete_structure(self):
        json_data = json.loads(EXAMPLE_JSON.read_text(encoding="utf-8"))
        yaml_data = yaml.safe_load(EXAMPLE_YAML.read_text(encoding="utf-8"))

        expected_root = {"module", "path", "declarations"}
        self.assertEqual(expected_root, set(json_data))
        self.assertEqual(expected_root, set(yaml_data))
        self.assertEqual(json_data, yaml_data)

        declarations = {item["kind"]: item for item in json_data["declarations"]}
        self.assertEqual({"function", "class", "unit"}, set(declarations))
        self.assertEqual(
            ["class", "function", "unit"],
            [item["kind"] for item in json_data["declarations"]],
        )

        class_members = declarations["class"]["members"]
        self.assertEqual(
            [
                "create",
                "CreateVirtual",
                "Prefix",
                "NamespaceUri",
                "value_",
                "Count",
                "DefaultPrefix",
                "MaximumAttributes",
            ],
            [member["name"] for member in class_members],
        )
        self.assertIn("class", [m.get("binding") for m in class_members])
        self.assertEqual(
            "string",
            next(m for m in class_members if m["name"] == "Prefix")["type"],
        )
        self.assertNotIn(
            "type",
            next(m for m in class_members if m["name"] == "NamespaceUri"),
        )
        self.assertEqual(
            {"method", "property", "field", "constant"},
            {member["kind"] for member in class_members},
        )
        self.assertTrue(
            next(m for m in class_members if m["name"] == "Count")["static"]
        )
        self.assertTrue(
            next(m for m in class_members if m["name"] == "MaximumAttributes")["static"]
        )

        unit_members = declarations["unit"]["members"]
        self.assertEqual(
            {"constant", "variable", "function", "class"},
            {member["kind"] for member in unit_members},
        )
        document = next(member for member in unit_members if member["kind"] == "class")
        self.assertEqual(
            {("method", "instance"), ("method", "class"), ("property", None)},
            {(m["kind"], m.get("binding")) for m in document["members"]},
        )

        page = (
            self.skill_dir
            / "references"
            / "codegen"
            / "project"
            / f"{json_data['path']}.md"
        )
        markdown_by_format = {}
        for output_format, recording in (
            ("json", EXAMPLE_JSON),
            ("yaml", EXAMPLE_YAML),
        ):
            generate = self.run_command(GENERATE, "--file", recording, cwd=self.root)
            self.assertEqual(0, generate.returncode, generate.stderr)
            markdown_by_format[output_format] = page.read_bytes()
            lint = self.run_command(LINT, "--file", page, "--strict")
            self.assertEqual(0, lint.returncode, lint.stdout + lint.stderr)
        self.assertEqual(markdown_by_format["json"], markdown_by_format["yaml"])


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