/**
 * Subscribe to /api/v1/stream and collect events. Tests can then assert "a
 * `like-update` arrived for upload X within 5 seconds" without driving a
 * second browser tab.
 *
 * The backend authenticates the SSE endpoint via a single-use `?ticket=` minted
 * at POST /api/v1/stream/ticket (the raw JWT is never put in the URL). This helper
 * does that exchange internally, so callers still just pass a JWT to `start()`.
 */

import { mintSseTicket } from './sse';

export type SseEvent = { type: string; data: any; receivedAt: number };

export class SseListener {
  private controller = new AbortController();
  private events: SseEvent[] = [];
  private closed = false;

  constructor(private baseUrl: string = process.env.E2E_FRONTEND_URL ?? 'http://localhost:3101') {}

  async start(token: string): Promise<void> {
    // Exchange the JWT for a single-use SSE ticket (the stream endpoint no longer
    // accepts ?token=).
    const ticket = await mintSseTicket(token);
    const url = `${this.baseUrl}/api/v1/stream?ticket=${encodeURIComponent(ticket)}`;
    // Use fetch with streaming since Node has no EventSource by default.
    const res = await fetch(url, { signal: this.controller.signal });
    if (!res.body) throw new Error('SSE response has no body');
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = '';
    // Fire-and-forget reader loop; the caller drives lifecycle via stop()/the AbortController.
    void (async () => {
      try {
        while (!this.closed) {
          const { done, value } = await reader.read();
          if (done) break;
          buffer += decoder.decode(value, { stream: true });
          // SSE messages are separated by \n\n
          const parts = buffer.split('\n\n');
          buffer = parts.pop() ?? '';
          for (const part of parts) {
            const evt = parseSseMessage(part);
            if (evt) this.events.push({ ...evt, receivedAt: Date.now() });
          }
        }
      } catch {
        /* aborted */
      }
    })();
  }

  async waitForEvent(
    type: string,
    predicate: (e: SseEvent) => boolean = () => true,
    timeoutMs = 10_000
  ): Promise<SseEvent> {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      const hit = this.events.find((e) => e.type === type && predicate(e));
      if (hit) return hit;
      await new Promise((r) => setTimeout(r, 50));
    }
    throw new Error(
      `SSE event ${type} did not arrive within ${timeoutMs}ms. Got: ${this.events.map((e) => e.type).join(', ')}`
    );
  }

  allEvents(): SseEvent[] {
    return [...this.events];
  }

  stop(): void {
    this.closed = true;
    this.controller.abort();
  }
}

function parseSseMessage(raw: string): { type: string; data: any } | null {
  const lines = raw.split('\n');
  let type = 'message';
  const dataLines: string[] = [];
  for (const line of lines) {
    if (line.startsWith('event:')) type = line.slice(6).trim();
    else if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
  }
  if (dataLines.length === 0) return null;
  const dataStr = dataLines.join('\n');
  let data: any = dataStr;
  try {
    data = JSON.parse(dataStr);
  } catch {
    /* leave as string */
  }
  return { type, data };
}
