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

import { connectWorkbenchKafkaSseDebug, workbenchKafkaSseDebugCorrelationIds, workbenchKafkaSseDebugPath } from "./workbench-debug";

test("Kafka SSE debug path preserves stream and explicit correlation filters", () => {
  assert.equal(
    workbenchKafkaSseDebugPath({
      stream: "agentrun",
      fromBeginning: true,
      traceId: "trc_debug",
      sessionId: "ses_debug",
      runId: "run_debug",
      commandId: "cmd_debug"
    }),
    "/v1/workbench/debug/kafka-sse/events?stream=agentrun&fromBeginning=true&traceId=trc_debug&sessionId=ses_debug&runId=run_debug&commandId=cmd_debug"
  );
});

test("isolated Kafka debug replay path is trace-scoped and explicit", () => {
  assert.equal(
    workbenchKafkaSseDebugPath({
      stream: "hwlab-debug",
      fromBeginning: true,
      traceId: "trc_isolated_debug"
    }),
    "/v1/workbench/debug/kafka-sse/events?stream=hwlab-debug&fromBeginning=true&traceId=trc_isolated_debug"
  );
});

test("isolated Kafka debug v2 path carries replay correlation and output barrier", () => {
  assert.equal(
    workbenchKafkaSseDebugPath({
      stream: "hwlab-debug",
      fromBeginning: true,
      traceId: "trc_isolated_v2",
      replayId: "rpl_isolated_v2",
      outputBarrier: { partition: 0, firstOffset: "42", lastOffset: "51" }
    }),
    "/v1/workbench/debug/kafka-sse/events?stream=hwlab-debug&fromBeginning=true&traceId=trc_isolated_v2&replayId=rpl_isolated_v2&partition=0&firstOffset=42&lastOffset=51"
  );
});

test("Kafka SSE debug correlation ids normalize stdio snake_case metadata", () => {
  assert.deepEqual(
    workbenchKafkaSseDebugCorrelationIds({
      value: {
        trace_id: "trc_stdio_debug",
        metadata: {
          session_id: "ses_stdio_debug",
          run_id: "run_stdio_debug",
          command_id: "cmd_stdio_debug"
        }
      }
    }),
    ["trc_stdio_debug", "ses_stdio_debug", "run_stdio_debug", "cmd_stdio_debug"]
  );
});

test("Kafka SSE debug client reports open from named consumer-ready and error from named Kafka error", () => {
  const originalEventSource = globalThis.EventSource;
  try {
    globalThis.EventSource = FakeEventSource as unknown as typeof EventSource;
    const opened: string[] = [];
    const errors: string[] = [];
    const events: string[] = [];
    const stream = connectWorkbenchKafkaSseDebug({
      stream: "hwlab",
      onOpen: () => opened.push("open"),
      onError: (event) => errors.push(event.type),
      onEvent: (_event, eventName) => events.push(eventName)
    });
    assert.ok(stream);
    const source = FakeEventSource.latest;
    assert.ok(source);

    source.emitTransportOpen();
    assert.deepEqual(opened, []);

    source.emitNamed("hwlab.kafka.connected", { ok: true, consumerReady: false, groupId: "not-ready" });
    assert.deepEqual(opened, []);
    source.emitNamed("hwlab.kafka.connected", { ok: true, consumerReady: true, groupId: "hwlab-v03-debug-sse-ready" });
    assert.deepEqual(opened, ["open"]);

    source.emitNamed("hwlab.kafka.error", { ok: false, error: { code: "kafka_sse_error", message: "consumer stopped" } });
    assert.deepEqual(errors, ["hwlab.kafka.error"]);
    assert.deepEqual(events, ["hwlab.kafka.connected", "hwlab.kafka.connected", "hwlab.kafka.error"]);
    stream.close();
    assert.equal(source.closed, true);
  } finally {
    globalThis.EventSource = originalEventSource;
    FakeEventSource.latest = null;
  }
});

class FakeEventSource {
  static latest: FakeEventSource | null = null;
  onopen: ((event: Event) => void) | null = null;
  onerror: ((event: Event) => void) | null = null;
  onmessage: ((event: MessageEvent) => void) | null = null;
  closed = false;
  private listeners = new Map<string, Set<(event: MessageEvent) => void>>();

  constructor(_url: string | URL, _eventSourceInitDict?: EventSourceInit) {
    FakeEventSource.latest = this;
  }

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

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

  close() {
    this.closed = true;
  }

  emitTransportOpen() {
    this.onopen?.(new Event("open"));
  }

  emitNamed(name: string, payload: Record<string, unknown>) {
    const event = new MessageEvent(name, { data: JSON.stringify(payload) });
    for (const listener of this.listeners.get(name) ?? []) listener(event);
  }
}
