import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";

test("conversation subscribes to redacted runtime errors", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");
  assert.match(source, /EVENT_NAMES\s*=\s*\[[^\]]*"error"/s);
});

test("token usage updates preserve the latest queued messages", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /const sessionRef = useRef\(session\)/);
  assert.match(source, /sessionRef\.current = session/);
  assert.match(source, /\{ \.\.\.sessionRef\.current, tokenUsage: data\.tokenUsage \}/);
  assert.doesNotMatch(source, /\{ \.\.\.session, tokenUsage: data\.tokenUsage \}/);
});

test("uploaded attachments keep browser-only preview urls and revoke them", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /const previewUrl = URL\.createObjectURL\(file\)/);
  assert.match(source, /setAttachments\(\(current\) => \[\.\.\.current, localAttachment\]\)/);
  assert.match(source, /setAttachments\(\(current\) => current\.map\(\(item\) => item\.id === localId/);
  assert.match(source, /URL\.revokeObjectURL\(item\.previewUrl\)/);
  assert.doesNotMatch(source, /revokeAttachmentPreviews\(submittedAttachments\)/);
});

test("removing an uploaded attachment waits for server deletion and keeps it on failure", async () => {
  const [source, api] = await Promise.all([
    readFile(new URL("./useConversation.js", import.meta.url), "utf8"),
    readFile(new URL("./conversation-api.js", import.meta.url), "utf8")
  ]);

  assert.match(api, /deleteSessionAttachment/);
  assert.match(source, /await deleteSessionAttachment\(requestSessionId, removed\.id\)/);
  assert.ok(source.indexOf("await deleteSessionAttachment") < source.indexOf("URL.revokeObjectURL(removed.previewUrl)"));
  assert.match(source, /cancelledAttachmentIdsRef/);
});

test("submitted turns keep their optimistic bubble when a race queues them", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /dispatch\(\{ type: "message\.submitted", id: optimisticId, text, queued: pendingKind === "queue"/);
  assert.match(source, /dispatch\(\{ type: "message\.queued", id: optimisticId, keepOptimistic: true \}\)/);
});

test("stop updates the local conversation log before backend confirmation", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /const optimisticStopId = crypto\.randomUUID\(\)/);
  assert.match(source, /dispatch\(\{ type: "stop\.requested", id: optimisticStopId, createdAt: new Date\(\)\.toISOString\(\) \}\)/);
  assert.match(source, /dispatch\(\{ type: "stop\.failed", id: optimisticStopId \}\)/);
  assert.ok(source.indexOf('type: "stop.requested"') < source.indexOf("stopSessionTurn(session.id)"));
});

test("local stopped turns immediately drive composer status and sending mode", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /const effectiveSession = sessionForConversation\(session, conversation\)/);
  assert.match(source, /const pendingKind = effectiveSession\.status === "running" \? "queue" : "send"/);
  assert.match(source, /CONVERSATION_SESSION_STATUSES\.includes\(latestTurn\?\.status\)/);
  assert.match(source, /return \{ \.\.\.session, status: latestTurn\.status \}/);
});

test("local running turns immediately drive the stop button before backend status arrives", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /const CONVERSATION_SESSION_STATUSES = \["sending", "thinking", "running", "waiting_approval", "waiting_response", "interrupted", "failed"\]/);
  assert.match(source, /CONVERSATION_SESSION_STATUSES\.includes\(latestTurn\?\.status\)/);
});

test("transient event stream errors do not invent an interrupted runtime state", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /createReconnectingEventSource/);
  assert.match(source, /onError: \(\) => \{ void request\("\/auth\/session"\)\.catch\(\(\) => \{\}\); \}/);
  assert.doesNotMatch(source, /const nextSession = \{ \.\.\.sessionRef\.current, status: "interrupted" \}/);
  assert.doesNotMatch(source, /onError:[\s\S]{0,200}sendSessionMessage/);
});

test("message sends attach a stable client id and retain it only for uncertain retries", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /createMessageAttemptTracker/);
  assert.match(source, /clientMessageId/);
  assert.match(source, /const messageAttempt = messageAttemptRef\.current/);
  assert.match(source, /messageAttempt\.begin/);
  assert.match(source, /messageAttempt\.failed/);
  assert.match(source, /messageAttempt\.succeeded/);
});

test("late send and upload responses are ignored after switching chats", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /const requestSessionId = session\.id/);
  assert.match(source, /isCurrentSessionRequest\(sessionRef, requestSessionId\)/);
  assert.match(source, /if \(!isCurrentSessionRequest\(sessionRef, requestSessionId\)\) return result/);
  assert.match(source, /if \(isCurrentSessionRequest\(sessionRef, requestSessionId\)\) setSubmitPending\(""\)/);
  assert.match(source, /if \(!isCurrentSessionRequest\(sessionRef, requestSessionId\)\) return/);
  assert.ok(source.indexOf("const requestSessionId = session.id") < source.indexOf("sendSessionMessage(requestSessionId"));
  assert.ok(source.indexOf("const requestSessionId = session.id") < source.indexOf("uploadSessionAttachment(requestSessionId"));
});

test("switching chats clears transient composer state without carrying attachments forward", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /revokeAttachmentPreviews\(current\)/);
  assert.match(source, /setAttachments\(\(current\) => \{/);
  assert.match(source, /setSelectedSkills\(\[\]\)/);
  assert.match(source, /setSubmitPending\(""\)/);
  assert.match(source, /setQueuedActionPending\(null\)/);
});

test("approval decisions are locked, optimistic, and scoped to the active chat", async () => {
  const source = await readFile(new URL("./useConversation.js", import.meta.url), "utf8");

  assert.match(source, /const \[approvalDecisionPending, setApprovalDecisionPending\] = useState\(null\)/);
  assert.match(source, /if \(!session\?\.id \|\| approval\.status !== "pending" \|\| approvalDecisionPending\) return/);
  assert.match(source, /setApprovalDecisionPending\(approval\.id\)/);
  assert.match(source, /type: "approval\.decision\.started"/);
  assert.match(source, /type: "approval\.decision\.failed"/);
  assert.match(source, /if \(!isCurrentSessionRequest\(sessionRef, requestSessionId\)\) return result/);
  assert.match(source, /approvalDecisionPending,/);
});
