/* eslint-env qunit */
import Tech from '../../../src/js/tech/tech.js';
import Html5 from '../../../src/js/tech/html5.js';
import Button from '../../../src/js/button.js';
import { createTimeRange } from '../../../src/js/utils/time.js';
import MediaError from '../../../src/js/media-error.js';
import AudioTrack from '../../../src/js/tracks/audio-track';
import VideoTrack from '../../../src/js/tracks/video-track';
import TextTrack from '../../../src/js/tracks/text-track';
import AudioTrackList from '../../../src/js/tracks/audio-track-list';
import VideoTrackList from '../../../src/js/tracks/video-track-list';
import TextTrackList from '../../../src/js/tracks/text-track-list';
import sinon from 'sinon';
import log from '../../../src/js/utils/log.js';
import TestHelpers from '../test-helpers.js';

function stubbedSourceHandler(handler) {
  return {
    canPlayType() {
      return true;
    },
    canHandleSource() {
      return true;
    },
    handleSource(source, tech, options) {
      return handler;
    }
  };
}

QUnit.module('Media Tech', {
  beforeEach(assert) {
    this.noop = function() {};
    this.clock = sinon.useFakeTimers();
    this.featuresProgessEvents = Tech.prototype.featuresProgessEvents;
    Tech.prototype.featuresProgressEvents = false;
  },
  afterEach(assert) {
    this.clock.restore();
    Tech.prototype.featuresProgressEvents = this.featuresProgessEvents;
  }
});

QUnit.test('Tech.registerTech and Tech.getTech', function(assert) {
  class MyTech extends Tech {}
  const oldTechs = Tech.techs_;
  const oldDefaultTechOrder = Tech.defaultTechOrder_;

  Tech.registerTech('MyTech', MyTech);

  assert.ok(Tech.techs_.MyTech, 'Tech is stored in the global list');
  assert.notEqual(Tech.defaultTechOrder_.indexOf('MyTech'), -1, 'Tech is stored in the defaultTechOrder array');
  assert.strictEqual(Tech.getTech('myTech'), MyTech, 'can get a tech using `camelCase` name');
  assert.strictEqual(Tech.getTech('MyTech'), MyTech, 'can get a tech using `titleCase` name');

  // reset techs and defaultTechOrder
  Tech.techs_ = oldTechs;
  Tech.defaultTechOrder_ = oldDefaultTechOrder;
});

QUnit.test('should synthesize timeupdate events by default', function(assert) {
  let timeupdates = 0;
  const tech = new Tech();

  tech.on('timeupdate', function() {
    timeupdates++;
  });

  tech.trigger('play');

  this.clock.tick(250);
  assert.equal(timeupdates, 1, 'triggered at least one timeupdate');

  tech.dispose();
});

QUnit.test('stops manual timeupdates while paused', function(assert) {
  let timeupdates = 0;
  const tech = new Tech();

  tech.on('timeupdate', function() {
    timeupdates++;
  });

  tech.trigger('play');
  this.clock.tick(10 * 250);
  assert.ok(timeupdates > 0, 'timeupdates fire during playback');

  tech.trigger('pause');
  timeupdates = 0;
  this.clock.tick(10 * 250);
  assert.equal(timeupdates, 0, 'timeupdates do not fire when paused');

  tech.trigger('play');
  this.clock.tick(10 * 250);
  assert.ok(timeupdates > 0, 'timeupdates fire when playback resumes');
  tech.dispose();
});

QUnit.test('should synthesize progress events by default', function(assert) {
  let progresses = 0;
  let bufferedPercent = 0.5;
  const tech = new Tech();

  tech.on('progress', function() {
    progresses++;
  });
  tech.bufferedPercent = function() {
    return bufferedPercent;
  };

  this.clock.tick(500);
  assert.equal(progresses, 0, 'waits until ready');

  tech.trigger('ready');
  this.clock.tick(500);
  assert.equal(progresses, 1, 'triggered one event');

  tech.trigger('ready');
  bufferedPercent = 0.75;
  this.clock.tick(500);
  assert.equal(progresses, 2, 'repeated readies are ok');
  tech.dispose();
});

QUnit.test('dispose() should stop time tracking', function(assert) {
  const tech = new Tech();

  tech.dispose();

  // progress and timeupdate events will throw exceptions after the
  // tech is disposed
  try {
    this.clock.tick(10 * 1000);
  } catch (e) {
    return assert.equal(e, undefined, 'threw an exception');
  }
  assert.ok(true, 'no exception was thrown');
});

QUnit.test('dispose() should clear all tracks that are passed when its created', function(assert) {
  const audioTracks = new AudioTrackList([new AudioTrack(), new AudioTrack()]);
  const videoTracks = new VideoTrackList([new VideoTrack(), new VideoTrack()]);
  const pretech = new Tech();
  const textTracks = new TextTrackList([new TextTrack({tech: pretech}),
    new TextTrack({tech: pretech})]);

  assert.equal(audioTracks.length, 2, 'should have two audio tracks at the start');
  assert.equal(videoTracks.length, 2, 'should have two video tracks at the start');
  assert.equal(textTracks.length, 2, 'should have two text tracks at the start');

  const tech = new Tech({audioTracks, videoTracks, textTracks});

  assert.equal(
    tech.videoTracks().length,
    videoTracks.length,
    'should hold video tracks that we passed'
  );
  assert.equal(
    tech.audioTracks().length,
    audioTracks.length,
    'should hold audio tracks that we passed'
  );
  assert.equal(
    tech.textTracks().length,
    textTracks.length,
    'should hold text tracks that we passed'
  );

  pretech.dispose();
  tech.dispose();

  assert.equal(audioTracks.length, 0, 'should have zero audio tracks after dispose');
  assert.equal(videoTracks.length, 0, 'should have zero video tracks after dispose');
  assert.equal(textTracks.length, 0, 'should have zero text tracks after dispose');
});

QUnit.test('dispose() should clear all tracks that are added after creation', function(assert) {
  const tech = new Tech();

  tech.addRemoteTextTrack({}, true);
  tech.addRemoteTextTrack({}, true);

  tech.audioTracks().addTrack(new AudioTrack());
  tech.audioTracks().addTrack(new AudioTrack());

  tech.videoTracks().addTrack(new VideoTrack());
  tech.videoTracks().addTrack(new VideoTrack());

  assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
  assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
  assert.equal(tech.textTracks().length, 2, 'should have two text tracks at the start');
  assert.equal(
    tech.remoteTextTrackEls().length,
    2,
    'should have two remote text tracks els'
  );
  assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');

  tech.dispose();

  assert.equal(
    tech.audioTracks().length,
    0,
    'should have zero audio tracks after dispose'
  );
  assert.equal(
    tech.videoTracks().length,
    0,
    'should have zero video tracks after dispose'
  );
  assert.equal(
    tech.remoteTextTrackEls().length,
    0,
    'should have zero remote text tracks els'
  );
  assert.equal(tech.remoteTextTracks().length, 0, 'should have zero remote text tracks');
  assert.equal(tech.textTracks().length, 0, 'should have zero video tracks after dispose');
});

QUnit.test('switching sources should clear all remote tracks that are added with the default manualCleanup = false', function(assert) {
  const oldLogWarn = log.warn;

  // Define a new tech class
  class MyTech extends Tech {}

  // Create source handler
  const handler = {
    canPlayType: () => 'probably',
    canHandleSource: () => 'probably',
    handleSource: () => {
      return {
        dispose: () => {}
      };
    }
  };

  // Extend Tech with source handlers
  Tech.withSourceHandlers(MyTech);

  MyTech.registerSourceHandler(handler);

  const tech = new MyTech();

  tech.triggerReady();

  // set the initial source
  tech.setSource({src: 'foo.mp4', type: 'mp4'});

  // should not be automatically cleaned up when source changes
  tech.addRemoteTextTrack({}, true);
  // should be automatically cleaned up when source changes
  tech.addRemoteTextTrack({});
  this.clock.tick(1);

  assert.equal(tech.textTracks().length, 2, 'should have two text tracks at the start');
  assert.equal(
    tech.remoteTextTrackEls().length,
    2,
    'should have two remote text tracks els'
  );
  assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');
  assert.equal(
    tech.autoRemoteTextTracks_.length,
    1,
    'should have one auto-cleanup remote text track'
  );

  // change source to force cleanup of auto remote text tracks
  tech.setSource({src: 'bar.mp4', type: 'mp4'});
  this.clock.tick(1);

  assert.equal(
    tech.textTracks().length,
    1,
    'should have one text track after source change'
  );
  assert.equal(
    tech.remoteTextTrackEls().length,
    1,
    'should have one remote remote text track els after source change'
  );
  assert.equal(
    tech.remoteTextTracks().length,
    1,
    'should have one remote text track after source change'
  );
  assert.equal(
    tech.autoRemoteTextTracks_.length,
    0,
    'should have zero auto-cleanup remote text tracks'
  );

  log.warn = oldLogWarn;
  tech.dispose();
});

QUnit.test('should add the source handler interface to a tech', function(assert) {
  const sourceA = { src: 'foo.mp4', type: 'video/mp4' };
  const sourceB = { src: 'no-support', type: 'no-support' };

  // Define a new tech class
  class MyTech extends Tech {}

  // Extend Tech with source handlers
  Tech.withSourceHandlers(MyTech);

  // Check for the expected class methods
  assert.ok(
    MyTech.registerSourceHandler,
    'added a registerSourceHandler function to the Tech'
  );
  assert.ok(
    MyTech.selectSourceHandler,
    'added a selectSourceHandler function to the Tech'
  );

  // Create an instance of Tech
  const tech = new MyTech();

  // Check for the expected instance methods
  assert.ok(tech.setSource, 'added a setSource function to the tech instance');

  // Create an internal state class for the source handler
  // The internal class would be used by a source handler to maintain state
  // and provde a dispose method for the handler.
  // This is optional for source handlers
  let disposeCalled = false;

  class HandlerInternalState {
    dispose() {
      disposeCalled = true;
    }
  }

  // Create source handlers
  const handlerOne = {
    canPlayType(type) {
      if (type !== 'no-support') {
        return 'probably';
      }
      return '';
    },
    canHandleSource(source, options) {
      assert.strictEqual(
        tech.options_,
        options,
        'tech options passed to canHandleSource'
      );
      if (source.type !== 'no-support') {
        return 'probably';
      }
      return '';
    },
    handleSource(s, t, o) {
      assert.strictEqual(
        tech,
        t,
        'tech instance passed to source handler'
      );
      assert.strictEqual(
        sourceA,
        s,
        'tech instance passed to the source handler'
      );
      assert.strictEqual(
        tech.options_,
        o,
        'tech options passed to the source handler handleSource'
      );
      return new HandlerInternalState();
    }
  };

  const handlerTwo = {
    canPlayType(type) {
      // no support
      return '';
    },
    canHandleSource(source, options) {
      // no support
      return '';
    },
    handleSource(source, tech_, options) {
      assert.ok(false, 'handlerTwo supports nothing and should never be called');
    }
  };

  // Test registering source handlers
  MyTech.registerSourceHandler(handlerOne);
  assert.strictEqual(
    MyTech.sourceHandlers[0],
    handlerOne,
    'handlerOne was added to the source handler array'
  );
  MyTech.registerSourceHandler(handlerTwo, 0);
  assert.strictEqual(
    MyTech.sourceHandlers[0],
    handlerTwo,
    'handlerTwo was registered at the correct index (0)'
  );

  // Test handler selection
  assert.strictEqual(
    MyTech.selectSourceHandler(sourceA, tech.options_),
    handlerOne,
    'handlerOne was selected to handle the valid source'
  );
  assert.strictEqual(
    MyTech.selectSourceHandler(sourceB, tech.options_),
    null,
    'no handler was selected to handle the invalid source'
  );

  // Test canPlayType return values
  assert.strictEqual(
    MyTech.canPlayType(sourceA.type),
    'probably',
    'the Tech returned probably for the valid source'
  );
  assert.strictEqual(
    MyTech.canPlayType(sourceB.type),
    '',
    'the Tech returned an empty string for the invalid source'
  );

  // Test canPlaySource return values
  assert.strictEqual(
    MyTech.canPlaySource(sourceA, tech.options_),
    'probably',
    'the Tech returned probably for the valid source'
  );
  assert.strictEqual(
    MyTech.canPlaySource(sourceB, tech.options_),
    '',
    'the Tech returned an empty string for the invalid source'
  );

  tech.addRemoteTextTrack({}, true);
  tech.addRemoteTextTrack({}, true);

  tech.audioTracks().addTrack(new AudioTrack());
  tech.audioTracks().addTrack(new AudioTrack());

  tech.videoTracks().addTrack(new VideoTrack());
  tech.videoTracks().addTrack(new VideoTrack());

  assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
  assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
  assert.equal(tech.textTracks().length, 2, 'should have two video tracks at the start');
  assert.equal(
    tech.remoteTextTrackEls().length,
    2,
    'should have two remote text tracks els'
  );
  assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');

  // Pass a source through the source handler process of a tech instance
  tech.setSource(sourceA);

  // verify that the Tracks are still there
  assert.equal(tech.audioTracks().length, 2, 'should have two audio tracks at the start');
  assert.equal(tech.videoTracks().length, 2, 'should have two video tracks at the start');
  assert.equal(tech.textTracks().length, 2, 'should have two video tracks at the start');
  assert.equal(
    tech.remoteTextTrackEls().length,
    2,
    'should have two remote text tracks els'
  );
  assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');

  assert.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored');
  assert.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored');

  // Pass a second source
  tech.setSource(sourceA);
  assert.strictEqual(tech.currentSource_, sourceA, 'sourceA was handled and stored');
  assert.ok(tech.sourceHandler_.dispose, 'the handlerOne state instance was stored');

  // verify that all the tracks were removed as we got a new source
  assert.equal(tech.audioTracks().length, 0, 'should have zero audio tracks');
  assert.equal(tech.videoTracks().length, 0, 'should have zero video tracks');
  assert.equal(tech.textTracks().length, 2, 'should have two text tracks');
  assert.equal(
    tech.remoteTextTrackEls().length,
    2,
    'should have two remote text tracks els'
  );
  assert.equal(tech.remoteTextTracks().length, 2, 'should have two remote text tracks');

  // Check that the handler dispose method works
  assert.ok(disposeCalled, 'dispose has been called for the handler yet');
  disposeCalled = false;
  tech.dispose();
  assert.ok(
    disposeCalled,
    'the handler dispose method was called when the tech was disposed'
  );
});

QUnit.test('should handle unsupported sources with the source handler API', function(assert) {
  // Define a new tech class
  class MyTech extends Tech {}

  // Extend Tech with source handlers
  Tech.withSourceHandlers(MyTech);
  // Create an instance of Tech
  const tech = new MyTech();
  let usedNative;

  MyTech.nativeSourceHandler = {
    handleSource() {
      usedNative = true;
    }
  };

  tech.setSource('');
  assert.ok(
    usedNative,
    'native source handler was used when an unsupported source was set'
  );
  tech.dispose();
});

QUnit.test('should allow custom error events to be set', function(assert) {
  const tech = new Tech();
  const errors = [];

  tech.on('error', function() {
    errors.push(tech.error());
  });

  assert.equal(tech.error(), null, 'error is null by default');

  tech.error(new MediaError(1));
  assert.equal(errors.length, 1, 'triggered an error event');
  assert.equal(errors[0].code, 1, 'set the proper code');

  tech.error(2);
  assert.equal(errors.length, 2, 'triggered an error event');
  assert.equal(errors[1].code, 2, 'wrapped the error code');
  tech.dispose();
});

QUnit.test('should track whether a video has played', function(assert) {
  const tech = new Tech();

  assert.equal(tech.played().length, 0, 'starts with zero length');
  tech.trigger('playing');
  assert.equal(tech.played().length, 1, 'has length after playing');
  tech.dispose();
});

QUnit.test('delegates deferrables to the source handler', function(assert) {
  class MyTech extends Tech {
    seekable() {
      throw new Error('You should not be calling me!');
    }
    seeking() {
      throw new Error('You should not be calling me!');
    }
    duration() {
      throw new Error('You should not be calling me!');
    }
  }

  Tech.withSourceHandlers(MyTech);

  let seekableCount = 0;
  let seekingCount = 0;
  let durationCount = 0;
  const handler = {
    seekable() {
      seekableCount++;
      return createTimeRange(0, 0);
    },
    seeking() {
      seekingCount++;
      return false;
    },
    duration() {
      durationCount++;
      return 0;
    }
  };

  MyTech.registerSourceHandler(stubbedSourceHandler(handler));

  const tech = new MyTech();

  tech.setSource({
    src: 'example.mp4',
    type: 'video/mp4'
  });
  tech.seekable();
  tech.seeking();
  tech.duration();
  assert.equal(seekableCount, 1, 'called the source handler');
  assert.equal(seekingCount, 1, 'called the source handler');
  assert.equal(durationCount, 1, 'called the source handler');
  tech.dispose();
});

QUnit.test('delegates only deferred deferrables to the source handler', function(assert) {
  let seekingCount = 0;

  class MyTech extends Tech {
    seekable() {
      throw new Error('You should not be calling me!');
    }
    seeking() {
      seekingCount++;
      return false;
    }
    duration() {
      throw new Error('You should not be calling me!');
    }
  }

  Tech.withSourceHandlers(MyTech);

  let seekableCount = 0;
  let durationCount = 0;
  const handler = {
    seekable() {
      seekableCount++;
      return createTimeRange(0, 0);
    },
    duration() {
      durationCount++;
      return 0;
    }
  };

  MyTech.registerSourceHandler(stubbedSourceHandler(handler));

  const tech = new MyTech();

  tech.setSource({
    src: 'example.mp4',
    type: 'video/mp4'
  });
  tech.seekable();
  tech.seeking();
  tech.duration();
  assert.equal(seekableCount, 1, 'called the source handler');
  assert.equal(seekingCount, 1, 'called the tech itself');
  assert.equal(durationCount, 1, 'called the source handler');
  tech.dispose();
});

QUnit.test('Tech.isTech returns correct answers for techs and components', function(assert) {
  const isTech = Tech.isTech;
  const tech = new Html5({}, {});
  const button = new Button(TestHelpers.makePlayer(), {});

  assert.ok(isTech(Tech), 'Tech is a Tech');
  assert.ok(isTech(Html5), 'Html5 is a Tech');
  assert.ok(isTech(tech), 'An html5 instance is a Tech');
  assert.ok(!isTech(5), 'A number is not a Tech');
  assert.ok(!isTech('this is a tech'), 'A string is not a Tech');
  assert.ok(!isTech(Button), 'A Button is not a Tech');
  assert.ok(!isTech(button), 'A Button instance is not a Tech');
  assert.ok(!isTech(isTech), 'A function is not a Tech');

  tech.dispose();
  button.dispose();
});

QUnit.test('setSource after tech dispose should dispose source handler once', function(assert) {
  class MyTech extends Tech {}

  Tech.withSourceHandlers(MyTech);

  let disposeCount = 0;
  const handler = {
    dispose() {
      disposeCount++;
    }
  };

  MyTech.registerSourceHandler({
    canPlayType() {
      return true;
    },
    canHandleSource() {
      return true;
    },
    handleSource(source, tech, options) {
      return handler;
    }
  });

  const tech = new MyTech();

  tech.setSource('test');

  assert.equal(disposeCount, 0, 'did not call sourceHandler_ dispose for initial dispose');
  tech.dispose();
  assert.ok(!tech.sourceHandler_, 'sourceHandler should be unset');
  assert.equal(disposeCount, 1, 'called the source handler dispose');

  // this would normally be done above tech on src after dispose
  tech.el_ = tech.createEl();

  tech.setSource('test');
  assert.equal(disposeCount, 1, 'did not dispose after initial setSource');

  tech.setSource('test');
  assert.equal(disposeCount, 2, 'did dispose on second setSource');
  tech.dispose();

});

QUnit.test('setSource after previous setSource should dispose source handler once', function(assert) {
  class MyTech extends Tech {}

  Tech.withSourceHandlers(MyTech);

  let disposeCount = 0;
  const handler = {
    dispose() {
      disposeCount++;
    }
  };

  MyTech.registerSourceHandler({
    canPlayType() {
      return true;
    },
    canHandleSource() {
      return true;
    },
    handleSource(source, tech, options) {
      return handler;
    }
  });

  const tech = new MyTech();

  tech.setSource('test');
  assert.equal(disposeCount, 0, 'did not call dispose for initial setSource');

  tech.setSource('test');
  assert.equal(disposeCount, 1, 'did dispose for second setSource');

  tech.setSource('test');
  assert.equal(disposeCount, 2, 'did dispose for third setSource');
  tech.dispose();

});

QUnit.test('returns an empty object for getVideoPlaybackQuality', function(assert) {
  const tech = new Tech();

  assert.deepEqual(tech.getVideoPlaybackQuality(), {}, 'returns an empty object');
  tech.dispose();
});

QUnit.test('requestVideoFrameCallback waits if tech not ready', function(assert) {
  const tech = new Tech();
  const cbSpy = sinon.spy();

  tech.paused = sinon.spy();
  tech.isReady_ = false;

  tech.requestVideoFrameCallback(cbSpy);

  assert.notOk(tech.paused.called, 'paused not called on tech that is not ready');

  tech.trigger('playing');

  assert.ok(cbSpy.called, 'callback was called on tech playing');

  tech.dispose();
});
