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

import { connectWorkbenchEvents, realtimeCoalesceKey, workbenchEventStreamPath, type WorkbenchRealtimeEvent, type WorkbenchSseIngressFrame } from "./workbench-events";

test("turn snapshot coalescing keys by trace instead of per sequence", () => {
  const first: WorkbenchRealtimeEvent = { type: "turn.snapshot", turn: { sessionId: "ses_queue", traceId: "trc_queue", status: "running" }, cursor: { traceSeq: 10 } };
  const next: WorkbenchRealtimeEvent = { type: "turn.snapshot", turn: { sessionId: "ses_queue", traceId: "trc_queue", status: "running" }, cursor: { traceSeq: 11 } };

  assert.equal(realtimeCoalesceKey(first, "workbench.turn.snapshot"), realtimeCoalesceKey(next, "workbench.turn.snapshot"));
});

test("trace events keep sequence-specific coalescing keys", () => {
  const first: WorkbenchRealtimeEvent = { type: "trace.event", traceId: "trc_queue", event: { traceId: "trc_queue", projectedSeq: 10 } };
  const next: WorkbenchRealtimeEvent = { type: "trace.event", traceId: "trc_queue", event: { traceId: "trc_queue", projectedSeq: 11 } };

  assert.notEqual(realtimeCoalesceKey(first, "workbench.trace.event"), realtimeCoalesceKey(next, "workbench.trace.event"));
});

test("product EventSource resumes the transactional projection authority with one durable outbox cursor", () => {
  assert.equal(
    workbenchEventStreamPath({
      realtimeCapabilities: { liveKafkaSse: false, kafkaRefreshReplay: false, projectionRealtime: true },
      sessionId: "ses_projection_authority",
      traceId: "trc_projection_authority",
      afterSeq: 42
    }),
    "/v1/workbench/events?sessionId=ses_projection_authority&traceId=trc_projection_authority&afterSeq=42"
  );
});

test("product EventSource tees projection frames once before reducer delivery", async () => {
  const originalEventSource = globalThis.EventSource;
  const ingress: WorkbenchSseIngressFrame[] = [];
  const delivered: WorkbenchRealtimeEvent[] = [];
  FakeProductEventSource.instances = [];
  globalThis.EventSource = FakeProductEventSource as unknown as typeof EventSource;
  try {
    const stream = connectWorkbenchEvents({
      realtimeCapabilities: { liveKafkaSse: false, kafkaRefreshReplay: false, projectionRealtime: true },
      sessionId: "ses_raw_tee",
      flushYieldMs: 1,
      onIngress: (frame) => ingress.push(frame),
      onEvent: (event) => delivered.push(event)
    });
    assert.ok(stream);
    const source = FakeProductEventSource.instances[0];
    assert.ok(source);
    source.emitRaw("workbench.trace.event", '{"type":"trace.event","traceId":"trc_raw_tee","event":{"traceId":"trc_raw_tee","projectedSeq":1}}');
    source.emitRaw("workbench.trace.event", "{invalid-json");
    await new Promise((resolve) => setTimeout(resolve, 10));

    assert.equal(FakeProductEventSource.instances.length, 1);
    assert.deepEqual(ingress.map((frame) => [frame.eventName, frame.decodeStatus]), [
      ["workbench.trace.event", "accepted"],
      ["workbench.trace.event", "invalid-json"]
    ]);
    assert.equal(delivered.length, 1);
    assert.equal(delivered[0]?.traceId, "trc_raw_tee");
    stream.close();
  } finally {
    globalThis.EventSource = originalEventSource;
  }
});

test("product EventSource batches a burst until the configured scheduler runs", async () => {
  const originalEventSource = globalThis.EventSource;
  const delivered: WorkbenchRealtimeEvent[] = [];
  FakeProductEventSource.instances = [];
  globalThis.EventSource = FakeProductEventSource as unknown as typeof EventSource;
  try {
    const stream = connectWorkbenchEvents({
      realtimeCapabilities: { liveKafkaSse: true, kafkaRefreshReplay: true, projectionRealtime: false },
      sessionId: "ses_frame_batch",
      flushYieldMs: 1,
      onEvent: (event) => delivered.push(event)
    });
    assert.ok(stream);
    const source = FakeProductEventSource.instances[0];
    source.emitRaw("hwlab.event.v1", '{"schema":"hwlab.event.v1","eventId":"evt_1"}');
    source.emitRaw("hwlab.event.v1", '{"schema":"hwlab.event.v1","eventId":"evt_2"}');

    assert.equal(delivered.length, 0);
    await Promise.resolve();
    assert.equal(delivered.length, 0);
    await new Promise((resolve) => setTimeout(resolve, 10));
    assert.equal(delivered.length, 2);
    stream.close();
  } finally {
    globalThis.EventSource = originalEventSource;
  }
});

test("retention replay reaches connected without animation frames", async () => {
  const originalEventSource = globalThis.EventSource;
  const originalWindow = globalThis.window;
  const delivered: Array<[string, string | null]> = [];
  let animationFrameCalls = 0;
  FakeProductEventSource.instances = [];
  globalThis.EventSource = FakeProductEventSource as unknown as typeof EventSource;
  Object.defineProperty(globalThis, "window", {
    configurable: true,
    value: {
      requestAnimationFrame: () => {
        animationFrameCalls += 1;
        return 1;
      },
      cancelAnimationFrame: () => undefined
    }
  });
  try {
    const stream = connectWorkbenchEvents({
      realtimeCapabilities: { liveKafkaSse: true, kafkaRefreshReplay: true, projectionRealtime: false },
      sessionId: "ses_background_replay",
      flushMaxItemsPerChunk: 4,
      flushMaxChunkMs: 8,
      flushYieldMs: 0,
      onEvent: (event, eventName) => delivered.push([eventName, event.eventId ?? null])
    });
    assert.ok(stream);
    const source = FakeProductEventSource.instances[0];
    for (let index = 0; index < 395; index += 1) {
      source.emitRaw("hwlab.event.v1", JSON.stringify({ schema: "hwlab.event.v1", eventId: `evt_${index}` }));
    }
    source.emitRaw("workbench.connected", JSON.stringify({ type: "connected", filters: { sessionId: "ses_background_replay" } }));

    await waitFor(() => delivered.length === 396, 1_000);
    assert.equal(animationFrameCalls, 0);
    assert.deepEqual(delivered.at(-1), ["workbench.connected", null]);
    stream.close();
  } finally {
    globalThis.EventSource = originalEventSource;
    Object.defineProperty(globalThis, "window", { configurable: true, value: originalWindow });
  }
});

async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
  const deadline = Date.now() + timeoutMs;
  while (!predicate()) {
    if (Date.now() >= deadline) throw new Error(`condition did not pass within ${timeoutMs}ms`);
    await new Promise((resolve) => setTimeout(resolve, 5));
  }
}

class FakeProductEventSource {
  static instances: FakeProductEventSource[] = [];
  onopen: ((event: Event) => void) | null = null;
  onerror: ((event: Event) => void) | null = null;
  onmessage: ((event: MessageEvent) => void) | null = null;
  private readonly listeners = new Map<string, Set<(event: MessageEvent) => void>>();

  constructor(readonly url: string | URL) {
    FakeProductEventSource.instances.push(this);
  }

  addEventListener(name: string, listener: (event: MessageEvent) => void): void {
    const listeners = this.listeners.get(name) ?? new Set();
    listeners.add(listener);
    this.listeners.set(name, listeners);
  }

  removeEventListener(name: string, listener: (event: MessageEvent) => void): void {
    this.listeners.get(name)?.delete(listener);
  }

  emitRaw(name: string, rawText: string): void {
    const event = { data: rawText } as MessageEvent;
    for (const listener of this.listeners.get(name) ?? []) listener(event);
  }

  close(): void {}
}
