import TaskRunner from "./taskRunner";

const mockPromise = async (id, ms) => {
  await new Promise(resolve => setTimeout(resolve, ms));
  return id;
}
const mockPromiseReject = async (id, ms) => {
  return new Promise((_, reject) => setTimeout(reject(id), ms));
}
const mockPromiseError = async (id, ms) => {
  await new Promise(resolve => setTimeout(resolve, ms));
  return new Error(`Task ${id} had an error`);
}

const asyncFn1 = () => mockPromise(1, 200);
const asyncFn2 = () => mockPromise(2, 100);
const asyncFn3 = () => mockPromise(3, 50);

describe("TaskRunner", () => {
  it("runs task added to queue, when space available", () => {
    const taskRunner = new TaskRunner(2);
    expect(taskRunner._running).toEqual(0);
    taskRunner.push(() => mockPromise(1, 300));
    expect(taskRunner._running).toEqual(1);
  });

  it("keeps task in queue when at concurrency limit", () => {
    const taskRunner = new TaskRunner(2);
    expect(taskRunner._running).toEqual(0);
    taskRunner.push(() => mockPromise(1, 100));
    taskRunner.push(() => mockPromise(2, 25));
    taskRunner.push(() => mockPromise(3, 10));
    expect(taskRunner._running).toEqual(2);
    expect(taskRunner._queue.length).toEqual(1);
  });

  it("runs queued tasks as space becomes available", async () => {
    const taskRunner = new TaskRunner(2);
    taskRunner.push(() => mockPromise(1, 600));
    taskRunner.push(() => mockPromise(2, 300));
    taskRunner.push(() => mockPromise(3, 100));
    expect(taskRunner._queue.length).toEqual(1);
    await new Promise(r => setTimeout(r, 301));
    expect(taskRunner._queue.length).toEqual(0);
  });

  it("runs tasks in order", async () => {
    const actual = [];
    const taskRunner = new TaskRunner(2);
    taskRunner.push(asyncFn1).then(id => actual.push(id));
    taskRunner.push(asyncFn2).then(id => actual.push(id));
    taskRunner.push(asyncFn3).then(id => actual.push(id));
    await new Promise(resolve => setTimeout(resolve, 200));
    expect(actual).toEqual([2, 3, 1]);
  });

  it("resolves a promise when all tasks are complete", async () => {
    const taskRunner = new TaskRunner(2, 5);
    taskRunner.push(() => mockPromise(1, 600));
    taskRunner.push(() => mockPromise(2, 300));
    taskRunner.push(() => mockPromise(3, 200));
    taskRunner.push(() => mockPromise(4, 600));
    taskRunner.push(() => mockPromise(5, 100));
    await taskRunner.onComplete().then((actual) => {
      expect(actual).toStrictEqual([1, 2, 3, 4, 5]);
    });
  });

  it("resolves a promise when all tasks are complete, even if some are rejected", async () => {
    const error = new Error(`Task was rejected: 3`);
    const taskRunner = new TaskRunner(2, 5);
    taskRunner.push(() => mockPromise(1, 600));
    taskRunner.push(() => mockPromise(2, 300));
    taskRunner.push(() => mockPromiseReject(3, 200)).catch(payload => expect(payload).toBe(3));
    taskRunner.push(() => mockPromise(4, 600));
    taskRunner.push(() => mockPromise(5, 100));

    await taskRunner.onComplete().then((response) => {
      expect(response).toStrictEqual([1, 2, error, 4, 5]);
    });
  });

  it("resolves a promise when all tasks are complete, even if some fail", async () => {
    const error = new Error(`Task 3 had an error`);
    const taskRunner = new TaskRunner(2, 5);
    taskRunner.push(() => mockPromise(1, 600));
    taskRunner.push(() => mockPromise(2, 300));
    taskRunner.push(() => mockPromiseError(3, 200));
    taskRunner.push(() => mockPromise(4, 600));
    taskRunner.push(() => mockPromise(5, 100));
    await taskRunner.onComplete().then((actual) => {
      expect(actual).toStrictEqual([1, 2, error, 4, 5]);
    });
  });

  it("immediately rejects onComplete when the total number of tasks is unknown", async () => {
    const taskRunner = new TaskRunner(2);
    taskRunner.push(() => mockPromise(1, 600));
    taskRunner.push(() => mockPromise(2, 300));
    taskRunner.push(() => mockPromise(3, 200));
    taskRunner.push(() => mockPromise(4, 600));
    taskRunner.push(() => mockPromise(5, 100));
    await taskRunner.onComplete().catch((error) => {
      expect(error.message).toBe("Total is required to determine onComplete.");
    });
  });

  it("cancels a task runner and returns progress", async () => {
    const taskRunner = new TaskRunner(2, 5);
    taskRunner.push(() => mockPromise(1, 600));
    taskRunner.push(() => mockPromise(2, 300));
    taskRunner.push(() => mockPromise(3, 200));
    taskRunner.push(() => mockPromise(4, 600));
    taskRunner.push(() => mockPromise(5, 100));

    setTimeout(() => {
      taskRunner.cancel();
    }, 550);

    await taskRunner.onComplete().catch((response) => {
      expect(response).toStrictEqual([undefined, 2, 3, undefined, undefined]);
    });
  });
});
