import { execFileSync } from "node:child_process";
import {
  copyFileSync,
  existsSync,
  mkdirSync,
  readdirSync,
  readFileSync,
  rmSync,
  statSync,
  writeFileSync
} from "node:fs";
import { dirname, join, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";

const root = resolve(fileURLToPath(new URL("../../..", import.meta.url)));
const webRoot = join(root, "kdl-wasm", "web");
const resultRoot = join(webRoot, "test-results", "abb120-spec");
const specRoot = join(webRoot, "tests", "fixtures", "abb120", "spec-programs");
const virtualControllerEvidencePath = join(webRoot, "test-results", "virtual-controller", "evidence.json");
const outputPath = join(root, "work", "doc", "通用机器人离线编程系统测试文档.docx");
const stagingRoot = join(webRoot, "test-results", "docx-build");

const jobId = process.argv[2] ?? latestJobId();
const jobDir = join(resultRoot, jobId);
const job = readJson(join(jobDir, "job.json"));
const report = readJson(join(jobDir, "report.json"));
const manifest = readJson(join(jobDir, "manifest.json"));
const uiEvidence = readJson(virtualControllerEvidencePath);
const generatedAt = formatShanghaiTime(new Date());

const descriptionByProgram = {
  W2_00_MinimalModule: "最小模块、language/module/proc main、常量 speed/zone、joint_target、MoveJ 基础执行。",
  W2_10_DataTargetToolFrame: "数据类型、tool、frame、pose、poseq、joint_target、pose_target、工具和工件坐标切换。",
  W2_20_ExpressionMath: "算术、比较、逻辑、括号、单位后缀、pi/e、三角函数和编译期求值。",
  W2_30_MotionAllTypes: "MoveJ、MoveL、MoveC、set_tool、set_frame、set_speed、set_zone 和 inline 参数优先级。",
  W2_40_PathEvents: "path defaults/source、point、event before/after/at distance、run_path。",
  W2_50_OperationProcess: "operation kind/process/start_action/end_action、run_operation。",
  W2_60_IOWaitPulse: "io.di/do/ai/ao/gi/go/ri/ro、wait、timeout、on_timeout、pulse 和边沿条件。",
  W2_70_ControlFlow: "if/elseif/else、while、for、switch、break、continue 流程控制。",
  W2_80_ProcFuncCall: "proc、in/out/inout、call、return、func 返回值和作用域。",
  W2_90_ExceptionAlarm: "alarm、raise、try/catch/finally、trap/task P1 边界诊断。",
  W2_99_FullSpecExample: "完整示例，覆盖 tool/frame、path、operation、IO event、run_operation 和回 home。",
  W2_E10_ExpressionDiagnostics: "表达式负向测试：未知符号、未知函数、参数数量、数学域、除零、单位不匹配。",
  W2_E20_SemanticDiagnostics: "语义负向测试：重复符号、out/inout 非左值、缺失调用目标、函数副作用、缺失目标。",
  W2_E30_MotionDiagnostics: "运动负向测试：关节限位、不可达 pose、空 path 等 KDL/Path 诊断。",
  W2_E40_ControlFlowDiagnostics: "流程控制负向测试：非法 break/continue、jump、重复 case、非常量 case、缺失 label。",
  W2_E50_RuntimeTimeout: "运行时超时负向测试：wait timeout、on_timeout、报警 source map。",
  W2_P10_BrandHints: "品牌提示后处理测试：post_hint、@brand.* 消费或报告。",
  W2_P20_CrossBrandMotion: "跨品牌运动后处理：ABB/FANUC/KUKA motion、speed、zone、IO、wait 映射。",
  W2_P30_Roundtrip: "后处理和导入回读测试：post 后 ABB import roundtrip，记录差异和警告。"
};

const commandResults = [
  {
    command: "npm run typecheck",
    scope: "TypeScript 静态类型检查",
    process: "执行 tsc -p kdl-wasm/web/tsconfig.json --noEmit，检查 Web、GRL、runtime、post 和 suite 代码类型。",
    result: "通过，未输出 TypeScript 错误。"
  },
  {
    command: "npm test",
    scope: "全量 Vitest 自动化测试",
    process: "执行 vitest run --config kdl-wasm/web/vitest.config.ts，覆盖 KDL、GRL、runtime、controller、post/import、workspace、report、docs 和 ABB120 integration。",
    result: "通过，41 个 test files、155 个 tests 全部 passed。"
  },
  {
    command: "npm run suite:abb120-spec",
    scope: "ABB120 GRL 规范程序套件",
    process: "读取 manifest.json 和 19 个 GRL 程序，逐项执行 parse、semantic compile、runtime/static/post 检查，并输出 job/report/program artifacts。",
    result: `通过，生成 ${job.job_id}；programs=${report.summary.programs}，pass=${report.summary.pass}，expected diagnostic=${report.summary.diagnostic}，fail=${report.summary.fail}，missingSections=${report.coverage.missingSections.length}。`
  },
  {
    command: "npm run verify:virtual-controller",
    scope: "虚拟控制器 UI 和截图验证",
    process: "使用 Chrome DevTools Protocol 打开 virtual-controller.html，分别验证 desktop/mobile 视口、点击 load/run/DI1、检查主要面板、状态、wait 和横向溢出，并截图。",
    result: `通过，desktop=${uiEvidence.desktop.screenshotBytes} bytes，mobile=${uiEvidence.mobile.screenshotBytes} bytes；state=${uiEvidence.desktop.state}/${uiEvidence.mobile.state}，wait=${uiEvidence.desktop.wait}/${uiEvidence.mobile.wait}。`
  }
];

const programs = job.programs.map((program) => {
  const sourcePath = join(specRoot, program.file);
  return {
    ...program,
    manifestEntry: manifest.programs.find((entry) => entry.program_id === program.program_id),
    sourcePath,
    source: readFileSync(sourcePath, "utf8")
  };
});

buildDocx();
console.log(JSON.stringify({ outputPath, jobId, programs: programs.length }, null, 2));

function buildDocx() {
  assertSafeStaging(stagingRoot);
  rmSync(stagingRoot, { recursive: true, force: true });
  mkdirSync(join(stagingRoot, "_rels"), { recursive: true });
  mkdirSync(join(stagingRoot, "docProps"), { recursive: true });
  mkdirSync(join(stagingRoot, "word", "_rels"), { recursive: true });
  mkdirSync(join(stagingRoot, "word", "media"), { recursive: true });

  const images = copyImages();
  writeFileSync(join(stagingRoot, "[Content_Types].xml"), contentTypesXml(), "utf8");
  writeFileSync(join(stagingRoot, "_rels", ".rels"), packageRelsXml(), "utf8");
  writeFileSync(join(stagingRoot, "docProps", "core.xml"), corePropsXml(), "utf8");
  writeFileSync(join(stagingRoot, "docProps", "app.xml"), appPropsXml(), "utf8");
  writeFileSync(join(stagingRoot, "word", "styles.xml"), stylesXml(), "utf8");
  writeFileSync(join(stagingRoot, "word", "_rels", "document.xml.rels"), documentRelsXml(images), "utf8");
  writeFileSync(join(stagingRoot, "word", "document.xml"), documentXml(images), "utf8");

  if (existsSync(outputPath)) {
    rmSync(outputPath, { force: true });
  }
  zipDirectory(stagingRoot, outputPath);
}

function copyImages() {
  const images = [
    {
      id: "rIdImage1",
      fileName: "virtual-controller-desktop.png",
      source: uiEvidence.desktop.screenshot,
      title: "虚拟控制器桌面端截图",
      cx: 5943600,
      cy: 3962400
    },
    {
      id: "rIdImage2",
      fileName: "virtual-controller-mobile.png",
      source: uiEvidence.mobile.screenshot,
      title: "虚拟控制器移动端截图",
      cx: 2743200,
      cy: 5943600
    }
  ];
  for (const image of images) {
    copyFileSync(image.source, join(stagingRoot, "word", "media", image.fileName));
  }
  return images;
}

function documentXml(images) {
  const body = [];
  body.push(heading("通用机器人离线编程系统测试文档", 1));
  body.push(p(`生成时间：${generatedAt}`));
  body.push(p(`测试对象：通用机器人离线编程系统 / ABB IRB120 虚拟控制器 / GRL 规范程序套件`));
  body.push(p(`工作区：${root}`));
  body.push(p(`最新测试 job：${job.job_id}`));

  body.push(heading("一、测试结论", 1));
  body.push(table([
    ["项目", "结果"],
    ["测试结论", "通过。本轮未发现非预期失败；5 个 diagnostic 程序为预期负向测试。"],
    ["规范覆盖", `覆盖章节 ${report.coverage.coveredSections.join("、")}；缺失章节：${report.coverage.missingSections.length === 0 ? "无" : report.coverage.missingSections.join("、")}`],
    ["程序统计", `共 ${report.summary.programs} 个 GRL 规范程序；pass=${report.summary.pass}，expected diagnostic=${report.summary.diagnostic}，fail=${report.summary.fail}`],
    ["测试层级", `Runtime=${report.coverage.runtimePrograms}，Static=${report.coverage.staticPrograms}，Post=${report.coverage.postPrograms}`],
    ["主要证据", `${relative(jobDir)}；${relative(dirname(uiEvidence.desktop.screenshot))}`]
  ]));

  body.push(heading("二、测试范围", 1));
  body.push(p("本次测试覆盖离线编程系统的语法解析、语义编译、KDL 运动规划接口、虚拟控制器状态机、IO/wait/pulse、路径和工艺操作、异常报警、三品牌后处理、导入回读、工作台 UI 响应和截图证据。"));
  body.push(table([
    ["范围", "覆盖内容"],
    ["GRL 语言", "词法、顶层结构、类型系统、表达式、目标点、速度、zone、motion、path、operation、IO、流程控制、proc/func、异常和 P1 边界。"],
    ["运动与 KDL", "ABB IRB120 fixture、MoveJ/MoveL/MoveC、path planning、joint limit、unreachable target、trajectory summary。"],
    ["虚拟控制器", "loadProgram、start、stepInto、hold、resume、stepMotion、stop、resetFault、runtime trace、motion queue、IO image。"],
    ["后处理与导入", "ABB/FANUC/KUKA 输出文件、post report、ABB import roundtrip。"],
    ["用户界面", "virtual-controller.html 桌面端和移动端渲染、交互、主要面板可见性和截图。"]
  ]));

  body.push(heading("三、测试命令与执行过程", 1));
  body.push(table([
    ["命令", "测试内容", "执行过程", "执行结果"],
    ...commandResults.map((item) => [item.command, item.scope, item.process, item.result])
  ]));

  body.push(heading("四、截图证据", 1));
  body.push(p(`截图验证应用：${uiEvidence.app}`));
  body.push(p(`Chrome：${uiEvidence.chrome}`));
  body.push(heading("4.1 桌面端截图", 2));
  body.push(p(`视口：${uiEvidence.desktop.viewport.width}x${uiEvidence.desktop.viewport.height}；state=${uiEvidence.desktop.state}；wait=${uiEvidence.desktop.wait}；trace=${uiEvidence.desktop.trace}；截图：${relative(uiEvidence.desktop.screenshot)}`));
  body.push(imageParagraph(images[0], 1));
  body.push(heading("4.2 移动端截图", 2));
  body.push(p(`视口：${uiEvidence.mobile.viewport.width}x${uiEvidence.mobile.viewport.height}，deviceScaleFactor=${uiEvidence.mobile.viewport.deviceScaleFactor}；state=${uiEvidence.mobile.state}；wait=${uiEvidence.mobile.wait}；trace=${uiEvidence.mobile.trace}；截图：${relative(uiEvidence.mobile.screenshot)}`));
  body.push(imageParagraph(images[1], 2));

  body.push(heading("五、规范覆盖矩阵", 1));
  body.push(table([
    ["规范章节", "覆盖层级", "程序"],
    ...manifest.coverage_matrix.map((item) => [
      String(item.spec_section),
      item.coverage_level,
      item.program_ids.join("、")
    ])
  ]));

  body.push(heading("六、全部测试程序明细与源码", 1));
  for (const [index, program] of programs.entries()) {
    appendProgramSection(body, program, index + 1);
  }

  body.push(heading("七、主要产物索引", 1));
  body.push(table([
    ["产物", "路径"],
    ["job.json", relative(join(jobDir, "job.json"))],
    ["report.json", relative(join(jobDir, "report.json"))],
    ["report.html", relative(join(jobDir, "report.html"))],
    ["manifest.json", relative(join(jobDir, "manifest.json"))],
    ["桌面端截图", relative(uiEvidence.desktop.screenshot)],
    ["移动端截图", relative(uiEvidence.mobile.screenshot)],
    ["UI evidence.json", relative(virtualControllerEvidencePath)]
  ]));

  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
  xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex"
  xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex"
  xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex"
  xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex"
  xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex"
  xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex"
  xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex"
  xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex"
  xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink"
  xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d"
  xmlns:o="urn:schemas-microsoft-com:office:office"
  xmlns:oel="http://schemas.microsoft.com/office/2019/extlst"
  xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
  xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
  xmlns:v="urn:schemas-microsoft-com:vml"
  xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
  xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
  xmlns:w10="urn:schemas-microsoft-com:office:word"
  xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
  xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"
  xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex"
  xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"
  xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml"
  xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du"
  xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash"
  xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex"
  xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
  xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
  xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
  xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
  mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16du wp14">
  <w:body>
    ${body.join("\n")}
    <w:sectPr>
      <w:pgSz w:w="11906" w:h="16838"/>
      <w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="850" w:header="708" w:footer="708" w:gutter="0"/>
      <w:cols w:space="708"/>
      <w:docGrid w:linePitch="312"/>
    </w:sectPr>
  </w:body>
</w:document>`;
}

function appendProgramSection(body, program, index) {
  const expectedDiagnostics = program.manifestEntry?.expected_diagnostics ?? [];
  const actualCodes = [...new Set(program.diagnostics.map((diagnostic) => diagnostic.code))];
  body.push(heading(`6.${index} ${program.program_id}`, 2));
  body.push(table([
    ["字段", "内容"],
    ["测试内容", descriptionByProgram[program.program_id] ?? "按 manifest 执行规范测试。"],
    ["源码文件", relative(program.sourcePath)],
    ["覆盖层级", program.coverage_level],
    ["覆盖章节", program.spec_sections.join("、")],
    ["预期状态", program.expected_status],
    ["实际状态", program.status],
    ["预期诊断", expectedDiagnostics.length === 0 ? "无" : expectedDiagnostics.join("、")],
    ["实际诊断代码", actualCodes.length === 0 ? "无" : actualCodes.join("、")],
    ["输入哈希", program.input_hash],
    ["源码行数", String(program.source.split(/\r?\n/).length)]
  ]));

  body.push(heading("测试过程", 3));
  for (const step of programSteps(program)) {
    body.push(p(step));
  }

  body.push(heading("执行结果摘要", 3));
  body.push(table(programResultRows(program)));

  if (program.controller?.commands?.length) {
    body.push(heading("虚拟控制器命令轨迹", 3));
    body.push(table([
      ["命令", "前状态", "后状态", "结果", "诊断"],
      ...program.controller.commands.map((command) => [
        command.command,
        command.before ?? "",
        command.after ?? "",
        command.result ?? "",
        command.diagnostics.length === 0 ? "无" : command.diagnostics.map((diagnostic) => diagnostic.code).join("、")
      ])
    ]));
  }

  body.push(heading("证据文件", 3));
  body.push(table([
    ["类型", "路径"],
    ...Object.entries(program.artifacts).map(([key, value]) => [key, relative(join(resultRoot, value))])
  ]));

  body.push(heading("程序源码", 3));
  body.push(codeBlock(program.source));
}

function programSteps(program) {
  const steps = [
    `1. 读取源码 ${relative(program.sourcePath)}，加载 program_id、spec_sections、coverage_level、expected_status 和 expected_diagnostics。`,
    "2. 执行 parseGrl，完成词法、语法、模块结构和 source map 解析。",
    "3. 执行 compileSemanticProgram，生成语义 IR、符号表、procedure/path/operation、KDL motion/path request 和诊断列表。"
  ];
  if (program.coverage_level === "Runtime") {
    steps.push(`4. 加载 IR 到 VirtualController，按 ${manifest.runtime_commands.join(" -> ")} 执行控制器命令，采集 controller snapshot、motion queue、runtime trace、IO image 和 trajectory summary。`);
    steps.push("5. 对比实际状态与 manifest 预期状态；对比实际诊断代码与 expected_diagnostics；写入 compile/controller/motion-queue/trace/io/trajectory/post/roundtrip 证据。");
  } else if (program.coverage_level === "Static") {
    steps.push("4. 执行 static preflight diagnostics，覆盖表达式、语义、运动、path、流程控制等负向检查；不要求进入 happy-path 运行。");
    steps.push("5. 对比实际诊断代码与 expected_diagnostics，确认 diagnostic 状态为预期负向结果，并写入 compile 及各类空/摘要证据文件。");
  } else {
    steps.push("4. 执行 postProcessAllBrands，生成 ABB/FANUC/KUKA 输出文件名和 post report。");
    steps.push("5. 执行 ABB import roundtrip 摘要检查，记录 pass/warn/fail 与导入诊断，并写入 post-report/roundtrip 证据。");
  }
  return steps;
}

function programResultRows(program) {
  const rows = [
    ["项目", "结果"],
    ["解析", program.compile.parsed ? "parsed=true" : "parsed=false"],
    ["模块名", program.compile.moduleName ?? ""],
    ["sourceMapEntries", String(program.compile.sourceMapEntries)],
    ["symbol/procedure/path/operation", `${program.compile.symbolCount}/${program.compile.procedureCount}/${program.compile.pathCount}/${program.compile.operationCount}`],
    ["KDL motion/path requests", `${program.compile.kdlMotionRequests}/${program.compile.kdlPathRequests}`],
    ["状态", program.status]
  ];
  if (program.controller) {
    rows.push(["controller final state", program.controller.snapshot.state.state]);
    rows.push(["runtime trace events", String(program.trace?.length ?? 0)]);
    rows.push(["motion queue items", String(program.motionQueue?.items?.length ?? 0)]);
    rows.push(["IO image count", String(Object.keys(program.io?.image ?? {}).length)]);
  }
  if (program.trajectory) {
    rows.push(["trajectory sample/duration", `${program.trajectory.sampleCount}/${program.trajectory.duration}`]);
  }
  if (program.post) {
    rows.push(["post filenames", program.post.filenames.join("、")]);
    rows.push(["post report codes", program.post.report.length === 0 ? "无" : [...new Set(program.post.report.map((issue) => issue.code))].join("、")]);
  }
  if (program.roundtrip) {
    rows.push(["roundtrip", `${program.roundtrip.status}; ${program.roundtrip.diagnostics.map((diagnostic) => diagnostic.code).join("、") || "无诊断"}`]);
  }
  rows.push(["诊断数量", String(program.diagnostics.length)]);
  return rows;
}

function heading(text, level) {
  return paragraph(text, `Heading${Math.min(level, 3)}`);
}

function p(text) {
  return paragraph(text, "BodyText");
}

function paragraph(text, style) {
  return `<w:p><w:pPr><w:pStyle w:val="${style}"/></w:pPr>${run(text)}</w:p>`;
}

function run(text) {
  return `<w:r><w:t xml:space="preserve">${escapeXml(String(text))}</w:t></w:r>`;
}

function codeBlock(text) {
  const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
  const lines = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
  return lines.map((line) =>
    `<w:p><w:pPr><w:pStyle w:val="Code"/></w:pPr><w:r><w:t xml:space="preserve">${escapeXml(line === "" ? " " : line)}</w:t></w:r></w:p>`
  ).join("\n");
}

function table(rows) {
  const width = Math.floor(9000 / Math.max(1, rows[0]?.length ?? 1));
  return `<w:tbl>
    <w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblLook w:val="04A0"/></w:tblPr>
    ${rows.map((row, rowIndex) => `<w:tr>${row.map((cell) => tableCell(cell, width, rowIndex === 0)).join("")}</w:tr>`).join("\n")}
  </w:tbl>`;
}

function tableCell(text, width, header) {
  return `<w:tc>
    <w:tcPr><w:tcW w:w="${width}" w:type="dxa"/></w:tcPr>
    <w:p><w:pPr>${header ? "<w:pStyle w:val=\"TableHeader\"/>" : ""}</w:pPr>${run(text)}</w:p>
  </w:tc>`;
}

function imageParagraph(image, docPrId) {
  return `<w:p><w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:drawing>
    <wp:inline distT="0" distB="0" distL="0" distR="0">
      <wp:extent cx="${image.cx}" cy="${image.cy}"/>
      <wp:effectExtent l="0" t="0" r="0" b="0"/>
      <wp:docPr id="${docPrId}" name="${escapeXml(image.title)}"/>
      <wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/></wp:cNvGraphicFramePr>
      <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
        <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
          <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
            <pic:nvPicPr><pic:cNvPr id="${docPrId}" name="${escapeXml(image.fileName)}"/><pic:cNvPicPr/></pic:nvPicPr>
            <pic:blipFill><a:blip r:embed="${image.id}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
            <pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${image.cx}" cy="${image.cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
          </pic:pic>
        </a:graphicData>
      </a:graphic>
    </wp:inline>
  </w:drawing></w:r></w:p>`;
}

function contentTypesXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
  <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
  <Default Extension="xml" ContentType="application/xml"/>
  <Default Extension="png" ContentType="image/png"/>
  <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
  <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
  <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
  <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
</Types>`;
}

function packageRelsXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>`;
}

function documentRelsXml(images) {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
  ${images.map((image) => `<Relationship Id="${image.id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${image.fileName}"/>`).join("\n")}
</Relationships>`;
}

function corePropsXml() {
  const iso = new Date().toISOString();
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
  xmlns:dc="http://purl.org/dc/elements/1.1/"
  xmlns:dcterms="http://purl.org/dc/terms/"
  xmlns:dcmitype="http://purl.org/dc/dcmitype/"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <dc:title>通用机器人离线编程系统测试文档</dc:title>
  <dc:creator>Codex</dc:creator>
  <cp:lastModifiedBy>Codex</cp:lastModifiedBy>
  <dcterms:created xsi:type="dcterms:W3CDTF">${iso}</dcterms:created>
  <dcterms:modified xsi:type="dcterms:W3CDTF">${iso}</dcterms:modified>
</cp:coreProperties>`;
}

function appPropsXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
  xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
  <Application>Codex</Application>
  <DocSecurity>0</DocSecurity>
  <ScaleCrop>false</ScaleCrop>
  <Company>KDL Work</Company>
  <LinksUpToDate>false</LinksUpToDate>
  <SharedDoc>false</SharedDoc>
  <HyperlinksChanged>false</HyperlinksChanged>
  <AppVersion>16.0000</AppVersion>
</Properties>`;
}

function stylesXml() {
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
    <w:name w:val="Normal"/>
    <w:qFormat/>
    <w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="BodyText">
    <w:name w:val="Body Text"/>
    <w:basedOn w:val="Normal"/>
    <w:pPr><w:spacing w:after="120" w:line="276" w:lineRule="auto"/></w:pPr>
    <w:rPr><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="21"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading1">
    <w:name w:val="heading 1"/>
    <w:basedOn w:val="Normal"/>
    <w:next w:val="BodyText"/>
    <w:qFormat/>
    <w:pPr><w:keepNext/><w:spacing w:before="360" w:after="180"/></w:pPr>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="32"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading2">
    <w:name w:val="heading 2"/>
    <w:basedOn w:val="Normal"/>
    <w:next w:val="BodyText"/>
    <w:qFormat/>
    <w:pPr><w:keepNext/><w:spacing w:before="280" w:after="140"/></w:pPr>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="27"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Heading3">
    <w:name w:val="heading 3"/>
    <w:basedOn w:val="Normal"/>
    <w:next w:val="BodyText"/>
    <w:qFormat/>
    <w:pPr><w:keepNext/><w:spacing w:before="180" w:after="100"/></w:pPr>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="23"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="Code">
    <w:name w:val="Code"/>
    <w:basedOn w:val="Normal"/>
    <w:pPr><w:spacing w:before="0" w:after="0" w:line="240" w:lineRule="auto"/></w:pPr>
    <w:rPr><w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:eastAsia="Consolas"/><w:sz w:val="16"/></w:rPr>
  </w:style>
  <w:style w:type="paragraph" w:styleId="TableHeader">
    <w:name w:val="Table Header"/>
    <w:basedOn w:val="Normal"/>
    <w:rPr><w:b/><w:rFonts w:ascii="Microsoft YaHei" w:hAnsi="Microsoft YaHei" w:eastAsia="Microsoft YaHei"/><w:sz w:val="20"/></w:rPr>
  </w:style>
  <w:style w:type="table" w:default="1" w:styleId="TableNormal">
    <w:name w:val="Normal Table"/>
    <w:tblPr><w:tblInd w:w="0" w:type="dxa"/><w:tblCellMar><w:top w:w="0" w:type="dxa"/><w:left w:w="108" w:type="dxa"/><w:bottom w:w="0" w:type="dxa"/><w:right w:w="108" w:type="dxa"/></w:tblCellMar></w:tblPr>
  </w:style>
  <w:style w:type="table" w:styleId="TableGrid">
    <w:name w:val="Table Grid"/>
    <w:basedOn w:val="TableNormal"/>
    <w:tblPr><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:left w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:right w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="B7C1CC"/></w:tblBorders></w:tblPr>
  </w:style>
</w:styles>`;
}

function latestJobId() {
  const jobs = readdirSync(resultRoot, { withFileTypes: true })
    .filter((entry) => entry.isDirectory() && existsSync(join(resultRoot, entry.name, "job.json")))
    .map((entry) => ({
      name: entry.name,
      mtimeMs: statSync(join(resultRoot, entry.name, "job.json")).mtimeMs
    }))
    .sort((a, b) => b.mtimeMs - a.mtimeMs);
  if (jobs.length === 0) {
    throw new Error(`No abb120 spec job found in ${resultRoot}`);
  }
  return jobs[0].name;
}

function readJson(path) {
  return JSON.parse(readFileSync(path, "utf8"));
}

function relative(path) {
  return path.replace(`${root}${sep}`, "").replaceAll("\\", "/");
}

function escapeXml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll("\"", "&quot;")
    .replaceAll("'", "&apos;");
}

function psSingleQuoted(value) {
  return `'${String(value).replaceAll("'", "''")}'`;
}

function zipDirectory(sourceDir, destinationFile) {
  const script = [
    "Add-Type -AssemblyName System.IO.Compression.FileSystem",
    `$src = ${psSingleQuoted(sourceDir)}`,
    `$dst = ${psSingleQuoted(destinationFile)}`,
    "if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Force }",
    "[System.IO.Compression.ZipFile]::CreateFromDirectory($src, $dst)"
  ].join("\n");
  execFileSync("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
    stdio: "inherit"
  });
}

function assertSafeStaging(path) {
  const allowedRoot = join(webRoot, "test-results") + sep;
  const resolved = resolve(path);
  if (!resolved.startsWith(allowedRoot)) {
    throw new Error(`Refusing to clear staging path outside test-results: ${resolved}`);
  }
}

function formatShanghaiTime(date) {
  const parts = new Intl.DateTimeFormat("zh-CN", {
    timeZone: "Asia/Shanghai",
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
    hour12: false
  }).formatToParts(date);
  const value = Object.fromEntries(parts.map((part) => [part.type, part.value]));
  return `${value.year}-${value.month}-${value.day} ${value.hour}:${value.minute}:${value.second} Asia/Shanghai`;
}
