import assert from "node:assert/strict";
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { test } from "bun:test";

import { nodeWebObserveCollectViewNodeScript } from "../hwlab-node-web-observe-collect";

function shellQuote(value: string): string {
  return `'${value.replace(/'/g, `'\\''`)}'`;
}

test("performance-summary labels LoAF-only evidence when CPU profile capture is pending", async () => {
  const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-performance-"));
  await mkdir(join(stateDir, "analysis"), { recursive: true });
  await mkdir(join(stateDir, "commands", "pending"), { recursive: true });
  await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-perf-test", status: "failed" }) + "\n");
  await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "failed", updatedAt: "2026-07-02T12:08:06Z" }) + "\n");
  await writeFile(join(stateDir, "performance-events.jsonl"), [
    JSON.stringify({
      type: "performance-event",
      ts: "2026-07-02T12:07:00Z",
      performance: { kind: "long-animation-frame", duration: 2083.8 },
    }),
  ].join("\n") + "\n");
  await writeFile(join(stateDir, "commands", "pending", "cmd-perf.json"), JSON.stringify({
    id: "cmd-perf",
    type: "performanceCapture",
    createdAt: "2026-07-02T12:08:00Z",
  }) + "\n");
  await writeFile(join(stateDir, "analysis", "report.json"), JSON.stringify({
    manifest: { status: "failed" },
    heartbeat: { status: "failed", updatedAt: "2026-07-02T12:08:06Z" },
    frontendPerformance: {
      summary: {
        eventCount: 1,
        longTaskCount: 0,
        longAnimationFrameCount: 1,
        eventLoopGapCount: 0,
        captureCount: 0,
        scriptHotspotCount: 1,
        maxLongAnimationFrameMs: 2083.8,
        longAnimationFrameRedMs: 200,
        cpuProfileStatus: "missing",
        attributionMode: "loaf-only-no-cpu-profile",
        noCpuProfile: true,
        loafOnly: true,
        valuesRedacted: true,
      },
      scriptHotspots: [{
        sourceFunctionName: "Response.json.then",
        sourceURL: "https://hwlab.example.test/app.js",
        sourceFile: "app.js",
        sourceCharPosition: 123456,
        sourceMapStatus: "missing",
        totalDurationMs: 2108,
        count: 2,
        valuesRedacted: true,
      }],
      profileHotspots: [],
      profileStacks: [],
      captures: [],
      longAnimationFrames: [{ ts: "2026-07-02T12:07:00Z", kind: "long-animation-frame", durationMs: 2083.8, sampleSeq: 42, pageRole: "control", scriptCount: 1 }],
    },
    findings: [{
      id: "frontend-performance-loaf-only-no-cpu-profile",
      severity: "amber",
      summary: "frontend performance evidence is LoAF/LongTask/event-loop only because no completed CPU profile capture is present",
      count: 1,
    }, {
      id: "tool-pending-commands-unconsumed",
      severity: "red",
      summary: "web-probe observe has pending/processing control commands that were not consumed by the runner",
      count: 1,
    }],
  }) + "\n");

  const script = nodeWebObserveCollectViewNodeScript({
    maxFiles: 100,
    view: "performance-summary",
    traceId: null,
    sampleSeq: null,
    timestamp: null,
    turn: null,
    commandId: null,
    windowMs: null,
  });
  const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], {
    cwd: join(import.meta.dir, "../../.."),
    encoding: "utf8",
  });
  assert.equal(result.status, 0, result.stderr || result.stdout);

  const output = JSON.parse(result.stdout);
  const text = String(output.renderedText ?? "");
  const debug = JSON.stringify({ summary: output.summary, evidenceMode: output.evidenceMode, text }, null, 2);
  assert.equal(output.evidenceMode.attributionMode, "loaf-only-no-cpu-profile", debug);
  assert.equal(output.evidenceMode.cpuProfileStatus, "pending-command-no-cpu-profile");
  assert.equal(output.evidenceMode.pendingPerformanceCapture, true);
  assert.match(text, /LoAF-only \/ no CPU profile/u);
  assert.match(text, /pending performanceCapture: cmd-perf\(pending\)/u);
  assert.match(text, /runner state: not-running\/failed/u);
  assert.match(text, /no CPU profile hotspots; no completed performanceCapture artifact/u);
  assert.match(text, /sourceMap=missing/u);
  assert.match(text, /char=123456/u);
}, 20_000);

test("performance-summary renders CPU profile window misses and source attribution context", async () => {
  const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-performance-window-"));
  await mkdir(join(stateDir, "analysis"), { recursive: true });
  await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-perf-window-test", status: "completed" }) + "\n");
  await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "completed", updatedAt: "2026-07-02T12:08:06Z" }) + "\n");
  await writeFile(join(stateDir, "performance-events.jsonl"), "");
  await writeFile(join(stateDir, "analysis", "report.json"), JSON.stringify({
    manifest: { status: "completed" },
    heartbeat: { status: "completed", updatedAt: "2026-07-02T12:08:06Z" },
    frontendPerformance: {
      summary: {
        eventCount: 2,
        longTaskCount: 0,
        longAnimationFrameCount: 1,
        eventLoopGapCount: 1,
        captureCount: 1,
        profileHotspotCount: 1,
        scriptHotspotCount: 1,
        windowCorrelationCount: 1,
        cpuProfileWindowCoveredCount: 0,
        cpuProfileWindowOverlappedCount: 0,
        cpuProfileWindowMissedCount: 1,
        cpuProfileWindowMissingCount: 0,
        maxLongAnimationFrameMs: 1811,
        maxEventLoopGapMs: 4490,
        longAnimationFrameRedMs: 200,
        eventLoopGapRedMs: 1000,
        cpuProfileStatus: "captured-hotspots-outside-performance-window",
        attributionMode: "cpu-profile-missed-performance-window",
        sourceMapStatus: "missing",
        valuesRedacted: true,
      },
      performanceWindows: [{
        kind: "long-animation-frame",
        ts: "2026-07-02T12:00:00Z",
        startAt: "2026-07-02T12:00:00.000Z",
        endAt: "2026-07-02T12:00:01.811Z",
        durationMs: 1811,
        sampleSeq: 7,
        pageRole: "control",
        pageId: "page-1",
        cpuProfileCoverageStatus: "missed",
        coverageReason: "nearest performanceCapture started after this frontend performance window ended",
        capture: {
          captureId: "perf-late",
          commandId: "cmd-late",
          startAt: "2026-07-02T12:00:10.000Z",
          endAt: "2026-07-02T12:00:18.158Z",
          durationMs: 8158,
          windowDistanceMs: 8189,
          profileSampleCount: 812,
          valuesRedacted: true,
        },
        topLoafScript: {
          sourceFunctionName: "Response.json.then",
          sourceURL: "https://hwlab.example.test/assets/CodeWorkbenchView.js",
          sourceFile: "CodeWorkbenchView.js",
          sourceCharPosition: 456789,
          lineNumber: 91,
          sourceMapStatus: "missing",
          totalDurationMs: 1811,
          count: 5,
          valuesRedacted: true,
        },
        relatedSamples: [{
          ts: "2026-07-02T12:00:01Z",
          sampleSeq: 7,
          pageRole: "control",
          activeSessionId: "ses_test",
          traceIds: ["trc_test"],
          valuesRedacted: true,
        }],
        relatedNetwork: [{
          ts: "2026-07-02T12:00:00.500Z",
          sampleSeq: 7,
          phase: "response",
          method: "GET",
          status: 200,
          urlPath: "/v1/workbench/sessions/ses_test/turns",
          bodyByteCount: 1048576,
          traceId: "trc_test",
          valuesRedacted: true,
        }],
        valuesRedacted: true,
      }],
      scriptHotspots: [{
        sourceFunctionName: "Response.json.then",
        sourceURL: "https://hwlab.example.test/assets/CodeWorkbenchView.js",
        sourceFile: "CodeWorkbenchView.js",
        sourceCharPosition: 456789,
        lineNumber: 91,
        sourceMapStatus: "missing",
        totalDurationMs: 1811,
        count: 5,
        valuesRedacted: true,
      }],
      profileHotspots: [{
        functionName: "(program)",
        sourceFile: "CodeWorkbenchView.js",
        sourceMapStatus: "missing",
        selfTimeMs: 120,
        totalTimeMs: 200,
        captureCount: 1,
        valuesRedacted: true,
      }],
      profileStacks: [],
      captures: [{
        captureId: "perf-late",
        commandId: "cmd-late",
        startAt: "2026-07-02T12:00:10.000Z",
        endAt: "2026-07-02T12:00:18.158Z",
        durationMs: 8158,
        profileSampleCount: 812,
        valuesRedacted: true,
      }],
      longAnimationFrames: [{ ts: "2026-07-02T12:00:00Z", startAt: "2026-07-02T12:00:00.000Z", endAt: "2026-07-02T12:00:01.811Z", kind: "long-animation-frame", durationMs: 1811, sampleSeq: 7, pageRole: "control", scriptCount: 1 }],
      sourceAttribution: {
        sourceMapStatus: "missing",
        note: "No source-map artifact is loaded by this analyzer; function/file/line fields are raw browser URLs or CPU profile callFrame locations.",
        sourceFiles: [{ sourceFile: "CodeWorkbenchView.js", count: 2, sourceMapStatus: "missing", valuesRedacted: true }],
        valuesRedacted: true,
      },
    },
    findings: [],
  }) + "\n");

  const script = nodeWebObserveCollectViewNodeScript({
    maxFiles: 100,
    view: "performance-summary",
    traceId: null,
    sampleSeq: null,
    timestamp: null,
    turn: null,
    commandId: null,
    windowMs: null,
  });
  const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], {
    cwd: join(import.meta.dir, "../../.."),
    encoding: "utf8",
  });
  assert.equal(result.status, 0, result.stderr || result.stdout);

  const output = JSON.parse(result.stdout);
  const text = String(output.renderedText ?? "");
  const debug = JSON.stringify({ summary: output.summary, evidenceMode: output.evidenceMode, text }, null, 2);
  assert.equal(output.summary.cpuProfileWindowMissedCount, 1);
  assert.equal(output.evidenceMode.attributionMode, "cpu-profile-missed-performance-window", debug);
  assert.equal(output.evidenceMode.cpuProfileMissedPerformanceWindows, true, debug);
  assert.match(text, /window correlation=1 covered=0 overlapped=0 missed=1 missing=0 sourceMap=missing/u);
  assert.match(text, /evidence attribution=cpu-profile-missed-performance-window/u);
  assert.match(text, /CPU profile artifacts exist but missed severe performance windows; do not cite CPU profile hotspots as same-window evidence/u);
  assert.doesNotMatch(text, /hotspot rows may be used as CPU profile evidence/u);
  assert.match(text, /cpuWindow=missed/u);
  assert.match(text, /nearest performanceCapture started after/u);
  assert.match(text, /CodeWorkbenchView\.js/u);
  assert.match(text, /bytes=1048576/u);
  assert.match(text, /session=ses_test trace=trc_test/u);
}, 20_000);
