from __future__ import annotations

import copy
import re
import shutil
import zipfile
from datetime import datetime
from pathlib import Path
from xml.etree import ElementTree as ET


ROOT = Path(__file__).resolve().parents[1]
WORKBOOK = ROOT / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
BACKUP_DIR = ROOT / "runtime" / "excel_backups"
SHEET_XML = "xl/worksheets/sheet2.xml"
NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
NS_REL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
ET.register_namespace("", NS)
ET.register_namespace("r", NS_REL)


HEADERS = [
    "用例编号",
    "用例层级",
    "来源/组合依据",
    "所属模块",
    "用例标题",
    "登录平台/角色账号",
    "前置条件",
    "测试数据",
    "执行步骤",
    "预期结果",
    "跨端/状态断言",
    "可组合/被引用关系",
    "自动化状态",
    "备注",
]


def numbered(items: list[str]) -> str:
    return "\n".join(f"{index}. {item}" for index, item in enumerate(items, 1))


def col_letter(index: int) -> str:
    result = ""
    while index:
        index, rem = divmod(index - 1, 26)
        result = chr(65 + rem) + result
    return result


def colnum(ref: str) -> int:
    value = 0
    for ch in "".join(item for item in ref if item.isalpha()):
        value = value * 26 + ord(ch.upper()) - 64
    return value


def cell_text(cell: ET.Element, shared: list[str]) -> str:
    typ = cell.attrib.get("t")
    if typ == "inlineStr":
        return "".join(text.text or "" for text in cell.iter(f"{{{NS}}}t"))
    value = cell.find(f"{{{NS}}}v")
    if value is None:
        return "".join(text.text or "" for text in cell.iter(f"{{{NS}}}t"))
    raw = value.text or ""
    return shared[int(raw)] if typ == "s" else raw


def load_shared_strings(archive: zipfile.ZipFile) -> list[str]:
    try:
        root = ET.fromstring(archive.read("xl/sharedStrings.xml"))
    except KeyError:
        return []
    return ["".join(text.text or "" for text in item.iter(f"{{{NS}}}t")) for item in root.findall(f"{{{NS}}}si")]


def load_rows(archive: zipfile.ZipFile) -> tuple[ET.ElementTree, list[dict[str, str]], dict[int, str]]:
    shared = load_shared_strings(archive)
    tree = ET.ElementTree(ET.fromstring(archive.read(SHEET_XML)))
    root = tree.getroot()
    sheet_data = root.find(f"{{{NS}}}sheetData")
    if sheet_data is None:
        raise RuntimeError("sheetData not found")

    rows: list[dict[str, str]] = []
    header_styles: dict[int, str] = {}
    data_styles: dict[int, str] = {}
    for row_index, row in enumerate(sheet_data.findall(f"{{{NS}}}row"), 1):
        values: dict[int, str] = {}
        for cell in row.findall(f"{{{NS}}}c"):
            column = colnum(cell.attrib["r"])
            values[column] = cell_text(cell, shared)
            if "s" in cell.attrib:
                if row_index == 1:
                    header_styles[column] = cell.attrib["s"]
                elif column not in data_styles:
                    data_styles[column] = cell.attrib["s"]
        if row_index == 1:
            continue
        if values.get(1, "").startswith("S-FLOW-"):
            rows.append({HEADERS[index - 1]: values.get(index, "") for index in range(1, len(HEADERS) + 1)})
    return tree, rows, {column: data_styles.get(column, header_styles.get(column, "")) for column in range(1, len(HEADERS) + 1)}


def new_case_from(base: dict[str, str], *, module: str, title: str, precondition: str, steps: list[str], expected: list[str], asserts: list[str], remark: str) -> dict[str, str]:
    item = copy.deepcopy(base)
    item["所属模块"] = module
    item["来源/组合依据"] = f"whiteboard_exported_image.png + 状态机对应关系梳理.html：【{module}】"
    item["用例标题"] = title
    item["前置条件"] = precondition
    item["执行步骤"] = numbered(steps)
    item["预期结果"] = numbered(expected)
    item["跨端/状态断言"] = numbered(asserts)
    item["备注"] = remark
    return item


def transform(rows: list[dict[str, str]]) -> list[dict[str, str]]:
    by_id = {row["用例编号"]: row for row in rows}
    new_rows: list[dict[str, str]] = []

    s001 = copy.deepcopy(by_id["S-FLOW-001"])
    s001["用例标题"] = "丸掌柜视角-主单待接单状态/无子单状态-创建主单未推单"
    s001["前置条件"] = "丸掌柜账号可创建外来订单；手工录单页面选择【不推单】，仅创建主单，不生成子单。"
    s001["执行步骤"] = numbered(
        [
            "登录丸掌柜账号。",
            "进入丸掌柜-商家管理-订单管理。",
            "点击手工录单，填写主单订单来源、来源单号、订单标题、游戏、区服、服务项目、价格、联系方式等必填信息。",
            "在手工录单页面选择【不推单】。",
            "提交订单。",
            "在丸掌柜订单管理页面，按来源单号或订单标题查询目标订单。",
            "点击主单号进入订单详情。",
            "记录丸掌柜主单订单状态，并确认未生成子单记录。",
            "登录丸友集账号，进入工作室履约应用-抢单大厅。",
            "按来源单号或订单标题查询目标订单，并记录空结果。",
        ]
    )
    s001["预期结果"] = numbered(
        [
            "丸掌柜主单订单状态为待接单。",
            "丸掌柜侧未生成子单，子单状态记录为无子单。",
            "丸友集抢单大厅查询不到此单。",
            "打手侧查询不到此单。",
        ]
    )
    s001["跨端/状态断言"] = numbered(
        [
            "未推单前，福通和丸友集不应出现可接单记录。",
            "主单号、来源单号、订单标题在丸掌柜详情中一致。",
            "该分支没有子单，因此不能设计子单撤销用例。",
        ]
    )
    s001["备注"] = "未推单分支起点；后续可接主单售后、取消售后、处理售后、继续推单/发子单。"
    new_rows.append(s001)

    s002 = copy.deepcopy(by_id["S-FLOW-002"])
    s002["前置条件"] = "丸掌柜主单为待接单；创建主单时选择推单，或从未推单主单继续推单；存在可用丸友集推单策略。"
    s002["备注"] = "已推单分支起点；创建子单并推送到丸友集。"
    new_rows.append(s002)

    base_003 = by_id["S-FLOW-003"]
    base_004 = by_id["S-FLOW-004"]
    base_005 = by_id["S-FLOW-005"]
    base_006 = by_id["S-FLOW-006"]

    new_rows.append(
        new_case_from(
            base_003,
            module="上游发起售后",
            title="丸掌柜视角-主单待接单状态/无子单状态阶段发起售后",
            precondition="已执行 S-FLOW-001，丸掌柜主单订单状态为待接单，当前主单未推单且无子单。",
            steps=[
                "登录丸掌柜账号。",
                "进入订单管理查询 S-FLOW-001 创建的目标主单。",
                "打开订单详情并确认主单订单状态为待接单，且未生成子单。",
                "点击主单发起售后。",
                "填写售后原因、售后金额和凭证。",
                "提交售后申请。",
                "在丸掌柜订单管理页面重新查询目标订单并记录主单状态。",
                "登录丸友集账号，进入工作室履约应用-抢单大厅并查询目标订单。",
            ],
            expected=[
                "丸掌柜主单订单状态为售后中。",
                "丸掌柜侧仍无子单记录。",
                "丸友集抢单大厅查询不到此单。",
                "售后原因、售后金额和凭证在丸掌柜售后记录中保存成功。",
            ],
            asserts=[
                "发起售后前状态必须为主单待接单且无子单。",
                "未推单分支售后只影响主单，不存在子单撤销或子单取消订单状态。",
                "丸友集和打手侧不应产生订单。",
            ],
            remark="未推单分支售后。",
        )
    )
    new_rows.append(
        new_case_from(
            base_003,
            module="上游发起售后",
            title="丸掌柜视角-主单待接单状态/子单待接单-已推单状态阶段发起售后",
            precondition="已执行 S-FLOW-002，丸掌柜主单订单状态为待接单，子单处于待接单-已推单状态。",
            steps=[
                "登录丸掌柜账号。",
                "进入订单管理查询目标已推单订单。",
                "打开订单详情并确认发起前的主单订单状态和子单状态。",
                "在子单列表中记录目标子单的订单状态为待接单-已推单。",
                "点击发起售后。",
                "填写售后原因、售后金额和凭证。",
                "提交售后申请。",
                "在丸掌柜订单管理页面重新查询目标订单并记录主单和子单状态。",
                "登录丸友集账号，进入工作室履约应用-抢单大厅并查询目标订单。",
            ],
            expected=[
                "丸掌柜主单订单状态为售后中。",
                "丸掌柜子单列表中目标子单的订单状态为取消订单或页面对应售后终结状态。",
                "丸友集抢单大厅查询不到此单，或该订单不可继续接单。",
                "售后原因、售后金额和凭证在丸掌柜售后记录中保存成功。",
            ],
            asserts=[
                "发起售后前状态必须为主单待接单、子单待接单-已推单。",
                "已推单分支售后会终结当前子单。",
                "丸掌柜、丸友集侧售后链路状态需要一致。",
            ],
            remark="已推单分支售后。",
        )
    )

    new_rows.append(
        new_case_from(
            base_004,
            module="上游处理售后",
            title="丸掌柜视角-主单待接单状态/无子单状态阶段处理售后",
            precondition="已从主单待接单/无子单状态发起售后，当前丸掌柜主单为售后中。",
            steps=[
                "登录丸掌柜账号或具备售后处理权限的账号。",
                "进入订单管理或售后处理页面。",
                "查询目标售后主单。",
                "打开售后详情并记录处理前主单状态为售后中，且无子单。",
                "点击处理售后。",
                "填写处理结果、退款金额和处理备注。",
                "提交售后处理。",
                "在丸掌柜订单管理页面重新查询目标订单并记录主单状态。",
            ],
            expected=[
                "丸掌柜主单订单状态为已售后。",
                "丸掌柜侧仍无子单记录。",
                "售后退款或资金处理记录生成并与处理金额一致。",
                "主单已售后后不能继续发子单，但后续仍可按主单流程完单或发起申诉。",
            ],
            asserts=[
                "处理售后前状态必须为主单售后中且无子单。",
                "处理售后后主单状态为已售后。",
                "不存在子单状态断言。",
            ],
            remark="未推单分支处理售后；共同规则：已售后后不能继续发子单。",
        )
    )
    new_rows.append(
        new_case_from(
            base_004,
            module="上游处理售后",
            title="丸掌柜视角-主单待接单状态/子单待接单-已推单状态阶段处理售后",
            precondition="已从主单待接单/子单待接单-已推单状态发起售后，当前丸掌柜主单为售后中。",
            steps=[
                "登录丸掌柜账号或具备售后处理权限的账号。",
                "进入订单管理或售后处理页面。",
                "查询目标售后订单。",
                "打开售后详情并记录售后前原主单状态和原子单状态。",
                "进入丸掌柜主单详情，在子单列表中记录目标子单的订单状态。",
                "点击处理售后。",
                "填写处理结果、退款金额和处理备注。",
                "提交售后处理。",
                "在丸掌柜订单管理页面重新查询目标订单并记录主单和子单状态。",
                "登录丸友集账号，进入工作室履约应用-抢单大厅或订单列表查询目标订单并记录状态。",
            ],
            expected=[
                "丸掌柜主单订单状态为已售后。",
                "丸掌柜子单列表中目标子单的订单状态保持为取消订单或页面对应售后终结状态。",
                "丸友集订单为售后完成，或待接单阶段订单仍不可继续接单。",
                "主单已售后后不能继续发子单，但后续仍可按主单流程完单或发起申诉。",
            ],
            asserts=[
                "处理售后前状态必须来自子单待接单-已推单分支。",
                "处理售后后主单状态为已售后，当前子单为取消订单或售后终结状态。",
                "售后处理结果、金额、备注在丸掌柜和丸友集侧一致。",
            ],
            remark="已推单分支处理售后；共同规则：已售后后不能继续发子单。",
        )
    )

    new_rows.append(
        new_case_from(
            base_005,
            module="上游取消售后",
            title="丸掌柜视角-主单待接单状态/无子单状态阶段取消售后",
            precondition="已从主单待接单/无子单状态发起售后，当前丸掌柜主单为售后中且未处理完成。",
            steps=[
                "登录丸掌柜账号。",
                "进入售后详情。",
                "记录取消前丸掌柜主单状态为售后中，且无子单。",
                "点击取消售后。",
                "填写取消原因并确认。",
                "在丸掌柜订单管理页面重新查询目标订单并记录主单状态。",
                "登录丸友集账号，进入工作室履约应用-抢单大厅并查询目标订单。",
            ],
            expected=[
                "丸掌柜主单订单状态恢复为待接单。",
                "丸掌柜侧仍无子单记录。",
                "丸友集抢单大厅查询不到此单。",
                "售后取消原因被记录，当前状态不再展示为售后中。",
            ],
            asserts=[
                "取消售后必须恢复到发起售后前的主单待接单/无子单状态。",
                "未推单分支取消售后后仍不能出现丸友集或打手侧订单。",
            ],
            remark="未推单分支取消售后。",
        )
    )
    new_rows.append(
        new_case_from(
            base_005,
            module="上游取消售后",
            title="丸掌柜视角-主单待接单状态/子单待接单-已推单状态阶段取消售后",
            precondition="已从主单待接单/子单待接单-已推单状态发起售后，当前丸掌柜主单为售后中且未处理完成。",
            steps=[
                "登录丸掌柜账号。",
                "进入售后详情。",
                "记录取消前丸掌柜主单状态为售后中，并记录目标子单状态。",
                "点击取消售后。",
                "填写取消原因并确认。",
                "在丸掌柜订单管理页面重新查询目标订单并记录主单和子单状态。",
                "登录丸友集账号，进入工作室履约应用-抢单大厅或订单列表查询目标订单并记录状态。",
            ],
            expected=[
                "丸掌柜主单订单状态恢复为待接单。",
                "丸掌柜目标子单恢复为待接单-已推单，或恢复到系统定义的售后发起前可接单状态。",
                "丸友集侧恢复到售后发起前的可见性和业务状态。",
                "售后取消原因被记录，当前状态不再展示为售后中。",
            ],
            asserts=[
                "取消售后必须恢复到发起售后前的主单状态和子单状态，不能统一恢复为代练中。",
                "已推单分支取消售后后，丸友集侧可见性需要恢复到发起售后前。",
            ],
            remark="已推单分支取消售后。",
        )
    )

    s006 = copy.deepcopy(base_006)
    s006["用例标题"] = "丸掌柜视角-主单待接单状态/子单待接单-已推单状态阶段发起撤销"
    s006["前置条件"] = "已执行 S-FLOW-002，丸掌柜主单订单状态为待接单，且存在待接单-已推单子单；撤销只能由子单发起。"
    s006["预期结果"] = numbered(
        [
            "丸掌柜主单订单状态仍为待接单或页面展示的可继续处理状态。",
            "丸掌柜子单列表中目标子单的订单状态变为取消订单。",
            "该子单状态终结，不能继续履约。",
            "丸友集侧查询不到此单，或该订单不可继续接单。",
        ]
    )
    s006["跨端/状态断言"] = numbered(
        [
            "撤销动作只存在于子单；未推单且无子单的主单不能发起撤销。",
            "上游对子单发起撤销只终结当前子单，不等于主单完结。",
            "当前子单取消订单后，如需继续履约，需要由丸掌柜主单继续发子单。",
        ]
    )
    s006["备注"] = "仅保留已推单有子单分支；未推单无子单分支不设计撤销用例。"
    new_rows.append(s006)

    skipped = {"S-FLOW-001", "S-FLOW-002", "S-FLOW-003", "S-FLOW-004", "S-FLOW-005", "S-FLOW-006"}
    new_rows.extend(copy.deepcopy(row) for row in rows if row["用例编号"] not in skipped)

    for index, row in enumerate(new_rows, 1):
        row["用例编号"] = f"S-FLOW-{index:03d}"
    return new_rows


def make_cell(row_index: int, column_index: int, value: str, style: str = "") -> ET.Element:
    cell = ET.Element(f"{{{NS}}}c", {"r": f"{col_letter(column_index)}{row_index}", "t": "inlineStr"})
    if style:
        cell.attrib["s"] = style
    inline = ET.SubElement(cell, f"{{{NS}}}is")
    text = ET.SubElement(inline, f"{{{NS}}}t")
    if value.strip() != value or "\n" in value:
        text.attrib["{http://www.w3.org/XML/1998/namespace}space"] = "preserve"
    text.text = value
    return cell


def write_rows(tree: ET.ElementTree, rows: list[dict[str, str]], styles: dict[int, str]) -> None:
    root = tree.getroot()
    dimension = root.find(f"{{{NS}}}dimension")
    if dimension is not None:
        dimension.attrib["ref"] = f"A1:N{len(rows) + 1}"
    sheet_data = root.find(f"{{{NS}}}sheetData")
    if sheet_data is None:
        raise RuntimeError("sheetData not found")
    sheet_data.clear()

    header_row = ET.SubElement(sheet_data, f"{{{NS}}}row", {"r": "1"})
    for column_index, header in enumerate(HEADERS, 1):
        header_row.append(make_cell(1, column_index, header, styles.get(column_index, "")))

    for row_index, row_data in enumerate(rows, 2):
        row = ET.SubElement(sheet_data, f"{{{NS}}}row", {"r": str(row_index)})
        for column_index, header in enumerate(HEADERS, 1):
            row.append(make_cell(row_index, column_index, row_data.get(header, ""), styles.get(column_index, "")))


def replace_sheet_xml(workbook: Path, sheet_bytes: bytes) -> None:
    temp = workbook.with_suffix(".tmp.xlsx")
    with zipfile.ZipFile(workbook, "r") as source, zipfile.ZipFile(temp, "w", zipfile.ZIP_DEFLATED) as target:
        for item in source.infolist():
            if item.filename == SHEET_XML:
                target.writestr(item, sheet_bytes)
            else:
                target.writestr(item, source.read(item.filename))
    temp.replace(workbook)


def main() -> None:
    if not WORKBOOK.exists():
        raise FileNotFoundError(WORKBOOK)
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    backup = BACKUP_DIR / f"{WORKBOOK.stem}_before_branch_split_{datetime.now():%Y%m%d_%H%M%S}.xlsx"
    shutil.copy2(WORKBOOK, backup)

    with zipfile.ZipFile(WORKBOOK, "r") as archive:
        tree, rows, styles = load_rows(archive)
    updated = transform(rows)
    write_rows(tree, updated, styles)
    xml_bytes = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True)
    replace_sheet_xml(WORKBOOK, xml_bytes)

    print(f"backup={backup}")
    print(f"old_count={len(rows)}")
    print(f"new_count={len(updated)}")
    for row in updated[:10]:
        print(f"{row['用例编号']}\t{row['所属模块']}\t{row['用例标题']}")


if __name__ == "__main__":
    main()
