import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ChannelRegistry } from './registry.js';
import type { ChannelAdapter, InboundMessage, OutboundMessage } from './types.js';

/** Create a mock adapter with spy functions and a triggerMessage helper. */
function createMockAdapter(name: string): ChannelAdapter & {
  connectFn: ReturnType<typeof vi.fn>;
  disconnectFn: ReturnType<typeof vi.fn>;
  sendFn: ReturnType<typeof vi.fn>;
  triggerMessage: (msg: InboundMessage) => void;
} {
  let messageHandler: ((msg: InboundMessage) => void) | undefined;
  let _status: 'disconnected' | 'connecting' | 'connected' | 'error' = 'disconnected';

  const connectFn = vi.fn(async () => { _status = 'connected'; });
  const disconnectFn = vi.fn(async () => { _status = 'disconnected'; });
  const sendFn = vi.fn(async () => {});

  return {
    name,
    get status() { return _status; },
    connect: connectFn,
    disconnect: disconnectFn,
    send: sendFn,
    onMessage: (handler: (msg: InboundMessage) => void) => { messageHandler = handler; },
    triggerMessage: (msg: InboundMessage) => { messageHandler?.(msg); },
    connectFn,
    disconnectFn,
    sendFn,
  };
}

/** Create a sample inbound message for a given channel. */
function makeMessage(channel: string): InboundMessage {
  return {
    id: 'msg-1',
    channel,
    senderId: 'user-42',
    senderName: 'Alice',
    text: 'Hello',
    timestamp: Date.now(),
  };
}

describe('ChannelRegistry', () => {
  let registry: ChannelRegistry;

  beforeEach(() => {
    registry = new ChannelRegistry();
  });

  it('registers and lists adapters', () => {
    const a1 = createMockAdapter('alpha');
    const a2 = createMockAdapter('beta');

    registry.register(a1);
    registry.register(a2);

    const listed = registry.list();
    expect(listed).toHaveLength(2);
    expect(listed.map((a) => a.name)).toContain('alpha');
    expect(listed.map((a) => a.name)).toContain('beta');
  });

  it('throws on duplicate registration', () => {
    const a1 = createMockAdapter('dup');
    registry.register(a1);

    const a2 = createMockAdapter('dup');
    expect(() => registry.register(a2)).toThrow('already registered');
  });

  it('gets adapter by name', () => {
    const adapter = createMockAdapter('test');
    registry.register(adapter);

    expect(registry.get('test')).toBe(adapter);
    expect(registry.get('unknown')).toBeUndefined();
  });

  it('starts all adapters', async () => {
    const a1 = createMockAdapter('one');
    const a2 = createMockAdapter('two');

    registry.register(a1);
    registry.register(a2);

    await registry.startAll();

    expect(a1.connectFn).toHaveBeenCalledOnce();
    expect(a2.connectFn).toHaveBeenCalledOnce();
  });

  it('stops all adapters', async () => {
    const a1 = createMockAdapter('one');
    const a2 = createMockAdapter('two');

    registry.register(a1);
    registry.register(a2);

    // Connect first so they are in connected state
    await a1.connect();
    await a2.connect();

    await registry.stopAll();

    expect(a1.disconnectFn).toHaveBeenCalled();
    expect(a2.disconnectFn).toHaveBeenCalled();
  });

  it('routes inbound messages to handler', async () => {
    const adapter = createMockAdapter('test-channel');
    registry.register(adapter);

    const handler = vi.fn(async (_msg: InboundMessage, reply: (r: OutboundMessage) => Promise<void>) => {
      await reply({ text: 'pong' });
    });
    registry.setMessageHandler(handler);

    const msg = makeMessage('test-channel');
    adapter.triggerMessage(msg);

    // Allow the async handler to settle
    await vi.waitFor(() => {
      expect(handler).toHaveBeenCalledOnce();
    });

    // Handler receives the original inbound message
    expect(handler.mock.calls[0][0]).toBe(msg);

    // The reply function should have called adapter.send with the sender's peerId
    expect(adapter.sendFn).toHaveBeenCalledWith('user-42', { text: 'pong' });
  });

  it('unregisters adapter', () => {
    const adapter = createMockAdapter('removeme');
    registry.register(adapter);

    registry.unregister('removeme');

    expect(registry.list()).toHaveLength(0);
    expect(registry.get('removeme')).toBeUndefined();
  });

  it('unregister disconnects connected adapter', async () => {
    const adapter = createMockAdapter('connected-one');
    registry.register(adapter);

    await adapter.connect();
    expect(adapter.status).toBe('connected');

    await registry.unregister('connected-one');

    expect(adapter.disconnectFn).toHaveBeenCalled();
  });

  it('logs warning when no message handler set', () => {
    const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

    const adapter = createMockAdapter('no-handler');
    registry.register(adapter);

    // Trigger a message WITHOUT calling setMessageHandler
    adapter.triggerMessage(makeMessage('no-handler'));

    expect(warnSpy).toHaveBeenCalledWith(
      expect.stringContaining('No message handler set'),
    );

    warnSpy.mockRestore();
  });

  it('handles errors in message handler gracefully', async () => {
    const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    const adapter = createMockAdapter('err-channel');
    registry.register(adapter);

    registry.setMessageHandler(async () => {
      throw new Error('handler exploded');
    });

    // Trigger a message — should not throw
    adapter.triggerMessage(makeMessage('err-channel'));

    // Allow the async error path to settle
    await vi.waitFor(() => {
      expect(errorSpy).toHaveBeenCalledWith(
        expect.stringContaining('Error handling message'),
        expect.any(Error),
      );
    });

    errorSpy.mockRestore();
  });
});
