import { afterEach, describe, it, expect, vi } from 'vitest';

// We need to import the singleton, but also be able to test a fresh instance.
// The module exports a singleton `eventDispatcher`. Since the class is not
// exported, we test through the singleton and reset between tests.

import {
  eventDispatcher,
  markSettled,
  onSettled,
  __resetSettledEventsForTests,
} from '@/utils/event';

describe('EventDispatcher', () => {
  // -----------------------------------------------------------------------
  // Async listeners (on / off / dispatch)
  // -----------------------------------------------------------------------
  describe('async listeners', () => {
    it('registers and dispatches an async listener', async () => {
      const fn = vi.fn();
      eventDispatcher.on('test-event', fn);
      await eventDispatcher.dispatch('test-event', { foo: 'bar' });
      expect(fn).toHaveBeenCalledOnce();
      expect(fn.mock.calls[0]![0]).toBeInstanceOf(CustomEvent);
      expect(fn.mock.calls[0]![0].detail).toEqual({ foo: 'bar' });
      eventDispatcher.off('test-event', fn);
    });

    it('dispatches to multiple listeners in order', async () => {
      const order: number[] = [];
      const fn1 = vi.fn(() => {
        order.push(1);
      });
      const fn2 = vi.fn(() => {
        order.push(2);
      });
      eventDispatcher.on('multi', fn1);
      eventDispatcher.on('multi', fn2);
      await eventDispatcher.dispatch('multi');
      expect(order).toEqual([1, 2]);
      eventDispatcher.off('multi', fn1);
      eventDispatcher.off('multi', fn2);
    });

    it('removes a listener with off()', async () => {
      const fn = vi.fn();
      eventDispatcher.on('rm-test', fn);
      eventDispatcher.off('rm-test', fn);
      await eventDispatcher.dispatch('rm-test');
      expect(fn).not.toHaveBeenCalled();
    });

    it('off() on non-existent event does not throw', () => {
      const fn = vi.fn();
      expect(() => eventDispatcher.off('nonexistent', fn)).not.toThrow();
    });

    it('dispatch on event with no listeners does not throw', async () => {
      await expect(eventDispatcher.dispatch('no-listeners', { x: 1 })).resolves.toBeUndefined();
    });

    it('dispatch with no detail sends undefined as detail argument', async () => {
      const fn = vi.fn();
      eventDispatcher.on('no-detail', fn);
      await eventDispatcher.dispatch('no-detail');
      // CustomEvent({ detail: undefined }) results in detail being null per spec
      expect(fn.mock.calls[0]![0].detail).toBeNull();
      eventDispatcher.off('no-detail', fn);
    });

    it('does not call listeners added during an in-progress dispatch (snapshot semantics)', async () => {
      // Regression: a listener that (re)subscribes another listener mid-dispatch
      // must not have that new listener fire for the *current* event. The live
      // Set used to be iterated directly, so a handler added during an awaited
      // listener got invoked in the same dispatch — causing paragraph mode to
      // toggle twice per keypress (#4717).
      const added = vi.fn();
      const adder = vi.fn(async () => {
        await Promise.resolve();
        eventDispatcher.on('reentrant', added);
      });
      eventDispatcher.on('reentrant', adder);

      await eventDispatcher.dispatch('reentrant');
      expect(adder).toHaveBeenCalledOnce();
      expect(added).not.toHaveBeenCalled();

      // The newly-added listener takes effect on the next dispatch.
      await eventDispatcher.dispatch('reentrant');
      expect(added).toHaveBeenCalledOnce();

      eventDispatcher.off('reentrant', adder);
      eventDispatcher.off('reentrant', added);
    });

    it('awaits async listeners sequentially', async () => {
      const order: string[] = [];
      const slowFn = vi.fn(async () => {
        await new Promise((r) => setTimeout(r, 10));
        order.push('slow');
      });
      const fastFn = vi.fn(() => {
        order.push('fast');
      });
      eventDispatcher.on('seq', slowFn);
      eventDispatcher.on('seq', fastFn);
      await eventDispatcher.dispatch('seq');
      // slow should complete before fast starts because dispatch awaits each
      expect(order).toEqual(['slow', 'fast']);
      eventDispatcher.off('seq', slowFn);
      eventDispatcher.off('seq', fastFn);
    });
  });

  // -----------------------------------------------------------------------
  // Sync listeners (onSync / offSync / dispatchSync)
  // -----------------------------------------------------------------------
  describe('sync listeners', () => {
    it('registers and dispatches a sync listener', () => {
      const fn = vi.fn((_event: CustomEvent) => false);
      eventDispatcher.onSync('sync-test', fn);
      eventDispatcher.dispatchSync('sync-test', 42);
      expect(fn).toHaveBeenCalledOnce();
      expect(fn.mock.calls[0]![0].detail).toBe(42);
      eventDispatcher.offSync('sync-test', fn);
    });

    it('returns true when a listener consumes the event', () => {
      const consumer = vi.fn(() => true);
      eventDispatcher.onSync('consume', consumer);
      const result = eventDispatcher.dispatchSync('consume');
      expect(result).toBe(true);
      eventDispatcher.offSync('consume', consumer);
    });

    it('returns false when no listener consumes the event', () => {
      const ignorer = vi.fn(() => false);
      eventDispatcher.onSync('ignore', ignorer);
      const result = eventDispatcher.dispatchSync('ignore');
      expect(result).toBe(false);
      eventDispatcher.offSync('ignore', ignorer);
    });

    it('stops at the first consumer (reverse order)', () => {
      const fn1 = vi.fn(() => true);
      const fn2 = vi.fn(() => false);
      // fn1 is registered first, fn2 second. Dispatch iterates in reverse.
      eventDispatcher.onSync('stop-early', fn1);
      eventDispatcher.onSync('stop-early', fn2);

      const result = eventDispatcher.dispatchSync('stop-early');

      // fn2 (last registered) is called first in reverse, returns false
      // then fn1 is called, returns true (consumed)
      expect(fn2).toHaveBeenCalledOnce();
      expect(fn1).toHaveBeenCalledOnce();
      expect(result).toBe(true);

      eventDispatcher.offSync('stop-early', fn1);
      eventDispatcher.offSync('stop-early', fn2);
    });

    it('stops at consumer and does not call earlier listeners', () => {
      const earlyFn = vi.fn(() => true);
      const lateFn = vi.fn(() => true);
      eventDispatcher.onSync('stop', earlyFn);
      eventDispatcher.onSync('stop', lateFn);

      eventDispatcher.dispatchSync('stop');

      // lateFn (last registered) is called first in reverse, returns true -> stop
      expect(lateFn).toHaveBeenCalledOnce();
      expect(earlyFn).not.toHaveBeenCalled();

      eventDispatcher.offSync('stop', earlyFn);
      eventDispatcher.offSync('stop', lateFn);
    });

    it('removes a sync listener with offSync()', () => {
      const fn = vi.fn(() => false);
      eventDispatcher.onSync('off-sync', fn);
      eventDispatcher.offSync('off-sync', fn);
      eventDispatcher.dispatchSync('off-sync');
      expect(fn).not.toHaveBeenCalled();
    });

    it('offSync on non-existent event does not throw', () => {
      const fn = vi.fn(() => false);
      expect(() => eventDispatcher.offSync('nope', fn)).not.toThrow();
    });

    it('dispatchSync on event with no listeners returns false', () => {
      expect(eventDispatcher.dispatchSync('nobody-listens')).toBe(false);
    });
  });

  // -----------------------------------------------------------------------
  // Isolation between event names
  // -----------------------------------------------------------------------
  describe('event isolation', () => {
    it('does not cross-fire between different event names', async () => {
      const fn1 = vi.fn();
      const fn2 = vi.fn();
      eventDispatcher.on('event-a', fn1);
      eventDispatcher.on('event-b', fn2);
      await eventDispatcher.dispatch('event-a');
      expect(fn1).toHaveBeenCalledOnce();
      expect(fn2).not.toHaveBeenCalled();
      eventDispatcher.off('event-a', fn1);
      eventDispatcher.off('event-b', fn2);
    });

    it('sync and async listeners with same event name are independent', async () => {
      const asyncFn = vi.fn();
      const syncFn = vi.fn(() => false);
      eventDispatcher.on('shared-name', asyncFn);
      eventDispatcher.onSync('shared-name', syncFn);

      await eventDispatcher.dispatch('shared-name');
      expect(asyncFn).toHaveBeenCalledOnce();
      expect(syncFn).not.toHaveBeenCalled();

      eventDispatcher.dispatchSync('shared-name');
      expect(syncFn).toHaveBeenCalledOnce();
      expect(asyncFn).toHaveBeenCalledOnce(); // still only once

      eventDispatcher.off('shared-name', asyncFn);
      eventDispatcher.offSync('shared-name', syncFn);
    });
  });
});

describe('settled events (markSettled / onSettled)', () => {
  afterEach(() => {
    __resetSettledEventsForTests();
  });

  it('fires listeners that subscribed BEFORE settling', async () => {
    const fn = vi.fn();
    onSettled('boot', fn);
    expect(fn).not.toHaveBeenCalled();
    await markSettled('boot', { ready: true });
    expect(fn).toHaveBeenCalledOnce();
    expect(fn).toHaveBeenCalledWith({ ready: true });
  });

  it('replays synchronously for listeners that subscribe AFTER settling', async () => {
    await markSettled('boot', { ready: true });
    const fn = vi.fn();
    onSettled('boot', fn);
    expect(fn).toHaveBeenCalledOnce();
    expect(fn).toHaveBeenCalledWith({ ready: true });
  });

  it('fires multiple listeners; each fires exactly once', async () => {
    const a = vi.fn();
    const b = vi.fn();
    onSettled('boot', a);
    onSettled('boot', b);
    await markSettled('boot');
    expect(a).toHaveBeenCalledOnce();
    expect(b).toHaveBeenCalledOnce();
    // Idempotent: a second markSettled is a no-op.
    await markSettled('boot');
    expect(a).toHaveBeenCalledOnce();
    expect(b).toHaveBeenCalledOnce();
  });

  it('auto-unsubscribes after fire — repeated dispatches do NOT re-fire', async () => {
    const fn = vi.fn();
    onSettled('boot', fn);
    await markSettled('boot');
    expect(fn).toHaveBeenCalledOnce();
    // The bare eventDispatcher would normally fire again on re-dispatch;
    // settled-event listeners must not.
    await eventDispatcher.dispatch('boot', { stale: true });
    expect(fn).toHaveBeenCalledOnce();
  });

  it('returned unsubscribe cancels a pre-settle subscription', async () => {
    const fn = vi.fn();
    const unsub = onSettled('boot', fn);
    unsub();
    await markSettled('boot');
    expect(fn).not.toHaveBeenCalled();
  });

  it('returned unsubscribe is a no-op for late (already-replayed) subscribers', async () => {
    await markSettled('boot');
    const fn = vi.fn();
    const unsub = onSettled('boot', fn);
    expect(fn).toHaveBeenCalledOnce();
    expect(() => unsub()).not.toThrow();
  });

  it('preserves typed payload through onSettled<T>', async () => {
    interface BootPayload {
      version: number;
    }
    let captured: BootPayload | null = null;
    onSettled<BootPayload>('typed-boot', (p) => {
      captured = p;
    });
    await markSettled('typed-boot', { version: 7 });
    expect(captured).toEqual({ version: 7 });
  });

  it('different event names are independent', async () => {
    const a = vi.fn();
    const b = vi.fn();
    onSettled('a', a);
    onSettled('b', b);
    await markSettled('a');
    expect(a).toHaveBeenCalledOnce();
    expect(b).not.toHaveBeenCalled();
  });
});
