import { describe, it, expect, expectTypeOf, vi } from 'vitest';
import { createIpcListener } from './createIpcListener';
import { IPC } from './ipc-channels';
import type { IpcChannel } from './ipc-channels';
import type { IpcRenderer, IpcRendererEvent } from 'electron';

function makeMockIpcRenderer() {
  const listeners = new Map<string, ((...args: unknown[]) => unknown)[]>();
  return {
    on: vi.fn((channel: string, handler: (...args: unknown[]) => unknown) => {
      if (!listeners.has(channel)) listeners.set(channel, []);
      const list = listeners.get(channel);
      if (!list) throw new Error('expected listener list');
      list.push(handler);
    }),
    removeListener: vi.fn((channel: string, handler: (...args: unknown[]) => unknown) => {
      const arr = listeners.get(channel);
      if (arr) {
        const idx = arr.indexOf(handler);
        if (idx >= 0) arr.splice(idx, 1);
      }
    }),
    // Helper to simulate an event
    _emit(channel: string, ...args: unknown[]) {
      const fakeEvent = {} as IpcRendererEvent;
      for (const fn of listeners.get(channel) || []) {
        fn(fakeEvent, ...args);
      }
    },
  } as unknown as IpcRenderer & { _emit: (channel: string, ...args: unknown[]) => void };
}

describe('createIpcListener', () => {
  it('registers a listener on the given channel', () => {
    const ipc = makeMockIpcRenderer();
    const callback = vi.fn();

    createIpcListener(ipc, IPC.CHAT.EVENT, callback);

    expect(ipc.on).toHaveBeenCalledWith('chat:event', expect.any(Function));
  });

  it('forwards IPC events to the callback without the event object', () => {
    const ipc = makeMockIpcRenderer();
    const callback = vi.fn();

    createIpcListener(ipc, IPC.CHAT.EVENT, callback);
    ipc._emit('chat:event', 'msg-1', { type: 'chunk', content: 'hi' });

    expect(callback).toHaveBeenCalledWith('msg-1', { type: 'chunk', content: 'hi' });
  });

  it('returns an unsubscribe function that removes the listener', () => {
    const ipc = makeMockIpcRenderer();
    const callback = vi.fn();

    const unsub = createIpcListener(ipc, IPC.MIND.CHANGED, callback);
    unsub();

    expect(ipc.removeListener).toHaveBeenCalledWith('mind:changed', expect.any(Function));
  });

  it('stops receiving events after unsubscribe', () => {
    const ipc = makeMockIpcRenderer();
    const callback = vi.fn();

    const unsub = createIpcListener(ipc, IPC.LENS.VIEWS_CHANGED, callback);
    ipc._emit('lens:viewsChanged', 'first');
    expect(callback).toHaveBeenCalledTimes(1);

    unsub();
    ipc._emit('lens:viewsChanged', 'second');
    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('only accepts known IpcChannel values at the type level', () => {
    // Every IpcChannel literal is assignable to the channel parameter.
    expectTypeOf(createIpcListener<[unknown]>).parameter(1).toEqualTypeOf<IpcChannel>();

    const ipc = makeMockIpcRenderer();
    const callback = vi.fn();

    // Real IPC.* values type-check.
    createIpcListener(ipc, IPC.CHAT.EVENT, callback);

    // An arbitrary string literal must not type-check — the constants are now a
    // contract, not decoration.
    // @ts-expect-error: 'not:a:real:channel' is not assignable to IpcChannel
    createIpcListener(ipc, 'not:a:real:channel', callback);

    // A free-typed string also must not type-check.
    const looseChannel: string = 'chat:event';
    // @ts-expect-error: a `string` is not assignable to the IpcChannel literal union
    createIpcListener(ipc, looseChannel, callback);
  });
});

