"""将 Markdown 测试用例导出为 XMind 思维导图。

用法:
    python scripts/export_XMind.py --base-name 运费核算
    python scripts/export_XMind.py --source-file output/test_cases/运费核算_测试用例.md
"""

import argparse
import re
import uuid
import zipfile
from datetime import datetime
from pathlib import Path
from lxml import etree

REPO_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_INPUT_DIR = REPO_ROOT / "output" / "test_cases"
DEFAULT_OUTPUT_DIR = REPO_ROOT / "output" / "xmind_reports"

XMAP_NS = "urn:xmind:xmap:xmlns:content:2.0"
FO_NS = "http://www.w3.org/1999/XSL/Format"
SVG_NS = "http://www.w3.org/2000/svg"
XHTML_NS = "http://www.w3.org/1999/xhtml"
XLINK_NS = "http://www.w3.org/1999/xlink"
MANIFEST_NS = "urn:xmind:xmap:xmlns:manifest:1.0"

SEPARATOR_RE = re.compile(r"^\|\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$")


def _new_id():
    return str(uuid.uuid4())


def _now_ms():
    return str(int(datetime.now().timestamp() * 1000))


def split_markdown_row(line: str) -> list[str]:
    stripped = line.strip()
    if stripped.startswith("|"):
        stripped = stripped[1:]
    if stripped.endswith("|"):
        stripped = stripped[:-1]
    cells: list[str] = []
    current: list[str] = []
    escaped = False
    for char in stripped:
        if escaped:
            current.append(char)
            escaped = False
            continue
        if char == "\\":
            escaped = True
            continue
        if char == "|":
            cells.append("".join(current).strip())
            current = []
            continue
        current.append(char)
    if escaped:
        current.append("\\")
    cells.append("".join(current).strip())
    return cells


def load_markdown_table(source_file: Path) -> tuple[list[str], list[list[str]]]:
    content = source_file.read_text(encoding="utf-8")
    all_table_lines = [line.rstrip() for line in content.splitlines() if line.strip().startswith("|")]

    if len(all_table_lines) < 3:
        raise ValueError(f"未在 {source_file} 中找到有效的 Markdown 表格。")

    separator_indices = [i for i, line in enumerate(all_table_lines) if SEPARATOR_RE.match(line.strip())]
    if not separator_indices:
        raise ValueError(f"{source_file} 中没有找到合法的 Markdown 表头分隔线。")

    headers = split_markdown_row(all_table_lines[0])
    header_values = split_markdown_row(all_table_lines[0])

    rows: list[list[str]] = []
    for i, sep_idx in enumerate(separator_indices):
        table_start = sep_idx - 1
        if table_start < 0:
            continue
        table_end = (separator_indices[i + 1] - 1) if i + 1 < len(separator_indices) else len(all_table_lines)
        for line in all_table_lines[sep_idx + 1:table_end]:
            row = split_markdown_row(line)
            if len(row) != len(headers):
                continue
            if i > 0 and row == header_values:
                continue
            rows.append(row)
    if not rows:
        raise ValueError(f"{source_file} 的 Markdown 表格没有数据行。")
    return headers, rows


def normalize_text(value: str) -> str:
    return re.sub(r"<br\s*/?>", "\n", value or "").replace("\\n", "\n").strip()


def build_xmind_content(headers: list[str], rows: list[list[str]], topic_name: str) -> bytes:
    ts = _now_ms()
    nsmap = {None: XMAP_NS, "fo": FO_NS, "svg": SVG_NS, "xhtml": XHTML_NS, "xlink": XLINK_NS}

    def _el(tag, parent=None, **attrs):
        elem = etree.Element(f"{{{XMAP_NS}}}{tag}", nsmap=nsmap, **attrs)
        if parent is not None:
            parent.append(elem)
        return elem

    root = _el("xmap-content", version="2.0", timestamp=ts)
    sheet = _el("sheet", parent=root, id=_new_id(), timestamp=ts)
    _el("title", parent=sheet).text = "画布 1"

    root_topic = _el("topic", parent=sheet, id=_new_id(), timestamp=ts,
                     **{"structure-class": "org.xmind.ui.logic.right"})
    _el("title", parent=root_topic).text = topic_name
    rc = _el("children", parent=root_topic)
    rt = _el("topics", parent=rc, type="attached")

    # 按模块路径分组
    grouped: dict[str, list[dict[str, str]]] = {}
    for row in rows:
        case = {headers[i]: (row[i] or "").strip() for i in range(len(headers))}
        module = case.get("模块", "") or "未分类"
        grouped.setdefault(module, []).append(case)

    for module_name, cases in grouped.items():
        mtopic = _el("topic", parent=rt, id=_new_id(), timestamp=ts)
        _el("title", parent=mtopic).text = module_name
        mc = _el("children", parent=mtopic)
        mtopics = _el("topics", parent=mc, type="attached")

        for case in cases:
            case_id = case.get("用例编号", "")
            case_title = case.get("用例标题", "")
            priority = case.get("优先级", "")
            case_type = case.get("类型", "")

            label_parts = ["tc", priority, case_type]
            label = "-".join([p for p in label_parts if p])
            label = f"{label}：{case_title}" if case_title else label

            c_topic = _el("topic", parent=mtopics, id=_new_id(), timestamp=ts)
            _el("title", parent=c_topic).text = label
            cc = _el("children", parent=c_topic)
            ct = _el("topics", parent=cc, type="attached")

            # 前置条件
            precond = normalize_text(case.get("前置条件", ""))
            if precond:
                pt = _el("topic", parent=ct, id=_new_id(), timestamp=ts)
                _el("title", parent=pt).text = f"pc：{precond}"

            # 测试数据
            test_data = normalize_text(case.get("测试数据", ""))
            if test_data:
                dt = _el("topic", parent=ct, id=_new_id(), timestamp=ts)
                _el("title", parent=dt).text = f"测试数据：{test_data}"

            # 步骤与预期结果
            steps_text = normalize_text(case.get("测试步骤", ""))
            expected_text = normalize_text(case.get("预期结果", ""))
            step_lines = [s.strip() for s in steps_text.split("\n") if s.strip()] if steps_text else []
            exp_lines = [e.strip() for e in expected_text.split("\n") if e.strip()] if expected_text else []
            max_len = max(len(step_lines), len(exp_lines), 1)

            for i in range(max_len):
                step = step_lines[i] if i < len(step_lines) else ""
                expected = exp_lines[i] if i < len(exp_lines) else ""
                if step or expected:
                    st = _el("topic", parent=ct, id=_new_id(), timestamp=ts)
                    _el("title", parent=st).text = f"步骤：{step}" if step else "预期结果"
                    if expected:
                        sc = _el("children", parent=st)
                        sst = _el("topics", parent=sc, type="attached")
                        rst = _el("topic", parent=sst, id=_new_id(), timestamp=ts)
                        _el("title", parent=rst).text = f"预期：{expected}"

            # 备注
            remark = case.get("备注", "").strip()
            if remark:
                rmk = _el("topic", parent=ct, id=_new_id(), timestamp=ts)
                _el("title", parent=rmk).text = f"备注：{remark}"

    return etree.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True)


def build_manifest() -> bytes:
    nsmap = {None: MANIFEST_NS}

    def _el(tag, parent=None, **attrs):
        elem = etree.Element(f"{{{MANIFEST_NS}}}{tag}", nsmap=nsmap, **attrs)
        if parent is not None:
            parent.append(elem)
        return elem

    root = _el("manifest", version="1.0")
    _el("file-entry", parent=root, **{"full-path": "content.xml", "media-type": "text/xml"})
    return etree.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True)


def build_meta() -> bytes:
    nsmap = {None: XMAP_NS}
    root = etree.Element(f"{{{XMAP_NS}}}meta", nsmap=nsmap, version="2.0")
    return etree.tostring(root, encoding="utf-8", xml_declaration=True, pretty_print=True)


def export_markdown_to_xmind(source_path: Path, output_dir: Path, topic_name: str = "") -> Path:
    headers, rows = load_markdown_table(source_path)
    if not topic_name:
        topic_name = source_path.stem.replace("_测试用例", "").replace("_分析", "")

    output_dir.mkdir(parents=True, exist_ok=True)
    output_path = output_dir / f"{topic_name}.xmind"

    content_xml = build_xmind_content(headers, rows, topic_name)
    manifest_xml = build_manifest()
    meta_xml = build_meta()

    with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("content.xml", content_xml)
        zf.writestr("meta.xml", meta_xml)
        zf.writestr("META-INF/manifest.xml", manifest_xml)

    return output_path


def resolve_source_file(source_file: str | None, base_name: str | None, input_dir: Path) -> Path:
    if source_file:
        path = Path(source_file)
        return path if path.is_absolute() else REPO_ROOT / path
    if base_name:
        return input_dir / f"{base_name}_测试用例.md"
    raise ValueError("必须指定 --source-file 或 --base-name")


def main() -> None:
    parser = argparse.ArgumentParser(description="将 Markdown 测试用例导出为 XMind 思维导图。")
    parser.add_argument("--source-file", help="测试用例 Markdown 文件路径")
    parser.add_argument("--base-name", help="按 BASE_NAME 查找 output/test_cases/{BASE_NAME}_测试用例.md")
    parser.add_argument("--topic-name", help="XMind 根主题名称（默认从文件名推断）", default="")
    parser.add_argument("--input-dir", default=str(DEFAULT_INPUT_DIR), help="Markdown 所在目录")
    parser.add_argument("--output-dir", default=str(DEFAULT_OUTPUT_DIR), help="XMind 输出目录")
    args = parser.parse_args()

    if not args.source_file and not args.base_name:
        parser.error("必须指定 --source-file 或 --base-name")

    input_dir = Path(args.input_dir)
    if not input_dir.is_absolute():
        input_dir = REPO_ROOT / input_dir
    output_dir = Path(args.output_dir)
    if not output_dir.is_absolute():
        output_dir = REPO_ROOT / output_dir

    source_path = resolve_source_file(args.source_file, args.base_name, input_dir)
    if not source_path.exists():
        raise FileNotFoundError(f"测试用例文件不存在：{source_path}")

    output_path = export_markdown_to_xmind(source_path, output_dir, args.topic_name)
    headers, rows = load_markdown_table(source_path)
    print(f"来源文件: {source_path}")
    print(f"用例数量: {len(rows)}")
    print(f"根主题: {args.topic_name or source_path.stem}")
    print(f"XMind 已生成: {output_path}")


if __name__ == "__main__":
    main()
