import assert from "node:assert/strict";
import { test } from "bun:test";

import type { ChatMessage, TraceEvent } from "../types";
import { projectWorkbenchLiveKafkaMessage, projectWorkbenchLiveKafkaUserMessage, projectWorkbenchLiveKafkaUserSession, workbenchAgentMessageIdForTrace, workbenchLiveKafkaEnvelope, workbenchLiveKafkaEventIsUserMessage, workbenchLiveKafkaProjectionTarget, workbenchLiveKafkaSessionSummaryChanged, workbenchUserMessageIdForTrace } from "./workbench-live-kafka-event";
import { createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, type WorkbenchServerState } from "./workbench-server-state";
import { sessionToSessionTab } from "./workbench-session";

test("only canonical HWLAB Kafka envelopes bypass terminal priority", () => {
  assert.equal(workbenchLiveKafkaEnvelope("hwlab.event.v1"), true);
  assert.equal(workbenchLiveKafkaEnvelope("workbench.trace.event.v1"), false);
  assert.equal(workbenchLiveKafkaEnvelope(null), false);
});

test("running Kafka events do not rebuild an unchanged session summary", () => {
  const previous = {
    sessionId: "ses_longrun",
    threadId: "thread_longrun",
    status: "running",
    lastTraceId: "trc_longrun",
    lastUserMessageAt: "2026-07-21T02:18:22.034Z",
    updatedAt: "2026-07-21T02:18:22.034Z"
  };
  assert.equal(workbenchLiveKafkaSessionSummaryChanged(previous, { ...previous }), false);
  assert.equal(workbenchLiveKafkaSessionSummaryChanged(previous, { ...previous, status: "completed" }), true);
});

test("live Kafka user event restores and idempotently updates the user bubble", () => {
  const event = traceEvent(1, "user", {
    userMessageId: "msg_live_user_user",
    messageId: "msg_live_user_user",
    text: "persisted user input",
    message: "persisted user input"
  });
  assert.equal(workbenchLiveKafkaEventIsUserMessage(event), true);
  const first = projectWorkbenchLiveKafkaUserMessage({
    previous: null,
    traceId: "trc_live_user",
    sessionId: "ses_live_user",
    event,
    receivedAt: "2026-07-10T10:00:05.000Z"
  });
  const second = projectWorkbenchLiveKafkaUserMessage({
    previous: first,
    traceId: "trc_live_user",
    sessionId: "ses_live_user",
    event,
    receivedAt: "2026-07-10T10:00:06.000Z"
  });
  assert.equal(first?.id, "msg_live_user_user");
  assert.equal(first?.role, "user");
  assert.equal(first?.text, "persisted user input");
  assert.equal(second?.id, first?.id);
  assert.equal(second?.createdAt, first?.createdAt);
  assert.equal(second?.updatedAt, event.createdAt);
});

test("Kafka user replay rebuilds the session rail title without optimistic state", () => {
  const sessionId = "ses_live_user_title";
  const traceId = "trc_live_user_title";
  const firstEvent = traceEvent(1, "user", {
    userMessageId: "msg_live_user_title_user",
    messageId: "msg_live_user_title_user",
    text: "persisted session title",
    createdAt: "2026-07-10T10:00:00.000Z"
  });
  const firstMessage = projectWorkbenchLiveKafkaUserMessage({
    previous: null,
    traceId,
    sessionId,
    event: firstEvent,
    receivedAt: "2026-07-10T10:01:00.000Z"
  });
  assert.ok(firstMessage);
  const replayedSession = projectWorkbenchLiveKafkaUserSession({ previous: null, message: firstMessage, sessionId });
  assert.ok(replayedSession);
  assert.equal(replayedSession.firstUserMessagePreview, "persisted session title");
  assert.equal(replayedSession.updatedAt, firstEvent.createdAt);
  assert.equal(sessionToSessionTab(replayedSession, sessionId).label, "persisted session title");

  const repeatedSession = projectWorkbenchLiveKafkaUserSession({ previous: replayedSession, message: firstMessage, sessionId });
  assert.deepEqual(repeatedSession, replayedSession);

  const secondEvent = traceEvent(2, "user", {
    traceId: "trc_live_user_title_second",
    userMessageId: "msg_live_user_title_second_user",
    messageId: "msg_live_user_title_second_user",
    text: "later user input",
    createdAt: "2026-07-10T10:02:00.000Z"
  });
  const secondMessage = projectWorkbenchLiveKafkaUserMessage({
    previous: null,
    traceId: "trc_live_user_title_second",
    sessionId,
    event: secondEvent,
    receivedAt: "2026-07-10T10:03:00.000Z"
  });
  assert.ok(secondMessage);
  const updatedSession = projectWorkbenchLiveKafkaUserSession({ previous: replayedSession, message: secondMessage, sessionId });
  assert.ok(updatedSession);
  assert.equal(updatedSession.firstUserMessagePreview, "persisted session title");
  assert.equal(updatedSession.updatedAt, secondEvent.createdAt);
  assert.equal(sessionToSessionTab(updatedSession, sessionId).label, "persisted session title");
});

test("HTTP-first and Kafka-first admission converge on one stable optimistic user bubble", () => {
  const sessionId = "ses_live_user_race";
  const traceId = "trc_live_user_race";
  const userMessageId = workbenchUserMessageIdForTrace(traceId);
  assert.equal(userMessageId, "msg_live_user_race_user");
  const optimistic = {
    id: userMessageId,
    messageId: userMessageId,
    role: "user",
    title: "用户",
    text: "race input",
    status: "sent",
    traceId,
    sessionId,
    createdAt: "2026-07-10T10:00:00.000Z",
    updatedAt: "2026-07-10T10:00:00.000Z"
  } as ChatMessage;
  const event = traceEvent(1, "user", {
    traceId,
    sessionId,
    userMessageId,
    messageId: userMessageId,
    text: "race input"
  });

  const runOrder = (order: "http-first" | "kafka-first"): ChatMessage[] => {
    let state = createWorkbenchServerState();
    state = reduceWorkbenchServerState(state, { type: "message.upsert", sessionId, message: optimistic });
    const applyAdmissionResponse = (source: WorkbenchServerState): WorkbenchServerState => {
      const previous = selectActiveMessages(source, sessionId).find((message) => message.role === "user" && message.traceId === traceId);
      if (!previous) throw new Error("optimistic user message missing");
      return reduceWorkbenchServerState(source, {
        type: "message.upsert",
        sessionId,
        message: { ...previous, id: userMessageId, messageId: userMessageId, turnId: traceId }
      });
    };
    const applyKafkaEvent = (source: WorkbenchServerState): WorkbenchServerState => {
      const previous = selectActiveMessages(source, sessionId).find((message) => (message.messageId ?? message.id) === userMessageId) ?? null;
      const message = projectWorkbenchLiveKafkaUserMessage({ previous, traceId, sessionId, event, receivedAt: "2026-07-10T10:00:01.000Z" });
      if (!message) throw new Error("formal Kafka user event was not projected");
      return reduceWorkbenchServerState(source, { type: "message.upsert", sessionId, message });
    };
    state = order === "http-first" ? applyKafkaEvent(applyAdmissionResponse(state)) : applyAdmissionResponse(applyKafkaEvent(state));
    return selectActiveMessages(state, sessionId).filter((message) => message.role === "user");
  };

  for (const order of ["http-first", "kafka-first"] as const) {
    const users = runOrder(order);
    assert.equal(users.length, 1, `${order} must keep one user bubble`);
    assert.equal(users[0]?.id, userMessageId);
    assert.equal(users[0]?.messageId, userMessageId);
    assert.equal(users[0]?.text, "race input");
  }
});

test("optimistic live and retained Kafka assistant rows share the trace-derived identity", () => {
  const sessionId = "ses_2947a007-1823-41bf-aa94-d89b468f74a7";
  const traceId = "trc_mrgpndrwsk0edt";
  const messageId = workbenchAgentMessageIdForTrace(traceId);
  assert.equal(messageId, "msg_mrgpndrwsk0edt_agent");
  const optimistic = {
    id: messageId,
    messageId,
    role: "agent",
    title: "Code Agent",
    text: "",
    status: "running",
    traceId,
    sessionId,
    createdAt: "2026-07-11T18:51:00.000Z",
    updatedAt: "2026-07-11T18:51:00.000Z"
  } as ChatMessage;
  const event = traceEvent(1, "assistant", { traceId, sessionId, assistantText: "retained reply" });
  const live = projectWorkbenchLiveKafkaMessage({ previous: optimistic, traceId, sessionId, event, receivedAt: "2026-07-11T18:51:01.000Z" });
  const replay = projectWorkbenchLiveKafkaMessage({ previous: null, traceId, sessionId, event, receivedAt: "2026-07-11T18:52:01.000Z" });

  assert.equal(live.id, messageId);
  assert.equal(live.messageId, messageId);
  assert.equal(replay.id, messageId);
  assert.equal(replay.messageId, messageId);
  assert.equal(live.id, replay.id);
});

test("Kafka assistant projection replaces a pre-contract placeholder with the trace identity", () => {
  const sessionId = "ses_live_agent_identity";
  const traceId = "trc_live_agent_identity";
  const previous = {
    id: "msg_random_placeholder",
    messageId: "msg_random_placeholder",
    role: "agent",
    title: "Code Agent",
    text: "",
    status: "running",
    traceId,
    sessionId,
    createdAt: "2026-07-11T18:51:00.000Z",
    updatedAt: "2026-07-11T18:51:00.000Z"
  } as ChatMessage;
  const projected = projectWorkbenchLiveKafkaMessage({
    previous,
    traceId,
    sessionId,
    event: traceEvent(1, "tool", { traceId, sessionId }),
    receivedAt: "2026-07-11T18:51:01.000Z"
  });

  assert.equal(projected.id, "msg_live_agent_identity_agent");
  assert.equal(projected.messageId, "msg_live_agent_identity_agent");
});

test("session replay keeps steer user input separate from the target agent lifecycle", () => {
  const sessionId = "ses_live_session_sequence";
  const targetTraceId = "trc_live_target";
  const steerTraceId = "trc_live_steer";
  const events = [
    traceEvent(1, "user", { traceId: targetTraceId, sessionId, userMessageId: "msg_target_user", messageId: "msg_target_user", text: "initial input" }),
    traceEvent(2, "backend", { traceId: targetTraceId, sessionId }),
    traceEvent(3, "assistant", { traceId: targetTraceId, sessionId, assistantText: "single final", text: "single final" }),
    traceEvent(4, "user", { traceId: steerTraceId, targetTraceId, sessionId, userMessageId: "msg_steer_user", messageId: "msg_steer_user", text: "steer input" }),
    traceEvent(5, "tool", { traceId: targetTraceId, sessionId }),
    traceEvent(6, "result", { traceId: targetTraceId, sessionId, terminal: true, status: "completed" })
  ];
  const userMessages = new Map<string, ChatMessage>();
  const agentMessages = new Map<string, ChatMessage>();

  events.forEach((event, index) => {
    const eventTraceId = String(event.traceId);
    const receivedAt = `2026-07-10T10:01:0${index}.000Z`;
    if (workbenchLiveKafkaProjectionTarget(event) === "user") {
      const messageId = String(event.userMessageId);
      const projected = projectWorkbenchLiveKafkaUserMessage({ previous: userMessages.get(messageId) ?? null, traceId: eventTraceId, sessionId, event, receivedAt });
      if (projected) userMessages.set(messageId, projected);
      return;
    }
    agentMessages.set(eventTraceId, projectWorkbenchLiveKafkaMessage({ previous: agentMessages.get(eventTraceId) ?? null, traceId: eventTraceId, sessionId, event, receivedAt }));
  });

  assert.deepEqual([...userMessages.keys()], ["msg_target_user", "msg_steer_user"]);
  assert.deepEqual([...agentMessages.keys()], [targetTraceId]);
  assert.equal(userMessages.get("msg_steer_user")?.targetTraceId, targetTraceId);
  assert.equal(agentMessages.has(steerTraceId), false);
  const target = agentMessages.get(targetTraceId);
  assert.equal(target?.status, "completed");
  assert.equal(target?.text, "single final");
  assert.equal((target?.finalResponse as { text?: string } | null)?.text, "single final");
  assert.equal([...agentMessages.values()].filter((message) => message.finalResponse).length, 1);
});

test("refresh rebuilds the completed turn from an empty store using only Kafka SSE events", () => {
  const sessionId = "ses_refresh_kafka_only";
  const traceId = "trc_refresh_kafka_only";
  const threadId = "thread_refresh_kafka_only";
  const events = [
    traceEvent(1, "user", { traceId, sessionId, userMessageId: "msg_refresh_user", messageId: "msg_refresh_user", text: "hi", threadId }),
    traceEvent(2, "backend", { traceId, sessionId, threadId }),
    traceEvent(3, "assistant", { traceId, sessionId, assistantText: "refresh survives", text: "refresh survives", threadId }),
    traceEvent(4, "result", { traceId, sessionId, terminal: true, status: "completed", threadId })
  ];

  const replay = (): ChatMessage[] => {
    let state = createWorkbenchServerState();
    state = reduceWorkbenchServerState(state, { type: "session.detail", session: { sessionId, messages: [] } });
    for (const [index, event] of events.entries()) {
      const receivedAt = `2026-07-10T10:02:0${index + 1}.000Z`;
      if (workbenchLiveKafkaProjectionTarget(event) === "user") {
        const previous = selectActiveMessages(state, sessionId).find((message) => message.role === "user" && message.traceId === traceId) ?? null;
        const message = projectWorkbenchLiveKafkaUserMessage({ previous, traceId, sessionId, event, receivedAt, threadId });
        if (message) state = reduceWorkbenchServerState(state, { type: "message.upsert", sessionId, message });
        continue;
      }
      const previous = selectActiveMessages(state, sessionId).find((message) => message.role === "agent" && message.traceId === traceId) ?? null;
      const message = projectWorkbenchLiveKafkaMessage({ previous, traceId, sessionId, event, receivedAt, threadId });
      state = reduceWorkbenchServerState(state, { type: "message.upsert", sessionId, message });
    }
    return selectActiveMessages(state, sessionId);
  };

  const beforeRefresh = replay();
  const afterRefresh = replay();
  assert.deepEqual(afterRefresh, beforeRefresh);
  assert.equal(afterRefresh.length, 2);
  assert.equal(afterRefresh[0]?.text, "hi");
  assert.equal(afterRefresh[1]?.status, "completed");
  assert.equal(afterRefresh[1]?.text, "refresh survives");
  assert.equal((afterRefresh[1]?.finalResponse as { text?: string } | null)?.text, "refresh survives");
  assert.equal(afterRefresh[1]?.threadId, threadId);
});

test("live Kafka projection uses business event time instead of replay receipt time", () => {
  const traceId = "trc_live_timing";
  const sessionId = "ses_live_timing";
  const firstReceivedAt = "2026-07-10T10:00:05.000Z";
  const secondReceivedAt = "2026-07-10T10:00:09.000Z";
  const first = projectWorkbenchLiveKafkaMessage({
    previous: null,
    traceId,
    sessionId,
    receivedAt: firstReceivedAt,
    event: traceEvent(1, "tool")
  });
  const second = projectWorkbenchLiveKafkaMessage({
    previous: first,
    traceId,
    sessionId,
    receivedAt: secondReceivedAt,
    event: traceEvent(2, "assistant", { assistantText: "visible response" })
  });

  assert.equal(first.lastEventAt, "2026-07-10T10:00:01.000Z");
  assert.equal(second.lastEventAt, "2026-07-10T10:00:02.000Z");
  assert.equal(second.timing?.lastEventAt, "2026-07-10T10:00:02.000Z");
  assert.equal(second.timing?.lastEventAgeMs, 0);
  assert.equal(second.timing?.observedAt, secondReceivedAt);
  assert.equal(second.runnerTrace?.lastEventAt, "2026-07-10T10:00:02.000Z");
  assert.equal(second.runnerTrace?.timing?.lastEventAt, "2026-07-10T10:00:02.000Z");
  assert.equal(second.runnerTrace?.eventCount, 2);
});

test("assistant item completion stays authoritative over a later progress event while raw events remain observable", () => {
  const traceId = "trc_live_assistant_lifecycle";
  const sessionId = "ses_live_assistant_lifecycle";
  const itemId = "msg_live_assistant_lifecycle";
  const progress = projectWorkbenchLiveKafkaMessage({
    previous: null,
    traceId,
    sessionId,
    receivedAt: "2026-07-12T10:00:01.000Z",
    event: traceEvent(136, "assistant", {
      eventType: "assistant_progress",
      itemId,
      assistantSource: "agent-message-delta-progress",
      progress: true,
      assistantText: "partial body"
    })
  });
  const completed = projectWorkbenchLiveKafkaMessage({
    previous: progress,
    traceId,
    sessionId,
    receivedAt: "2026-07-12T10:00:02.000Z",
    event: traceEvent(138, "assistant", {
      eventType: "assistant",
      itemId,
      assistantSource: "completed-agent-message",
      durableText: true,
      assistantText: "durable body"
    })
  });
  const lateProgress = projectWorkbenchLiveKafkaMessage({
    previous: completed,
    traceId,
    sessionId,
    receivedAt: "2026-07-12T10:00:03.000Z",
    event: traceEvent(139, "assistant", {
      eventType: "assistant_progress",
      itemId,
      assistantSource: "agent-message-delta-progress",
      progress: true,
      assistantText: "stale late progress"
    })
  });
  const terminal = projectWorkbenchLiveKafkaMessage({
    previous: lateProgress,
    traceId,
    sessionId,
    receivedAt: "2026-07-12T10:00:04.000Z",
    event: traceEvent(143, "result", { eventType: "terminal", terminal: true, status: "completed" })
  });

  assert.equal(progress.text, "partial body");
  assert.equal(completed.text, "durable body");
  assert.equal(lateProgress.text, "durable body");
  assert.equal(lateProgress.runnerTrace?.events?.length, 3, "visibility keeps the superseded progress source event");
  assert.equal(terminal.text, "durable body");
  assert.equal((terminal.finalResponse as { text?: string } | null)?.text, "durable body");
  assert.equal(terminal.runnerTrace?.events?.length, 4);
});

test("live Kafka projection keeps terminal lifecycle while accepting a later event", () => {
  const traceId = "trc_live_reopened";
  const sessionId = "ses_live_reopened";
  const terminalAt = "2026-07-10T10:00:10.000Z";
  const terminal = projectWorkbenchLiveKafkaMessage({
    previous: seededMessage(traceId, sessionId),
    traceId,
    sessionId,
    receivedAt: terminalAt,
    event: traceEvent(2, "result", { terminal: true, status: "completed" })
  });
  const acceptedAt = "2026-07-10T10:00:12.000Z";
  const running = projectWorkbenchLiveKafkaMessage({
    previous: terminal,
    traceId,
    sessionId,
    receivedAt: acceptedAt,
    event: traceEvent(3, "tool")
  });

  assert.equal(terminal.finishedAt, "2026-07-10T10:00:02.000Z");
  const terminalFinalResponse = terminal.finalResponse as { text?: string } | null | undefined;
  assert.equal(terminalFinalResponse?.text, "final answer");
  assert.equal(running.status, "completed");
  assert.equal(running.lastEventAt, "2026-07-10T10:00:03.000Z");
  assert.equal(running.timing?.lastEventAgeMs, 0);
  assert.equal(running.finishedAt, "2026-07-10T10:00:02.000Z");
  assert.equal(running.durationMs, terminal.durationMs);
  assert.equal((running.finalResponse as { text?: string } | null)?.text, "final answer");
  assert.equal(running.runnerTrace?.finishedAt, "2026-07-10T10:00:02.000Z");
  assert.equal(running.runnerTrace?.durationMs, terminal.durationMs);
  assert.equal(running.runnerTrace?.eventCount, 3);
  assert.equal(running.traceAutoLifecycle, "terminal");
});

test("live and retained Kafka terminal duration share the user-send-to-terminal business clock", () => {
  const turnStartedAt = "2026-07-10T00:00:00.000Z";
  const event = traceEvent(2, "result", {
    traceId: "trc_replay_duration",
    sessionId: "ses_replay_duration",
    terminal: true,
    status: "completed",
    createdAt: "2026-07-10T00:01:03.000Z"
  });
  const live = projectWorkbenchLiveKafkaMessage({
    previous: null,
    traceId: "trc_replay_duration",
    sessionId: "ses_replay_duration",
    turnStartedAt,
    receivedAt: "2026-07-10T00:01:03.010Z",
    event
  });
  const replay = projectWorkbenchLiveKafkaMessage({
    previous: null,
    traceId: "trc_replay_duration",
    sessionId: "ses_replay_duration",
    turnStartedAt,
    receivedAt: "2026-07-10T10:30:00.000Z",
    event
  });

  assert.equal(live.startedAt, turnStartedAt);
  assert.equal(live.finishedAt, "2026-07-10T00:01:03.000Z");
  assert.equal(live.durationMs, 63000);
  assert.equal(replay.startedAt, live.startedAt);
  assert.equal(replay.finishedAt, live.finishedAt);
  assert.equal(replay.durationMs, live.durationMs);
  assert.equal(replay.timing?.observedAt, "2026-07-10T10:30:00.000Z");
});

function seededMessage(traceId: string, sessionId: string): ChatMessage {
  return projectWorkbenchLiveKafkaMessage({
    previous: null,
    traceId,
    sessionId,
    receivedAt: "2026-07-10T10:00:01.000Z",
    event: traceEvent(1, "assistant", { assistantText: "final answer" })
  });
}

function traceEvent(sourceSeq: number, type: string, extra: Record<string, unknown> = {}): TraceEvent {
  return {
    traceId: "trc_live_timing",
    sessionId: "ses_live_timing",
    source: "agentrun.kafka",
    sourceEventId: `evt_live_${sourceSeq}`,
    sourceSeq,
    type,
    eventType: type,
    status: "running",
    createdAt: `2026-07-10T10:00:0${sourceSeq}.000Z`,
    ...extra
  };
}
