import { createHash } from "node:crypto";

import { runWebProbeApiStateSmoke } from "./web-probe-apistate-smoke";
import { runWebProbeSuperApiSmoke } from "./web-probe-superapi-smoke";
import { runWebProbeTemporalSmoke } from "./web-probe-temporal-smoke";
import { hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane } from "./hwlab-node-lanes";
import { readDeclaredSecretValues } from "./secrets";
import {
  pikaoaDevelopmentRuntimeConfigRef,
  pikaoaReleaseRuntimeConfigRef,
  resolvePikaoaDevelopmentRuntimeTargetByConfigRef,
  resolvePikaoaReleaseRuntimeTargetByConfigRef,
  type PikaoaRuntimeSpec,
  type PikaoaWebProbeSmokeProfileSpec,
} from "./pikaoa-runtime";
import {
  resolveSelfMediaDeliveryTarget,
  selfMediaDeliveryConfigRef,
  type SelfMediaDeliveryTarget,
  type SelfMediaWebProbeSmokeProfileSpec,
} from "./selfmedia-config";
import type { NodeWebProbeScriptOptions } from "./hwlab-node/entry";
import { runManagedNodeWebProbeScript } from "./hwlab-node/web-observe-scripts";

export interface WebProbeProductSmokeOptions {
  readonly product: string;
  readonly target: string;
  readonly profile: string | null;
  readonly edition: string | null;
  readonly algorithm: string | null;
  readonly runtime?: string | null;
}

interface ResolvedProductSmoke {
  readonly target: SelfMediaDeliveryTarget | PikaoaRuntimeSpec;
  readonly profileName: string;
  readonly profile: SelfMediaWebProbeSmokeProfileSpec | PikaoaWebProbeSmokeProfileSpec;
  readonly configRef: string;
  readonly commandLabel: string;
  readonly scriptOptions: NodeWebProbeScriptOptions;
  readonly webProbe: SelfMediaDeliveryTarget["webProbe"] | PikaoaRuntimeSpec["webProbe"];
}

export function parseWebProbeProductSmokeOptions(args: string[]): WebProbeProductSmokeOptions {
  const values = new Map<string, string>();
  const known = new Set(["--product", "--target", "--profile", "--edition", "--algorithm", "--runtime"]);
  for (let index = 0; index < args.length; index += 1) {
    const item = args[index];
    if (item === "--full" || item === "--raw") continue;
    const equals = item.indexOf("=");
    const key = equals > 0 ? item.slice(0, equals) : item;
    if (!known.has(key)) throw new Error(`web-probe product-smoke 不支持参数 ${item}`);
    const value = equals > 0 ? item.slice(equals + 1) : args[++index];
    if (value === undefined || value.startsWith("--")) throw new Error(`${key} 需要一个值`);
    if (values.has(key)) throw new Error(`${key} 不能重复`);
    values.set(key, simpleId(value, key));
  }
  return {
    product: requiredValue(values, "--product"),
    target: requiredValue(values, "--target"),
    profile: values.get("--profile") ?? null,
    edition: values.get("--edition") ?? null,
    algorithm: values.get("--algorithm") ?? null,
    runtime: values.get("--runtime") ?? null,
  };
}

export function compactWebProbeProductSmokeProjection(result: Record<string, unknown>): Record<string, unknown> {
  const probe = machineRecord(result.probe);
  const summary = machineRecord(result.summary);
  const issueEvidence = machineRecord(result.issueEvidence);
  const probeIssueEvidence = machineRecord(probe.issueEvidence);
  const auth = machineRecord(probe.auth);
  const failureEvidence = machineRecord(probe.failureEvidence);
  const reportLoad = machineRecord(result.reportLoad);
  const artifactItems = machineArtifacts(probe, machineRecord(result.recoveredArtifacts));
  const evidenceSources = [issueEvidence, probeIssueEvidence, summary, auth];
  const reportPath = firstMachineText([reportLoad.path, probe.reportPath, issueEvidence.reportPath, probeIssueEvidence.reportPath]);
  const reportSha256 = firstMachineText([probe.reportSha256, issueEvidence.reportSha256, probeIssueEvidence.reportSha256]);
  const node = machineText(result.node);
  const lane = machineText(result.lane);
  const reportInspect = node !== null && lane !== null && reportPath !== null && reportSha256 !== null
    ? `bun scripts/cli.ts web-probe artifact inspect --node ${node} --lane ${lane} --path ${reportPath} --sha256 ${reportSha256}`
    : null;
  return {
    commandPromotionHint: machineRecord(result.commandPromotionHint),
    ok: result.ok === true,
    status: machineText(result.status),
    command: machineText(result.command),
    node,
    lane,
    url: machineText(result.url),
    degradedReason: firstMachineText([
      result.degradedReason,
      issueEvidence.degradedReason,
      probeIssueEvidence.degradedReason,
      summary.degradedReason,
      probe.error,
    ]),
    failureKind: firstMachineText([
      result.failureKind,
      issueEvidence.failureKind,
      probeIssueEvidence.failureKind,
      summary.failureKind,
      probe.failureKind,
    ]),
    reachability: {
      credentialResolved: firstMachineBoolean(evidenceSources, "credentialResolved"),
      requestReached: firstMachineBoolean(evidenceSources, "requestReached"),
      apiResponseReached: firstMachineBoolean(evidenceSources, "apiResponseReached"),
      errorSummary: firstMachineRecord(evidenceSources, "errorSummary"),
    },
    failedCondition: firstMachineText([issueEvidence.failedCondition, probeIssueEvidence.failedCondition, summary.failedCondition]),
    failureEvidence: Object.keys(failureEvidence).length === 0 ? null : failureEvidence,
    report: { source: machineText(reportLoad.source), path: reportPath, sha256: reportSha256 },
    artifacts: artifactItems,
    productSmoke: machineRecord(result.productSmoke),
    next: { report: reportInspect, rerun: machineText(result.command) },
    valuesPrinted: false,
  };
}

export function resolveWebProbeProductSmoke(options: WebProbeProductSmokeOptions): ResolvedProductSmoke {
  if (options.product === "pikaoa") return resolvePikaoaProductSmoke(options);
  if (options.product !== "selfmedia") throw new Error(`未知 --product ${options.product}；当前声明的产品为 pikaoa、selfmedia`);
  if (options.runtime !== null && options.runtime !== undefined) throw new Error("SelfMedia product-smoke 不接受 --runtime");
  const edition = requiredProductValue(options.edition, "--edition", options.product);
  const algorithm = requiredProductValue(options.algorithm, "--algorithm", options.product);
  const target = resolveSelfMediaDeliveryTarget(options.target);
  const webProbe = target.webProbe;
  if (webProbe.productId !== options.product) {
    throw new Error(`${selfMediaDeliveryConfigRef(target.id)}.webProbe.productId 与 --product 不一致`);
  }
  if (!webProbe.enabled) throw new Error(`${selfMediaDeliveryConfigRef(target.id)}.webProbe.enabled=false，禁止执行产品 Web smoke`);
  const profileName = options.profile ?? webProbe.defaultSmokeProfile;
  const profile = webProbe.smokeProfiles[profileName];
  if (profile === undefined) {
    throw new Error(`未知 --profile ${profileName}；可用项：${Object.keys(webProbe.smokeProfiles).sort().join(", ")}`);
  }
  if (!isHwlabRuntimeLane(webProbe.runner.lane)) throw new Error(`Web smoke runner lane 未在 HWLAB YAML 中声明：${webProbe.runner.lane}`);
  hwlabRuntimeLaneSpecForNode(webProbe.runner.lane, webProbe.runner.node);

  const configRef = `${selfMediaDeliveryConfigRef(target.id)}.webProbe.smokeProfiles.${profileName}`;
  const commandLabel = [
    "web-probe product-smoke",
    `--product ${options.product}`,
    `--target ${target.id}`,
    `--profile ${profileName}`,
    `--edition ${edition}`,
    `--algorithm ${algorithm}`,
  ].join(" ");
  const scriptText = selfMediaVoiceAlgorithmSmokeScript(edition, algorithm, profileName, profile, configRef);
  const scriptOptions: NodeWebProbeScriptOptions = {
    action: "script",
    node: webProbe.runner.node,
    lane: webProbe.runner.lane,
    url: webProbe.origin.baseUrl,
    originName: webProbe.origin.name,
    originMode: webProbe.origin.mode,
    originConfigPath: `${selfMediaDeliveryConfigRef(target.id)}.webProbe.origin`,
    browserProxyMode: webProbe.origin.browserProxyMode,
    browserProxyModeSource: "yaml-origin",
    timeoutMs: profile.navigationTimeoutMs,
    viewport: `${profile.viewport.width}x${profile.viewport.height}`,
    commandTimeoutSeconds: profile.commandTimeoutSeconds,
    scriptText,
    commandLabel,
    suppressAdHocWarning: true,
    generatedHints: [
      "该命令由 SelfMedia owning YAML 驱动，并复用受控认证、远端浏览器、内存门禁及证据恢复链。",
      "再次验证同一功能时复用 product-smoke，不要复制生成的浏览器脚本。",
    ],
    generatedPreferredCommands: { rerun: commandLabel },
    scriptSource: {
      kind: "generated",
      path: `builtin:product-smoke/${options.product}/${profileName}`,
      byteCount: Buffer.byteLength(scriptText),
      sha256: `sha256:${createHash("sha256").update(scriptText).digest("hex")}`,
    },
  };
  return { target, profileName, profile, configRef, commandLabel, scriptOptions, webProbe };
}

export function runWebProbeProductSmoke(args: string[]): Record<string, unknown> {
  const parsed = parseWebProbeProductSmokeOptions(args);
  if (parsed.product === "apistate") {
    if (parsed.edition !== null || parsed.algorithm !== null) throw new Error("ApiState product-smoke 不接受 --edition 或 --algorithm");
    return runWebProbeApiStateSmoke({ target: parsed.target, profile: parsed.profile, runtime: parsed.runtime });
  }
  if (parsed.product === "temporal") {
    if (parsed.edition !== null || parsed.algorithm !== null || parsed.runtime !== null) {
      throw new Error("Temporal product-smoke 不接受 --edition、--algorithm 或 --runtime");
    }
    return runWebProbeTemporalSmoke({ target: parsed.target, profile: parsed.profile });
  }
  if (parsed.product === "superapi") {
    if (parsed.edition !== null || parsed.algorithm !== null || parsed.runtime !== null) {
      throw new Error("SuperAPI product-smoke 不接受 --edition、--algorithm 或 --runtime");
    }
    return runWebProbeSuperApiSmoke({ target: parsed.target, profile: parsed.profile });
  }
  const resolved = resolveWebProbeProductSmoke(parsed);
  const webProbe = resolved.webProbe;
  const credentials = webProbe.authentication.credentials;
  const targetKeys = "usernameTargetKey" in credentials
    ? [credentials.usernameTargetKey, credentials.passwordTargetKey]
    : [credentials.passwordTargetKey];
  const declared = readDeclaredSecretValues({
    configPath: credentials.configRef,
    targetId: credentials.targetId,
    secretName: credentials.secretName,
    targetKeys,
  });
  const username = "usernameTargetKey" in credentials
    ? declared.values[credentials.usernameTargetKey]
    : "runtime" in resolved.target ? resolved.target.runtime.administrator.username : undefined;
  const password = declared.values[credentials.passwordTargetKey];
  if (username === undefined || password === undefined) throw new Error(`${parsed.product} Web smoke 凭据声明不完整`);
  const lane = webProbe.runner.lane;
  if (!isHwlabRuntimeLane(lane)) throw new Error(`Web smoke runner lane 未在 HWLAB YAML 中声明：${lane}`);
  const spec = hwlabRuntimeLaneSpecForNode(lane, webProbe.runner.node);
  const result = runManagedNodeWebProbeScript(
    resolved.scriptOptions,
    spec,
    { username, password },
    {
      product: parsed.product,
      target: resolved.target.id,
      source: declared.summary,
      valuesPrinted: false,
    },
    webProbe.authentication.mode === "json-post-session-cookie" ? {
      mode: "json-post-session-cookie",
      loginPath: webProbe.authentication.path,
      usernameField: webProbe.authentication.requestFields.username,
      passwordField: webProbe.authentication.requestFields.password,
      cookieNames: webProbe.authentication.cookieNames,
    } : {
      mode: "ui-form-memory",
      loginPath: webProbe.authentication.path,
      usernameSelector: webProbe.authentication.usernameSelector,
      passwordSelector: webProbe.authentication.passwordSelector,
      submitSelector: webProbe.authentication.submitSelector,
      authenticatedPath: webProbe.authentication.authenticatedPath,
      authenticatedSelector: webProbe.authentication.authenticatedSelector,
    },
  );
  return {
    ...result,
    productSmoke: {
      product: parsed.product,
      target: resolved.target.id,
      profile: resolved.profileName,
      edition: parsed.edition,
      algorithm: parsed.algorithm,
      configRef: resolved.configRef,
      valuesPrinted: false,
    },
    valuesPrinted: false,
  };
}

function resolvePikaoaProductSmoke(options: WebProbeProductSmokeOptions): ResolvedProductSmoke {
  if (options.edition !== null || options.algorithm !== null) throw new Error("PikaOA product-smoke 不接受 --edition 或 --algorithm");
  const runtime = options.runtime ?? "development";
  if (runtime !== "development" && runtime !== "release" && runtime !== "native") throw new Error("PikaOA --runtime 只支持 development、release 或 native");
  const configRef = runtime === "release" ? pikaoaReleaseRuntimeConfigRef(options.target) : pikaoaDevelopmentRuntimeConfigRef(options.target);
  const target = runtime === "release" ? resolvePikaoaReleaseRuntimeTargetByConfigRef(configRef) : resolvePikaoaDevelopmentRuntimeTargetByConfigRef(configRef);
  const webProbe = runtime === "native" ? { ...target.webProbe, ...target.native.webProbe } : target.webProbe;
  if (webProbe.productId !== options.product) throw new Error(`${configRef}.webProbe.productId 与 --product 不一致`);
  if (!webProbe.enabled) throw new Error(`${configRef}.webProbe.enabled=false，禁止执行产品 Web smoke`);
  const profileName = options.profile ?? webProbe.defaultSmokeProfile;
  const profile = webProbe.smokeProfiles[profileName];
  if (profile === undefined) throw new Error(`未知 --profile ${profileName}；可用项：${Object.keys(webProbe.smokeProfiles).sort().join(", ")}`);
  if (!isHwlabRuntimeLane(webProbe.runner.lane)) throw new Error(`Web smoke runner lane 未在 HWLAB YAML 中声明：${webProbe.runner.lane}`);
  hwlabRuntimeLaneSpecForNode(webProbe.runner.lane, webProbe.runner.node);
  const profileConfigRef = `${configRef}.webProbe.smokeProfiles.${profileName}`;
  const commandLabel = [
    "web-probe product-smoke",
    `--product ${options.product}`,
    `--target ${target.id}`,
    `--profile ${profileName}`,
    `--runtime ${runtime}`,
  ].join(" ");
  const scriptText = pikaoaAdminMvpSmokeScript(profileName, profile, profileConfigRef);
  const scriptOptions: NodeWebProbeScriptOptions = {
    action: "script",
    node: webProbe.runner.node,
    lane: webProbe.runner.lane,
    url: webProbe.origin.baseUrl,
    originName: webProbe.origin.name,
    originMode: webProbe.origin.mode,
    originConfigPath: `${configRef}.${runtime === "native" ? "native.webProbe.origin" : "webProbe.origin"}`,
    browserProxyMode: webProbe.origin.browserProxyMode,
    browserProxyModeSource: "yaml-origin",
    timeoutMs: profile.navigationTimeoutMs,
    viewport: `${profile.viewport.width}x${profile.viewport.height}`,
    commandTimeoutSeconds: profile.commandTimeoutSeconds,
    scriptText,
    commandLabel,
    suppressAdHocWarning: true,
    generatedHints: [
      "该命令由 PikaOA owning YAML 驱动，并复用同页内存会话、远端浏览器、内存门禁及证据恢复链。",
      "再次验证同一功能时复用 product-smoke，不要复制生成的浏览器脚本。",
    ],
    generatedPreferredCommands: { rerun: commandLabel },
    scriptSource: {
      kind: "generated",
      path: `builtin:product-smoke/${options.product}/${profileName}`,
      byteCount: Buffer.byteLength(scriptText),
      sha256: `sha256:${createHash("sha256").update(scriptText).digest("hex")}`,
    },
  };
  return { target, profileName, profile, configRef: profileConfigRef, commandLabel, scriptOptions, webProbe };
}

export function pikaoaAdminMvpSmokeScript(profileName: string, profile: PikaoaWebProbeSmokeProfileSpec, configRef: string): string {
  if (profile.businessAssertions === null) throw new Error(`${configRef}.webProbe.smokeProfiles.${profileName}.businessAssertions 必须显式声明`);
  const config = { profileName, configRef, ...profile };
  return String.raw`const config = ${JSON.stringify(config)};

export default async function pikaoaAdminMvpSmoke({ page, wait, screenshot, jsonArtifact, recordStep }) {
  const failures = [];
  const routes = [];
  const network = [];
  let networkCount = 0;
  const addFailure = (value) => {
    if (failures.length < config.outputLimits.failures) failures.push(String(value).replace(/\s+/gu, " ").slice(0, 300));
  };
  const addNetwork = (value) => {
    networkCount += 1;
    if (network.length < config.outputLimits.network) network.push(value);
  };
  page.on("response", (response) => {
    const parsed = safeUrl(response.url());
    if (parsed === null || parsed.origin !== new URL(page.url() || "http://invalid").origin) return;
    addNetwork({ method: response.request().method(), path: parsed.pathname, status: response.status() });
  });
  page.on("requestfailed", (request) => {
    const parsed = safeUrl(request.url());
    if (parsed !== null) addNetwork({ method: request.method(), path: parsed.pathname, status: null, failure: String(request.failure()?.errorText || "request failed").slice(0, 160) });
  });

  for (const route of config.routes.slice(0, config.outputLimits.routes)) {
    try {
      const navigation = page.locator(route.navigationSelector).first();
      if (await navigation.count() !== 1) throw new Error("声明的导航入口不存在或不唯一：" + route.navigationSelector);
      await Promise.all([
        page.waitForURL((url) => url.pathname === route.path, { timeout: config.navigationTimeoutMs }),
        navigation.click(),
      ]);
      await page.locator(route.readySelector).first().waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
      if (config.settleMs > 0) await wait(config.settleMs);
      const finalPath = new URL(page.url()).pathname;
      const heading = (await page.locator(route.readySelector).first().textContent())?.replace(/\s+/gu, " ").trim() || "";
      if (finalPath !== route.path) throw new Error("路由未保持在 " + route.path + "，实际为 " + finalPath);
      if (heading !== route.heading) throw new Error("页面标题不一致，预期 " + route.heading + "，实际为 " + heading);
      const item = { path: route.path, finalPath, navigationSelector: route.navigationSelector, readySelector: route.readySelector, heading, ok: true };
      routes.push(item);
      recordStep("验证 " + route.path, item);
    } catch (error) {
      const failure = error instanceof Error ? error.message : String(error);
      addFailure(route.path + "：" + failure);
      routes.push({ path: route.path, ok: false, failure: failure.slice(0, 240) });
      recordStep("验证 " + route.path, { ok: false, failure });
    }
  }

  const documents = { contract: null, invoice: null, attachments: [] };
  const resourceReferences = { filter: null, details: { contract: null, invoice: null, attachment: null } };
  let invoiceLayout = null;
  let invoiceShot = null;
  let invoiceMobileShot = null;
  const pdfs = [];
  try {
    await navigateTo("/contracts");
    const contractRow = page.locator(config.businessAssertions.contract.rowSelector).filter({ hasText: config.businessAssertions.contract.number });
    await contractRow.first().waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    if (await contractRow.count() !== 1) throw new Error("合同列表没有唯一命中：" + config.businessAssertions.contract.number);
    resourceReferences.filter = await verifyPartnerFilter(contractRow);
    await contractRow.click();
    resourceReferences.details.contract = await verifyDetailResourceLink(config.businessAssertions.resourceReferences.detail.contract);
    const versionLink = page.locator(config.businessAssertions.contract.currentVersionLinkSelector).first();
    await versionLink.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    await versionLink.click();
    const contractAttachment = page.locator(config.businessAssertions.contract.attachmentSectionSelector).filter({ hasText: config.businessAssertions.contract.attachmentName });
    await contractAttachment.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    const contractPdf = await verifyPdf(contractAttachment, "contract");
    pdfs.push(contractPdf);
    documents.contract = { number: config.businessAssertions.contract.number, attachmentName: config.businessAssertions.contract.attachmentName, finalPath: new URL(page.url()).pathname, ok: true };
    recordStep("验证合同与 PDF", documents.contract);

    await navigateTo("/invoices");
    const activeRow = page.locator(config.businessAssertions.invoice.rowSelector).filter({ hasText: config.businessAssertions.invoice.activeNumber });
    const voidedRow = page.locator(config.businessAssertions.invoice.rowSelector).filter({ hasText: config.businessAssertions.invoice.voidedNumber });
    await activeRow.first().waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    await voidedRow.first().waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    if (await activeRow.count() !== 1) throw new Error("有效发票列表没有唯一命中：" + config.businessAssertions.invoice.activeNumber);
    if (await voidedRow.count() !== 1 || !(await voidedRow.innerText()).includes(config.businessAssertions.invoice.voidedLabel)) {
      throw new Error("废弃发票或废弃状态不可见：" + config.businessAssertions.invoice.voidedNumber);
    }
    const attachmentHeader = page.locator(config.businessAssertions.invoice.attachmentHeaderSelector).filter({ hasText: config.businessAssertions.invoice.attachmentHeaderText });
    if (await attachmentHeader.count() !== 1) throw new Error("发票列表附件表头不存在或不唯一");
    const activeAttachmentCell = activeRow.locator(config.businessAssertions.invoice.attachmentCellSelector).first();
    const activeAttachmentCount = Number(await activeAttachmentCell.getAttribute("data-attachment-count"));
    if (!Number.isFinite(activeAttachmentCount) || activeAttachmentCount < config.businessAssertions.invoice.minimumActiveAttachmentCount) {
      throw new Error("有效发票附件数量未达到 YAML 声明的最小值");
    }
    const layout = config.businessAssertions.invoice.layout;
    invoiceLayout = await captureInvoiceLayout("desktop", config.viewport, layout);
    invoiceShot = await screenshot(layout.screenshotName);
    invoiceLayout.mobile = await captureInvoiceLayout("mobile", layout.mobile.viewport, layout.mobile);
    invoiceMobileShot = await screenshot(layout.mobile.screenshotName);
    await page.setViewportSize(config.viewport);
    if (config.settleMs > 0) await wait(config.settleMs);
    recordStep("验证发票列表信息密度", invoiceLayout);
    await activeRow.click();
    resourceReferences.details.invoice = await verifyDetailResourceLink(config.businessAssertions.resourceReferences.detail.invoice);
    const invoiceAttachment = page.locator(config.businessAssertions.invoice.attachmentSectionSelector).filter({ hasText: config.businessAssertions.invoice.attachmentName });
    await invoiceAttachment.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    const invoicePdf = await verifyPdf(invoiceAttachment, "invoice");
    pdfs.push(invoicePdf);
    documents.invoice = { activeNumber: config.businessAssertions.invoice.activeNumber, voidedNumber: config.businessAssertions.invoice.voidedNumber, voidedLabel: config.businessAssertions.invoice.voidedLabel, attachmentName: config.businessAssertions.invoice.attachmentName, activeAttachmentCount, ok: true };
    recordStep("验证发票与 PDF", documents.invoice);

    await navigateTo("/attachments");
    for (const [index, name] of config.businessAssertions.attachments.names.entries()) {
      const row = page.locator(config.businessAssertions.attachments.rowSelector).filter({ hasText: name });
      const count = await row.count();
      if (count < 1) throw new Error("附件中心没有命中：" + name);
      documents.attachments.push({ name, count, ok: true });
      if (index === 0) {
        await row.first().click();
        resourceReferences.details.attachment = await verifyDetailResourceLink(config.businessAssertions.resourceReferences.detail.attachment);
      }
    }
    recordStep("验证全局附件中心", { items: documents.attachments });
  } catch (error) {
    const failure = error instanceof Error ? error.message : String(error);
    addFailure("业务文档验证：" + failure);
    recordStep("验证业务文档", { ok: false, failure });
  }

  const shot = await screenshot(config.screenshotName).catch((error) => {
    addFailure("截图失败：" + (error instanceof Error ? error.message : String(error)));
    return null;
  });
  const evidence = {
    profileName: config.profileName,
    configRef: config.configRef,
    viewport: config.viewport,
    routes,
    documents,
    resourceReferences,
    invoiceLayout,
    pdfs,
    network: { count: networkCount, items: network },
    failures,
    valuesRedacted: true,
  };
  const report = await jsonArtifact("pikaoa-admin-mvp-smoke.json", evidence).catch((error) => {
    addFailure("报告写入失败：" + (error instanceof Error ? error.message : String(error)));
    return null;
  });
  const ok = failures.length === 0
    && routes.length === config.routes.length
    && routes.every((item) => item.ok === true)
    && documents.contract?.ok === true
    && documents.invoice?.ok === true
    && documents.attachments.length === config.businessAssertions.attachments.names.length
    && pdfs.length === 2
    && pdfs.every((item) => item.ok === true);
  return {
    ok,
    status: ok ? "pass" : "blocked",
    ...evidence,
    report,
    screenshot: shot,
    issueEvidence: {
      ok,
      status: ok ? "pass" : "blocked",
      failedCondition: ok ? null : failures[0] || "PikaOA admin MVP 页面矩阵失败",
      result: evidence,
      reportPath: report?.path || null,
      reportSha256: report?.sha256 || null,
      screenshots: [invoiceShot, invoiceMobileShot, shot].filter(Boolean),
      valuesRedacted: true,
    },
    valuesRedacted: true,
  };

  function safeUrl(value) {
    try { return new URL(value); } catch { return null; }
  }

  async function captureInvoiceLayout(mode, viewport, limits) {
    await page.setViewportSize(viewport);
    if (config.settleMs > 0) await wait(config.settleMs);
    const layout = config.businessAssertions.invoice.layout;
    const activeRow = page.locator(config.businessAssertions.invoice.rowSelector).filter({ hasText: config.businessAssertions.invoice.activeNumber });
    const voidedRow = page.locator(config.businessAssertions.invoice.rowSelector).filter({ hasText: config.businessAssertions.invoice.voidedNumber });
    const toolbarBox = await page.locator(layout.toolbarSelector).first().boundingBox();
    const listBox = await page.locator(layout.listWorkspaceSelector).first().boundingBox();
    const activeRowBox = await activeRow.boundingBox();
    const voidedRowBox = await voidedRow.boundingBox();
    const rowInsideViewport = (box) => box !== null && box.y >= 0 && box.y + box.height <= viewport.height;
    const advancedFiltersVisible = await page.locator(layout.advancedFiltersSelector).isVisible().catch(() => false);
    const listVisibleHeight = listBox === null ? 0 : Math.max(0, Math.min(listBox.y + listBox.height, viewport.height) - Math.max(0, listBox.y));
    const evidence = { mode, toolbarBox, listBox, listVisibleHeight, activeRowBox, voidedRowBox, advancedFiltersVisible, viewport: { width: viewport.width, height: viewport.height } };
    if (toolbarBox === null || toolbarBox.height > limits.maxToolbarHeight) throw new Error(mode + " 发票工具带高度超限");
    if (listBox === null || listBox.y > limits.maxListTop) throw new Error(mode + " 发票列表进入首屏过晚");
    if (listVisibleHeight < limits.minListVisibleHeight) throw new Error(mode + " 发票列表首屏可见高度不足");
    if (advancedFiltersVisible) throw new Error(mode + " 高级筛选未默认收起");
    if (!rowInsideViewport(activeRowBox) || !rowInsideViewport(voidedRowBox)) throw new Error(mode + " 有效或废弃发票行不在首屏可见区域");
    return evidence;
  }

  async function verifyPartnerFilter(contractRow) {
    const filter = config.businessAssertions.resourceReferences.filter;
    const partnerLink = contractRow.locator(filter.partnerLinkSelector).first();
    await partnerLink.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    const partnerId = (await partnerLink.getAttribute("title"))?.trim() || "";
    const partnerName = (await partnerLink.textContent())?.replace(/\s+/gu, " ").trim() || "";
    const partnerHref = await partnerLink.getAttribute("href");
    if (!partnerId || !partnerName || partnerName === partnerId) throw new Error("合同伙伴链接没有同时提供稳定 ID 与名称");
    if (!partnerHref || !decodeURIComponent(new URL(partnerHref, page.url()).pathname).endsWith("/" + partnerId)) throw new Error("合同伙伴名称链接没有保持伙伴 ID 路由身份");

    const combobox = page.getByRole("combobox", { name: filter.comboboxLabel });
    if (await combobox.count() !== 1 || !(await combobox.isVisible())) {
      await page.locator(filter.advancedToggleSelector).click();
      await combobox.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    }
    const byName = await applyPartnerFilter(combobox, partnerName, partnerId);
    const idTerm = partnerId.slice(-filter.idSearchLength);
    const byId = await applyPartnerFilter(combobox, idTerm, partnerId);
    await page.locator(filter.clearSelector).click();
    await submitPartnerFilter(partnerId, false);
    await contractRow.first().waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    const evidence = {
      partnerName,
      partnerIdSuffix: partnerId.slice(-8),
      partnerPath: new URL(partnerHref, page.url()).pathname,
      byName,
      byId,
      selectedDisplayIsName: byName.selectedDisplay === partnerName && byId.selectedDisplay === partnerName,
      urlAndApiIdentityRemainId: byName.urlIdentity && byName.apiIdentity && byId.urlIdentity && byId.apiIdentity,
      ok: true,
    };
    recordStep("验证资源筛选名称与 ID 身份", evidence);
    return evidence;

    async function applyPartnerFilter(input, term, expectedId) {
      await input.focus();
      await input.fill(term);
      const option = page.locator(filter.optionSelector).filter({ hasText: partnerName }).first();
      await option.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
      const optionText = (await option.innerText()).replace(/\s+/gu, " ").trim();
      if (!optionText.includes(expectedId)) throw new Error("资源选项没有同时暴露名称与稳定 ID");
      await option.click();
      const selectedDisplay = await input.inputValue();
      if (selectedDisplay !== partnerName) throw new Error("资源筛选选中后没有默认显示名称");
      const identities = await submitPartnerFilter(expectedId, true);
      return { termKind: term === partnerName ? "name" : "id", termSuffix: term === partnerName ? null : term, selectedDisplay, optionIncludesId: true, ...identities, ok: true };
    }

    async function submitPartnerFilter(expectedId, expectIdentity) {
      await page.locator(filter.submitSelector).click();
      if (expectIdentity) {
        await page.waitForURL((url) => url.searchParams.get(filter.queryParameter) === expectedId, { timeout: config.navigationTimeoutMs });
        let apiUrl = null;
        for (let attempt = 0; attempt < 20; attempt += 1) {
          const resources = await page.evaluate(() => performance.getEntriesByType("resource").map((entry) => entry.name));
          apiUrl = resources.find((value) => {
            const parsed = safeUrl(value);
            return parsed !== null && parsed.pathname.startsWith(filter.apiPath) && [...parsed.searchParams.values()].includes(expectedId);
          }) || null;
          if (apiUrl !== null) break;
          await wait(100);
        }
        if (apiUrl === null) throw new Error("合同筛选 API 记录没有保持伙伴 ID 查询身份");
        const api = new URL(apiUrl);
        return {
          urlIdentity: new URL(page.url()).searchParams.get(filter.queryParameter) === expectedId,
          apiIdentity: [...api.searchParams.values()].includes(expectedId),
          apiPath: api.pathname,
          apiQueryKeys: [...api.searchParams.keys()].sort(),
        };
      }
      await page.waitForURL((url) => !url.searchParams.has(filter.queryParameter), { timeout: config.navigationTimeoutMs });
      return { urlIdentity: true, apiIdentity: true };
    }
  }

  async function verifyDetailResourceLink(expectation) {
    const detail = config.businessAssertions.resourceReferences.detail;
    const definitionList = page.locator(detail.definitionListSelector).first();
    await definitionList.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    const items = definitionList.locator("div");
    let item = null;
    for (let attempt = 0; attempt < 20 && item === null; attempt += 1) {
      for (let index = 0; index < await items.count(); index += 1) {
        const candidate = items.nth(index);
        const label = (await candidate.locator("dt").textContent())?.replace(/\s+/gu, " ").trim() || "";
        if (label === expectation.label) { item = candidate; break; }
      }
      if (item === null) await wait(100);
    }
    if (item === null) throw new Error("详情没有声明的关联项：" + expectation.label);
    const link = item.locator(detail.resourceLinkSelector).first();
    await link.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    let name = "";
    for (let attempt = 0; attempt < 20; attempt += 1) {
      name = (await link.textContent())?.replace(/\s+/gu, " ").trim() || "";
      const id = (await link.getAttribute("title"))?.trim() || "";
      if (name && id && name !== id && name !== id.slice(0, 8)) break;
      await wait(100);
    }
    const id = (await link.getAttribute("title"))?.trim() || "";
    const href = await link.getAttribute("href");
    const path = href ? decodeURIComponent(new URL(href, page.url()).pathname) : "";
    if (!id || !name || name === id || name === id.slice(0, 8)) throw new Error(expectation.label + " 仍显示裸 ID");
    if (!path.startsWith(expectation.pathPrefix) || !path.endsWith("/" + id)) throw new Error(expectation.label + " 名称链接没有保持资源 ID 路由身份");
    return { label: expectation.label, name, idSuffix: id.slice(-8), path, nameLink: true, idRouteIdentity: true, ok: true };
  }

  async function navigateTo(path) {
    const route = config.routes.find((item) => item.path === path);
    if (!route) throw new Error("YAML 未声明业务路由：" + path);
    const navigation = page.locator(route.navigationSelector).first();
    await Promise.all([
      page.waitForURL((url) => url.pathname === path, { timeout: config.navigationTimeoutMs }),
      navigation.click(),
    ]);
    await page.locator(route.readySelector).first().waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    if (config.settleMs > 0) await wait(config.settleMs);
  }

  async function verifyPdf(section, documentType) {
    const button = section.locator(config.businessAssertions.pdf.openButtonSelector).first();
    await button.waitFor({ state: "visible", timeout: config.navigationTimeoutMs });
    const responsePromise = page.waitForResponse((response) => {
      const parsed = safeUrl(response.url());
      return parsed !== null
        && parsed.pathname.startsWith(config.businessAssertions.pdf.contentPathPrefix)
        && parsed.pathname.endsWith(config.businessAssertions.pdf.contentPathSuffix);
    }, { timeout: config.navigationTimeoutMs });
    const popupPromise = page.waitForEvent("popup", { timeout: config.navigationTimeoutMs });
    await button.click();
    const [response, popup] = await Promise.all([responsePromise, popupPromise]);
    let popupProtocol = new URL(popup.url()).protocol;
    let popupClosedBeforeBlob = false;
    try {
      await popup.waitForURL((url) => url.protocol === "blob:", { timeout: config.navigationTimeoutMs });
      popupProtocol = new URL(popup.url()).protocol;
    } catch (error) {
      if (!popup.isClosed()) throw error;
      popupClosedBeforeBlob = true;
    }
    const mediaType = response.headers()["content-type"] || "";
    const item = { documentType, status: response.status(), mediaType, popupCreated: true, popupProtocol, popupClosedBeforeBlob, path: safeUrl(response.url())?.pathname || "", ok: response.status() === 200 && mediaType.includes(config.businessAssertions.pdf.mediaType) };
    if (!popup.isClosed()) await popup.close();
    if (!item.ok) throw new Error(documentType + " PDF 响应无效：" + JSON.stringify(item));
    return item;
  }
}`;
}

export function selfMediaVoiceAlgorithmSmokeScript(
  editionId: string,
  algorithm: string,
  profileName: string,
  profile: SelfMediaWebProbeSmokeProfileSpec,
  configRef: string,
): string {
  const config = { editionId, algorithm, profileName, configRef, ...profile };
  return String.raw`const config = ${JSON.stringify(config)};

export default async function selfMediaVoiceAlgorithmSmoke({ page, goto, reloadStable, wait, screenshot, jsonArtifact, recordStep }) {
  const failures = [];
  const network = [];
  let networkCount = 0;
  let beforeAlgorithm = null;
  let savedAlgorithm = null;
  let reloadedAlgorithm = null;
  let editionValue = null;
  let statusText = null;
  let options = [];

  const addFailure = (value) => {
    if (failures.length < config.outputLimits.failures) failures.push(String(value).replace(/\s+/gu, " ").slice(0, 300));
  };
  const addNetwork = (value) => {
    networkCount += 1;
    if (network.length < config.outputLimits.network) network.push(value);
  };
  page.on("response", (response) => {
    const request = response.request();
    const parsed = safeUrl(response.url());
    if (parsed === null || parsed.origin !== new URL(page.url() || "http://invalid").origin) return;
    addNetwork({ method: request.method(), path: parsed.pathname, status: response.status() });
  });
  page.on("requestfailed", (request) => {
    const parsed = safeUrl(request.url());
    if (parsed !== null) addNetwork({ method: request.method(), path: parsed.pathname, status: null, failure: String(request.failure()?.errorText || "request failed").slice(0, 160) });
  });

  const route = renderPath(config.routeTemplate);
  const savePath = renderPath(config.saveRequest.pathTemplate);
  try {
    await goto(route, { selectors: [config.readySelector], readinessTimeoutMs: config.navigationTimeoutMs, attempts: 3 });
    if (config.settleMs > 0) await wait(config.settleMs);
    const identity = page.locator(config.identitySelector).first();
    const select = page.locator(config.selectSelector).first();
    if (await identity.count() !== 1) throw new Error("期次身份控件不存在或不唯一");
    if (await select.count() !== 1) throw new Error("语音算法控件不存在或不唯一");
    editionValue = await identity.inputValue();
    if (editionValue !== config.editionId) throw new Error("深链期次不一致：" + editionValue);
    options = await select.evaluate((element, limit) => [...element.options].slice(0, limit).map((item) => ({ value: item.value, label: item.textContent?.trim().slice(0, 120) || "", disabled: item.disabled })), config.outputLimits.options);
    const selectedOption = options.find((item) => item.value === config.algorithm && item.disabled !== true);
    if (!selectedOption) throw new Error("指定语音算法不在可选项中：" + config.algorithm);
    beforeAlgorithm = await select.inputValue();
    await select.selectOption(config.algorithm);
    savedAlgorithm = await select.inputValue();
    if (savedAlgorithm !== config.algorithm) throw new Error("语音算法选择未生效");
    recordStep("选择语音算法", { ok: true, editionId: editionValue, beforeAlgorithm, selectedAlgorithm: savedAlgorithm, optionCount: options.length });

    const saveButton = page.getByRole("button", { name: config.saveButtonName, exact: true }).first();
    if (await saveButton.count() !== 1) throw new Error("保存设置按钮不存在或不唯一");
    await saveButton.click();
    await page.waitForFunction(({ selector, successTexts }) => {
      const value = document.querySelector(selector)?.textContent?.replace(/\s+/gu, " ").trim() || "";
      return successTexts.includes(value);
    }, { selector: config.statusSelector, successTexts: config.successTexts }, { timeout: config.navigationTimeoutMs });
    statusText = (await page.locator(config.statusSelector).first().textContent())?.replace(/\s+/gu, " ").trim() || "";
    const saveNetwork = network.filter((item) => item.method === config.saveRequest.method && item.path === savePath);
    if (beforeAlgorithm !== config.algorithm && !saveNetwork.some((item) => Number(item.status) >= 200 && Number(item.status) < 300)) {
      throw new Error("算法发生变化，但未观察到成功的保存请求");
    }
    recordStep("保存语音算法", { ok: true, statusText, request: saveNetwork.slice(-1)[0] || null });

    await reloadStable({ selectors: [config.readySelector], readinessTimeoutMs: config.navigationTimeoutMs, attempts: 3 });
    if (config.settleMs > 0) await wait(config.settleMs);
    editionValue = await page.locator(config.identitySelector).first().inputValue();
    reloadedAlgorithm = await page.locator(config.selectSelector).first().inputValue();
    if (editionValue !== config.editionId) throw new Error("重载后期次不一致：" + editionValue);
    if (reloadedAlgorithm !== config.algorithm) throw new Error("重载后语音算法未持久化：" + reloadedAlgorithm);
    recordStep("重载确认持久化", { ok: true, editionId: editionValue, algorithm: reloadedAlgorithm, finalPath: new URL(page.url()).pathname });
  } catch (error) {
    addFailure(error instanceof Error ? error.message : String(error));
    recordStep("语音算法闭环失败", { ok: false, failure: failures[failures.length - 1] || "unknown" });
  }

  const shot = await screenshot(config.screenshotName).catch((error) => {
    addFailure("截图失败：" + (error instanceof Error ? error.message : String(error)));
    return null;
  });
  const finalPath = safeUrl(page.url())?.pathname || null;
  const evidence = {
    profileName: config.profileName,
    configRef: config.configRef,
    editionId: config.editionId,
    requestedAlgorithm: config.algorithm,
    beforeAlgorithm,
    savedAlgorithm,
    reloadedAlgorithm,
    statusText,
    finalPath,
    dom: { editionValue, selectedAlgorithm: reloadedAlgorithm || savedAlgorithm, options, optionCount: options.length },
    network: { count: networkCount, items: network },
    failures,
    valuesRedacted: true,
  };
  const report = await jsonArtifact("selfmedia-voice-algorithm-smoke.json", evidence).catch((error) => {
    addFailure("报告写入失败：" + (error instanceof Error ? error.message : String(error)));
    return null;
  });
  const ok = failures.length === 0 && reloadedAlgorithm === config.algorithm && editionValue === config.editionId;
  return {
    ok,
    status: ok ? "pass" : "blocked",
    ...evidence,
    failures,
    report,
    screenshot: shot,
    issueEvidence: {
      ok,
      status: ok ? "pass" : "blocked",
      failedCondition: ok ? null : failures[0] || "语音算法闭环失败",
      result: evidence,
      reportPath: report?.path || null,
      reportSha256: report?.sha256 || null,
      screenshots: shot ? [shot] : [],
      valuesRedacted: true,
    },
    valuesRedacted: true,
  };

  function renderPath(template) {
    return template.replaceAll("{editionId}", encodeURIComponent(config.editionId));
  }
  function safeUrl(value) {
    try { return new URL(value); } catch { return null; }
  }
}`;
}

function requiredValue(values: Map<string, string>, key: string): string {
  const value = values.get(key);
  if (value === undefined) throw new Error(`web-probe product-smoke 缺少 ${key}`);
  return value;
}

function requiredProductValue(value: string | null, key: string, product: string): string {
  if (value === null) throw new Error(`web-probe product-smoke --product ${product} 缺少 ${key}`);
  return value;
}

function simpleId(value: string, key: string): string {
  if (!/^[A-Za-z0-9._-]+$/u.test(value) || value.length > 120) throw new Error(`${key} 必须是最长 120 字符的简单标识`);
  return value;
}

function machineRecord(value: unknown): Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}

function machineText(value: unknown): string | null {
  return typeof value === "string" && value.trim() !== "" ? value : null;
}

function firstMachineText(values: unknown[]): string | null {
  for (const value of values) {
    const parsed = machineText(value);
    if (parsed !== null) return parsed;
  }
  return null;
}

function firstMachineBoolean(sources: Record<string, unknown>[], key: string): boolean | null {
  for (const source of sources) if (typeof source[key] === "boolean") return source[key] as boolean;
  return null;
}

function firstMachineRecord(sources: Record<string, unknown>[], key: string): Record<string, unknown> | null {
  for (const source of sources) {
    const value = machineRecord(source[key]);
    if (Object.keys(value).length > 0) return value;
  }
  return null;
}

function machineArtifacts(probe: Record<string, unknown>, recovered: Record<string, unknown>): Record<string, unknown>[] {
  const primary = machineRecord(probe.artifacts);
  const fallback = machineRecord(recovered.artifacts);
  const values = Array.isArray(primary.items) ? primary.items : Array.isArray(fallback.items) ? fallback.items : [];
  return values.slice(-6).map(machineRecord).map((item) => ({
    kind: machineText(item.kind),
    path: machineText(item.path),
    sha256: machineText(item.sha256),
    byteCount: typeof item.byteCount === "number" ? item.byteCount : null,
  }));
}
