// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0
// Exercises the browser-side Workbench performance probe without sending network traffic.

import assert from "node:assert/strict";
import test from "node:test";

import type { ChatMessage, TraceEvent } from "../src/types/index.ts";
import {
  acknowledgeWorkbenchVisible,
  drainWorkbenchPerformanceEventsForTest,
  failWorkbenchSubmitJourney,
  finishWorkbenchOpenFullLoad,
  finishWorkbenchSessionSwitchFullLoad,
  markWorkbenchSubmitApiAccepted,
  markWorkbenchTraceEventsReceived,
  markWorkbenchTraceProjected,
  resetWorkbenchPerformanceForTest,
  startWorkbenchOpenJourney,
  startWorkbenchSessionSwitch,
  startWorkbenchSubmitJourney
} from "../src/utils/workbench-performance.ts";

test("Workbench performance probe records journeys and strips high-cardinality identifiers", () => {
  resetWorkbenchPerformanceForTest();
  startWorkbenchOpenJourney({ route: "/workbench/sessions/ses_secret", cache: "cold", authState: "warm" });
  acknowledgeWorkbenchVisible({ messages: [], activeSessionId: "ses_secret", detailLoading: false });
  finishWorkbenchOpenFullLoad("ok");

  startWorkbenchSessionSwitch({ sessionId: "ses_secret", source: "rail", targetState: "running", cache: "warm" });
  acknowledgeWorkbenchVisible({ messages: [userMessage("ses_secret")], activeSessionId: "ses_secret", detailLoading: false });
  finishWorkbenchSessionSwitchFullLoad("ses_secret", "ok");

  const wallBase = Date.now();
  const createdAt = new Date(wallBase - 300).toISOString();
  const appendedAt = new Date(wallBase - 220).toISOString();
  const serverSentAt = new Date(wallBase - 120).toISOString();
  startWorkbenchSubmitJourney({ traceId: "trc_secret", sessionId: "ses_secret", entry: "existing", backend: "codex", transport: "sse" });
  markWorkbenchSubmitApiAccepted("trc_secret");
  markWorkbenchTraceEventsReceived({ traceId: "trc_secret", transport: "sse", serverSentAt, traceSeq: 42, events: [{ type: "assistant_message", eventType: "assistant", backend: "agentrun-v01/codex", createdAt, appendedAt, ts: createdAt, sourceSeq: 42 } as TraceEvent] });
  markWorkbenchTraceProjected("trc_secret");
  acknowledgeWorkbenchVisible({ messages: [agentMessage("ses_secret", "trc_secret")], activeSessionId: "ses_secret", detailLoading: false });

  const events = drainWorkbenchPerformanceEventsForTest();
  const payload = JSON.stringify(events);
  assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "workbench_open_first_visible"));
  assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "session_switch_first_visible"));
  assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible" && event.backend === "agentrun-v01/codex"));
  assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "created_to_append" && event.eventType === "assistant"));
  assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "append_to_sse" && event.eventType === "assistant"));
  assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "sse_to_receive" && event.eventType === "assistant"));
  assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "receive_to_project" && event.eventType === "assistant"));
  assert.ok(events.some((event) => event.kind === "workbench_event_phase" && event.phase === "project_to_paint" && event.eventType === "assistant"));
  assert.ok(events.some((event) => event.kind === "workbench_backend_event_visible" && event.eventType === "assistant"));
  assert.doesNotMatch(payload, /trc_secret|ses_secret|sessionId|runId|commandId/u);
});

test("Workbench submit failures do not pollute first visible output SLI", () => {
  resetWorkbenchPerformanceForTest();
  startWorkbenchSubmitJourney({ traceId: "trc_network", sessionId: "ses_secret", entry: "new", backend: "codex", transport: "sse" });
  failWorkbenchSubmitJourney("trc_network", "network");

  const events = drainWorkbenchPerformanceEventsForTest();
  assert.equal(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);
  assert.ok(events.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_failure" && event.outcome === "network"));
});

test("Workbench session switch treats visible empty sessions as successful first paint", () => {
  resetWorkbenchPerformanceForTest();
  startWorkbenchSessionSwitch({ sessionId: "ses_empty_visible", source: "rail", targetState: "empty", cache: "cold" });
  acknowledgeWorkbenchVisible({ messages: [], activeSessionId: "ses_empty_visible", detailLoading: false });

  const events = drainWorkbenchPerformanceEventsForTest();
  const switchEvent = events.find((event) => event.kind === "workbench_journey" && event.journey === "session_switch_first_visible");
  assert.equal(switchEvent?.outcome, "ok");
  assert.equal(switchEvent?.targetState, "empty");
});

test("Workbench submit first visible waits for terminal final or tool output", () => {
  resetWorkbenchPerformanceForTest();
  const wallBase = Date.now();
  const createdAt = new Date(wallBase - 12_000).toISOString();
  const appendedAt = new Date(wallBase - 11_500).toISOString();
  const backendEvent = { label: "agentrun:run:createdAgentRun", backend: "agentrun-v01/codex", createdAt, appendedAt, message: "AgentRun created." } as TraceEvent;
  startWorkbenchSubmitJourney({ traceId: "trc_secret", sessionId: "ses_secret", entry: "existing", backend: "codex", transport: "sse" });
  markWorkbenchTraceEventsReceived({ traceId: "trc_secret", transport: "rest_gap", events: [backendEvent] });
  markWorkbenchTraceProjected("trc_secret");
  acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("ses_secret", "trc_secret", [backendEvent])], activeSessionId: "ses_secret", detailLoading: false });

  const backendOnlyEvents = drainWorkbenchPerformanceEventsForTest();
  assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);
  assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_event_phase" && event.phase === "sse_to_receive"), false);
  assert.ok(backendOnlyEvents.some((event) => event.kind === "workbench_event_phase" && event.phase === "created_to_append" && event.eventType === "backend" && event.transport === "rest_gap"));
  assert.ok(backendOnlyEvents.some((event) => event.kind === "workbench_event_phase" && event.phase === "receive_to_project" && event.eventType === "backend" && event.transport === "rest_gap"));
  assert.equal(backendOnlyEvents.some((event) => event.kind === "workbench_backend_event_visible" && event.eventType === "backend" && event.outcome === "stale"), false);

  acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("ses_secret", "trc_secret", [{ type: "assistant_message", status: "running", message: "progress only" } as TraceEvent])], activeSessionId: "ses_secret", detailLoading: false });
  const progressEvents = drainWorkbenchPerformanceEventsForTest();
  assert.equal(progressEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible"), false);

  acknowledgeWorkbenchVisible({ messages: [traceOnlyAgentMessage("ses_secret", "trc_secret", [{ type: "tool_call", command: "pwd" } as TraceEvent])], activeSessionId: "ses_secret", detailLoading: false });
  const toolEvents = drainWorkbenchPerformanceEventsForTest();
  assert.ok(toolEvents.some((event) => event.kind === "workbench_journey" && event.journey === "submit_to_first_visible" && event.outcome === "ok"));
});

function userMessage(sessionId: string): ChatMessage {
  return { id: "msg_user", role: "user", title: "User", text: "hello", status: "sent", createdAt: new Date().toISOString(), sessionId };
}

function agentMessage(sessionId: string, traceId: string): ChatMessage {
  return {
    id: "msg_agent",
    role: "agent",
    title: "Code Agent",
    text: "visible",
    status: "completed",
    createdAt: new Date().toISOString(),
    sessionId,
    traceId,
    runnerTrace: { traceId, sessionId, eventCount: 1, events: [{ type: "assistant_message" }] }
  };
}

function traceOnlyAgentMessage(sessionId: string, traceId: string, events: TraceEvent[]): ChatMessage {
  return {
    id: "msg_agent_trace",
    role: "agent",
    title: "Code Agent",
    text: "",
    status: "running",
    createdAt: new Date().toISOString(),
    sessionId,
    traceId,
    runnerTrace: { traceId, sessionId, eventCount: events.length, events }
  };
}
