/* eslint-env qunit */
import * as middleware from '../../../src/js/tech/middleware.js';
import sinon from 'sinon';
import window from 'global/window';

QUnit.module('Middleware', {
  beforeEach(assert) {
    this.clock = sinon.useFakeTimers();
  },
  afterEach(assert) {
    this.clock.restore();
  }
});

QUnit.test('middleware can be added with the use method', function(assert) {
  const myMw = {};
  const mwFactory = () => myMw;

  middleware.use('foo', mwFactory);

  assert.equal(middleware.getMiddleware('foo').pop(), mwFactory, 'we are able to add middleware');
});

QUnit.test('middleware get iterates through the middleware array in the right order', function(assert) {
  const cts = [];
  const durs = [];
  const foos = [];
  const mws = [
    {
      currentTime(ct) {
        cts.push(ct);
        return ct * 2;
      },
      duration(dur) {
        durs.push(dur);
        return dur + 2;
      },
      foo(f) {
        foos.push(f);
        return f / 2;
      }
    },
    {
      currentTime(ct) {
        cts.push(ct);
        return ct + 2;
      },
      duration(dur) {
        durs.push(dur);
        return dur / 2;
      },
      foo(f) {
        foos.push(f);
        return f + 3;
      }
    }
  ];
  const tech = {
    currentTime(ct) {
      return 5;
    },
    duration(dur) {
      return 5;
    },
    foo(f) {
      return 5;
    }
  };

  const ct = middleware.get(mws, tech, 'currentTime');
  const dur = middleware.get(mws, tech, 'duration');
  const foo = middleware.get(mws, tech, 'foo');

  const assertion = (actual, expected, actualArr, expectedArr, type) => {
    assert.equal(actual, expected, `our middleware chain return correctly for ${type}`);
    assert.deepEqual(actualArr, expectedArr, `we got called in the correct order for ${type}`);
  };

  assertion(ct, 14, cts, [5, 7], 'currentTime');
  assertion(dur, 4.5, durs, [5, 2.5], 'duration');
  assertion(foo, 4, foos, [5, 8], 'foo');
});

QUnit.test('middleware set iterates through the middleware array in the right order', function(assert) {
  const cts = [];
  const durs = [];
  const foos = [];
  const mws = [
    {
      currentTime(ct) {
        cts.push(ct);
        return ct * 2;
      },
      duration(dur) {
        durs.push(dur);
        return dur + 2;
      },
      foo(f) {
        foos.push(f);
        return f / 2;
      }
    },
    {
      currentTime(ct) {
        cts.push(ct);
        return ct + 2;
      },
      duration(dur) {
        durs.push(dur);
        return dur / 2;
      },
      foo(f) {
        foos.push(f);
        return f + 3;
      }
    }
  ];
  const tech = {
    currentTime(ct) {
      cts.push(ct);
      return ct / 2;
    },
    duration(dur) {
      durs.push(dur);
      return dur;
    },
    foo(f) {
      foos.push(f);
      return f;
    }
  };

  const ct = middleware.set(mws, tech, 'currentTime', 10);
  const dur = middleware.set(mws, tech, 'duration', 10);
  const foo = middleware.set(mws, tech, 'foo', 10);

  const assertion = (actual, expected, actualArr, expectedArr, type) => {
    assert.equal(actual, expected, `our middleware chain return correctly for ${type}`);
    assert.deepEqual(actualArr, expectedArr, `we got called in the correct order for ${type}`);
  };

  assertion(ct, 11, cts, [10, 20, 22], 'currentTime');
  assertion(dur, 6, durs, [10, 12, 6], 'duration');
  assertion(foo, 8, foos, [10, 5, 8], 'foo');
});

QUnit.test('middleware mediate iterates through the middleware array twice', function(assert) {
  let playsToTech = 0;
  let playsToPlayer = 0;
  let techPlays = 0;
  let techPlay;
  let pv1;
  let pv2;
  let pc1;
  let pc2;

  const mws = [{
    callPlay() {
      playsToTech++;
    },
    play(cancelled, value) {
      playsToPlayer++;
      pv1 = value;
      pc1 = cancelled;
    }
  }, {
    callPlay() {
      playsToTech++;
    },
    play(cancelled, value) {
      playsToPlayer++;
      pv2 = value;
      pc2 = cancelled;
    }
  }];
  const tech = {
    play() {
      techPlays++;
      techPlay = {then: () => {}};

      return techPlay;
    }
  };

  const pp = middleware.mediate(mws, tech, 'play');

  assert.equal(playsToTech, playsToPlayer, 'middleware got called the same number of times');
  assert.equal(playsToTech, 2, 'both middleware got called before the tech');
  assert.equal(techPlays, 1, 'the tech method only gets called once');
  assert.equal(playsToPlayer, 2, 'both middleware got called after the tech');

  assert.deepEqual(pv1.then, techPlay.then, 'the value returned by the tech is passed through the middleware');
  assert.deepEqual(pv2, techPlay, 'the value returned by the tech is passed through the middleware');
  assert.deepEqual(pp, techPlay, 'the value returned to the player is the value returned from the tech');
  assert.equal(pc1, false, 'the play has not been cancelled in middleware 1');
  assert.equal(pc2, false, 'the play has not been cancelled in middleware 2');
});

QUnit.test('middleware mediate allows and can detect cancellation', function(assert) {
  let playsToTech = 0;
  let playsToPlayer = 0;
  let techPlays = 0;
  let pv1;
  let pv2;
  let pc1;
  let pc2;

  const mws = [{
    callPlay() {
      playsToTech++;
    },
    play(cancelled, value) {
      playsToPlayer++;
      pv1 = value;
      pc1 = cancelled;
    }
  }, {
    callPlay() {
      playsToTech++;
      return middleware.TERMINATOR;
    },
    play(cancelled, value) {
      playsToPlayer++;
      pv2 = value;
      pc2 = cancelled;
    }
  }];
  const tech = {
    play() {
      techPlays++;
      return {then: () => {}};
    }
  };

  const pp = middleware.mediate(mws, tech, 'play');

  assert.equal(playsToTech, 2, 'both middleware run until middleware terminates');
  assert.equal(techPlays, 0, 'the tech should not be called if a middleware terminates');
  assert.equal(playsToPlayer, 2, 'both middleware run after the tech');

  assert.equal(pv1, null, 'null is returned through the middleware if a middleware terminated previously');
  assert.equal(pv2, null, 'null is returned through the middleware if a middleware terminated previously');
  assert.equal(pp, null, 'null is returned to the player if a middleware terminated previously');
  assert.equal(pc1, true, 'the play has been cancelled in middleware 1');
  assert.equal(pc2, true, 'the play has been cancelled in middleware 2');
});

QUnit.test('setSource is run asynchronously', function(assert) {
  let src;
  let acc;

  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, { src: 'foo', type: 'video/foo' }, function(_src, _acc) {
    src = _src;
    acc = _acc;
  });

  assert.equal(src, undefined, 'no src was returned yet');
  assert.equal(acc, undefined, 'no accumulator was returned yet');

  this.clock.tick(1);

  assert.deepEqual(src, {src: 'foo', type: 'video/foo'}, 'we got the same source back');
  assert.equal(acc.length, 0, 'we did not accumulate any middleware since there were none');
});

QUnit.test('setSource selects a source based on the middleware given', function(assert) {
  let src;
  let acc;
  const mw = {
    setSource(_src, next) {
      next(null, {
        src: 'http://example.com/video.mp4',
        type: 'video/mp4'
      });
    }
  };
  const fooFactory = () => mw;

  middleware.use('video/foo', fooFactory);

  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, {src: 'foo', type: 'video/foo'}, function(_src, _acc) {
    src = _src;
    acc = _acc;
  });

  this.clock.tick(1);

  assert.equal(src.type, 'video/mp4', 'we selected a new type of video/mp4');
  assert.equal(src.src, 'http://example.com/video.mp4', 'we selected a new src of video.mp4');
  assert.equal(acc.length, 1, 'we got one middleware');
  assert.equal(acc[0], mw, 'we chose the one middleware');

  middleware.getMiddleware('video/foo').pop();
});

QUnit.test('setSource can select multiple middleware from multiple types', function(assert) {
  let src;
  let acc;
  const foomw = {
    setSource(_src, next) {
      next(null, {
        src: 'bar',
        type: 'video/bar'
      });
    }
  };
  const barmw = {
    setSource(_src, next) {
      next(null, {
        src: 'http://example.com/video.mp4',
        type: 'video/mp4'
      });
    }
  };
  const fooFactory = () => foomw;
  const barFactory = () => barmw;

  middleware.use('video/foo', fooFactory);
  middleware.use('video/bar', barFactory);

  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, {src: 'foo', type: 'video/foo'}, function(_src, _acc) {
    src = _src;
    acc = _acc;
  });

  this.clock.tick(1);

  assert.equal(src.type, 'video/mp4', 'we selected a new type of video/mp4');
  assert.equal(src.src, 'http://example.com/video.mp4', 'we selected a new src of video.mp4');
  assert.equal(acc.length, 2, 'we got two middleware');
  assert.equal(acc[0], foomw, 'foomw is the first middleware');
  assert.equal(acc[1], barmw, 'barmw is the first middleware');

  middleware.getMiddleware('video/foo').pop();
  middleware.getMiddleware('video/bar').pop();
});

QUnit.test('setSource will select all middleware of a given type, until src change', function(assert) {
  let src;
  let acc;
  const foomw1 = {
    setSource(_src, next) {
      next(null, {
        src: 'bar',
        type: 'video/foo'
      });
    }
  };
  const foomw2 = {
    setSource(_src, next) {
      next(null, {
        src: 'http://example.com/video.mp4',
        type: 'video/mp4'
      });
    }
  };
  const foomw3 = {
    setSource(_src, next) {
      next(null, {
        src: 'http://example.com/video.mp4',
        type: 'video/mp4'
      });
    }
  };
  const fooFactory1 = () => foomw1;
  const fooFactory2 = () => foomw2;
  const fooFactory3 = () => foomw3;

  middleware.use('video/foo', fooFactory1);
  middleware.use('video/foo', fooFactory2);
  middleware.use('video/foo', fooFactory3);

  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, {src: 'foo', type: 'video/foo'}, function(_src, _acc) {
    src = _src;
    acc = _acc;
  });

  this.clock.tick(1);

  assert.equal(src.type, 'video/mp4', 'we selected a new type of video/mp4');
  assert.equal(src.src, 'http://example.com/video.mp4', 'we selected a new src of video.mp4');
  assert.equal(acc.length, 2, 'we got two middleware');
  assert.equal(acc[0], foomw1, 'foomw is the first middleware');
  assert.equal(acc[1], foomw2, 'foomw is the first middleware');

  middleware.getMiddleware('video/foo').pop();
  middleware.getMiddleware('video/foo').pop();
  middleware.getMiddleware('video/foo').pop();
});

QUnit.test('a middleware without a mediator method will not throw an error', function(assert) {
  let pauseCalled = 0;
  const myMw = {};
  const mwFactory = () => myMw;
  const mwFactory2 = () => ({
    pause() {
      pauseCalled++;
    }
  });

  middleware.mediate([mwFactory(), mwFactory2()], {pause: () => {}}, 'pause');

  assert.equal(pauseCalled, 1, 'pauseCalled was called once and no error was thrown');
});

QUnit.test('a middleware factory is not called on source change', function(assert) {
  let mwfactoryCalled = 0;
  const mw = {
    setSource(_src, next) {
      next(null, {
        src: 'http://example.com/video.mp4',
        type: 'video/mp4'
      });
    }
  };
  const fooFactory = () => {
    mwfactoryCalled++;
    return mw;
  };

  middleware.use('video/foo', fooFactory);

  // set "initial" source"
  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, {src: 'foo', type: 'video/foo'}, function() {});

  this.clock.tick(1);

  assert.equal(mwfactoryCalled, 1, 'the factory was called once');

  // "change" source
  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, {src: 'bar', type: 'video/foo'}, function() {});

  this.clock.tick(1);

  assert.equal(mwfactoryCalled, 1, 'the factory was not called again');

  middleware.getMiddleware('video/foo').pop();
});

QUnit.test('a middleware factory is called on a new source with a new player', function(assert) {
  let mwfactoryCalled = 0;
  const mw = {
    setSource(_src, next) {
      next(null, {
        src: 'http://example.com/video.mp4',
        type: 'video/mp4'
      });
    }
  };
  const fooFactory = () => {
    mwfactoryCalled++;
    return mw;
  };

  middleware.use('video/foo', fooFactory);

  // set "initial" source with player vid1
  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, {src: 'foo', type: 'video/foo'}, function() {});

  this.clock.tick(1);

  assert.equal(mwfactoryCalled, 1, 'the factory was called once');

  // set "initial" source with player vid2
  middleware.setSource({
    id() {
      return 'vid2';
    },
    setTimeout: window.setTimeout
  }, {src: 'bar', type: 'video/foo'}, function() {});

  this.clock.tick(1);

  assert.equal(mwfactoryCalled, 2, 'the factory was called again');

  middleware.getMiddleware('video/foo').pop();
});

QUnit.test('a middleware without a setSource gets chosen implicitly', function(assert) {
  let mws = [];
  const mw = {
    currentTime(ct) {
    }
  };
  const mwFactory = () => mw;

  middleware.use('video/foo', mwFactory);

  middleware.setSource({
    id() {
      return 'vid1';
    },
    setTimeout: window.setTimeout
  }, {src: 'foo', type: 'video/foo'}, function(src, _mws) {
    mws = _mws;
  });

  this.clock.tick(1);

  assert.equal(mws.length, 1, 'we have 1 middleware set');

  middleware.getMiddleware('video/foo').pop();
});
