import {Interval} from 'app/common/Interval';
import {delay} from 'bluebird';
import {assert} from 'chai';
import * as sinon from 'sinon';

describe('Interval', function() {
  const delayMs = 100;
  const varianceMs = 50;
  const promiseDelayMs = 200;
  const delayBufferMs = 20;

  let interval: Interval;
  let spy: sinon.SinonSpy;

  beforeEach(() => {
    spy = sinon.spy();
  });

  afterEach(async () => {
    if (interval) {
      await interval.disableAndFinish();
    }
  });

  it('is not enabled by default', async function() {
    interval = new Interval(spy, {delayMs}, {onError: () => { /* do nothing */ }});
    assert.equal(spy.callCount, 0);
    await delay(delayMs + delayBufferMs);
    assert.equal(spy.callCount, 0);
  });

  it('can be disabled', async function() {
    interval = new Interval(spy, {delayMs}, {onError: () => { /* do nothing */ }});
    interval.enable();
    await delay(delayMs + delayBufferMs);
    assert.equal(spy.callCount, 1);

    // Disable the interval, and check that the calls stop.
    interval.disable();
    await delay(delayMs + delayBufferMs);
    assert.equal(spy.callCount, 1);

    // Enable the interval again, and check that the calls resume.
    interval.enable();
    await delay(delayMs + delayBufferMs);
    assert.equal(spy.callCount, 2);
    spy.resetHistory();
  });

  it('calls onError if callback throws an error', async function() {
    const callback = () => { throw new Error('Something bad happened.'); };
    const onErrorSpy = sinon.spy();
    interval = new Interval(callback, {delayMs}, {onError: onErrorSpy});
    interval.enable();

    // Check that onError is called when the callback throws.
    assert.equal(onErrorSpy.callCount, 0);
    await delay(delayMs + delayBufferMs);
    assert.equal(onErrorSpy.callCount, 1);

    // Check that the interval didn't stop (since the onError spy silenced the error).
    await delay(delayMs + delayBufferMs);
    assert.equal(onErrorSpy.callCount, 2);
  });

  describe('with a fixed delay', function() {
    beforeEach(() => {
      interval = new Interval(spy, {delayMs}, {onError: () => { /* do nothing */ }});
      interval.enable();
    });

    it('calls the callback on a fixed interval', async function() {
      await delay(delayMs + delayBufferMs);
      assert.equal(spy.callCount, 1);
      await delay(delayMs + delayBufferMs);
      assert.equal(spy.callCount, 2);
    });
  });

  describe('with a randomized delay', function() {
    beforeEach(() => {
      interval = new Interval(spy, {delayMs, varianceMs}, {
        onError: () => { /* do nothing */ }
      });
      interval.enable();
    });

    it('calls the callback on a randomized interval', async function() {
      const delays: number[] = [];
      for (let i = 1; i <= 10; i++) {
        // Get the current delay and check that it's within the expected range.
        const currentDelayMs = interval.getDelayMs();
        delays.push(currentDelayMs!);
        assert.isDefined(currentDelayMs);
        assert.isAtMost(currentDelayMs!, delayMs + varianceMs);
        assert.isAtLeast(currentDelayMs!, delayMs - varianceMs);

        // Wait for the delay, and check that the spy was called.
        await delay(currentDelayMs!);
        assert.equal(spy.callCount, i);
      }

      // Check that we didn't use the same delay all 10 times.
      assert.notEqual([...new Set(delays)].length, 1);
    });
  });

  describe('with a promise-based callback', function() {
    let promiseSpy: sinon.SinonSpy;

    beforeEach(() => {
      const promise = () => delay(promiseDelayMs);
      promiseSpy = sinon.spy(promise);
      interval = new Interval(promiseSpy, {delayMs}, {onError: () => { /* do nothing */ }});
      interval.enable();
    });

    it('waits for promises to settle before scheduling the next call', async function() {
      assert.equal(promiseSpy.callCount, 0);
      await delay(delayMs + delayBufferMs);
      assert.equal(promiseSpy.callCount, 1);
      await delay(delayMs + delayBufferMs);
      assert.equal(promiseSpy.callCount, 1); // Still 1, because the first promise hasn't settled yet.
      await delay(delayMs + delayBufferMs);
      assert.equal(promiseSpy.callCount, 1); // Promise now settled, but there's still a 100ms delay.
      await delay(delayMs + delayBufferMs);
      assert.equal(promiseSpy.callCount, 2); // Now we finally call the callback again.
    });

    it('can wait for last promise to settle when disabling', async function() {
      assert.equal(promiseSpy.callCount, 0);
      await delay(delayMs + delayBufferMs);
      assert.equal(promiseSpy.callCount, 1);
      await interval.disableAndFinish();

      // Check that once disabled, no more calls are scheduled.
      await delay(promiseDelayMs + delayMs + delayBufferMs);
      assert.equal(promiseSpy.callCount, 1);
    });
  });
});