import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, expect, test } from "bun:test";
import type { CommandResult } from "../command";
import type { NodeWebProbeObserveOptions } from "./entry";
import { buildNodeWebProbeObserveCollectPayload } from "./web-probe-observe-collect";
import { withWebObserveCollectRendered } from "../hwlab-node-web-observe-render";
import { nodeWebObserveCollectNodeScript } from "./web-observe-scripts";

describe("web-probe observe collect child JSON recovery", () => {
  test("recovers collect JSON from trans stdout dump", () => {
    const dir = join(tmpdir(), `unidesk-collect-recovery-${Date.now()}-${process.pid}`);
    mkdirSync(dir, { recursive: true });
    const dumpPath = join(dir, "stdout.json");
    writeFileSync(dumpPath, JSON.stringify({
      ok: true,
      command: "web-probe-observe collect",
      view: "performance-summary",
      stateDir: "/remote/state",
      renderedText: "WEB PERFORMANCE SUMMARY\npayloads=2 events=3 groups=1",
      valuesRedacted: true,
    }));
    const summary = {
      stdout: {
        stream: "stdout",
        truncated: true,
        dumpPath,
        valuesRedacted: true,
      },
      valuesRedacted: true,
    };
    const payload = buildNodeWebProbeObserveCollectPayload(collectOptions(), { workspace: "/workspace" }, {
      command: ["trans", "JD01:/workspace", "sh"],
      cwd: "/repo",
      exitCode: 0,
      stdout: "tail only WEB PERFORMANCE SUMMARY",
      stderr: `UNIDESK_SSH_TRUNCATION_SUMMARY ${JSON.stringify(summary)}\n`,
      signal: null,
      timedOut: false,
    } satisfies CommandResult);
    expect(payload.ok).toBe(true);
    expect(payload.status).toBe("collected");
    const collect = payload.collect as Record<string, unknown>;
    expect(collect.view).toBe("performance-summary");
    expect(collect.renderedText).toContain("payloads=2");
    const recovery = payload.stdoutRecovery as Record<string, unknown>;
    expect(recovery.stdoutKind).toBe("ssh-truncation-summary");
    expect(recovery.dumpPath).toBe(dumpPath);
    expect(recovery.source).toBe("dump");
  });
});

test("propagates structured turn-summary not-found status without treating it as collected", () => {
  const options = collectOptions();
  options.collectView = "turn-summary";
  options.collectCommandId = "cmd-missing";
  const payload = buildNodeWebProbeObserveCollectPayload(options, { workspace: "/workspace" }, {
    command: ["trans", "JD01:/workspace", "sh"],
    cwd: "/repo",
    exitCode: 0,
    stdout: JSON.stringify({
      ok: false,
      command: "web-probe-observe collect",
      view: "turn-summary",
      status: "not-found",
      stateDir: "/remote/state",
      filter: { mode: "exact-command-id", requestedCommandId: "cmd-missing", matchedCount: 0 },
      error: { code: "turn-summary-command-not-found" },
      rows: [],
      renderedText: "NOT_FOUND turn-summary-command-not-found; unrelated turns were not returned",
      valuesRedacted: true,
    }),
    stderr: "",
    signal: null,
    timedOut: false,
  } satisfies CommandResult);
  expect(payload.ok).toBe(false);
  expect(payload.status).toBe("not-found");
  const collect = payload.collect as Record<string, unknown>;
  expect(collect.status).toBe("not-found");
  expect(collect.rows).toEqual([]);
});

test("collects an exact failed command artifact without treating artifact failure as collect failure", () => {
  const options = collectOptions();
  options.collectView = "files";
  options.collectFile = "commands/failed/cmd-fixture.json";
  const failedArtifact = {
    ok: false,
    commandId: "cmd-fixture",
    type: "validateRealtimeFanout",
    failedAt: "2026-07-10T12:00:00.000Z",
    error: {
      name: "Error",
      message: "terminal event arrived before subscriber disconnect",
      details: {
        reportPath: "artifacts/realtime-fanout/cmd-fixture/report.json",
        reportSha256: "sha256:fixture-report",
        sessionToken: "must-not-render",
      },
    },
    failureSample: { ok: true, sampleSeq: 22, valuesRedacted: true },
  };
  const payload = buildNodeWebProbeObserveCollectPayload(options, { workspace: "/workspace" }, {
    command: ["trans", "NC01:/workspace", "sh"],
    cwd: "/repo",
    exitCode: 0,
    stdout: JSON.stringify(failedArtifact),
    stderr: "",
    signal: null,
    timedOut: false,
  } satisfies CommandResult);

  expect(payload.ok).toBe(true);
  expect(payload.status).toBe("collected");
  expect(payload.degradedReason).toBeNull();
  const recovery = payload.stdoutRecovery as Record<string, unknown>;
  expect(recovery.source).toBe("stdout");
  expect(recovery.stdoutContractAccepted).toBe(true);
  const collect = payload.collect as Record<string, any>;
  expect(collect.ok).toBe(true);
  expect(collect.artifactOutcome).toEqual({
    ok: false,
    commandId: "cmd-fixture",
    type: "validateRealtimeFanout",
    bucket: "failed",
    valuesRedacted: true,
  });
  expect(collect.file.jsonContent.commandId).toBe("cmd-fixture");
  expect(collect.file.jsonContent.error.message).toBe("terminal event arrived before subscriber disconnect");
  expect(collect.file.jsonContent.error.details.sessionToken).toBe("[redacted]");

  const rendered = withWebObserveCollectRendered(payload);
  expect(String(rendered.renderedText)).toContain("cmd-fixture");
  expect(String(rendered.renderedText)).toContain("terminal event arrived before subscriber disconnect");
});

test("accepts the standard file collect envelope without requiring a summary view", () => {
  const options = collectOptions();
  options.collectView = "files";
  options.collectFile = "commands/failed/cmd-fixture.json";
  const payload = buildNodeWebProbeObserveCollectPayload(options, { workspace: "/workspace" }, {
    command: ["trans", "NC01:/workspace", "sh"],
    cwd: "/repo",
    exitCode: 0,
    stdout: JSON.stringify({
      ok: true,
      command: "web-probe-observe collect",
      stateDir: "/remote/state",
      mode: "file",
      requestedFile: options.collectFile,
      resolvedFile: options.collectFile,
      file: {
        relative: options.collectFile,
        byteCount: 21885,
        sha256: "sha256:fixture",
        jsonSummary: { topLevelKeys: ["ok", "commandId", "type", "failedAt", "error"] },
      },
      valuesRedacted: true,
    }),
    stderr: "",
    signal: null,
    timedOut: false,
  } satisfies CommandResult);

  expect(payload.ok).toBe(true);
  expect(payload.status).toBe("collected");
  expect(payload.degradedReason).toBeNull();
  const collect = payload.collect as Record<string, any>;
  expect(collect.mode).toBe("file");
  expect(collect.file.byteCount).toBe(21885);
});

test("accepts the structured files listing envelope when --file is absent", () => {
  const options = collectOptions();
  options.collectView = "files";
  const stateDir = mkdtempSync(join(tmpdir(), "unidesk-web-observe-files-list-"));
  writeFileSync(join(stateDir, "manifest.json"), "{}\n");
  const child = Bun.spawnSync(["bash", "-lc", nodeWebObserveCollectNodeScript(options.maxFiles, null, null, null)], {
    env: { ...process.env, state_dir: stateDir },
    stdout: "pipe",
    stderr: "pipe",
  });
  expect(child.exitCode).toBe(0);
  const payload = buildNodeWebProbeObserveCollectPayload(options, { workspace: "/workspace" }, {
    command: ["trans", "NC01:/workspace", "sh"], cwd: "/repo", exitCode: 0,
    stdout: Buffer.from(child.stdout).toString("utf8"),
    stderr: Buffer.from(child.stderr).toString("utf8"), signal: null, timedOut: false,
  } satisfies CommandResult);
  expect(payload.ok).toBe(true);
  expect(payload.status).toBe("collected");
  expect((payload.collect as Record<string, unknown>).mode).toBe("list");
  expect((payload.collect as Record<string, unknown>).view).toBe("files");
});

function collectOptions(): NodeWebProbeObserveOptions {
  return {
    action: "observe",
    observeAction: "collect",
    id: "webobs-test",
    node: "JD01",
    lane: "v03",
    url: "https://example.test",
    targetPath: "/",
    viewport: "1920x1080",
    browserProxyMode: "auto",
    sampleIntervalMs: 1000,
    screenshotIntervalMs: 0,
    observerRefreshIntervalMs: 1000,
    maxSamples: 10,
    maxRunSeconds: 60,
    commandTimeoutSeconds: 30,
    waitMs: 0,
    tailLines: 20,
    maxFiles: 100,
    gcKeepHours: 24,
    gcLimit: 10,
    confirm: false,
    dryRun: false,
    collectView: "performance-summary",
    collectFile: null,
    collectFinding: null,
    collectGrep: null,
    collectTraceId: null,
    collectSampleSeq: null,
    collectTimestamp: null,
    collectTurn: null,
    collectCommandId: null,
    collectWindowMs: null,
    analyzeArchivePrefix: null,
    analyzeTailSamples: null,
    full: false,
    raw: false,
    compactRaw: false,
    stateDir: null,
    jobId: null,
    force: false,
    commandType: null,
    commandText: null,
    commandSecondText: null,
    commandPath: null,
    commandLabel: null,
    commandSessionId: null,
    commandProvider: null,
    commandProfile: null,
    commandAfterRound: null,
    commandSeverity: null,
    commandAlternateSessionStrategy: null,
    commandExpectedSentinelRange: null,
    commandExpectedActionWaitMs: null,
    commandDurationMs: null,
    commandRequireComposerReady: false,
    commandWaitProjectManagementReady: false,
    commandFindingId: null,
    commandBlocking: null,
    commandAccountId: null,
    commandFromAccountId: null,
    commandToAccountId: null,
    commandSourceId: null,
    commandFileRef: null,
    commandFilename: null,
    commandTaskRef: null,
    commandTaskId: null,
    commandField: null,
    commandLink: null,
    commandTitle: null,
    commandBody: null,
    commandStatus: null,
    commandHwpodId: null,
    commandNodeId: null,
    commandWorkspaceRoot: null,
    commandRoot: null,
  };
}
