from __future__ import annotations

from collections import defaultdict
from datetime import datetime
from pathlib import Path

import openpyxl
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter


ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "测试用例" / "SaaS_E2E测试用例_单Sheet用例测试列表.xlsx"
OUTPUT = ROOT / "测试用例" / "订单状态流转_链路流程线整理.xlsx"
CASE_SHEET = "订单状态流转"


def load_cases() -> dict[str, dict[str, str]]:
    workbook = openpyxl.load_workbook(SOURCE, data_only=True)
    sheet = workbook[CASE_SHEET]
    headers = [sheet.cell(1, column).value for column in range(1, sheet.max_column + 1)]
    cases: dict[str, dict[str, str]] = {}
    for row in range(2, sheet.max_row + 1):
        item = {
            str(headers[column - 1]): sheet.cell(row, column).value or ""
            for column in range(1, sheet.max_column + 1)
        }
        case_id = str(item.get("用例编号", "")).strip()
        if case_id.startswith("S-FLOW-"):
            cases[case_id] = item
    return cases


def ref(cases: dict[str, dict[str, str]], case_id: str) -> str:
    title = cases[case_id]["用例标题"]
    return f"{case_id}({title})"


def refs(cases: dict[str, dict[str, str]], case_ids: list[str]) -> str:
    return " -> ".join(ref(cases, case_id) for case_id in case_ids)


def modules(cases: dict[str, dict[str, str]], case_ids: list[str]) -> str:
    seen: list[str] = []
    for case_id in case_ids:
        module = str(cases[case_id].get("所属模块", ""))
        if module and module not in seen:
            seen.append(module)
    return " -> ".join(seen)


def flow(
    flow_id: str,
    name: str,
    case_ids: list[str],
    start_state: str,
    end_state: str,
    flow_type: str,
    scene: str,
    data_reuse: str,
    note: str = "",
) -> dict[str, object]:
    return {
        "链路编号": flow_id,
        "链路名称": name,
        "链路类型": flow_type,
        "起点状态": start_state,
        "终点状态": end_state,
        "用例编号序列": " -> ".join(case_ids),
        "适用场景": scene,
        "数据复用建议": data_reuse,
        "注意事项/缺口": note,
        "case_ids": case_ids,
    }


def build_flows() -> list[dict[str, object]]:
    return [
        flow(
            "FLOW-001",
            "待接单/待推单-发起售后-处理售后-已售后申诉赔偿",
            ["S-FLOW-001", "S-FLOW-002", "S-FLOW-003", "S-FLOW-051", "S-FLOW-052"],
            "主单待接单；子单待接单-待推单",
            "已售后；申诉完成/确认赔偿",
            "售后终态 + 申诉正向",
            "验证未推单主单可直接售后，售后完成后仍可按已售后状态发起申诉。",
            "可用一条未推单订单一次性走完；走到已售后后不可再回到待接单线。",
        ),
        flow(
            "FLOW-002",
            "待接单/待推单-发起售后-处理售后-已售后申诉驳回",
            ["S-FLOW-001", "S-FLOW-002", "S-FLOW-003", "S-FLOW-051", "S-FLOW-053"],
            "主单待接单；子单待接单-待推单",
            "已售后；申诉驳回",
            "售后终态 + 申诉逆向",
            "验证已售后状态发起申诉后，丸友集侧驳回申诉的逆向结果。",
            "需要另一条已售后订单，避免与确认赔偿分支互斥。",
        ),
        flow(
            "FLOW-003",
            "待接单/待推单-发起售后-取消售后-继续推单",
            ["S-FLOW-001", "S-FLOW-002", "S-FLOW-004", "S-FLOW-006"],
            "主单待接单；子单待接单-待推单",
            "主单待接单；子单待接单-已推单",
            "售后取消后回到发单线",
            "验证未推单售后取消后可继续发给丸友集。",
            "适合做探测链路；取消售后后订单仍可继续复用。",
        ),
        flow(
            "FLOW-004",
            "待接单/待推单-直接推单-丸友集拒绝接单",
            ["S-FLOW-001", "S-FLOW-005", "S-FLOW-013"],
            "主单待接单；子单待接单-待推单",
            "供应链拒绝/待继续处理",
            "待接单已推单逆向",
            "验证丸友集抢单大厅待接单阶段拒绝上游订单。",
            "拒绝后一般不可继续普通接单；需根据页面状态决定是否可继续发子单。",
        ),
        flow(
            "FLOW-005",
            "待接单/已推单-发起售后-处理售后-已售后申诉赔偿",
            ["S-FLOW-007", "S-FLOW-008", "S-FLOW-009", "S-FLOW-051", "S-FLOW-052"],
            "主单待接单；子单待接单-已推单",
            "已售后；申诉完成/确认赔偿",
            "已推单售后正向",
            "验证已推送丸友集但尚未接单时，丸掌柜发起并处理售后。",
            "可使用已存在的待接单-已推单订单；走到已售后后终结。",
        ),
        flow(
            "FLOW-006",
            "待接单/已推单-发起售后-处理售后-已售后申诉驳回",
            ["S-FLOW-007", "S-FLOW-008", "S-FLOW-009", "S-FLOW-051", "S-FLOW-053"],
            "主单待接单；子单待接单-已推单",
            "已售后；申诉驳回",
            "已推单售后逆向",
            "验证已售后后申诉被驳回的结果。",
            "需要与 FLOW-005 分开用订单。",
        ),
        flow(
            "FLOW-007",
            "待接单/已推单-售后取消-继续发单-已发布子单撤销-继续发子单",
            ["S-FLOW-007", "S-FLOW-008", "S-FLOW-010", "S-FLOW-006", "S-FLOW-011", "S-FLOW-012"],
            "主单待接单；子单待接单-已推单",
            "旧子单取消；新子单待接单/待接单-已推单",
            "售后取消后续发，再由丸掌柜撤销新已发布子单",
            "验证 S-FLOW-010 后子单列表为取消订单时不能直接撤销，需先通过 S-FLOW-006 继续发出新已发布子单，再走丸掌柜发起撤销。",
            "已实测取消订单状态不能再发起子单撤销；撤销按钮位于订单详情子单列表中订单状态为已发布的行。",
        ),
        flow(
            "FLOW-008",
            "丸友集已接单未发打手-申请撤销-丸掌柜同意撤销-继续发子单",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-015", "S-FLOW-016", "S-FLOW-012"],
            "丸友集已接单；未发打手",
            "旧子单取消；新子单待接单/已推单",
            "丸友集撤销正向",
            "验证工作室接单后申请撤销，丸掌柜同意后的继续履约能力。",
            "撤销同意后主单可继续发子单；适合复用一个主单。",
        ),
        flow(
            "FLOW-009",
            "丸友集已接单未发打手-申请撤销-丸掌柜拒绝撤销-异常中发起售后",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-015", "S-FLOW-017", "S-FLOW-034"],
            "丸友集已接单；未发打手；撤销中",
            "丸掌柜最新子单异常中；不可继续发单；转入售后中",
            "丸友集撤销逆向",
            "验证丸掌柜拒绝/取消撤销后，因存在异常中子单无法继续发单，需衔接发起售后流程。",
            "按平台实测：拒绝撤销后最新子单为异常中，不能继续发单；后续使用异常中发起售后用例推进。",
        ),
        flow(
            "FLOW-010",
            "丸友集已接单未发打手-发起售后-处理售后",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-018", "S-FLOW-019", "S-FLOW-051"],
            "丸友集已接单；未发打手",
            "已售后；可发起已售后申诉",
            "已接单未发打手售后正向",
            "验证工作室接单但未发打手阶段，丸掌柜发起并处理售后。",
            "此阶段标题中状态命名需页面实测确认；用例已列但建议重点探测。",
            "当前标题写有服务中/代练中，和未发打手阶段存在命名不一致，需页面校准。",
        ),
        flow(
            "FLOW-011",
            "丸友集已接单未发打手-发起售后-取消售后-继续立即发单",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-018", "S-FLOW-020", "S-FLOW-021"],
            "丸友集已接单；未发打手",
            "恢复已接单；打手待接单",
            "已接单未发打手售后取消",
            "验证该阶段售后取消后恢复到可继续发打手。",
            "同一订单可先取消售后，再继续发打手。",
            "同 FLOW-010，需要页面确认实际状态名。",
        ),
        flow(
            "FLOW-012",
            "已发打手待接单-撤回发单-重新发单",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-022", "S-FLOW-021"],
            "打手待接单",
            "打手待接单/重新发单",
            "打手未接单撤回",
            "验证丸友集发给打手后、打手未接单前可撤回，并可继续发单。",
            "撤回后是否允许再次发同一打手或换打手需页面确认。",
        ),
        flow(
            "FLOW-013",
            "已发打手待接单-发起售后-处理售后",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-023", "S-FLOW-024", "S-FLOW-051"],
            "已发打手；打手未接单",
            "已售后；可发起已售后申诉",
            "打手未接单售后正向",
            "验证发给打手但打手未接单时，上游发起并处理售后。",
            "该分支会终结普通履约链。",
        ),
        flow(
            "FLOW-014",
            "已发打手待接单-发起售后-取消售后-立即抢单",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-023", "S-FLOW-025", "S-FLOW-026"],
            "已发打手；打手未接单",
            "打手代练中",
            "打手未接单售后取消",
            "验证售后取消后打手抢单大厅恢复可接单。",
            "适合复用同一订单继续进入代练中。",
        ),
        flow(
            "FLOW-015",
            "打手代练中-发起撤销-取消撤销-继续代练",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-027", "S-FLOW-028"],
            "打手代练中",
            "打手代练中",
            "打手撤销取消",
            "验证打手发起撤销后自行取消，链路恢复代练中。",
            "取消撤销后同一订单可继续后续验收或其他撤销分支。",
        ),
        flow(
            "FLOW-016",
            "打手代练中-发起撤销-丸友集拒绝撤销-继续代练",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-027", "S-FLOW-029"],
            "打手代练中；撤销中",
            "打手代练中",
            "打手撤销拒绝",
            "验证丸友集拒绝打手撤销申请。",
            "拒绝后仍可继续履约。",
        ),
        flow(
            "FLOW-017",
            "打手代练中-发起撤销-丸友集同意撤销-二发新子单",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-027", "S-FLOW-030", "S-FLOW-031"],
            "打手代练中；撤销中",
            "旧打手子单已撤销；新子单待接单/代练中",
            "打手撤销同意后续履约",
            "验证同意撤销后工作室继续二发新子单。",
            "旧子单终态和新子单状态需分别断言。",
        ),
        flow(
            "FLOW-018",
            "撤销/取消后多子单续发",
            ["S-FLOW-030", "S-FLOW-032"],
            "前一子单已撤销/已取消",
            "下一子单待接单/代练中",
            "多子单续发",
            "作为所有撤销/取消终态后的通用续发链路。",
            "可接在 S-FLOW-016、030、039 等子单终态之后。",
        ),
        flow(
            "FLOW-019",
            "丸友集申请上游撤销-异常中-发起售后-处理售后-已售后申诉",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-078", "S-FLOW-033", "S-FLOW-017", "S-FLOW-034", "S-FLOW-035", "S-FLOW-051"],
            "丸友集父单代练中；丸掌柜异常中",
            "已售后；可发起已售后申诉",
            "异常中售后正向",
            "验证工作室申请上游撤销导致异常中后，丸掌柜改走售后处理。",
            "售后处理后进入已售后终态。",
        ),
        flow(
            "FLOW-020",
            "丸友集申请上游撤销-异常中-发起售后-取消售后",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-078", "S-FLOW-033", "S-FLOW-017", "S-FLOW-034", "S-FLOW-036"],
            "丸友集父单代练中；丸掌柜异常中",
            "恢复异常中/代练中异常",
            "异常中售后取消",
            "验证异常中售后取消后恢复异常处理状态。",
            "取消后后续仍需确定是继续履约、撤销审批还是再售后。",
        ),
        flow(
            "FLOW-021",
            "指派单取消-丸友集拒绝取消-继续代练",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-079", "S-FLOW-037", "S-FLOW-038"],
            "丸掌柜已创建主单并推给丸友集；丸友集已确认接单；丸友集已按百分比发单价90%指派打手发单；打手在抢单大厅-指派单提交取消订单后进入取消中",
            "打手代练中",
            "指派单取消拒绝",
            "验证打手取消订单申请被丸友集拒绝。",
            "S-FLOW-037 前必须先执行 S-FLOW-079 指派打手发单；指派单取消入口在打手抢单大厅-指派单，不接 S-FLOW-026。",
        ),
        flow(
            "FLOW-022",
            "指派单取消-丸友集同意取消-多子单续发",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-079", "S-FLOW-037", "S-FLOW-039", "S-FLOW-032"],
            "丸掌柜已创建主单并推给丸友集；丸友集已确认接单；丸友集已按百分比发单价90%指派打手发单；打手在抢单大厅-指派单提交取消订单后进入取消中",
            "旧子单已取消；下一子单待接单/代练中",
            "指派单取消同意",
            "验证打手取消订单被同意后，工作室/主单可继续履约。",
            "S-FLOW-037 前必须先执行 S-FLOW-079 指派打手发单；指派单取消入口在打手抢单大厅-指派单；同意取消后需校验旧子单与新子单互不覆盖。",
        ),
        flow(
            "FLOW-023",
            "打手提交验收-取消验收-重新代练",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-041"],
            "打手待验收",
            "打手代练中",
            "验收取消",
            "验证打手提交验收后取消验收恢复代练中。",
            "恢复后可继续走验收、撤销、取消或售后相关分支。",
        ),
        flow(
            "FLOW-024",
            "打手提交验收-丸友集拒绝验收-重新代练",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-042"],
            "丸友集打手子单待验收",
            "打手代练中",
            "子单验收拒绝",
            "验证丸友集拒绝打手验收。",
            "拒绝后可继续提交验收。",
        ),
        flow(
            "FLOW-025",
            "打手提交验收-丸友集同意验收-打手发起结算-丸友集完成结算-续发下一子单",
            [
                "S-FLOW-007",
                "S-FLOW-014",
                "S-FLOW-021",
                "S-FLOW-026",
                "S-FLOW-040",
                "S-FLOW-043",
                "S-FLOW-080",
                "S-FLOW-081",
                "S-FLOW-044",
            ],
            "丸友集打手子单待验收",
            "前一子单已验收并已完成结算；下一子单待接单/代练中",
            "子单验收结算后多子单续发",
            "验证丸友集同意验收后，需先由打手发起结算、丸友集完成结算，使前一子单从已验收转为已完成，再继续发下一子单。",
            "S-FLOW-043 后不直接接 S-FLOW-044；先接 S-FLOW-080/S-FLOW-081 完成打手结算，主单不应因单个子单完成直接完结。",
        ),
        flow(
            "FLOW-044",
            "打手提交验收-丸友集同意验收-申请上游验收-取消验收",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-082"],
            "丸友集打手子单已完成，丸掌柜主单可申请上游验收",
            "上游验收申请已撤回，订单回到可重新申请上游验收/继续履约状态",
            "上游验收取消分支",
            "验证丸友集申请上游验收后可撤回，并核对丸友集侧与丸掌柜侧状态同步回退。",
            "该链路编号按当前最大编号追加为 FLOW-044，但业务排序放在 FLOW-026 前；取消验收后只开放重新申请 S-FLOW-045，不应直接接丸掌柜同意验收。",
        ),
        flow(
            "FLOW-026",
            "打手提交验收-丸友集同意验收-申请上游验收-丸掌柜同意验收-已完成申诉赔偿",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-049", "S-FLOW-050", "S-FLOW-052"],
            "打手待验收/丸友集子单待验收",
            "已完成；申诉完成/确认赔偿",
            "正常履约完结 + 申诉正向",
            "验证正常履约完成后，丸掌柜已完成状态发起申诉并由丸友集确认赔偿。",
            "这是完成主单/验收应接的链路，不建议接在待推单已售后线后。",
        ),
        flow(
            "FLOW-027",
            "打手提交验收-丸友集同意验收-申请上游验收-丸掌柜同意验收-已完成申诉驳回",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-049", "S-FLOW-050", "S-FLOW-053"],
            "打手待验收/丸友集子单待验收",
            "已完成；申诉驳回",
            "正常履约完结 + 申诉逆向",
            "验证已完成状态申诉被丸友集驳回。",
            "需要与确认赔偿分支分开用订单。",
        ),
        flow(
            "FLOW-028",
            "待验收阶段-发起售后-处理售后-已售后申诉赔偿",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-046", "S-FLOW-047", "S-FLOW-051", "S-FLOW-052"],
            "主单待验收；子单待验收",
            "已售后；申诉完成/确认赔偿",
            "待验收售后正向",
            "验证待验收阶段转售后并处理完成，随后按已售后状态申诉。",
            "售后处理后不再走普通同意验收。",
        ),
        flow(
            "FLOW-029",
            "待验收阶段-发起售后-处理售后-已售后申诉驳回",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-046", "S-FLOW-047", "S-FLOW-051", "S-FLOW-053"],
            "主单待验收；子单待验收",
            "已售后；申诉驳回",
            "待验收售后逆向",
            "验证待验收阶段售后完成后的申诉驳回分支。",
            "需要与赔偿分支分开用订单。",
        ),
        flow(
            "FLOW-030",
            "待验收阶段-发起售后-取消售后-丸掌柜同意验收-验收成功子单处罚同意赔偿",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-046", "S-FLOW-048", "S-FLOW-049", "S-FLOW-054", "S-FLOW-055"],
            "主单待验收；子单待验收",
            "已完成；验收成功子单处罚同意赔偿",
            "待验收售后取消后回归验收 + 处罚追缴正向",
            "验证待验收阶段售后取消后恢复验收前状态，再走丸掌柜同意验收，并对子单验收成功状态发起处罚追缴。",
            "取消售后后同一订单可继续普通验收；丸掌柜同意上游验收后，子单列表验收成功状态可衔接 S-FLOW-054，再由 S-FLOW-055 覆盖同意赔偿分支。",
        ),
        flow(
            "FLOW-045",
            "待验收阶段-发起售后-取消售后-丸掌柜同意验收-验收成功子单处罚驳回",
            ["S-FLOW-007", "S-FLOW-014", "S-FLOW-021", "S-FLOW-026", "S-FLOW-040", "S-FLOW-043", "S-FLOW-045", "S-FLOW-046", "S-FLOW-048", "S-FLOW-049", "S-FLOW-054", "S-FLOW-056"],
            "主单待验收；子单待验收",
            "已完成；验收成功子单处罚驳回",
            "待验收售后取消后回归验收 + 处罚追缴逆向",
            "验证待验收阶段售后取消后恢复验收前状态，再走丸掌柜同意验收，并对子单验收成功状态发起处罚追缴后由丸友集驳回。",
            "与 FLOW-030 使用不同订单；S-FLOW-049 后同样先接 S-FLOW-054，再由 S-FLOW-056 覆盖驳回处罚分支。",
        ),
    ]


def write_sheet_header(sheet, headers: list[str]) -> None:
    fill = PatternFill("solid", fgColor="1F4E78")
    font = Font(color="FFFFFF", bold=True)
    for column, header in enumerate(headers, start=1):
        cell = sheet.cell(1, column, header)
        cell.fill = fill
        cell.font = font
        cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
    sheet.freeze_panes = "A2"


def append_row(sheet, values: list[object]) -> None:
    sheet.append(values)
    row = sheet.max_row
    for column in range(1, sheet.max_column + 1):
        sheet.cell(row, column).alignment = Alignment(vertical="top", wrap_text=True)


def autosize(sheet, widths: dict[int, int] | None = None) -> None:
    widths = widths or {}
    for column in range(1, sheet.max_column + 1):
        width = widths.get(column)
        if width is None:
            width = min(
                max(
                    len(str(sheet.cell(row, column).value or "")) + 2
                    for row in range(1, min(sheet.max_row, 120) + 1)
                ),
                80,
            )
        sheet.column_dimensions[get_column_letter(column)].width = width


def main() -> None:
    cases = load_cases()
    flows = build_flows()

    workbook = openpyxl.Workbook()
    summary = workbook.active
    summary.title = "链路流程线"
    summary_headers = [
        "链路编号",
        "链路名称",
        "链路类型",
        "起点状态",
        "终点状态",
        "用例编号序列",
        "用例链路（编号+标题）",
        "涉及模块",
        "适用场景",
        "数据复用建议",
        "注意事项/缺口",
    ]
    write_sheet_header(summary, summary_headers)
    for item in flows:
        case_ids = item["case_ids"]
        assert isinstance(case_ids, list)
        append_row(
            summary,
            [
                item["链路编号"],
                item["链路名称"],
                item["链路类型"],
                item["起点状态"],
                item["终点状态"],
                item["用例编号序列"],
                refs(cases, case_ids),
                modules(cases, case_ids),
                item["适用场景"],
                item["数据复用建议"],
                item["注意事项/缺口"],
            ],
        )
    autosize(summary, {1: 12, 2: 34, 3: 22, 4: 28, 5: 28, 6: 54, 7: 110, 8: 40, 9: 46, 10: 46, 11: 52})

    detail = workbook.create_sheet("链路步骤明细")
    detail_headers = ["链路编号", "步骤序号", "用例编号", "所属模块", "用例标题", "前置条件", "预期结果"]
    write_sheet_header(detail, detail_headers)
    for item in flows:
        case_ids = item["case_ids"]
        assert isinstance(case_ids, list)
        for index, case_id in enumerate(case_ids, start=1):
            case = cases[case_id]
            append_row(
                detail,
                [
                    item["链路编号"],
                    index,
                    case_id,
                    case.get("所属模块", ""),
                    case.get("用例标题", ""),
                    case.get("前置条件", ""),
                    case.get("预期结果", ""),
                ],
            )
    autosize(detail, {1: 12, 2: 10, 3: 14, 4: 24, 5: 58, 6: 70, 7: 80})

    coverage = workbook.create_sheet("用例覆盖索引")
    coverage_headers = ["用例编号", "所属模块", "用例标题", "覆盖次数", "出现链路", "是否已覆盖", "备注"]
    write_sheet_header(coverage, coverage_headers)
    flow_by_case: dict[str, list[str]] = defaultdict(list)
    for item in flows:
        case_ids = item["case_ids"]
        assert isinstance(case_ids, list)
        for case_id in case_ids:
            flow_by_case[case_id].append(str(item["链路编号"]))
    for case_id in sorted(cases):
        case = cases[case_id]
        covered = flow_by_case.get(case_id, [])
        append_row(
            coverage,
            [
                case_id,
                case.get("所属模块", ""),
                case.get("用例标题", ""),
                len(covered),
                "、".join(covered),
                "是" if covered else "否",
                "" if covered else "未被任何链路线引用，请检查是否需要新增链路。",
            ],
        )
    autosize(coverage, {1: 14, 2: 24, 3: 62, 4: 12, 5: 80, 6: 12, 7: 48})

    raw = workbook.create_sheet("原始用例索引")
    raw_headers = ["用例编号", "所属模块", "用例标题", "前置条件", "预期结果", "执行步骤"]
    write_sheet_header(raw, raw_headers)
    for case_id in sorted(cases):
        case = cases[case_id]
        append_row(
            raw,
            [
                case_id,
                case.get("所属模块", ""),
                case.get("用例标题", ""),
                case.get("前置条件", ""),
                case.get("预期结果", ""),
                case.get("执行步骤", ""),
            ],
        )
    autosize(raw, {1: 14, 2: 24, 3: 62, 4: 72, 5: 82, 6: 92})

    notes = workbook.create_sheet("说明")
    write_sheet_header(notes, ["项目", "内容"])
    append_row(notes, ["生成时间", datetime.now().strftime("%Y-%m-%d %H:%M:%S")])
    append_row(notes, ["来源", f"{SOURCE.name} / {CASE_SHEET}"])
    append_row(notes, ["整理原则", "以现有 S-FLOW 用例为节点，按状态可达关系组织链路线；互斥结果拆成不同链路。"])
    append_row(notes, ["阅读方式", "优先看【链路流程线】；需要逐步核对前置和预期时看【链路步骤明细】；检查覆盖情况看【用例覆盖索引】。"])
    append_row(notes, ["重要判断", "待接单/待推单售后处理后的已售后状态适合承接【已售后状态发起申诉】，不建议承接完成主单/一键验收。正常完成主单应接在待验收链路后。"])
    append_row(notes, ["注意", "部分中后段链路依赖页面实测确认具体状态名，已在注意事项/缺口列标出。"])
    autosize(notes, {1: 18, 2: 120})

    workbook.save(OUTPUT)
    print(OUTPUT)


if __name__ == "__main__":
    main()
