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


SCRIPT = Path(__file__).parents[1] / "scripts" / "enrich_metadata.py"


def load_script():
    spec = importlib.util.spec_from_file_location("tsl_enrich_metadata", SCRIPT)
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


class SourceCatalogTest(unittest.TestCase):
    def setUp(self):
        self.module = load_script()
        self.temp_dir = tempfile.TemporaryDirectory()
        self.docs = Path(self.temp_dir.name)

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

    def write_corpus(self, corpus, rows, descriptions):
        root = self.docs / corpus
        pages = root / "pages"
        pages.mkdir(parents=True)
        manifest = [
            "id\ttitle\turl\tpath\tsource_bytes\tutf8_bytes\tsha256\tstatus"
        ]
        for page_id, title in rows:
            manifest.append(
                f"{page_id}\t{title}\thttp://example/{page_id}\t"
                f"pages/{page_id}.html\t1\t1\tsha\tok"
            )
            description = descriptions[page_id]
            (pages / f"{page_id}.html").write_text(
                "<div id='help_content'><h3>"
                + title
                + "</h3><div><div class='DescriteTitle'>简述</div></div>"
                + f"<div>{description}</div></div>",
                encoding="utf-8",
            )
        (root / "manifest.tsv").write_text(
            "\n".join(manifest) + "\n", encoding="utf-8"
        )

    def test_exact_matching_preserves_underscores_and_prefers_scope_corpus(self):
        self.write_corpus(
            "tsl_base",
            [("1", "alpha_test")],
            {"1": "基础解释器说明"},
        )
        self.write_corpus(
            "net_function",
            [("2", "alpha_test"), ("3", "alphaTest")],
            {"2": "NET 说明", "3": "无下划线的另一个函数"},
        )
        catalog = self.module.SourceCatalog.from_docs_root(self.docs)

        builtin = self.module.EntryContext(
            name="alpha_test",
            signature="alpha_test()",
            kind="function",
            scope="builtin",
            page="builtin/demo.md",
            summary="原说明",
        )
        dotnet = self.module.EntryContext(
            name="alpha_test",
            signature="alpha_test()",
            kind="function",
            scope="dotnet",
            page="dotnet/demo.md",
            summary="原说明",
        )

        self.assertEqual("tsl_base", catalog.match(builtin)[0].corpus)
        self.assertEqual("net_function", catalog.match(dotnet)[0].corpus)
        self.assertNotIn(
            "alphaTest", {record.title for record in catalog.match(dotnet)}
        )

    def test_duplicate_titles_remain_visible_for_audit(self):
        self.write_corpus(
            "net_function",
            [("1", "same_name"), ("2", "same_name")],
            {"1": "说明一", "2": "说明二"},
        )
        catalog = self.module.SourceCatalog.from_docs_root(self.docs)
        entry = self.module.EntryContext(
            name="same_name",
            signature="same_name()",
            kind="function",
            scope="dotnet",
            page="dotnet/demo.md",
            summary="原说明",
        )

        self.assertEqual(["1", "2"], [item.page_id for item in catalog.match(entry)])


class DescriptionQualityTest(unittest.TestCase):
    def setUp(self):
        self.module = load_script()

    def entry(self, **overrides):
        values = {
            "name": "profitRatio",
            "signature": "profitRatio(report_date)",
            "kind": "function",
            "scope": "dotnet",
            "page": "dotnet/financial/profitability.md",
            "summary": "总资产收益率(%)。",
        }
        values.update(overrides)
        return self.module.EntryContext(**values)

    def test_removes_only_terminal_sentence_punctuation(self):
        entry = self.entry(summary="总资产收益率(%)。")

        self.assertEqual(
            "返回总资产收益率(%)",
            self.module.improve_description(entry, []),
        )

    def test_preserves_formula_and_identifiers(self):
        entry = self.entry(
            summary="总资产报酬率(%)=利润总额/平均资产总额*100。"
        )

        self.assertEqual(
            "计算总资产报酬率(%)，公式为利润总额/平均资产总额*100",
            self.module.improve_description(entry, []),
        )

    def test_rejects_source_process_prose(self):
        entry = self.entry(summary="返回指定值")
        source = self.module.SourceRecord(
            corpus="net_function",
            page_id="1",
            title="profitRatio",
            page=Path("1.html"),
            description="Windows 已验证通过，Linux 返回 not found",
        )

        self.assertEqual(
            "返回指定值", self.module.improve_description(entry, [source])
        )

    def test_current_environment_can_be_real_api_semantics(self):
        self.assertEqual(
            "获取当前环境时间系统参数",
            self.module.safe_source_description("获取当前环境时间系统参数。"),
        )

    def test_property_access_is_stated_explicitly(self):
        entry = self.entry(
            name="Host",
            signature="Host",
            kind="property",
            scope="builtin",
            page="builtin/object/ftp.md",
            summary="远程服务器地址。",
            access="read/write",
            owner="FTP",
        )

        self.assertEqual(
            "获取或设置远程服务器地址",
            self.module.improve_description(entry, []),
        )

    def test_boolean_property_description_is_idempotent(self):
        entry = self.entry(
            name="UseTLS",
            signature="UseTLS",
            kind="property",
            scope="builtin",
            page="builtin/object/imap.md",
            summary="是否采用 SSL 连接",
            access="read/write",
            owner="IMAP",
        )

        first = self.module.improve_description(entry, [])
        repeated = self.module.improve_description(
            self.module.EntryContext(
                **{**entry.__dict__, "summary": first}
            ),
            [],
        )

        self.assertEqual("控制是否采用 SSL 连接", first)
        self.assertEqual(first, repeated)

    def test_low_confidence_sentence_is_kept_except_terminal_punctuation(self):
        entry = self.entry(
            name="opaqueApi",
            page="dotnet.md",
            summary="根据调用上下文处理结果，具体规则取决于输入。",
        )

        self.assertEqual(
            "根据调用上下文处理结果，具体规则取决于输入",
            self.module.improve_description(entry, []),
        )

    def test_short_description_is_not_replaced_by_ambiguous_source_text(self):
        entry = self.entry(
            name="clRed",
            page="builtin/color.md",
            summary="红色",
        )
        sources = [
            self.module.SourceRecord(
                corpus="net_function",
                page_id="1",
                title="clRed",
                page=Path("1.html"),
                description="定义",
            )
        ]

        self.assertEqual("红色", self.module.improve_description(entry, sources))

    def test_existing_action_word_is_normalized_without_duplicate_verb(self):
        getter = self.entry(
            name="getCValue",
            page="builtin/color.md",
            summary="得到颜色的 CMYK 模式 C 值。",
        )
        writer = self.entry(
            name="write",
            page="builtin/cgi.md",
            summary="输出字符串。",
        )

        self.assertEqual(
            "获取颜色的 CMYK 模式 C 值",
            self.module.improve_description(getter, []),
        )
        self.assertEqual("输出字符串", self.module.improve_description(writer, []))

    def test_equation_inside_algorithm_sentence_is_not_rewritten_as_formula(self):
        entry = self.entry(
            name="se_Gauss",
            page="builtin/optimization.md",
            summary="用高斯消去法求解线性方程组 AX = B",
        )

        self.assertEqual(
            "用高斯消去法求解线性方程组 AX = B",
            self.module.improve_description(entry, []),
        )

    def test_builtin_class_gets_a_purpose_description(self):
        entry = self.entry(
            name="TStringList",
            signature="TStringList",
            kind="class",
            scope="builtin",
            page="builtin/object/tstringlist.md",
            summary="TStringList 内置对象",
        )

        self.assertEqual(
            "提供字符串集合存储、查找、排序和名称值管理能力的内置对象",
            self.module.improve_description(entry, []),
        )

    def test_long_property_explanation_is_not_wrapped_in_access_boilerplate(self):
        entry = self.entry(
            name="CommaTextW",
            signature="CommaTextW",
            kind="property",
            scope="builtin",
            page="builtin/object/tstringlist.md",
            summary=(
                "功能同 CommaText，区别是在读取时返回宽字节字符串，"
                "而 CommaText 返回多字节字符串"
            ),
            access="read/write",
            owner="TStringList",
        )

        self.assertEqual(
            entry.summary, self.module.improve_description(entry, [])
        )

    def test_short_read_and_load_phrases_are_normalized(self):
        read_entry = self.entry(
            name="read",
            page="builtin/object/tstream.md",
            summary="读出内容",
        )
        load_entry = self.entry(
            name="loadFromFile",
            page="builtin/object/tstringlist.md",
            summary="从指定的文件中装载内容",
        )

        self.assertEqual(
            "读取内容", self.module.improve_description(read_entry, [])
        )
        self.assertEqual(
            "从指定的文件中加载内容",
            self.module.improve_description(load_entry, []),
        )

    def test_embedded_read_out_is_normalized_without_duplicate_prefix(self):
        entry = self.entry(
            name="readExcelSheets",
            page="dotnet.md",
            summary="从Excel文件中读出Sheets列表。",
        )

        self.assertEqual(
            "从Excel文件中读取Sheets列表",
            self.module.improve_description(entry, []),
        )

    def test_existing_decomposition_verb_is_not_prefixed(self):
        entry = self.entry(
            name="decodeGraphGroup",
            page="builtin/graph.md",
            summary="分解图形组合并写入输出参数",
        )

        self.assertEqual(
            entry.summary, self.module.improve_description(entry, [])
        )

    def test_comparison_expression_is_not_rewritten_as_formula(self):
        entry = self.entry(
            name="stockStepAmount",
            page="dotnet/financial/stock-capital-flow.md",
            summary="分档区间 V1<=Value<V2 的成交金额",
        )

        self.assertEqual(
            entry.summary, self.module.improve_description(entry, [])
        )

    def test_financial_action_sentence_is_not_prefixed_with_return(self):
        for summary in (
            "提取股票的财务比率，与系统证券相关",
            "根据报告期获取财务指标",
            "将日线数据转换为周线数据",
            "统计指定报告期的基金数量",
            "生成指定区间的分析结果",
        ):
            with self.subTest(summary=summary):
                entry = self.entry(
                    name="annualRatio",
                    page="dotnet/financial/financial-analysis.md",
                    summary=summary,
                )

                self.assertEqual(
                    entry.summary, self.module.improve_description(entry, [])
                )

    def test_financial_action_clauses_are_not_prefixed_with_return(self):
        for summary in (
            "用于生成投资组合结果名称",
            "如果存在对应记录则返回证券代码",
            "对矩阵进行正交处理",
            "从净值表中获取基金列表",
            "由交易价格倒推出隐含波动率",
            "按照周期读取数据",
            "与系统参数证券和日期相关",
        ):
            with self.subTest(summary=summary):
                entry = self.entry(
                    name="financialHelper",
                    page="dotnet/financial/financial-analysis.md",
                    summary=summary,
                )

                self.assertEqual(
                    entry.summary, self.module.improve_description(entry, [])
                )

    def test_financial_boolean_description_uses_judgment_wording(self):
        entry = self.entry(
            name="isBankBond",
            page="dotnet/financial/bond-basic-info.md",
            summary="是否银行间债券。如果是，返回1，否则返回0",
        )

        self.assertEqual(
            "判断是否银行间债券。如果是，返回1，否则返回0",
            self.module.improve_description(entry, []),
        )

    def test_function_boilerplate_is_removed_from_action_description(self):
        entry = self.entry(
            name="cb_ytm",
            page="dotnet/financial/convertible-bond.md",
            summary="该函数采用牛顿迭代法获取可转债的到期收益率",
        )

        self.assertEqual(
            "采用牛顿迭代法获取可转债的到期收益率",
            self.module.improve_description(entry, []),
        )

    def test_financial_numbered_explanation_is_not_prefixed_with_return(self):
        entry = self.entry(
            name="financialMode",
            page="dotnet/financial/financial-analysis.md",
            summary="1 表示合并报表，2 表示母公司报表",
        )

        self.assertEqual(
            entry.summary, self.module.improve_description(entry, [])
        )

    def test_related_data_placeholder_is_made_explicit(self):
        entry = self.entry(
            name="cb_downPeriodConversionPeriod",
            page="dotnet/financial/convertible-bond.md",
            summary="CB_DownPeriodConversionPeriod 相关函数",
        )

        self.assertEqual(
            "返回 CB_DownPeriodConversionPeriod 对应的数据",
            self.module.improve_description(entry, []),
        )

    def test_reviewed_generic_descriptions_use_precise_purpose_text(self):
        cases = (
            (
                "sf_Normal",
                "统计分布相关函数。",
                "计算正态分布函数值",
            ),
            (
                "readFile",
                "文件访问函数相关函数。",
                "读取本地文件中的数据",
            ),
            (
                "unicodeEsc2",
                "多语言支持函数相关函数。",
                "将字符串转换为 \\uxxxx 形式的 Unicode 编码串",
            ),
        )
        for name, summary, expected in cases:
            with self.subTest(name=name):
                entry = self.entry(name=name, page="dotnet.md", summary=summary)
                self.assertEqual(
                    expected, self.module.improve_description(entry, [])
                )

    def test_reviewed_awkward_descriptions_are_rewritten_directly(self):
        cases = (
            (
                "encodeGraph",
                "返回根据指定的类型、名称、数据以及属性数组生成的图形。",
                "根据指定的类型、名称、数据和属性数组生成图形并返回",
            ),
            (
                "dupeString",
                "返回将指定的字符串AText重复ACount次后的字符串",
                "将字符串 AText 重复 ACount 次并返回结果",
            ),
            (
                "stockpjcj",
                "返回:区间平均成交,区间成交金额/区间成交量",
                "返回区间平均成交价，计算公式为区间成交金额 / 区间成交量",
            ),
        )
        for name, summary, expected in cases:
            with self.subTest(name=name):
                entry = self.entry(
                    name=name, page="builtin/string.md", summary=summary
                )
                self.assertEqual(
                    expected, self.module.improve_description(entry, [])
                )


class TagDerivationTest(unittest.TestCase):
    def setUp(self):
        self.module = load_script()

    def test_array_deduplication_gets_controlled_chinese_and_english_aliases(self):
        entry = self.module.EntryContext(
            name="arrDropDuplicate",
            signature="arrDropDuplicate(data)",
            kind="function",
            scope="builtin",
            page="builtin/array.md",
            summary="删除数组中的重复元素",
        )
        page = self.module.PageContext(
            title="Builtin / 数组", path="builtin/array.md"
        )

        tags = self.module.derive_tags(entry, page)

        self.assertTrue(
            {"数组", "列表", "去重", "删除重复", "array", "deduplicate"}
            <= set(tags)
        )
        self.assertLessEqual(len(tags), 12)
        self.assertNotIn("函数", tags)
        self.assertNotIn("builtin", tags)

    def test_file_digest_gets_digest_and_hash_aliases(self):
        entry = self.module.EntryContext(
            name="getMsgDigest",
            signature="getMsgDigest(s, mode)",
            kind="function",
            scope="builtin",
            page="builtin/string.md",
            summary="计算字符串摘要，支持 CRC32、MD5、SHA 和 SM3",
        )
        page = self.module.PageContext(
            title="Builtin / 字符串", path="builtin/string.md"
        )

        tags = self.module.derive_tags(entry, page)

        self.assertTrue(
            {"字符串", "摘要", "哈希", "散列", "digest", "hash"}
            <= set(tags)
        )

    def test_existing_tags_are_preserved_first_and_output_is_idempotent(self):
        entry = self.module.EntryContext(
            name="readFile",
            signature="readFile(alias, file_name)",
            kind="function",
            scope="builtin",
            page="builtin/file.md",
            summary="读取文件内容",
            tags=("自定义", "读取"),
        )
        page = self.module.PageContext(
            title="Builtin / 文件", path="builtin/file.md"
        )

        first = self.module.derive_tags(entry, page)
        second = self.module.derive_tags(
            self.module.EntryContext(**{**entry.__dict__, "tags": tuple(first)}),
            page,
        )

        self.assertEqual(["自定义", "读取"], first[:2])
        self.assertEqual(first, second)

    def test_substrings_do_not_create_digest_or_com_tags(self):
        reshape = self.module.EntryContext(
            name="reshape",
            signature="reshape(data, shape)",
            kind="function",
            scope="builtin",
            page="builtin/array.md",
            summary="重构数组形状",
        )
        uncompress = self.module.EntryContext(
            name="uniuncompress",
            signature="uniuncompress(data, type)",
            kind="function",
            scope="builtin",
            page="builtin/string.md",
            summary="统一解压缩数据",
        )

        reshape_tags = self.module.derive_tags(
            reshape,
            self.module.PageContext("Builtin / 数组", reshape.page),
        )
        uncompress_tags = self.module.derive_tags(
            uncompress,
            self.module.PageContext("Builtin / 字符串", uncompress.page),
        )

        self.assertNotIn("摘要", reshape_tags)
        self.assertNotIn("hash", reshape_tags)
        self.assertNotIn("COM", uncompress_tags)
        self.assertNotIn("OLE", uncompress_tags)

    def test_runtime_and_character_names_do_not_create_time_or_graph_tags(self):
        random_entry = self.module.EntryContext(
            name="randomfrom",
            signature="randomfrom(values)",
            kind="function",
            scope="builtin",
            page="builtin/runtime.md",
            summary="返回随机数组元素",
        )
        char_entry = self.module.EntryContext(
            name="charToByteLen",
            signature="charToByteLen(s, max_len)",
            kind="function",
            scope="builtin",
            page="builtin/string.md",
            summary="计算字符串占用的字节数",
        )

        random_tags = self.module.derive_tags(
            random_entry,
            self.module.PageContext("Builtin / 运行时", random_entry.page),
        )
        char_tags = self.module.derive_tags(
            char_entry,
            self.module.PageContext("Builtin / 字符串", char_entry.page),
        )

        self.assertNotIn("日期时间", random_tags)
        self.assertNotIn("图形", char_tags)

    def test_parameter_name_does_not_create_unrelated_time_domain(self):
        entry = self.module.EntryContext(
            name="futureValue",
            signature=(
                "futureValue(rate, n_periods, payment, present_value, payment_time)"
            ),
            kind="function",
            scope="builtin",
            page="builtin/numeric.md",
            summary="返回一项投资的未来值",
        )
        tags = self.module.derive_tags(
            entry,
            self.module.PageContext("Builtin / 数值计算", entry.page),
        )

        self.assertNotIn("日期时间", tags)

    def test_hash_index_is_not_tagged_as_cryptographic_digest(self):
        entry = self.module.EntryContext(
            name="THashedStringList",
            signature="THashedStringList",
            kind="class",
            scope="builtin",
            page="builtin/object/thashedstringlist.md",
            summary="使用哈希索引加速查找的字符串列表内置对象",
        )
        tags = self.module.derive_tags(
            entry,
            self.module.PageContext(
                "Object / THashedStringList 字符串列表", entry.page
            ),
        )

        self.assertIn("哈希索引", tags)
        self.assertIn("hash", tags)
        self.assertNotIn("摘要", tags)
        self.assertNotIn("digest", tags)

    def test_business_statistics_verb_does_not_add_probability_domain(self):
        entry = self.module.EntryContext(
            name="fundCount",
            signature="fundCount(report_date)",
            kind="function",
            scope="dotnet",
            page="dotnet/financial/fund-holdings.md",
            summary="统计指定报告期的基金数量",
        )
        tags = self.module.derive_tags(
            entry,
            self.module.PageContext("金融 / 基金 / 持仓", entry.page),
        )

        self.assertNotIn("概率", tags)
        self.assertNotIn("statistics", tags)
        self.assertNotIn("统计指定报告期的基金数量", tags)
        self.assertNotIn("证券", tags)

    def test_taxonomy_tags_remove_generic_suffixes(self):
        entry = self.module.EntryContext(
            name="weightedMedian",
            signature="weightedMedian(data, weights)",
            kind="function",
            scope="dotnet",
            page="dotnet/financial/weighted-statistics.md",
            summary="计算加权中位数",
        )
        tags = self.module.derive_tags(
            entry,
            self.module.PageContext(
                "基础算法常见加权统计量及其实现 / 时间相关函数",
                entry.page,
            ),
        )

        self.assertIn("基础算法常见加权统计量", tags)
        self.assertIn("时间", tags)
        self.assertNotIn("基础算法常见加权统计量及其实现", tags)
        self.assertNotIn("时间相关函数", tags)

    def test_taxonomy_tags_drop_parenthetical_explanations_and_punctuation(self):
        entry = self.module.EntryContext(
            name="close",
            signature="close()",
            kind="function",
            scope="dotnet",
            page="dotnet/datawarehouse/after_market.md",
            summary="收盘价，与系统参数股票、时间、周期和复权相关",
        )
        tags = self.module.derive_tags(
            entry,
            self.module.PageContext(
                "数据仓库 / 盘后相关(在当日可用于盘中)", entry.page
            ),
        )

        self.assertIn("数据仓库", tags)
        self.assertIn("盘后", tags)
        self.assertNotIn("(", tags)
        self.assertNotIn(")", tags)
        self.assertNotIn("盘后相关在当日可用于盘中", tags)
        self.assertNotIn("关闭", tags)
        self.assertNotIn("close", tags)

    def test_name_intent_does_not_override_an_explicit_action_clause(self):
        entry = self.module.EntryContext(
            name="openEndFundFilterByFundIndex",
            signature="openEndFundFilterByFundIndex(fund_inds)",
            kind="function",
            scope="dotnet",
            page="dotnet/financial/fund-pool.md",
            summary="根据基金指数获取成分基金，并对基金进行筛选",
        )
        tags = self.module.derive_tags(
            entry,
            self.module.PageContext("金融 / 基金 / 基金池", entry.page),
        )

        self.assertNotIn("打开", tags)
        self.assertNotIn("open", tags)


class MarkdownEnrichmentTest(unittest.TestCase):
    def setUp(self):
        self.module = load_script()

    def test_inserts_tags_after_description_without_touching_code(self):
        text = (
            "# Builtin / 数组\n\n"
            "## `arrDropDuplicate(data)`\n\n"
            "声明：function\n\n"
            "删除数组中的重复元素。\n\n"
            "| 参数 | 类型 | 说明 |\n"
            "| --- | --- | --- |\n"
            "| `data` | array | 输入数组 |\n\n"
            "返回：array\n\n"
            "### 示例\n\n"
            "```tsl\nreturn arrDropDuplicate(array(1, 1));\n```\n"
        )

        result, audit = self.module.enrich_markdown(
            text, "builtin/array.md", self.module.SourceCatalog.empty()
        )
        repeated, repeated_audit = self.module.enrich_markdown(
            result, "builtin/array.md", self.module.SourceCatalog.empty()
        )

        self.assertIn("删除数组中的重复元素\n\n<!-- tags:", result)
        self.assertIn("return arrDropDuplicate(array(1, 1));", result)
        self.assertEqual(result, repeated)
        self.assertEqual(1, audit.changed_descriptions)
        self.assertEqual(1, audit.changed_tags)
        self.assertEqual(0, repeated_audit.changed_descriptions)
        self.assertEqual(0, repeated_audit.changed_tags)


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