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

const { ioMock } = vi.hoisted(() => ({
  ioMock: vi.fn(),
}));

vi.mock('socket.io-client', () => ({
  io: ioMock,
}));

import { destroySocket, getSocket } from './socket';

interface MockChatSocket {
  on: ReturnType<typeof vi.fn>;
  offAny: ReturnType<typeof vi.fn>;
  disconnect: ReturnType<typeof vi.fn>;
  /** Test-only helper: fires every handler registered for `event` via
   * `.on`, mirroring how a real socket.io-client instance invokes its own
   * listeners (e.g. calling the registered `disconnect` handler(s) on a
   * real transient disconnect). */
  trigger(event: string): void;
}

function createMockSocket(): MockChatSocket {
  const handlers = new Map<string, Set<() => void>>();
  const mockSocket: MockChatSocket = {
    on: vi.fn((event: string, handler: () => void) => {
      if (!handlers.has(event)) handlers.set(event, new Set());
      handlers.get(event)?.add(handler);
      return mockSocket;
    }),
    offAny: vi.fn(() => mockSocket),
    disconnect: vi.fn(() => mockSocket),
    trigger(event: string): void {
      for (const handler of handlers.get(event) ?? []) handler();
    },
  };
  return mockSocket;
}

let currentMock!: MockChatSocket;

describe('chat socket', () => {
  beforeEach(() => {
    ioMock.mockReset();
    // A fresh object per io() call so identity assertions (same singleton vs.
    // a genuinely new instance) are meaningful.
    ioMock.mockImplementation(() => {
      currentMock = createMockSocket();
      return currentMock;
    });
  });

  afterEach(() => {
    destroySocket();
  });

  it('creates one same-origin /chat namespace socket', () => {
    const first = getSocket();
    const second = getSocket();

    expect(first).toBe(second);
    expect(ioMock).toHaveBeenCalledOnce();
    expect(ioMock).toHaveBeenCalledWith('/chat', {
      withCredentials: true,
      autoConnect: false,
      transports: ['websocket', 'polling'],
    });
  });

  it('keeps the same singleton instance across a transient disconnect', () => {
    const first = getSocket();

    // socket.ts must not react to a real socket's `disconnect` event by
    // nulling the singleton — it registers no such handler at all now.
    // Actually fire every handler registered via `.on('disconnect', ...)`
    // (mirroring a real socket.io-client reconnect) instead of merely
    // calling getSocket() again: this is what makes the test fail if
    // production reintroduces `socket.on('disconnect', () => { socket =
    // null; })`, since that handler would run here and null the singleton
    // before the next getSocket() call.
    currentMock.trigger('disconnect');
    const second = getSocket();

    expect(second).toBe(first);
    expect(ioMock).toHaveBeenCalledOnce();
  });

  it('only creates a new singleton after an explicit destroySocket()', () => {
    const first = getSocket();

    destroySocket();
    const second = getSocket();

    expect(second).not.toBe(first);
    expect(ioMock).toHaveBeenCalledTimes(2);
  });
});
