import cluster from 'cluster';
import EventEmitter from 'events';
import { cpus } from 'os';
import { ClusterManager } from '../../../../src';
import * as LogUtil from '../../../../src/logging/LogUtil';

jest.mock('cluster');
jest.mock('os', (): any => ({
  ...jest.requireActual('os'),
  cpus: jest.fn().mockImplementation((): any => [{}, {}, {}, {}, {}, {}]),
}));

const mockWorker = new EventEmitter() as any;
mockWorker.process = { pid: 666 };

describe('A ClusterManager', (): void => {
  const emitter = new EventEmitter();
  const mockCluster = jest.requireMock('cluster');
  const mockLogger = { info: jest.fn(), warn: jest.fn() };
  jest.spyOn(LogUtil, 'getLoggerFor').mockImplementation((): any => mockLogger);

  beforeAll((): void => {
    Object.assign(mockCluster, {
      fork: jest.fn().mockImplementation((): any => mockWorker),
      on: jest.fn().mockImplementation(emitter.on),
      emit: jest.fn().mockImplementation(emitter.emit),
      isMaster: true,
      isWorker: false,
    });
  });

  it('can handle workers input as string.', (): void => {
    const cm = new ClusterManager(4);
    expect(cm.isSingleThreaded()).toBeFalsy();
  });

  it('can distinguish between ClusterModes.', (): void => {
    const cm1 = new ClusterManager(-1);
    const cm2 = new ClusterManager(0);
    const cm3 = new ClusterManager(1);
    const cm4 = new ClusterManager(2);
    expect(cm1.isSingleThreaded()).toBeFalsy();
    expect(cm2.isSingleThreaded()).toBeFalsy();
    expect(cm3.isSingleThreaded()).toBeTruthy();
    expect(cm4.isSingleThreaded()).toBeFalsy();
  });

  it('errors on invalid workers amount.', (): void => {
    expect((): ClusterManager => new ClusterManager(10)).toBeDefined();
    expect((): ClusterManager => new ClusterManager(2)).toBeDefined();
    expect((): ClusterManager => new ClusterManager(1)).toBeDefined();
    expect((): ClusterManager => new ClusterManager(0)).toBeDefined();
    expect((): ClusterManager => new ClusterManager(-1)).toBeDefined();
    expect((): ClusterManager => new ClusterManager(-5)).toBeDefined();
    expect((): ClusterManager => new ClusterManager(-6)).toThrow('Invalid workers value');
    expect((): ClusterManager => new ClusterManager(-10)).toThrow('Invalid workers value');
  });

  it('has an isPrimary() that works.', (): void => {
    const cm = new ClusterManager(-1);
    expect(cm.isPrimary()).toBeTruthy();
  });

  it('has an isWorker() that works.', (): void => {
    const cm = new ClusterManager(-1);
    expect(cm.isWorker()).toBeFalsy();
  });

  it('can autoscale to num_cpu and applies proper logging.', (): void => {
    const cm = new ClusterManager(-1);
    const workers = cpus().length - 1;
    expect(cpus()).toHaveLength(workers + 1);
    Object.assign(cm, { logger: mockLogger });

    cm.spawnWorkers();

    expect(mockLogger.info).toHaveBeenCalledWith(`Setting up ${workers} workers`);

    for (let i = 0; i < workers; i++) {
      mockCluster.emit('online', mockWorker);
    }

    expect(cluster.on).toHaveBeenCalledWith('online', expect.any(Function));
    expect(cluster.fork).toHaveBeenCalledTimes(workers);
    expect(mockLogger.info).toHaveBeenLastCalledWith(`All ${workers} requested workers have been started.`);

    expect(cluster.on).toHaveBeenCalledWith('exit', expect.any(Function));
    const code = 333;
    const signal = 'exiting';
    mockCluster.emit('exit', mockWorker, code, signal);
    expect(mockLogger.warn).toHaveBeenCalledWith(
      `Worker ${mockWorker.process.pid} died with code ${code} and signal ${signal}`,
    );
    expect(mockLogger.warn).toHaveBeenCalledWith(`Starting a new worker`);
  });

  it('can receive message from spawned workers.', (): void => {
    const cm = new ClusterManager(2);
    Object.assign(cm, { logger: mockLogger });

    cm.spawnWorkers();
    const msg = 'Hi from worker!';
    mockWorker.emit('message', msg);
    expect(mockLogger.info).toHaveBeenCalledWith(msg);
  });
});
