from __future__ import annotations

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


BOOK = Path("SaaS_E2E测试用例_单Sheet用例测试列表.xlsx")
MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
PKG_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
NS = f"{{{MAIN_NS}}}"
ET.register_namespace("", MAIN_NS)
ET.register_namespace("r", REL_NS)


def col_name(cell_ref: str) -> str:
    return re.match(r"[A-Z]+", cell_ref).group(0)


def row_no(cell_ref: str) -> int:
    return int(re.search(r"\d+", cell_ref).group(0))


def col_to_num(col: str) -> int:
    n = 0
    for ch in col:
        n = n * 26 + ord(ch) - 64
    return n


def split_ref(cell_ref: str) -> tuple[str, int]:
    return col_name(cell_ref), row_no(cell_ref)


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


def build_shared_strings(values: list[str]) -> bytes:
    root = ET.Element(
        f"{NS}sst",
        {
            "count": str(len(values)),
            "uniqueCount": str(len(values)),
        },
    )
    for value in values:
        si = ET.SubElement(root, f"{NS}si")
        t = ET.SubElement(si, f"{NS}t")
        if value.startswith((" ", "\n")) or value.endswith((" ", "\n")):
            t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve")
        t.text = value
    return ET.tostring(root, encoding="utf-8", xml_declaration=True)


class WorkbookEditor:
    def __init__(self, zf: zipfile.ZipFile) -> None:
        self.zf = zf
        self.shared = load_shared_strings(zf)
        self.shared_index = {value: idx for idx, value in enumerate(self.shared)}
        self.changed_sheets: dict[str, ET.Element] = {}
        self.sheet_paths = self._sheet_paths()

    def _sheet_paths(self) -> dict[str, str]:
        wb = ET.fromstring(self.zf.read("xl/workbook.xml"))
        rels = ET.fromstring(self.zf.read("xl/_rels/workbook.xml.rels"))
        rid_to_target = {
            rel.attrib["Id"]: rel.attrib["Target"]
            for rel in rels.findall(f"{{{PKG_REL_NS}}}Relationship")
        }
        paths: dict[str, str] = {}
        for sheet in wb.find(f"{NS}sheets"):
            rid = sheet.attrib[f"{{{REL_NS}}}id"]
            paths[sheet.attrib["name"]] = posixpath.normpath("xl/" + rid_to_target[rid])
        return paths

    def sheet(self, name: str) -> ET.Element:
        path = self.sheet_paths[name]
        if path not in self.changed_sheets:
            self.changed_sheets[path] = ET.fromstring(self.zf.read(path))
        return self.changed_sheets[path]

    def intern(self, value: str) -> int:
        if value not in self.shared_index:
            self.shared_index[value] = len(self.shared)
            self.shared.append(value)
        return self.shared_index[value]

    def read_cell(self, cell: ET.Element) -> str:
        v = cell.find(f"{NS}v")
        if v is None:
            return ""
        raw = v.text or ""
        if cell.attrib.get("t") == "s":
            return self.shared[int(raw)]
        return raw

    def set_cell_value(self, cell: ET.Element, value: str) -> None:
        cell.attrib["t"] = "s"
        v = cell.find(f"{NS}v")
        if v is None:
            v = ET.SubElement(cell, f"{NS}v")
        v.text = str(self.intern(value))

    def row_values(self, row: ET.Element) -> dict[str, str]:
        return {col_name(c.attrib["r"]): self.read_cell(c) for c in row.findall(f"{NS}c")}

    def find_row_by_id(self, sheet_name: str, case_id: str) -> ET.Element:
        sheet_data = self.sheet(sheet_name).find(f"{NS}sheetData")
        for row in sheet_data.findall(f"{NS}row"):
            values = self.row_values(row)
            if values.get("A") == case_id:
                return row
        raise KeyError(case_id)

    def set_row_values(self, row: ET.Element, values: dict[str, str]) -> None:
        cells = {col_name(c.attrib["r"]): c for c in row.findall(f"{NS}c")}
        for col, value in values.items():
            if col not in cells:
                cell = ET.Element(f"{NS}c", {"r": f"{col}{row.attrib['r']}"})
                inserted = False
                for idx, existing in enumerate(list(row)):
                    if col_to_num(col_name(existing.attrib["r"])) > col_to_num(col):
                        row.insert(idx, cell)
                        inserted = True
                        break
                if not inserted:
                    row.append(cell)
                cells[col] = cell
            self.set_cell_value(cells[col], value)

    def insert_row_after(self, sheet_name: str, after_row_number: int, values: dict[str, str]) -> ET.Element:
        sheet = self.sheet(sheet_name)
        sheet_data = sheet.find(f"{NS}sheetData")
        rows = sheet_data.findall(f"{NS}row")
        template = None
        insert_at = len(rows)
        for idx, row in enumerate(rows):
            current = int(row.attrib["r"])
            if current == after_row_number:
                template = row
                insert_at = idx + 1
            elif current > after_row_number:
                self._shift_row(row, 1)
        if template is None:
            raise KeyError(after_row_number)
        new_row = copy.deepcopy(template)
        self._retarget_row(new_row, after_row_number + 1)
        sheet_data.insert(insert_at, new_row)
        self.set_row_values(new_row, values)
        self._update_dimension(sheet)
        return new_row

    def _shift_row(self, row: ET.Element, delta: int) -> None:
        self._retarget_row(row, int(row.attrib["r"]) + delta)

    def _retarget_row(self, row: ET.Element, new_number: int) -> None:
        row.attrib["r"] = str(new_number)
        for cell in row.findall(f"{NS}c"):
            col, _ = split_ref(cell.attrib["r"])
            cell.attrib["r"] = f"{col}{new_number}"

    def _update_dimension(self, sheet: ET.Element) -> None:
        dim = sheet.find(f"{NS}dimension")
        if dim is None:
            return
        max_row = 1
        max_col = 1
        for row in sheet.find(f"{NS}sheetData").findall(f"{NS}row"):
            max_row = max(max_row, int(row.attrib["r"]))
            for cell in row.findall(f"{NS}c"):
                max_col = max(max_col, col_to_num(col_name(cell.attrib["r"])))
        letters = ""
        n = max_col
        while n:
            n, rem = divmod(n - 1, 26)
            letters = chr(65 + rem) + letters
        dim.attrib["ref"] = f"A1:{letters}{max_row}"


def add_titles(text: str, title_map: dict[str, str]) -> str:
    ids = sorted(title_map, key=len, reverse=True)
    pattern = re.compile(r"(?<![A-Z0-9-])(" + "|".join(re.escape(x) for x in ids) + r")(?![A-Z0-9-])")

    def repl(match: re.Match[str]) -> str:
        end = match.end()
        if text[end : end + 1] == "（":
            return match.group(1)
        return f"{match.group(1)}（{title_map[match.group(1)]}）"

    return pattern.sub(repl, text)


def main() -> None:
    backup = BOOK.with_suffix(".xlsx.bak")
    shutil.copy2(BOOK, backup)

    with zipfile.ZipFile(BOOK, "r") as src:
        editor = WorkbookEditor(src)

        main_001 = {
            "A": "F-MAIN-001",
            "B": "组合流程用例",
            "C": "A-SAAS-001 -> A-SAAS-003 -> A-AUTH-001 -> A-PUSH-001 -> A-PUSH-003 -> A-ORDER-001",
            "D": "主流程",
            "E": "注册开通授权配置发单接单主流程闭环",
            "F": "租户平台：新注册企业主账号；SaaS主站：主站管理员账号；丸友集：企业主账号；丸掌柜：固定企业主账号。",
            "G": "测试手机号可用；主站管理员账号可用；固定丸掌柜账号19147509308可登录；策略类型支持单平台指定/多工作室同时分发/多平台分发。",
            "H": "企业注册资料；丸友集应用；授权密钥；固定丸掌柜账号；工作室授权；推单策略；手工录单订单。",
            "I": "\n".join(
                [
                    "1. 先执行用例A-SAAS-001，注册新商户、提交企业认证，并由SaaS主站审核通过。",
                    "2. 先执行用例A-SAAS-003，使用刚认证通过的商户申请开通丸友集，并由SaaS主站审批。",
                    "3. 先执行用例A-AUTH-001，丸友集生成授权密钥，固定丸掌柜激活密钥，并完成权限配置与回查。",
                    "4. 先执行用例A-PUSH-001，固定丸掌柜新增丸友集工作室授权。",
                    "5. 先执行用例A-PUSH-003，固定丸掌柜新增推单策略。",
                    "6. 先执行用例A-ORDER-001，固定丸掌柜手工录单发单，并登录丸友集验证订单到达抢单大厅。",
                    "7. 回到丸掌柜-商家管理-订单管理，按来源单号或订单标题回查订单。",
                    "8. 回到丸友集-工作室履约应用-抢单大厅，确认外来订单、策略名称、来源单号与丸掌柜一致。",
                ]
            ),
            "J": "新商户认证通过；丸友集开通完成；授权密钥激活并配置权限；丸掌柜具备工作室授权和推单策略；手工录单订单成功发布到丸友集抢单大厅。",
            "K": "同一企业账号贯穿注册、开通、授权、策略、发单链路；固定丸掌柜账号、授权关系、策略类型、来源单号、订单标题在丸掌柜和丸友集侧一致；台账状态按认证通过 -> 应用已开通 -> 授权已激活 -> 策略已创建 -> 订单已派单推进。",
            "L": "组合举证：A-SAAS-001产出认证商户；A-SAAS-003产出丸友集应用；A-AUTH-001产出授权关系；A-PUSH-001产出工作室授权；A-PUSH-003产出推单策略；A-ORDER-001产出已派单订单。",
            "M": "已自动化组合",
            "N": "对应test_e2e_main_001_full_closed_loop.py；代码标记@pytest.mark.e2e_main_001。",
        }
        editor.set_row_values(editor.find_row_by_id("组合流程用例", "F-MAIN-001"), main_001)

        main_002 = {
            "A": "F-MAIN-002",
            "B": "组合流程用例",
            "C": "A-ORDER-001 -> A-TASK-001 -> A-DELIVERY-001 -> A-DELIVERY-002 -> B-SETTLE-PLAYER-ACCOUNT-001 -> B-SETTLE-PLAYER-APPLY-001",
            "D": "主流程",
            "E": "丸掌柜发单到打手提交结算账单主流程",
            "F": "丸掌柜：固定企业主账号；丸友集：工作室主账号；打手应用：打手子账号。",
            "G": "已完成授权、工作室授权和推单策略；固定丸掌柜账号19147509308可登录；丸友集存在可用打手子账号；打手打款账户可完善。",
            "H": "来源单号；订单标题；策略名称；分配方式=抢单大厅；打手账号；打手打款账户信息；预期打手收入金额。",
            "I": "\n".join(
                [
                    "1. 先执行用例A-ORDER-001，丸掌柜手工录单并发布外部订单到丸友集。",
                    "2. 先执行用例A-TASK-001，丸友集确认外部订单并发给打手，打手接单。",
                    "3. 先执行用例A-DELIVERY-001，打手确认上号、上传首图并提交验收。",
                    "4. 先执行用例A-DELIVERY-002，丸友集确认验收打手子单并校验打手收入。",
                    "5. 先执行用例B-SETTLE-PLAYER-ACCOUNT-001，确认打手打款账户可用于一键结算。",
                    "6. 先执行用例B-SETTLE-PLAYER-APPLY-001，打手在我的收入中提交结算账单，并由丸友集查看员工结算汇总详情金额。",
                    "7. 回到打手应用-我的收入，确认目标收入已进入结算中/待审核/已申请结算状态。",
                    "8. 回到丸友集-工作室基础应用-财务结算列表，确认员工结算汇总详情金额与打手收入一致。",
                ]
            ),
            "J": "外部订单从丸掌柜发布到丸友集，丸友集发给打手并完成子单验收；打手收入生成后可提交结算账单；丸友集可回查员工结算汇总详情金额。",
            "K": "来源单号、订单标题、打手账号和预期收入金额贯穿订单、任务、验收、收入、一键结算、员工结算汇总；台账状态按已派单 -> 打手已抢单 -> 已提交验收 -> 收入已验证 -> 打款账户ready -> 结算账单已提交推进。",
            "L": "组合举证：A-ORDER-001产出已派单订单；A-TASK-001产出打手已抢单；A-DELIVERY-001产出已提交验收；A-DELIVERY-002产出收入已验证；B-SETTLE-PLAYER-ACCOUNT-001产出打款账户ready；B-SETTLE-PLAYER-APPLY-001产出打手结算账单。",
            "M": "已自动化组合",
            "N": "新增，对应test_e2e_main_002_order_to_player_settlement.py；代码标记@pytest.mark.e2e_main_002。",
        }
        try:
            editor.set_row_values(editor.find_row_by_id("组合流程用例", "F-MAIN-002"), main_002)
        except KeyError:
            editor.insert_row_after("组合流程用例", 4, main_002)

        # Refresh the title map after the F-MAIN updates, then annotate every case id occurrence.
        title_map: dict[str, str] = {}
        for sheet_name in ("用例测试列表", "组合流程用例"):
            sheet_data = editor.sheet(sheet_name).find(f"{NS}sheetData")
            for row in sheet_data.findall(f"{NS}row"):
                values = editor.row_values(row)
                if values.get("A") and values.get("E") and values["A"] != "用例编号":
                    title_map[values["A"]] = values["E"]

        for sheet_name in ("用例测试列表", "组合流程用例"):
            sheet_data = editor.sheet(sheet_name).find(f"{NS}sheetData")
            for row in sheet_data.findall(f"{NS}row"):
                for cell in row.findall(f"{NS}c"):
                    current = editor.read_cell(cell)
                    if not current:
                        continue
                    updated = add_titles(current, title_map)
                    if updated != current:
                        editor.set_cell_value(cell, updated)

        with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp:
            tmp_path = Path(tmp.name)
        with zipfile.ZipFile(BOOK, "r") as zin, zipfile.ZipFile(tmp_path, "w", zipfile.ZIP_DEFLATED) as zout:
            for item in zin.infolist():
                if item.filename == "xl/sharedStrings.xml":
                    zout.writestr(item, build_shared_strings(editor.shared))
                elif item.filename in editor.changed_sheets:
                    zout.writestr(item, ET.tostring(editor.changed_sheets[item.filename], encoding="utf-8", xml_declaration=True))
                else:
                    zout.writestr(item, zin.read(item.filename))
        shutil.move(tmp_path, BOOK)

    with zipfile.ZipFile(BOOK, "r") as check:
        check.testzip()
        verifier = WorkbookEditor(check)
        for case_id in ("F-MAIN-001", "F-MAIN-002"):
            values = verifier.row_values(verifier.find_row_by_id("组合流程用例", case_id))
            print(case_id, values["E"])
            print(values["C"])
            print(values["I"].splitlines()[0])
        print(f"backup={backup}")


if __name__ == "__main__":
    main()
