/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @emails oncall+react_native
 */

'use strict';

const Promise = require('promise');

function expectToBeCalledOnce(fn) {
  expect(fn.mock.calls.length).toBe(1);
}

function clearTaskQueue(taskQueue) {
  do {
    jest.runAllTimers();
    taskQueue.processNext();
    jest.runAllTimers();
  } while (taskQueue.hasTasksToProcess());
}

describe('TaskQueue', () => {
  let taskQueue;
  let onMoreTasks;
  let sequenceId;
  function createSequenceTask(expectedSequenceId) {
    return jest.fn(() => {
      expect(++sequenceId).toBe(expectedSequenceId);
    });
  }
  beforeEach(() => {
    jest.resetModules();
    onMoreTasks = jest.fn();
    const TaskQueue = require('../TaskQueue');
    taskQueue = new TaskQueue({onMoreTasks});
    sequenceId = 0;
  });

  it('should run a basic task', () => {
    const task1 = createSequenceTask(1);
    taskQueue.enqueue({run: task1, name: 'run1'});
    expect(taskQueue.hasTasksToProcess()).toBe(true);
    taskQueue.processNext();
    expectToBeCalledOnce(task1);
  });

  it('should handle blocking promise task', () => {
    const task1 = jest.fn(() => {
      return new Promise(resolve => {
        setTimeout(() => {
          expect(++sequenceId).toBe(1);
          resolve();
        }, 1);
      });
    });
    const task2 = createSequenceTask(2);
    taskQueue.enqueue({gen: task1, name: 'gen1'});
    taskQueue.enqueue({run: task2, name: 'run2'});

    taskQueue.processNext();

    expectToBeCalledOnce(task1);
    expect(task2).not.toBeCalled();
    expect(onMoreTasks).not.toBeCalled();
    expect(taskQueue.hasTasksToProcess()).toBe(false);

    clearTaskQueue(taskQueue);

    expectToBeCalledOnce(onMoreTasks);
    expectToBeCalledOnce(task2);
  });

  it('should handle nested simple tasks', () => {
    const task1 = jest.fn(() => {
      expect(++sequenceId).toBe(1);
      taskQueue.enqueue({run: task3, name: 'run3'});
    });
    const task2 = createSequenceTask(2);
    const task3 = createSequenceTask(3);
    taskQueue.enqueue({run: task1, name: 'run1'});
    taskQueue.enqueue({run: task2, name: 'run2'}); // not blocked by task 1

    clearTaskQueue(taskQueue);

    expectToBeCalledOnce(task1);
    expectToBeCalledOnce(task2);
    expectToBeCalledOnce(task3);
  });

  it('should handle nested promises', () => {
    const task1 = jest.fn(() => {
      return new Promise(resolve => {
        setTimeout(() => {
          expect(++sequenceId).toBe(1);
          taskQueue.enqueue({gen: task2, name: 'gen2'});
          taskQueue.enqueue({run: resolve, name: 'resolve1'});
        }, 1);
      });
    });
    const task2 = jest.fn(() => {
      return new Promise(resolve => {
        setTimeout(() => {
          expect(++sequenceId).toBe(2);
          taskQueue.enqueue({run: task3, name: 'run3'});
          taskQueue.enqueue({run: resolve, name: 'resolve2'});
        }, 1);
      });
    });
    const task3 = createSequenceTask(3);
    const task4 = createSequenceTask(4);
    taskQueue.enqueue({gen: task1, name: 'gen1'});
    taskQueue.enqueue({run: task4, name: 'run4'}); // blocked by task 1 promise

    clearTaskQueue(taskQueue);

    expectToBeCalledOnce(task1);
    expectToBeCalledOnce(task2);
    expectToBeCalledOnce(task3);
    expectToBeCalledOnce(task4);
  });

  it('should be able to cancel tasks', () => {
    const task1 = jest.fn();
    const task2 = createSequenceTask(1);
    const task3 = jest.fn();
    const task4 = createSequenceTask(2);
    taskQueue.enqueue(task1);
    taskQueue.enqueue(task2);
    taskQueue.enqueue(task3);
    taskQueue.enqueue(task4);
    taskQueue.cancelTasks([task1, task3]);
    clearTaskQueue(taskQueue);
    expect(task1).not.toBeCalled();
    expect(task3).not.toBeCalled();
    expectToBeCalledOnce(task2);
    expectToBeCalledOnce(task4);
    expect(taskQueue.hasTasksToProcess()).toBe(false);
  });

  it('should not crash when last task is cancelled', () => {
    const task1 = jest.fn();
    taskQueue.enqueue(task1);
    taskQueue.cancelTasks([task1]);
    clearTaskQueue(taskQueue);
    expect(task1).not.toBeCalled();
    expect(taskQueue.hasTasksToProcess()).toBe(false);
  });
});
