import { Channel, ChannelTransport, Listener } from '.';

jest.useFakeTimers();

describe('Channel', () => {
  let transport: ChannelTransport;
  let channel: Channel;

  beforeEach(() => {
    transport = { setHandler: jest.fn(), send: jest.fn() };
    channel = new Channel({ transport });
  });

  describe('constructor', () => {
    it('should set the handler if handler is preset', () => {
      channel = new Channel({ transport });
      expect(transport.setHandler).toHaveBeenCalled();
    });

    it('should not set transport if not passed as an argument', () => {
      channel = new Channel();
      expect(channel.hasTransport).toBeFalsy();
    });

    it('should set transport if passed as an argument', () => {
      channel = new Channel({ transport });
      expect(channel.hasTransport).toBeTruthy();
    });

    it('should set isAsync to false as default value', () => {
      channel = new Channel();
      expect(channel.isAsync).toBeFalsy();
    });

    it('should set isAsync to true if passed as an argument', () => {
      channel = new Channel({ async: true });
      expect(channel.isAsync).toBeTruthy();
    });
  });

  describe('method:addListener', () => {
    it('should create one listener', () => {
      const eventName = 'event1';

      channel.addListener(eventName, jest.fn());
      expect(channel.listeners(eventName).length).toBe(1);
    });
  });

  describe('method:on', () => {
    it('should do the same as addListener', () => {
      const eventName = 'event1';

      channel.on(eventName, jest.fn());
      expect(channel.listeners(eventName).length).toBe(1);
    });
  });

  describe('method:off', () => {
    it('should remove listeners', () => {
      const eventName = 'event1';
      const fn = jest.fn();

      channel.on(eventName, fn);
      expect(channel.listeners(eventName).length).toBe(1);
      channel.off(eventName, fn);
      expect(channel.listeners(eventName).length).toBe(0);
    });
  });

  describe('method:emit', () => {
    it('should execute the callback fn of a listener', () => {
      const eventName = 'event1';
      const listenerInputData = ['string1', 'string2', 'string3'];
      let listenerOutputData: string[] = null;
      const mockListener: Listener = (data) => {
        listenerOutputData = data;
      };

      channel.addListener(eventName, mockListener);
      channel.emit(eventName, listenerInputData);
      expect(listenerOutputData).toBe(listenerInputData);
    });

    it('should be callable with a spread operator as event arguments', () => {
      const eventName = 'event1';
      const listenerInputData = ['string1', 'string2', 'string3'];
      let listenerOutputData: string[] = null;

      channel.addListener(eventName, (...data) => {
        listenerOutputData = data;
      });
      channel.emit(eventName, ...listenerInputData);
      expect(listenerOutputData).toEqual(listenerInputData);
    });

    it('should be callable with options on the event', () => {
      const eventName = 'event1';
      const listenerInputData = [{ event: {}, options: { depth: 1 } }];
      let listenerOutputData: any = null;

      channel.addListener(eventName, (...data) => {
        listenerOutputData = data;
      });
      const sendSpy = jest.fn();
      // @ts-expect-error (Converted from ts-ignore)
      channel.transport.send = sendSpy;
      channel.emit(eventName, ...listenerInputData);
      expect(listenerOutputData).toEqual(listenerInputData);
      expect(sendSpy.mock.calls[0][1]).toEqual({ depth: 1 });
    });

    it('should use setImmediate if async is true', () => {
      channel = new Channel({ async: true, transport });
      channel.addListener('event1', jest.fn());
    });
  });

  describe('method:eventNames', () => {
    it('should return a list of all registered events', () => {
      const eventNames = ['event1', 'event2', 'event3'];
      eventNames.forEach((eventName) => channel.addListener(eventName, jest.fn()));

      expect(channel.eventNames()).toEqual(eventNames);
    });
  });

  describe('method:listenerCount', () => {
    it('should return a list of all registered events', () => {
      const events = [
        { eventName: 'event1', listeners: [jest.fn(), jest.fn(), jest.fn()], listenerCount: 0 },
        { eventName: 'event2', listeners: [jest.fn()], listenerCount: 0 },
      ];
      events.forEach((event) => {
        event.listeners.forEach((listener) => {
          channel.addListener(event.eventName, listener);
          // eslint-disable-next-line no-plusplus, no-param-reassign
          event.listenerCount++;
        });
      });

      events.forEach((event) => {
        expect(channel.listenerCount(event.eventName)).toBe(event.listenerCount);
      });
    });
  });

  describe('method:once', () => {
    it('should execute a listener once and remove it afterwards', () => {
      const eventName = 'event1';
      channel.once(eventName, jest.fn());
      channel.emit(eventName);

      expect(channel.listenerCount(eventName)).toBe(0);
    });

    it('should pass all event arguments correctly to the listener', () => {
      const eventName = 'event1';
      const listenerInputData = ['string1', 'string2', 'string3'];
      let listenerOutputData = null;
      const mockListener: Listener = (data: string[]) => {
        listenerOutputData = data;
      };

      channel.once(eventName, (args) => mockListener(args));
      channel.emit(eventName, listenerInputData);

      expect(listenerOutputData).toEqual(listenerInputData);
    });

    it('should be removable', () => {
      const eventName = 'event1';
      const listenerToBeRemoved = jest.fn();

      channel.once(eventName, listenerToBeRemoved);
      channel.removeListener(eventName, listenerToBeRemoved);
    });
  });

  describe('method:removeAllListeners', () => {
    it('should remove all listeners', () => {
      const eventName1 = 'event1';
      const eventName2 = 'event2';
      const listeners1 = [jest.fn(), jest.fn(), jest.fn()];
      const listeners2 = [jest.fn()];

      listeners1.forEach((fn) => channel.addListener(eventName1, fn));
      listeners2.forEach((fn) => channel.addListener(eventName2, fn));
      channel.removeAllListeners();

      expect(channel.listenerCount(eventName1)).toBe(0);
      expect(channel.listenerCount(eventName2)).toBe(0);
    });

    it('should remove all listeners of a certain event', () => {
      const eventName = 'event1';
      const listeners = [jest.fn(), jest.fn(), jest.fn()];

      listeners.forEach((fn) => channel.addListener(eventName, fn));
      expect(channel.listenerCount(eventName)).toBe(listeners.length);

      channel.removeAllListeners(eventName);
      expect(channel.listenerCount(eventName)).toBe(0);
    });
  });

  describe('method:removeListener', () => {
    it('should remove one listener', () => {
      const eventName = 'event1';
      const listenerToBeRemoved = jest.fn();
      const listeners = [jest.fn(), jest.fn()];
      const findListener = (listener: Listener) =>
        channel.listeners(eventName).find((_listener) => _listener === listener);

      listeners.forEach((fn) => channel.addListener(eventName, fn));
      channel.addListener(eventName, listenerToBeRemoved);
      expect(findListener(listenerToBeRemoved)).toBe(listenerToBeRemoved);

      channel.removeListener(eventName, listenerToBeRemoved);
      expect(findListener(listenerToBeRemoved)).toBeUndefined();
    });
  });
});
