import { describe, it, expect, afterEach, vi } from 'vitest';
import { jobEventBus, type JobStreamEvent } from './job-events.js';

// jobEventBus is a module-level singleton. Track every jobId used so we can
// clean its listeners up after each test without disturbing other suites.
const usedJobIds: string[] = [];

function track(jobId: string): string {
  usedJobIds.push(jobId);
  return jobId;
}

afterEach(() => {
  for (const id of usedJobIds.splice(0)) {
    jobEventBus.removeAllListeners(`job:${id}`);
  }
});

describe('jobEventBus', () => {
  it('delivers an emitted event to a subscribed handler', () => {
    const jobId = track('job-events-test-1');
    const handler = vi.fn();
    jobEventBus.onJob(jobId, handler);

    const event: JobStreamEvent = { type: 'text', text: 'hello' };
    jobEventBus.emitJob(jobId, event);

    expect(handler).toHaveBeenCalledTimes(1);
    expect(handler).toHaveBeenCalledWith(event);
  });

  it('does not deliver events across different job ids', () => {
    const jobA = track('job-events-test-a');
    const jobB = track('job-events-test-b');
    const handlerA = vi.fn();
    const handlerB = vi.fn();
    jobEventBus.onJob(jobA, handlerA);
    jobEventBus.onJob(jobB, handlerB);

    jobEventBus.emitJob(jobA, { type: 'done' });

    expect(handlerA).toHaveBeenCalledTimes(1);
    expect(handlerB).not.toHaveBeenCalled();
  });

  it('stops delivering after offJob', () => {
    const jobId = track('job-events-test-off');
    const handler = vi.fn();
    jobEventBus.onJob(jobId, handler);
    jobEventBus.emitJob(jobId, { type: 'text', text: '1' });
    jobEventBus.offJob(jobId, handler);
    jobEventBus.emitJob(jobId, { type: 'text', text: '2' });

    expect(handler).toHaveBeenCalledTimes(1);
  });

  it('offJob only removes the given handler, not other subscribers', () => {
    const jobId = track('job-events-test-multi');
    const kept = vi.fn();
    const removed = vi.fn();
    jobEventBus.onJob(jobId, kept);
    jobEventBus.onJob(jobId, removed);

    jobEventBus.offJob(jobId, removed);
    jobEventBus.emitJob(jobId, { type: 'done' });

    expect(kept).toHaveBeenCalledTimes(1);
    expect(removed).not.toHaveBeenCalled();
  });

  it('hasListeners reflects subscription state', () => {
    const jobId = track('job-events-test-has');
    expect(jobEventBus.hasListeners(jobId)).toBe(false);

    const handler = vi.fn();
    jobEventBus.onJob(jobId, handler);
    expect(jobEventBus.hasListeners(jobId)).toBe(true);

    jobEventBus.offJob(jobId, handler);
    expect(jobEventBus.hasListeners(jobId)).toBe(false);
  });

  it('supports the full event-type union with payload fields', () => {
    const jobId = track('job-events-test-payload');
    const received: JobStreamEvent[] = [];
    jobEventBus.onJob(jobId, e => received.push(e));

    jobEventBus.emitJob(jobId, { type: 'prompt_progress', processed: 10, total: 100, timeMs: 5, cache: 2 });
    jobEventBus.emitJob(jobId, { type: 'tool_use', toolName: 'Read', toolInput: '{"path":"x"}', callId: 'c1' });
    jobEventBus.emitJob(jobId, { type: 'tool_use_delta', callId: 'c1', name: 'Read', chunk: '{"pa' });
    jobEventBus.emitJob(jobId, { type: 'tool_result', toolName: 'Read', toolOutput: 'ok', toolIsError: false, callId: 'c1' });
    jobEventBus.emitJob(jobId, { type: 'done' });

    expect(received.map(e => e.type)).toEqual([
      'prompt_progress', 'tool_use', 'tool_use_delta', 'tool_result', 'done',
    ]);
    expect(received[0].processed).toBe(10);
    expect(received[3].toolIsError).toBe(false);
  });

  it('raises the max listener limit to 200 (no warning for many SSE clients)', () => {
    expect(jobEventBus.getMaxListeners()).toBe(200);
  });
});
