import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "bun:test";

import { hwlabRuntimeLaneSpecForNode } from "../hwlab-node-lanes";
import { parseNodeWebProbeObserveOptions, recoverWebObserveAnalyzeFromArtifacts } from "./web-probe-observe";

const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");

function parseRealtimeCommand(extra: string[]) {
  return parseNodeWebProbeObserveOptions(
    ["command", "--type", "validateRealtimeFanout", ...extra],
    "NC01",
    "v03",
    spec,
    "webobs-fixture",
    null,
  );
}

test("artifact recovery helper projects report time without filesystem mtime", () => {
  let recoveryScript = "";
  const recovered = recoverWebObserveAnalyzeFromArtifacts({ node: "NC01", lane: "v03", stateDir: "/state/run-1", jobId: null, commandTimeoutSeconds: 30 } as never, spec, { exitCode: 124, timedOut: true }, (script) => {
    recoveryScript = script;
    return { stdout: JSON.stringify({ reportUpdatedAt: "2026-07-12T00:00:00.000Z", valuesRedacted: true }) };
  });
  assert.equal(recovered?.reportUpdatedAt, "2026-07-12T00:00:00.000Z");
  assert.match(recoveryScript, /const reportJson = objectOrNull\(readJson\(reportJsonPath\)\) \|\| \{\};/u);
  assert.equal((recoveryScript.match(/const reportJson =/gu) ?? []).length, 1);
  assert.match(recoveryScript, /source\.reportUpdatedAt \?\? source\.updatedAt \?\? source\.analyzedAt \?\? reportJson\.reportUpdatedAt \?\? reportJson\.updatedAt \?\? reportJson\.analyzedAt \?\? source\.generatedAt \?\? reportJson\.generatedAt/u);
  assert.doesNotMatch(recoveryScript, /mtime/u);
  const marker = "UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT";
  const start = recoveryScript.indexOf(`<<'${marker}'\n`) + marker.length + 5;
  const end = recoveryScript.indexOf(`\n${marker}`, start);
  const directory = mkdtempSync(join(tmpdir(), "web-observe-recovery-"));
  try {
    const programPath = join(directory, "recovery.js");
    writeFileSync(programPath, recoveryScript.slice(start, end));
    const syntax = spawnSync(process.execPath, ["--check", programPath], { encoding: "utf8" });
    assert.equal(syntax.status, 0, syntax.stderr);
    const reportJsonPath = join(directory, "report.json");
    writeFileSync(reportJsonPath, JSON.stringify({ generatedAt: "2026-07-12T00:00:00.000Z" }));
    const executed = spawnSync(process.execPath, [programPath, directory, join(directory, "stdout.json"), join(directory, "stderr.log"), reportJsonPath, join(directory, "report.md"), "124", "true"], { encoding: "utf8" });
    assert.equal(executed.status, 0, executed.stderr);
    assert.equal(JSON.parse(executed.stdout).reportUpdatedAt, "2026-07-12T00:00:00.000Z");
  } finally {
    rmSync(directory, { recursive: true, force: true });
  }
});

test("validateRealtimeFanout parses the YAML profile and two independent prompts", () => {
  const options = parseRealtimeCommand([
    "--profile", "pure-kafka-live",
    "--provider", "gpt.pika",
    "--text", "first turn",
    "--second-text", "second turn",
  ]);

  assert.equal(options.commandType, "validateRealtimeFanout");
  assert.equal(options.commandProfile, "pure-kafka-live");
  assert.equal(options.commandProvider, "gpt.pika");
  assert.equal(options.commandText, "first turn");
  assert.equal(options.commandSecondText, "second turn");
  assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.debugStreams, ["stdio", "agentrun", "hwlab"]);
  assert.equal(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.fromBeginning, false);
  assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.expectedProductSse, {
    deliverySemantics: "kafka-retention-then-live",
    liveOnly: false,
    replay: true,
    replaySupported: true,
    lossPossible: false,
    refreshHandoff: true,
    replayPriorTraceOnReconnect: true,
  });
  assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.requiredEventTypes, {
    agentrun: ["user_message", "assistant_message", "terminal_status"],
    hwlab: ["user", "assistant", "terminal"],
  });
  assert.deepEqual(spec.webProbe?.realtimeFanoutProfiles?.["pure-kafka-live"]?.conditionalEventTypePairs, {
    "tool-call": { agentrun: "tool_call", hwlab: "tool" },
    "command-output": { agentrun: "command_output", hwlab: "status" },
  });
});

test("validateRealtimeFanout refuses body as a substitute for the second prompt", () => {
  assert.throws(
    () => parseRealtimeCommand([
      "--profile", "pure-kafka-live",
      "--provider", "gpt.pika",
      "--text", "first turn",
      "--body", "not a second prompt",
    ]),
    /requires --second-text/u,
  );
});

test("validateRealtimeFanout rejects profiles absent from the owning YAML", () => {
  assert.throws(
    () => parseRealtimeCommand([
      "--profile", "projection-mode",
      "--provider", "gpt.pika",
      "--text", "first turn",
      "--second-text", "second turn",
    ]),
    /profile is not declared by the owning YAML/u,
  );
});

test("validateExistingSessionRefresh parses an existing session without prompts", () => {
  const options = parseNodeWebProbeObserveOptions(
    ["command", "--type", "validateExistingSessionRefresh", "--profile", "pure-kafka-live", "--session-id", "ses_existing"],
    "NC01", "v03", spec, "webobs-fixture", null,
  );
  assert.equal(options.commandType, "validateExistingSessionRefresh");
  assert.equal(options.commandProfile, "pure-kafka-live");
  assert.equal(options.commandSessionId, "ses_existing");
  assert.equal(options.commandText, null);
});

test("validateWorkbenchKafkaDebugReplay is a YAML-enabled argument-free typed command", () => {
  const options = parseNodeWebProbeObserveOptions(
    ["command", "--type", "validateWorkbenchKafkaDebugReplay"],
    "NC01",
    "v03",
    spec,
    "webobs-fixture",
    null,
  );

  assert.equal(options.commandType, "validateWorkbenchKafkaDebugReplay");
  assert.deepEqual(spec.webProbe?.workbenchKafkaDebugReplay, {
    enabled: true,
    topic: "hwlab.event.debug.v1",
    groupPrefix: "hwlab-v03-workbench-isolated-debug",
    completionTimeoutMs: 30_000,
  });
});

test("validateWorkbenchTraceReadability 是由独立 YAML 启用的无参数 typed command", () => {
  const options = parseNodeWebProbeObserveOptions(
    ["command", "--type", "validateWorkbenchTraceReadability"],
    "NC01",
    "v03",
    spec,
    "webobs-fixture",
    null,
  );

  assert.equal(options.commandType, "validateWorkbenchTraceReadability");
  assert.equal(spec.webProbe?.workbenchTraceReadability?.terminalTimeoutMs, 480_000);
  assert.equal(spec.webProbe?.workbenchTraceReadability?.runningCardTimeoutMs, 15_000);
});

test("observe start selects the YAML public origin semantically", () => {
  const options = parseNodeWebProbeObserveOptions(
    ["start", "--origin", "public"],
    "NC01",
    "v03",
    spec,
    null,
    null,
  );

  assert.equal(options.originName, "public");
  assert.equal(options.originMode, "public");
  assert.equal(options.url, spec.publicWebUrl);
  assert.equal(options.browserProxyMode, "auto");
  assert.equal(options.browserProxyModeSource, "yaml-origin");
});

test("observe start selects the YAML L1 native Workbench HTTPS origin", () => {
  const options = parseNodeWebProbeObserveOptions(
    ["start", "--origin", "native"],
    "NC01",
    "v03",
    spec,
    null,
    null,
  );

  assert.equal(options.originName, "native");
  assert.equal(options.originMode, "native-public");
  assert.equal(options.url, spec.nativeDevelopment?.workbench.publicExposure.publicBaseUrl);
  assert.match(options.originConfigPath ?? "", /nativeDevelopment\.workbench\.publicExposure\.publicBaseUrl/u);
});

test("observe start inherits finite observer lifecycle limits from the owning YAML", () => {
  const options = parseNodeWebProbeObserveOptions(
    ["start", "--origin", "native"],
    "NC01",
    "v03",
    spec,
    null,
    null,
  );

  assert.equal(options.maxRunSeconds, 3600);
  assert.equal(options.maxSamples, 720);
  assert.equal(options.memoryGuardMode, "manual-start");
  assert.ok(options.maxRunSeconds > 0);
  assert.ok(options.maxSamples > 0);
  assert.deepEqual(spec.webProbe?.resourcePolicy?.observerLifecycle, {
    maxRunSeconds: 3600,
    maxSamples: 720,
    maxScreenshots: 24,
    maxPerformanceEntries: 1000,
    maxBrowserProcessHistoryEntries: 360,
    maxBrowserFreezeSignalHistoryEntries: 120,
    maxRealtimeEventEntries: 256,
    maxRealtimeEventBodyBytes: 65536,
    maxDomEvidenceRows: 1000,
    maxProcessScanEntries: 8192,
    maxPerformanceCaptureSeconds: 30,
    maxResponseBodyBytes: 524288,
    maxWebPerformanceBodyBytes: 65536,
    maxJsonlQueueEntries: 512,
    maxJsonlQueueBytes: 4194304,
    maxJsonlLineBytes: 262144,
    jsonlFlushTimeoutMs: 5000,
    maxResponseBodyInFlight: 2,
    maxPassiveListenerTasks: 64,
    passiveTaskFlushTimeoutMs: 5000,
    closeStepTimeoutMs: 5000,
    maxProcessReadConcurrency: 32,
    maxRealtimeTotalEntries: 512,
    maxRealtimeTotalBodyBytes: 4194304,
  });
});

test("sentinel observe start preserves the cadence comparator through the child CLI", () => {
  const options = parseNodeWebProbeObserveOptions(
    ["start", "--origin", "native", "--sentinel-cadence"],
    "NC01",
    "v03",
    spec,
    null,
    null,
  );
  assert.equal(options.memoryGuardMode, "sentinel-cadence");
});

test("observe start cannot disable or exceed the YAML lifecycle caps", () => {
  assert.throws(
    () => parseNodeWebProbeObserveOptions(["start", "--origin", "native", "--max-run-seconds", "0"], "NC01", "v03", spec, null, null),
    /must be a positive integer/u,
  );
  assert.throws(
    () => parseNodeWebProbeObserveOptions(["start", "--origin", "native", "--max-samples", "721"], "NC01", "v03", spec, null, null),
    /exceeds the owning YAML observer lifecycle cap 720/u,
  );
});

test("observe start refuses URL-based internal/public selection ambiguity", () => {
  assert.throws(
    () => parseNodeWebProbeObserveOptions(
      ["start", "--origin", "public", "--url", "http://127.0.0.1:4173"],
      "NC01",
      "v03",
      spec,
      null,
      null,
    ),
    /accepts --origin or --url, not both/u,
  );
});

test("observe command inherits the indexed origin and rejects replacement origins", () => {
  const indexed = {
    id: "webobs-fixture",
    node: "NC01",
    lane: "v03",
    workspace: spec.workspace,
    stateDir: ".state/web-observe/NC01/v03/2026/07/10/20260710T000000Z_workbench_webobs-fixture",
    url: "http://10.43.99.7:8080",
    originName: "internal" as const,
    originMode: "k8s-service-cluster-ip" as const,
    originConfigPath: "config/hwlab-node-lanes.yaml#webProbe.origins.internal",
    browserProxyMode: "direct" as const,
    browserProxyModeSource: "yaml-origin" as const,
    targetPath: "/workbench",
    status: "running",
    pid: 42,
    startedAt: "2026-07-10T00:00:00.000Z",
    updatedAt: "2026-07-10T00:00:01.000Z",
  };
  const inherited = parseNodeWebProbeObserveOptions(
    ["command", "--type", "mark", "--label", "checkpoint"],
    "NC01",
    "v03",
    spec,
    "webobs-fixture",
    indexed,
  );
  assert.equal(inherited.originName, "internal");
  assert.equal(inherited.url, indexed.url);
  assert.equal(inherited.browserProxyMode, "direct");

  assert.throws(
    () => parseNodeWebProbeObserveOptions(
      ["command", "--origin", "public", "--type", "mark", "--label", "checkpoint"],
      "NC01",
      "v03",
      spec,
      "webobs-fixture",
      indexed,
    ),
    /later actions inherit the observer origin/u,
  );
});
