/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

describe('events', () => {
  let dispatcher;

  beforeEach(() => {
    const EventEmitter = require('../events').default;

    dispatcher = new EventEmitter();
  });

  it('can dispatch an event with no listeners', () => {
    dispatcher.emit('event', 123);
  });

  it('handles a listener being attached multiple times', () => {
    const callback = jest.fn();

    dispatcher.addListener('event', callback);
    dispatcher.addListener('event', callback);

    dispatcher.emit('event', 123);
    expect(callback).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenCalledWith(123);
  });

  it('notifies all attached listeners of events', () => {
    const callback1 = jest.fn();
    const callback2 = jest.fn();
    const callback3 = jest.fn();

    dispatcher.addListener('event', callback1);
    dispatcher.addListener('event', callback2);
    dispatcher.addListener('other-event', callback3);
    dispatcher.emit('event', 123);

    expect(callback1).toHaveBeenCalledTimes(1);
    expect(callback1).toHaveBeenCalledWith(123);
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback2).toHaveBeenCalledWith(123);
    expect(callback3).not.toHaveBeenCalled();
  });

  it('calls later listeners before re-throwing if an earlier one throws', () => {
    const callbackThatThrows = jest.fn(() => {
      throw Error('expected');
    });
    const callback = jest.fn();

    dispatcher.addListener('event', callbackThatThrows);
    dispatcher.addListener('event', callback);

    expect(() => {
      dispatcher.emit('event', 123);
    }).toThrow('expected');

    expect(callbackThatThrows).toHaveBeenCalledTimes(1);
    expect(callbackThatThrows).toHaveBeenCalledWith(123);
    expect(callback).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenCalledWith(123);
  });

  it('preserves the first thrown value and reports later errors', () => {
    const laterError = new Error('later error');
    const errorHandler = jest.fn(event => {
      event.preventDefault();
    });
    const firstCallback = jest.fn(() => {
      // This verifies that the emitter preserves any legal thrown value.
      // eslint-disable-next-line no-throw-literal
      throw null;
    });
    const secondCallback = jest.fn(() => {
      throw laterError;
    });
    const thirdCallback = jest.fn();

    dispatcher.addListener('event', firstCallback);
    dispatcher.addListener('event', secondCallback);
    dispatcher.addListener('event', thirdCallback);

    let caughtValue = undefined;
    window.addEventListener('error', errorHandler);
    try {
      dispatcher.emit('event', 123);
    } catch (error) {
      caughtValue = error;
    } finally {
      window.removeEventListener('error', errorHandler);
    }

    expect(caughtValue).toBe(null);
    expect(errorHandler).toHaveBeenCalledTimes(1);
    expect(errorHandler.mock.calls[0][0]).toEqual(
      expect.objectContaining({
        error: laterError,
        message: 'later error',
      }),
    );
    expect(thirdCallback).toHaveBeenCalledWith(123);
  });

  it('removes attached listeners', () => {
    const callback1 = jest.fn();
    const callback2 = jest.fn();

    dispatcher.addListener('event', callback1);
    dispatcher.addListener('other-event', callback2);
    dispatcher.removeListener('event', callback1);
    dispatcher.emit('event', 123);
    expect(callback1).not.toHaveBeenCalled();
    dispatcher.emit('other-event', 123);
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback2).toHaveBeenCalledWith(123);
  });

  it('removes all listeners', () => {
    const callback1 = jest.fn();
    const callback2 = jest.fn();
    const callback3 = jest.fn();

    dispatcher.addListener('event', callback1);
    dispatcher.addListener('event', callback2);
    dispatcher.addListener('other-event', callback3);
    dispatcher.removeAllListeners();
    dispatcher.emit('event', 123);
    dispatcher.emit('other-event', 123);

    expect(callback1).not.toHaveBeenCalled();
    expect(callback2).not.toHaveBeenCalled();
    expect(callback3).not.toHaveBeenCalled();
  });

  it('should call the initial listeners even if others are added or removed during a dispatch', () => {
    const callback1 = jest.fn(() => {
      dispatcher.removeListener('event', callback2);
      dispatcher.addListener('event', callback3);
    });
    const callback2 = jest.fn();
    const callback3 = jest.fn();

    dispatcher.addListener('event', callback1);
    dispatcher.addListener('event', callback2);

    dispatcher.emit('event', 123);
    expect(callback1).toHaveBeenCalledTimes(1);
    expect(callback1).toHaveBeenCalledWith(123);
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback2).toHaveBeenCalledWith(123);
    expect(callback3).not.toHaveBeenCalled();

    dispatcher.emit('event', 456);
    expect(callback1).toHaveBeenCalledTimes(2);
    expect(callback1).toHaveBeenCalledWith(456);
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback3).toHaveBeenCalledTimes(1);
    expect(callback3).toHaveBeenCalledWith(456);
  });
});