/**
 * @license
 * Copyright 2022 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '../../test/common-test-setup';
import {assertFails, waitEventLoop} from '../../test/test-utils';
import {Scheduler} from './scheduler';
import {RetryScheduler, RetryError} from './retry-scheduler';
import {FakeScheduler} from './fake-scheduler';
import {SinonFakeTimers} from 'sinon';
import {assert} from '@open-wc/testing';

suite('retry scheduler', () => {
  let clock: SinonFakeTimers;
  let fakeScheduler: FakeScheduler<number>;
  let scheduler: Scheduler<number>;
  setup(() => {
    clock = sinon.useFakeTimers();
    fakeScheduler = new FakeScheduler<number>();
    scheduler = new RetryScheduler<number>(fakeScheduler, 3, 50, 1);
  });

  async function waitForRetry(ms: number) {
    // Flush the promise so that we can reach untilTimeout
    await waitEventLoop();
    // Advance the clock.
    clock.tick(ms);
    // Flush the promise that waits for the clock.
    await waitEventLoop();
  }

  test('executes tasks', async () => {
    const promise = scheduler.schedule(async () => 1);
    assert.equal(fakeScheduler.scheduled.length, 1);
    fakeScheduler.resolve();
    assert.equal(fakeScheduler.scheduled.length, 0);
    const val = await promise;
    assert.equal(val, 1);
  });

  test('propagates task errors', async () => {
    const error = new Error('This is an error');
    const promise = scheduler.schedule(async () => {
      throw error;
    });
    assert.equal(fakeScheduler.scheduled.length, 1);
    assertFails(promise, error);
    fakeScheduler.resolve();
    assert.equal(fakeScheduler.scheduled.length, 0);
    await promise.catch((reason: Error) => {
      assert.equal(reason, error);
    });
  });

  test('propagates subscheduler errors', async () => {
    const error = new Error('This is an error');
    const promise = scheduler.schedule(async () => 1);
    assert.equal(fakeScheduler.scheduled.length, 1);
    assertFails(promise, error);
    fakeScheduler.reject(error);
    assert.equal(fakeScheduler.scheduled.length, 0);
    await promise.catch((reason: Error) => {
      assert.equal(reason, error);
    });
  });

  test('retries on retryable error', async () => {
    let retries = 1;
    const promise = scheduler.schedule(async () => {
      if (retries-- > 0) throw new RetryError('Retrying');
      return 1;
    });
    assert.equal(fakeScheduler.scheduled.length, 1);
    fakeScheduler.resolve();
    assert.equal(fakeScheduler.scheduled.length, 0);
    await waitForRetry(50);
    assert.equal(fakeScheduler.scheduled.length, 1);
    fakeScheduler.resolve();
    const val = await promise;
    assert.equal(val, 1);
  });

  test('retries up to 3 times', async () => {
    let retries = 3;
    const promise = scheduler.schedule(async () => {
      if (retries-- > 0) throw new RetryError('Retrying');
      return 1;
    });
    assert.equal(fakeScheduler.scheduled.length, 1);
    for (let i = 0; i < 3; i++) {
      fakeScheduler.resolve();
      assert.equal(fakeScheduler.scheduled.length, 0);
      await waitForRetry(50);
      assert.equal(fakeScheduler.scheduled.length, 1);
    }
    fakeScheduler.resolve();
    const val = await promise;
    assert.equal(val, 1);
  });

  test('fails after more than 3 times', async () => {
    let retries = 4;
    const promise = scheduler.schedule(async () => {
      if (retries-- > 0) throw new RetryError(retries, `Retrying ${retries}`);
      return 1;
    });
    assert.equal(fakeScheduler.scheduled.length, 1);
    for (let i = 0; i < 3; i++) {
      fakeScheduler.resolve();
      assert.equal(fakeScheduler.scheduled.length, 0);
      await waitForRetry(50);
      assert.equal(fakeScheduler.scheduled.length, 1);
    }
    fakeScheduler.resolve();
    assertFails(promise);
    // The error we get back should be the last error.
    await promise.catch((reason: RetryError<number>) => {
      assert.equal(reason.payload, 0);
      assert.equal(reason.message, 'Retrying 0');
    });
  });
});
