from __future__ import annotations

import copy
import json
import shutil
from datetime import datetime
from pathlib import Path

from openpyxl import load_workbook


ROOT = Path(__file__).resolve().parents[1]
EXCEL_PATH = ROOT / "订单状态流转_链路流程线整理.xlsx"
BACKUP_DIR = ROOT / "runtime" / "excel_backups"
JSON_PATHS = [
    ROOT / "scripts" / "order_status_flow_cases.json",
    ROOT / "runtime" / "order_status_flow_cases_for_sheet.json",
    ROOT / "runtime" / "order_status_flow_cases_for_excel.json",
]


S_FLOW_035_PATCH = {
    "执行步骤": "\n".join(
        [
            "1. 登录丸友集主账号或客服账号。",
            "2. 进入工作室履约应用-订单列表。",
            "3. 按来源单号或上游单号查询目标订单。",
            "4. 如该丸友集企业下没有客服角色成员，则先通过企业管理-成员管理邀请客服角色用户注册并完成审核。",
            "5. 确认目标订单状态为售后中。",
            "6. 点击操作列【主单售后】。",
            "7. 在订单售后处理弹窗中选择售后客服。",
            "8. 核对上游要求退回金额、售后原因和凭证。",
            "9. 点击【同意】并提交售后处理。",
            "10. 回到丸友集订单列表，按来源单号回读订单状态。",
            "11. 登录丸掌柜账号，进入订单详情回读主单、子单、子单列表和售后结果。",
        ]
    ),
    "预期结果": "\n".join(
        [
            "1. 丸友集侧目标订单同意售后后进入售后结束/售后处理完成的实际终态。",
            "2. 丸掌柜订单详情主单状态展示为异常中，子单状态展示为代练中异常。",
            "3. 丸掌柜子单列表中目标子单订单状态展示为售后结束，售后详情展示“售后处理完成”和退款金额。",
            "4. 售后金额、售后原因、处理结果在丸掌柜和丸友集侧一致。",
            "5. 该异常中子单售后处理完成后，不能再按售后中状态重复处理。",
        ]
    ),
    "跨端/状态断言": "\n".join(
        [
            "1. S-FLOW-035 的实际操作方是丸友集，不是丸掌柜。",
            "2. 操作入口为丸友集-工作室履约应用-订单列表-主单售后-同意。",
            "3. 售后客服下拉无数据时，应先补客服角色成员前置，不应继续提交同意/拒绝。",
            "4. 同意处理后需同时回读丸友集订单列表和丸掌柜订单详情。",
            "5. 丸友集侧回读目标订单售后结束/售后处理完成状态。",
            "6. 丸掌柜侧回读主单异常中、子单代练中异常、目标子单售后结束。",
            "7. 后续继续发单和处罚不属于本用例预期，分别由 S-FLOW-061、S-FLOW-062 承接。",
        ]
    ),
}


NEW_CASES = [
    {
        "用例编号": "S-FLOW-061",
        "用例层级": "订单状态流转用例",
        "来源/组合依据": "S-FLOW-035 实测结果：异常中子单售后同意后，主单仍可继续发单。",
        "所属模块": "上游发单",
        "用例标题": "【丸掌柜】子单售后结束状态-售后结束后主单继续发单",
        "登录平台/角色账号": "丸掌柜：企业主账号或客服账号；丸友集：工作室主账号或客服账号。",
        "前置条件": "已执行 S-FLOW-035，丸友集同意异常中子单售后；丸掌柜订单详情目标子单列表状态为售后结束，且订单详情展示【继续发单】入口；丸掌柜详情顶部主单状态/子单汇总状态不作为本用例硬性前置，只记录实际展示。",
        "测试数据": "来源单号、订单标题、丸掌柜主单号、原售后结束子单号、丸友集推单策略、新子单发单价、服务时限、联系电话。",
        "执行步骤": "\n".join(
            [
                "1. 登录丸掌柜主账号或客服账号。",
                "2. 进入商家管理-订单管理。",
                "3. 按主单号、来源单号或订单标题查询已完成 S-FLOW-035 的目标订单。",
                "4. 打开订单详情，记录顶部主单状态和子单汇总状态，仅作为实际展示回读。",
                "5. 在子单列表中确认原目标子单订单状态为售后结束，并记录售后详情中的退款金额和处理结果。",
                "6. 点击订单详情中的【继续发单】入口。",
                "7. 选择丸友集推单策略，填写或确认新子单发单价、服务时限、联系电话等必填信息。",
                "8. 提交并推送新子单。",
                "9. 回到丸掌柜订单详情，记录原售后结束子单和新子单状态。",
                "10. 登录丸友集账号，进入工作室履约应用-抢单大厅或订单列表。",
                "11. 按来源单号、上游单号或订单标题查询新推送订单并回读丸友集侧状态。",
            ]
        ),
        "预期结果": "\n".join(
            [
                "1. 原售后结束子单状态保持售后结束，售后处理结果和退款金额不被覆盖。",
                "2. 丸掌柜成功生成新的子单，状态为待接单/待接单-已推单或页面对应可接单状态。",
                "3. 丸友集侧可查询到新推送订单，进入待接单或页面对应状态。",
                "4. 主单继续进入后续履约链路，不因上一笔子单售后结束而阻断继续发单。",
            ]
        ),
        "跨端/状态断言": "\n".join(
            [
                "1. 本用例承接 S-FLOW-035，验证售后结束后的主单继续发单分支。",
                "2. 继续发单必须生成新子单，不得覆盖原售后结束子单。",
                "3. 丸掌柜新子单与丸友集新订单的来源单号、上游单号、订单标题和价格信息一致。",
                "4. 丸掌柜详情顶部主单状态/子单汇总状态只回读记录，不限制为固定状态。",
            ]
        ),
        "可组合/被引用关系": "承接 S-FLOW-035；后续可继续接 S-FLOW-014/S-FLOW-015/S-FLOW-034 等新子单履约、撤销、售后分支。",
        "自动化状态": "待自动化",
        "备注": "2026-06-24 新增：从 S-FLOW-035 拆出的售后结束后续分支。",
    },
    {
        "用例编号": "S-FLOW-062",
        "用例层级": "订单状态流转用例",
        "来源/组合依据": "S-FLOW-035 实测结果：异常中子单售后同意后，售后结束子单展示处罚入口。",
        "所属模块": "丸掌柜发起处罚追缴",
        "用例标题": "【丸掌柜】子单售后结束状态-对售后结束子单发起处罚",
        "登录平台/角色账号": "丸掌柜：企业主账号或处罚权限账号；丸友集：工作室主账号或处罚处理权限账号；打手应用：打手子账号按实际责任方需要。",
        "前置条件": "已执行 S-FLOW-035，丸友集同意异常中子单售后；丸掌柜订单详情中目标子单状态为售后结束，售后详情展示“售后处理完成”和退款金额，且目标子单操作列展示【处罚】入口；丸掌柜详情顶部主单状态/子单汇总状态不作为本用例硬性前置，只记录实际展示。",
        "测试数据": "来源单号、订单标题、丸掌柜主单号、售后结束子单号、处罚对象、处罚类型、处罚金额、处罚原因、处罚凭证。",
        "执行步骤": "\n".join(
            [
                "1. 登录丸掌柜主账号或具备处罚权限的账号。",
                "2. 进入商家管理-订单管理。",
                "3. 按主单号、来源单号或订单标题查询已完成 S-FLOW-035 的目标订单。",
                "4. 打开订单详情，定位订单状态为售后结束的目标子单。",
                "5. 点击该售后结束子单操作列中的【处罚】入口。",
                "6. 选择处罚类型和处罚对象，填写处罚金额、处罚原因和说明。",
                "7. 按页面要求上传或关联处罚凭证。",
                "8. 提交处罚申请。",
                "9. 进入丸掌柜处罚记录/子单处罚单或订单详情售后记录，按来源单号或子单号查询处罚记录。",
                "10. 登录丸友集账号，进入处罚/申诉处理入口或订单关联记录，查询该处罚记录。",
            ]
        ),
        "预期结果": "\n".join(
            [
                "1. 丸掌柜可对售后结束的目标子单发起处罚。",
                "2. 处罚记录生成成功，处罚类型、对象、金额、原因和目标子单一致。",
                "3. 原售后结束状态不被错误回退，售后处理结果和退款金额仍可追溯。",
                "4. 丸友集侧可查询到待处理处罚/申诉记录或页面对应处罚状态。",
            ]
        ),
        "跨端/状态断言": "\n".join(
            [
                "1. 本用例承接 S-FLOW-035，验证售后结束子单处罚分支。",
                "2. 处罚只作用于目标售后结束子单，不误影响同主单其他子单。",
                "3. 处罚记录中的主单号、来源单号、子单号、责任方和金额在丸掌柜与丸友集侧一致。",
                "4. 丸掌柜详情顶部主单状态/子单汇总状态只回读记录，不限制为固定状态。",
            ]
        ),
        "可组合/被引用关系": "承接 S-FLOW-035；后续可接丸友集处理处罚/赔偿/驳回等处罚追缴链路。",
        "自动化状态": "待自动化",
        "备注": "2026-06-24 新增：从 S-FLOW-035 拆出的售后结束子单处罚分支。",
    },
    {
        "用例编号": "S-FLOW-063",
        "用例层级": "订单状态流转用例",
        "来源/组合依据": "S-FLOW-062 后续处理分支：丸掌柜对售后结束子单发起处罚后，丸友集同意处罚/赔偿。",
        "所属模块": "处罚追缴-丸友集处理",
        "用例标题": "【丸友集】售后结束子单处罚追缴-同意处罚/赔偿",
        "登录平台/角色账号": "丸友集：工作室主账号或处罚处理权限账号；丸掌柜：企业主账号或处罚权限账号；打手应用：打手子账号按实际责任方需要。",
        "前置条件": "已执行 S-FLOW-062，丸掌柜已对售后结束子单发起处罚追缴，丸友集侧出现待处理处罚/申诉记录。",
        "测试数据": "来源单号、订单标题、售后结束子单号、处罚单号、赔偿/处罚金额、处理备注、处理凭证。",
        "执行步骤": "\n".join(
            [
                "1. 登录丸友集主账号或具备处罚处理权限的账号。",
                "2. 进入处罚/申诉处理入口，或从订单关联处罚记录进入待处理处罚。",
                "3. 按来源单号、订单标题、子单号或处罚单号查询 S-FLOW-062 生成的处罚记录。",
                "4. 打开处罚/申诉详情，核对处罚对象、处罚金额、处罚原因和凭证。",
                "5. 点击【同意处罚】、【同意赔偿】或页面对应同意处理入口。",
                "6. 填写赔偿金额、处理备注和必要凭证。",
                "7. 提交处理结果。",
                "8. 登录丸掌柜账号，进入子单处罚单或订单详情，查询目标处罚记录。",
                "9. 登录打手账号，进入我的处罚/我的收入/账户流水，查询处罚或金额影响记录。",
            ]
        ),
        "预期结果": "\n".join(
            [
                "1. 丸友集处理结果为同意处罚/同意赔偿或页面对应赔偿完成状态。",
                "2. 丸掌柜侧处罚记录进入处理完成或页面对应完成状态。",
                "3. 原售后结束子单状态不被错误回退，售后处理结果仍可追溯。",
                "4. 打手侧我的处罚/我的收入/账户流水体现对应赔偿或扣减影响。",
            ]
        ),
        "跨端/状态断言": "\n".join(
            [
                "1. 本用例承接 S-FLOW-062，验证售后结束子单处罚同意分支。",
                "2. 处罚单号、子单号、责任方、赔偿/处罚金额、处理结果在丸掌柜、丸友集、打手侧一致。",
                "3. 同意处理不得重复生效导致重复扣款。",
            ]
        ),
        "可组合/被引用关系": "承接 S-FLOW-062；处罚同意赔偿分支；可继续只读核对或进入财务流水校验。",
        "自动化状态": "待自动化",
        "备注": "2026-06-24 新增：售后结束子单处罚追缴后续处理分支。",
    },
    {
        "用例编号": "S-FLOW-064",
        "用例层级": "订单状态流转用例",
        "来源/组合依据": "S-FLOW-062 后续处理分支：丸掌柜对售后结束子单发起处罚后，丸友集拒绝/驳回处罚。",
        "所属模块": "处罚追缴-丸友集处理",
        "用例标题": "【丸友集】售后结束子单处罚追缴-拒绝处罚/驳回申诉",
        "登录平台/角色账号": "丸友集：工作室主账号或处罚处理权限账号；丸掌柜：企业主账号或处罚权限账号。",
        "前置条件": "已执行 S-FLOW-062，丸掌柜已对售后结束子单发起处罚追缴，丸友集侧出现待处理处罚/申诉记录。",
        "测试数据": "来源单号、订单标题、售后结束子单号、处罚单号、驳回原因、处理备注。",
        "执行步骤": "\n".join(
            [
                "1. 登录丸友集主账号或具备处罚处理权限的账号。",
                "2. 进入处罚/申诉处理入口，或从订单关联处罚记录进入待处理处罚。",
                "3. 按来源单号、订单标题、子单号或处罚单号查询 S-FLOW-062 生成的处罚记录。",
                "4. 打开处罚/申诉详情，核对处罚对象、处罚金额、处罚原因和凭证。",
                "5. 点击【拒绝处罚】、【驳回申诉】或页面对应驳回入口。",
                "6. 填写驳回原因和处理备注。",
                "7. 提交处理结果。",
                "8. 登录丸掌柜账号，进入子单处罚单或订单详情，查询目标处罚记录。",
                "9. 记录丸掌柜侧处罚被驳回状态及是否可继续申请处罚。",
            ]
        ),
        "预期结果": "\n".join(
            [
                "1. 丸友集处理结果为拒绝处罚/驳回申诉或页面对应驳回状态。",
                "2. 丸掌柜侧处罚记录展示被驳回或页面对应驳回状态，并可追溯驳回原因。",
                "3. 原售后结束子单状态不被错误回退，售后处理结果仍可追溯。",
                "4. 不生成同意赔偿造成的扣减或赔偿流水。",
            ]
        ),
        "跨端/状态断言": "\n".join(
            [
                "1. 本用例承接 S-FLOW-062，验证售后结束子单处罚拒绝分支。",
                "2. 处罚单号、子单号、驳回原因、处理结果在丸掌柜和丸友集侧一致。",
                "3. 拒绝分支不得产生同意赔偿的资金影响。",
            ]
        ),
        "可组合/被引用关系": "承接 S-FLOW-062；处罚拒绝/驳回分支；驳回后是否可再次发起处罚按平台规则继续验证。",
        "自动化状态": "待自动化",
        "备注": "2026-06-24 新增：售后结束子单处罚追缴后续处理分支。",
    },
]


NEW_FLOWS = [
    {
        "链路编号": "FLOW-036",
        "链路名称": "异常中子单售后同意后-主单继续发单",
        "链路类型": "售后结束 + 继续发单",
        "起点状态": "S-FLOW-033 后目标子单行异常中；经 S-FLOW-034/035 后目标子单售后结束并展示继续发单入口；顶部主单/子单汇总状态仅记录实际展示",
        "终点状态": "新子单待接单/待接单-已推单，丸友集侧可接单",
        "用例编号序列": "S-FLOW-033 -> S-FLOW-034 -> S-FLOW-035 -> S-FLOW-061",
        "用例链路（编号+标题）": "S-FLOW-033 【丸友集】拒绝取消后目标子单进入异常中 -> S-FLOW-034 【丸掌柜】子单异常中状态-子单发起售后 -> S-FLOW-035 【丸友集】子单异常中售后中状态-同意子单售后 -> S-FLOW-061 【丸掌柜】子单售后结束状态-售后结束后主单继续发单",
        "涉及模块": "丸掌柜发起售后 / 丸友集处理售后 / 上游发单",
        "适用场景": "异常中子单售后处理完成后，主单仍需继续履约并重新推送新子单。",
        "数据复用建议": "可复用已完成 S-FLOW-035 且仍展示继续发单入口的订单；继续发单会创建新子单，避免与处罚分支共用同一后续动作。",
        "注意事项/缺口": "该分支接在 S-FLOW-033 后的异常中子单分支；判断以目标子单行状态为准，不硬性限制丸掌柜详情顶部主单/子单汇总状态；该分支是 S-FLOW-035 后续独立链路，不写入 S-FLOW-035 本身预期。",
    },
    {
        "链路编号": "FLOW-037",
        "链路名称": "异常中子单售后同意后-售后结束子单处罚同意",
        "链路类型": "售后结束 + 处罚追缴",
        "起点状态": "S-FLOW-033 后目标子单行异常中；经 S-FLOW-034/035 后目标子单售后结束并展示处罚入口；顶部主单/子单汇总状态仅记录实际展示",
        "终点状态": "丸友集同意处罚/赔偿，处罚处理完成并产生对应金额影响",
        "用例编号序列": "S-FLOW-033 -> S-FLOW-034 -> S-FLOW-035 -> S-FLOW-062 -> S-FLOW-063",
        "用例链路（编号+标题）": "S-FLOW-033 【丸友集】拒绝取消后目标子单进入异常中 -> S-FLOW-034 【丸掌柜】子单异常中状态-子单发起售后 -> S-FLOW-035 【丸友集】子单异常中售后中状态-同意子单售后 -> S-FLOW-062 【丸掌柜】子单售后结束状态-对售后结束子单发起处罚 -> S-FLOW-063 【丸友集】售后结束子单处罚追缴-同意处罚/赔偿",
        "涉及模块": "丸掌柜发起售后 / 丸友集处理售后 / 丸掌柜发起处罚追缴 / 处罚追缴-丸友集处理",
        "适用场景": "异常中子单售后处理完成后，对售后结束子单继续追责处罚，丸友集同意赔偿。",
        "数据复用建议": "可复用已完成 S-FLOW-035 且目标子单展示处罚入口的订单；同意赔偿会产生金额影响，避免同一子单重复生效。",
        "注意事项/缺口": "该分支接在 S-FLOW-033 后的异常中子单分支；判断以目标子单行状态为准，不硬性限制丸掌柜详情顶部主单/子单汇总状态；该分支是 S-FLOW-035 后续独立链路，不写入 S-FLOW-035 本身预期；发起处罚后必须继续覆盖同意/拒绝结果。",
    },
    {
        "链路编号": "FLOW-038",
        "链路名称": "异常中子单售后同意后-售后结束子单处罚拒绝",
        "链路类型": "售后结束 + 处罚追缴",
        "起点状态": "S-FLOW-033 后目标子单行异常中；经 S-FLOW-034/035 后目标子单售后结束并展示处罚入口；顶部主单/子单汇总状态仅记录实际展示",
        "终点状态": "丸友集拒绝处罚/驳回申诉，处罚记录被驳回且不产生赔偿扣减",
        "用例编号序列": "S-FLOW-033 -> S-FLOW-034 -> S-FLOW-035 -> S-FLOW-062 -> S-FLOW-064",
        "用例链路（编号+标题）": "S-FLOW-033 【丸友集】拒绝取消后目标子单进入异常中 -> S-FLOW-034 【丸掌柜】子单异常中状态-子单发起售后 -> S-FLOW-035 【丸友集】子单异常中售后中状态-同意子单售后 -> S-FLOW-062 【丸掌柜】子单售后结束状态-对售后结束子单发起处罚 -> S-FLOW-064 【丸友集】售后结束子单处罚追缴-拒绝处罚/驳回申诉",
        "涉及模块": "丸掌柜发起售后 / 丸友集处理售后 / 丸掌柜发起处罚追缴 / 处罚追缴-丸友集处理",
        "适用场景": "异常中子单售后处理完成后，对售后结束子单继续追责处罚，丸友集拒绝处罚或驳回申诉。",
        "数据复用建议": "需要与同意处罚分支分开使用数据；拒绝分支不应产生赔偿扣减流水。",
        "注意事项/缺口": "该分支接在 S-FLOW-033 后的异常中子单分支；判断以目标子单行状态为准，不硬性限制丸掌柜详情顶部主单/子单汇总状态；该分支是 S-FLOW-035 后续独立链路，不写入 S-FLOW-035 本身预期；发起处罚后必须继续覆盖同意/拒绝结果。",
    },
]


def upsert_cases(data: list[dict]) -> list[dict]:
    existing = {item.get("用例编号"): item for item in data}
    for case in NEW_CASES:
        if case["用例编号"] in existing:
            existing[case["用例编号"]].update(case)
        else:
            data.append(copy.deepcopy(case))
            existing[case["用例编号"]] = data[-1]
    flow_order = ["S-FLOW-061", "S-FLOW-062", "S-FLOW-063", "S-FLOW-064"]
    pulled = [item for case_id in flow_order for item in data if item.get("用例编号") == case_id]
    remaining = [item for item in data if item.get("用例编号") not in set(flow_order)]
    insert_at = next((index + 1 for index, item in enumerate(remaining) if item.get("用例编号") == "S-FLOW-035"), len(remaining))
    data = remaining[:insert_at] + pulled + remaining[insert_at:]
    return data


def update_json_files() -> None:
    for path in JSON_PATHS:
        if not path.exists():
            continue
        data = json.loads(path.read_text(encoding="utf-8"))
        data = upsert_cases(data)
        path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")


def copy_row_style(ws, source_row: int, target_row: int) -> None:
    ws.row_dimensions[target_row].height = ws.row_dimensions[source_row].height
    for col in range(1, ws.max_column + 1):
        src = ws.cell(source_row, col)
        dst = ws.cell(target_row, col)
        if src.has_style:
            dst._style = copy.copy(src._style)
        if src.number_format:
            dst.number_format = src.number_format
        if src.alignment:
            dst.alignment = copy.copy(src.alignment)
        if src.font:
            dst.font = copy.copy(src.font)
        if src.fill:
            dst.fill = copy.copy(src.fill)
        if src.border:
            dst.border = copy.copy(src.border)


def headers(ws) -> dict[str, int]:
    return {str(cell.value): cell.column for cell in ws[1] if cell.value}


def find_row_by_value(ws, col: int, value: str) -> int | None:
    for row in range(2, ws.max_row + 1):
        if str(ws.cell(row, col).value or "") == value:
            return row
    return None


def delete_rows_by_values(ws, col: int, values: set[str]) -> None:
    for row in range(ws.max_row, 1, -1):
        if str(ws.cell(row, col).value or "") in values:
            ws.delete_rows(row, 1)


def insert_case_row_after(ws, after_row: int, case: dict, *, source_row: int) -> int:
    row = after_row + 1
    ws.insert_rows(row, 1)
    copy_row_style(ws, source_row, row)
    h = headers(ws)
    for field, value in case.items():
        col = h.get(field)
        if col:
            ws.cell(row, col).value = value
    if "覆盖次数" in h:
        ws.cell(row, h["覆盖次数"]).value = 1
    if "出现链路" in h:
        flow_map = {
            "S-FLOW-061": "FLOW-036",
            "S-FLOW-062": "FLOW-037/FLOW-038",
            "S-FLOW-063": "FLOW-037",
            "S-FLOW-064": "FLOW-038",
        }
        ws.cell(row, h["出现链路"]).value = flow_map.get(case["用例编号"], "")
    if "是否已覆盖" in h:
        ws.cell(row, h["是否已覆盖"]).value = "是"
    return row


def upsert_case_sheet_row(ws, case: dict, *, source_case_id: str = "S-FLOW-035") -> None:
    h = headers(ws)
    id_col = h.get("用例编号")
    if not id_col:
        return
    row = find_row_by_value(ws, id_col, case["用例编号"])
    source_row = find_row_by_value(ws, id_col, source_case_id) or 2
    if row is None:
        row = ws.max_row + 1
        copy_row_style(ws, source_row, row)
    for field, value in case.items():
        col = h.get(field)
        if col:
            ws.cell(row, col).value = value
    if "覆盖次数" in h:
        ws.cell(row, h["覆盖次数"]).value = 1
    if "出现链路" in h:
        ws.cell(row, h["出现链路"]).value = "FLOW-036" if case["用例编号"] == "S-FLOW-061" else "FLOW-037"
    if "是否已覆盖" in h:
        ws.cell(row, h["是否已覆盖"]).value = "是"


def upsert_case_sheet_rows_in_flow_order(ws) -> None:
    h = headers(ws)
    id_col = h.get("用例编号")
    if not id_col:
        return
    delete_rows_by_values(ws, id_col, {case["用例编号"] for case in NEW_CASES})
    source_row = find_row_by_value(ws, id_col, "S-FLOW-035") or 2
    insert_after = source_row
    for case in NEW_CASES:
        insert_after = insert_case_row_after(ws, insert_after, case, source_row=source_row)


def update_s_flow_035_excel_rows(wb) -> None:
    # Keep existing S-FLOW-035 text intact. This script only maintains newly added follow-up cases.
    return


def upsert_chain_detail(wb) -> None:
    ws = wb["链路步骤明细"]
    h = headers(ws)
    source_row = find_row_by_value(ws, h["用例编号"], "S-FLOW-035") or 2
    case_by_id = {
        "S-FLOW-033": None,
        "S-FLOW-034": None,
        "S-FLOW-035": None,
        **{case["用例编号"]: case for case in NEW_CASES},
    }
    for row in range(2, ws.max_row + 1):
        case_id = str(ws.cell(row, h["用例编号"]).value or "")
        if case_id in ["S-FLOW-033", "S-FLOW-034", "S-FLOW-035"] and case_by_id[case_id] is None:
            case_by_id[case_id] = {
                "用例编号": case_id,
                "所属模块": ws.cell(row, h["所属模块"]).value,
                "用例标题": ws.cell(row, h["用例标题"]).value,
                "前置条件": ws.cell(row, h["前置条件"]).value,
                "预期结果": ws.cell(row, h["预期结果"]).value,
            }

    rows = [
        ("FLOW-036", 1, case_by_id["S-FLOW-033"]),
        ("FLOW-036", 2, case_by_id["S-FLOW-034"]),
        ("FLOW-036", 3, case_by_id["S-FLOW-035"]),
        ("FLOW-036", 4, NEW_CASES[0]),
        ("FLOW-037", 1, case_by_id["S-FLOW-033"]),
        ("FLOW-037", 2, case_by_id["S-FLOW-034"]),
        ("FLOW-037", 3, case_by_id["S-FLOW-035"]),
        ("FLOW-037", 4, NEW_CASES[1]),
        ("FLOW-037", 5, NEW_CASES[2]),
        ("FLOW-038", 1, case_by_id["S-FLOW-033"]),
        ("FLOW-038", 2, case_by_id["S-FLOW-034"]),
        ("FLOW-038", 3, case_by_id["S-FLOW-035"]),
        ("FLOW-038", 4, NEW_CASES[1]),
        ("FLOW-038", 5, NEW_CASES[3]),
    ]
    delete_rows_by_values(ws, h["链路编号"], {"FLOW-035A", "FLOW-035B", "FLOW-035C", "FLOW-036", "FLOW-037", "FLOW-038"})
    insert_after = find_row_by_value(ws, h["用例编号"], "S-FLOW-035") or ws.max_row
    for flow_id, step_no, case in rows:
        if not case:
            continue
        target = insert_after + 1
        ws.insert_rows(target, 1)
        copy_row_style(ws, source_row, target)
        ws.cell(target, h["链路编号"]).value = flow_id
        ws.cell(target, h["步骤序号"]).value = step_no
        ws.cell(target, h["用例编号"]).value = case["用例编号"]
        ws.cell(target, h["所属模块"]).value = case["所属模块"]
        ws.cell(target, h["用例标题"]).value = case["用例标题"]
        ws.cell(target, h["前置条件"]).value = case["前置条件"]
        ws.cell(target, h["预期结果"]).value = case["预期结果"]
        insert_after = target


def upsert_flow_rows(wb) -> None:
    ws = wb["链路流程线"]
    h = headers(ws)
    delete_rows_by_values(ws, h["链路编号"], {"FLOW-035A", "FLOW-035B", "FLOW-035C", "FLOW-036", "FLOW-037", "FLOW-038"})
    source_row = None
    seq_col = h["用例编号序列"]
    for row in range(2, ws.max_row + 1):
        if "S-FLOW-034" in str(ws.cell(row, seq_col).value or "") and "S-FLOW-035" in str(ws.cell(row, seq_col).value or ""):
            source_row = row
            break
    source_row = source_row or 2
    insert_after = source_row
    for flow in NEW_FLOWS:
        row = insert_after + 1
        ws.insert_rows(row, 1)
        copy_row_style(ws, source_row, row)
        for field, value in flow.items():
            col = h.get(field)
            if col:
                ws.cell(row, col).value = value
        insert_after = row


def update_excel() -> None:
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)
    backup = BACKUP_DIR / f"{EXCEL_PATH.stem}.before_s_flow_061_062_{datetime.now():%Y%m%d-%H%M%S}.xlsx"
    shutil.copy2(EXCEL_PATH, backup)

    wb = load_workbook(EXCEL_PATH)
    update_s_flow_035_excel_rows(wb)
    for sheet_name in ["用例覆盖索引", "原始用例索引"]:
        ws = wb[sheet_name]
        upsert_case_sheet_rows_in_flow_order(ws)
    upsert_chain_detail(wb)
    upsert_flow_rows(wb)

    wb.save(EXCEL_PATH)
    print(f"updated: {EXCEL_PATH}")
    print(f"backup: {backup}")


def main() -> None:
    update_json_files()
    update_excel()


if __name__ == "__main__":
    main()
