import document from 'global/document';
import window from 'global/window';
import QUnit from 'qunit';
import sinon from 'sinon';
import videojs from 'video.js';
import muxjs from 'mux.js';
import FlashSourceBuffer from '../src/flash-source-buffer';
import FlashConstants from '../src/flash-constants';

// we disable this because browserify needs to include these files
// but the exports are not important
/* eslint-disable no-unused-vars */
import {MediaSource, URL} from '../src/videojs-contrib-media-sources.js';
/* eslint-disable no-unused-vars */

// return the sequence of calls to append to the SWF
const appendCalls = function(calls) {
  return calls.filter(function(call) {
    return call.callee && call.callee === 'vjs_appendChunkReady';
  });
};

const getFlvHeader = function() {
  return new Uint8Array([1, 2, 3]);
};

const makeFlvTag = function(pts, data) {
  return {
    pts,
    dts: pts,
    bytes: data
  };
};

let timers;
let oldSTO;

const fakeSTO = function() {
  oldSTO = window.setTimeout;
  timers = [];

  timers.run = function(num) {
    let timer;

    while (num--) {
      timer = this.pop();
      if (timer) {
        timer();
      }
    }
  };

  timers.runAll = function() {
    while (this.length) {
      this.pop()();
    }
  };

  window.setTimeout = function(callback) {
    timers.push(callback);
  };
  window.setTimeout.fake = true;
};

const unfakeSTO = function() {
  timers = [];
  window.setTimeout = oldSTO;
};

// Create a WebWorker-style message that signals the transmuxer is done
const createDataMessage = function(data, audioData, metadata, captions) {
  let captionStreams = {};

  if (captions) {
    captions.forEach((caption) => {
      captionStreams[caption.stream] = true;
    });
  }
  return {
    data: {
      action: 'data',
      segment: {
        tags: {
          videoTags: data.map((tag) => {
            return makeFlvTag(tag.pts, tag.bytes);
          }),
          audioTags: audioData ? audioData.map((tag) => {
            return makeFlvTag(tag.pts, tag.bytes);
          }) : []
        },
        metadata,
        captions,
        captionStreams
      }
    }
  };
};
const doneMessage = {
  data: {
    action: 'done'
  }
};
const postMessage_ = function(msg) {
  if (msg.action === 'push') {
    window.setTimeout(()=> {
      this.onmessage(createDataMessage([{
        bytes: new Uint8Array(msg.data, msg.byteOffset, msg.byteLength),
        pts: 0
      }]));
    }, 1);
  } else if (msg.action === 'flush') {
    window.setTimeout(() => {
      this.onmessage(doneMessage);
    }, 1);
  }
};

QUnit.module('Flash MediaSource', {
  beforeEach(assert) {
    let swfObj;

    // Mock the environment's timers because certain things - particularly
    // player readiness - are asynchronous in video.js 5.
    this.clock = sinon.useFakeTimers();

    this.fixture = document.getElementById('qunit-fixture');
    this.video = document.createElement('video');
    this.fixture.appendChild(this.video);
    this.player = videojs(this.video);

    this.oldMediaSource = window.MediaSource || window.WebKitMediaSource;

    window.MediaSource = null;
    window.WebKitMediaSource = null;

    this.Flash = videojs.getTech('Flash');
    this.oldFlashSupport = this.Flash.isSupported;
    this.oldCanPlay = this.Flash.canPlaySource;
    this.Flash.canPlaySource = this.Flash.isSupported = function() {
      return true;
    };

    this.oldFlashTransmuxerPostMessage = muxjs.flv.Transmuxer.postMessage;
    this.oldGetFlvHeader = muxjs.flv.getFlvHeader;
    muxjs.flv.getFlvHeader = getFlvHeader;

    this.swfCalls = [];
    this.mediaSource = new videojs.MediaSource();
    this.player.src({
      src: videojs.URL.createObjectURL(this.mediaSource),
      type: 'video/mp2t'
    });
    // vjs6 takes 1 tick to set source async
    this.clock.tick(1);
    swfObj = document.createElement('fake-object');
    swfObj.id = 'fake-swf-' + assert.test.testId;
    this.player.el().replaceChild(swfObj, this.player.tech_.el());
    this.player.tech_.hls = new videojs.EventTarget();
    this.player.tech_.el_ = swfObj;
    swfObj.tech = this.player.tech_;

    /* eslint-disable camelcase */
    swfObj.vjs_abort = () => {
      this.swfCalls.push('abort');
    };
    swfObj.vjs_getProperty = (attr) => {
      if (attr === 'buffered') {
        return [];
      } else if (attr === 'currentTime') {
        return 0;
      // ignored for vjs6
      } else if (attr === 'videoWidth') {
        return 0;
      }
      this.swfCalls.push({ attr });
    };
    swfObj.vjs_load = () => {
      this.swfCalls.push('load');
    };
    swfObj.vjs_setProperty = (attr, value) => {
      this.swfCalls.push({ attr, value });
    };
    swfObj.vjs_discontinuity = (attr, value) => {
      this.swfCalls.push({ attr, value });
    };
    swfObj.vjs_appendChunkReady = (method) => {
      window.setTimeout(() => {
        let chunk = window[method]();

        // only care about the segment data, not the flv header
        if (method.substr(0, 21) === 'vjs_flashEncodedData_') {
          let call = {
            callee: 'vjs_appendChunkReady',
            arguments: [window.atob(chunk).split('').map((c) => c.charCodeAt(0))]
          };

          this.swfCalls.push(call);
        }
      }, 1);
    };
    swfObj.vjs_adjustCurrentTime = (value) => {
      this.swfCalls.push({ call: 'adjustCurrentTime', value });
    };
    /* eslint-enable camelcase */

    this.mediaSource.trigger({
      type: 'sourceopen',
      swfId: swfObj.id
    });
    fakeSTO();
  },
  afterEach() {
    window.MediaSource = this.oldMediaSource;
    window.WebKitMediaSource = window.MediaSource;
    this.Flash.isSupported = this.oldFlashSupport;
    this.Flash.canPlaySource = this.oldCanPlay;
    muxjs.flv.Transmuxer.postMessage = this.oldFlashTransmuxerPostMessage;
    muxjs.flv.getFlvHeader = this.oldGetFlvHeader;
    this.player.dispose();
    this.clock.restore();
    this.swfCalls = [];
    unfakeSTO();
  }
});

QUnit.test('raises an exception for unrecognized MIME types', function() {
  try {
    this.mediaSource.addSourceBuffer('video/garbage');
  } catch (e) {
    QUnit.ok(e, 'an error was thrown');
    return;
  }
  QUnit.ok(false, 'no error was thrown');
});

QUnit.test('creates FlashSourceBuffers for video/mp2t', function() {
  QUnit.ok(this.mediaSource.addSourceBuffer('video/mp2t') instanceof FlashSourceBuffer,
      'create source buffer');
});

QUnit.test('creates FlashSourceBuffers for audio/mp2t', function() {
  QUnit.ok(this.mediaSource.addSourceBuffer('audio/mp2t') instanceof FlashSourceBuffer,
      'create source buffer');
});

QUnit.test('waits for the next tick to append', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  QUnit.equal(this.swfCalls.length, 1, 'made one call on init');
  QUnit.equal(this.swfCalls[0], 'load', 'called load');
  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  this.swfCalls = appendCalls(this.swfCalls);
  QUnit.strictEqual(this.swfCalls.length, 0, 'no appends were made');
});

QUnit.test('passes bytes to Flash', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  this.swfCalls.length = 0;
  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  timers.runAll();
  timers.runAll();

  QUnit.ok(this.swfCalls.length, 'the SWF was called');
  this.swfCalls = appendCalls(this.swfCalls);
  QUnit.strictEqual(this.swfCalls[0].callee, 'vjs_appendChunkReady', 'called vjs_appendChunkReady');
  QUnit.deepEqual(this.swfCalls[0].arguments[0],
            [0, 1],
            'passed the base64 encoded data');
});

QUnit.test('passes chunked bytes to Flash', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let oldChunkSize = FlashConstants.BYTES_PER_CHUNK;

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  FlashConstants.BYTES_PER_CHUNK = 2;

  this.swfCalls.length = 0;
  sourceBuffer.appendBuffer(new Uint8Array([0, 1, 2, 3, 4]));
  timers.runAll();

  QUnit.ok(this.swfCalls.length, 'the SWF was called');
  this.swfCalls = appendCalls(this.swfCalls);
  QUnit.equal(this.swfCalls.length, 3, 'the SWF received 3 chunks');
  QUnit.strictEqual(this.swfCalls[0].callee, 'vjs_appendChunkReady', 'called vjs_appendChunkReady');
  QUnit.deepEqual(this.swfCalls[0].arguments[0],
            [0, 1],
            'passed the base64 encoded data');
  QUnit.deepEqual(this.swfCalls[1].arguments[0],
            [2, 3],
            'passed the base64 encoded data');
  QUnit.deepEqual(this.swfCalls[2].arguments[0],
            [4],
            'passed the base64 encoded data');

  FlashConstants.BYTES_PER_CHUNK = oldChunkSize;
});

QUnit.test('clears the SWF on seeking', function() {
  let aborts = 0;

  this.mediaSource.addSourceBuffer('video/mp2t');
  // track calls to abort()

  /* eslint-disable camelcase */
  this.mediaSource.swfObj.vjs_abort = function() {
    aborts++;
  };
  /* eslint-enable camelcase */

  this.mediaSource.tech_.trigger('seeking');
  QUnit.strictEqual(1, aborts, 'aborted pending buffer');
});

QUnit.test('drops tags before currentTime when seeking', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let i = 10;
  let currentTime;
  let tags_ = [];

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  this.mediaSource.tech_.currentTime = function() {
    return currentTime;
  };

  // push a tag into the buffer to establish the starting PTS value
  currentTime = 0;

  sourceBuffer.transmuxer_.onmessage(createDataMessage([{
    pts: 19 * 1000,
    bytes: new Uint8Array(1)
  }]));

  timers.runAll();

  sourceBuffer.appendBuffer(new Uint8Array(10));
  timers.runAll();

  // mock out a new segment of FLV tags, starting 10s after the
  // starting PTS value
  while (i--) {
    tags_.unshift(
      {
        pts: (i * 1000) + (29 * 1000),
        bytes: new Uint8Array([i])
      }
    );
  }

  let dataMessage = createDataMessage(tags_);

  // mock gop start at seek point
  dataMessage.data.segment.tags.videoTags[7].keyFrame = true;

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  // seek to 7 seconds into the new swegment
  this.mediaSource.tech_.seeking = function() {
    return true;
  };
  currentTime = 10 + 7;
  this.mediaSource.tech_.trigger('seeking');
  sourceBuffer.appendBuffer(new Uint8Array(10));
  this.swfCalls.length = 0;
  timers.runAll();

  QUnit.deepEqual(this.swfCalls[0].arguments[0], [7, 8, 9],
            'three tags are appended');
});

QUnit.test('drops audio and video (complete gops) tags before the buffered end always', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let endTime;
  let videoTags_ = [];
  let audioTags_ = [];

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  this.mediaSource.tech_.buffered = function() {
    return videojs.createTimeRange([[0, endTime]]);
  };

  // push a tag into the buffer to establish the starting PTS value
  endTime = 0;

  // mock buffering 17 seconds of data so flash source buffer internal end of buffer
  // tracking is accurate
  let i = 17;

  while (i--) {
    videoTags_.unshift({
      pts: (i * 1000) + (19 * 1000),
      bytes: new Uint8Array(1)
    });
  }

  i = 17;

  while (i--) {
    audioTags_.unshift({
      pts: (i * 1000) + (19 * 1000),
      bytes: new Uint8Array(1)
    });
  }

  let dataMessage = createDataMessage(videoTags_, audioTags_);

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  timers.runAll();

  sourceBuffer.appendBuffer(new Uint8Array(10));
  timers.runAll();

  i = 10;
  videoTags_ = [];
  audioTags_ = [];

  // mock out a new segment of FLV tags, starting 10s after the
  // starting PTS value
  while (i--) {
    videoTags_.unshift({
      pts: (i * 1000) + (29 * 1000),
      bytes: new Uint8Array([i])
    });
  }

  i = 10;

  while (i--) {
    audioTags_.unshift({
      pts: (i * 1000) + (29 * 1000),
      bytes: new Uint8Array([i + 100])
    });
  }

  dataMessage = createDataMessage(videoTags_, audioTags_);

  dataMessage.data.segment.tags.videoTags[0].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[3].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[6].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[8].keyFrame = true;

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  endTime = 10 + 7;
  sourceBuffer.appendBuffer(new Uint8Array(10));
  this.swfCalls.length = 0;
  timers.runAll();

  // end of buffer is 17 seconds
  // frames 0-6 for video have pts values less than 17 seconds
  // since frame 6 is a key frame, it should still be appended to preserve the entire gop
  // so we should have appeneded frames 6 - 9
  // frames 100-106 for audio have pts values less than 17 seconds
  // but since we appended an extra video frame, we should also append audio frames
  // to fill in the gap in audio. This means we should be appending audio frames
  // 106, 107, 108, 109
  // Append order is 6, 7, 107, 8, 108, 9, 109 since we order tags based on dts value
  QUnit.deepEqual(this.swfCalls[0].arguments[0], [6, 106, 7, 107, 8, 108, 9, 109],
            'audio and video tags properly dropped');
});

QUnit.test('seeking into the middle of a GOP adjusts currentTime to the start of the GOP', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let i = 10;
  let currentTime;
  let tags_ = [];

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  this.mediaSource.tech_.currentTime = function() {
    return currentTime;
  };

  // push a tag into the buffer to establish the starting PTS value
  currentTime = 0;

  let dataMessage = createDataMessage([{
    pts: 19 * 1000,
    bytes: new Uint8Array(1)
  }]);

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  timers.runAll();

  sourceBuffer.appendBuffer(new Uint8Array(10));
  timers.runAll();

  // mock out a new segment of FLV tags, starting 10s after the
  // starting PTS value
  while (i--) {
    tags_.unshift(
      {
        pts: (i * 1000) + (29 * 1000),
        bytes: new Uint8Array([i])
      }
    );
  }

  dataMessage = createDataMessage(tags_);

  // mock the GOP structure
  dataMessage.data.segment.tags.videoTags[0].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[3].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[5].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[8].keyFrame = true;

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  // seek to 7 seconds into the new swegment
  this.mediaSource.tech_.seeking = function() {
    return true;
  };
  currentTime = 10 + 7;
  this.mediaSource.tech_.trigger('seeking');
  sourceBuffer.appendBuffer(new Uint8Array(10));
  this.swfCalls.length = 0;
  timers.runAll();

  QUnit.deepEqual(this.swfCalls[0], { call: 'adjustCurrentTime', value: 15 });
  QUnit.deepEqual(this.swfCalls[1].arguments[0], [5, 6, 7, 8, 9],
            '5 tags are appended');
});

QUnit.test('GOP trimming accounts for metadata tags prepended to key frames by mux.js', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let i = 10;
  let currentTime;
  let tags_ = [];

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  this.mediaSource.tech_.currentTime = function() {
    return currentTime;
  };

  // push a tag into the buffer to establish the starting PTS value
  currentTime = 0;

  let dataMessage = createDataMessage([{
    pts: 19 * 1000,
    bytes: new Uint8Array(1)
  }]);

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  timers.runAll();

  sourceBuffer.appendBuffer(new Uint8Array(10));
  timers.runAll();

  // mock out a new segment of FLV tags, starting 10s after the
  // starting PTS value
  while (i--) {
    tags_.unshift(
      {
        pts: (i * 1000) + (29 * 1000),
        bytes: new Uint8Array([i])
      }
    );
  }

  // add in the metadata tags
  tags_.splice(8, 0, {
    pts: tags_[8].pts,
    bytes: new Uint8Array([8])
  }, {
    pts: tags_[8].pts,
    bytes: new Uint8Array([8])
  });

  tags_.splice(5, 0, {
    pts: tags_[5].pts,
    bytes: new Uint8Array([5])
  }, {
    pts: tags_[5].pts,
    bytes: new Uint8Array([5])
  });

  tags_.splice(0, 0, {
    pts: tags_[0].pts,
    bytes: new Uint8Array([0])
  }, {
    pts: tags_[0].pts,
    bytes: new Uint8Array([0])
  });

  dataMessage = createDataMessage(tags_);

  // mock the GOP structure + metadata tags
  // if we see a metadata tag, that means the next tag will also be a metadata tag with
  // keyFrame true and the tag after that will be the keyFrame
  // e.g.
  // { keyFrame: false, metaDataTag: true},
  // { keyFrame: true, metaDataTag: true},
  // { keyFrame: true, metaDataTag: false}
  dataMessage.data.segment.tags.videoTags[0].metaDataTag = true;
  dataMessage.data.segment.tags.videoTags[1].metaDataTag = true;
  dataMessage.data.segment.tags.videoTags[1].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[2].keyFrame = true;

  // no metadata tags in front of this key to test the case where mux.js does not prepend
  // the metadata tags
  dataMessage.data.segment.tags.videoTags[5].keyFrame = true;

  dataMessage.data.segment.tags.videoTags[7].metaDataTag = true;
  dataMessage.data.segment.tags.videoTags[8].metaDataTag = true;
  dataMessage.data.segment.tags.videoTags[8].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[9].keyFrame = true;

  dataMessage.data.segment.tags.videoTags[12].metaDataTag = true;
  dataMessage.data.segment.tags.videoTags[13].metaDataTag = true;
  dataMessage.data.segment.tags.videoTags[13].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[14].keyFrame = true;

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  // seek to 7 seconds into the new swegment
  this.mediaSource.tech_.seeking = function() {
    return true;
  };
  currentTime = 10 + 7;
  this.mediaSource.tech_.trigger('seeking');
  sourceBuffer.appendBuffer(new Uint8Array(10));
  this.swfCalls.length = 0;
  timers.runAll();

  QUnit.deepEqual(this.swfCalls[0], { call: 'adjustCurrentTime', value: 15 });
  QUnit.deepEqual(this.swfCalls[1].arguments[0], [5, 5, 5, 6, 7, 8, 8, 8, 9],
            '10 tags are appended, 4 of which are metadata tags');
});

QUnit.test('drops all tags if target pts append time does not fall within segment', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let i = 10;
  let currentTime;
  let tags_ = [];

  this.mediaSource.tech_.currentTime = function() {
    return currentTime;
  };

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  // push a tag into the buffer to establish the starting PTS value
  currentTime = 0;

  let dataMessage = createDataMessage([{
    pts: 19 * 1000,
    bytes: new Uint8Array(1)
  }]);

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  timers.runAll();

  sourceBuffer.appendBuffer(new Uint8Array(10));
  timers.runAll();

  // mock out a new segment of FLV tags, starting 10s after the
  // starting PTS value
  while (i--) {
    tags_.unshift(
      {
        pts: (i * 1000) + (19 * 1000),
        bytes: new Uint8Array([i])
      }
    );
  }

  dataMessage = createDataMessage(tags_);

  // mock the GOP structure
  dataMessage.data.segment.tags.videoTags[0].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[3].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[5].keyFrame = true;
  dataMessage.data.segment.tags.videoTags[8].keyFrame = true;

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  // seek to 7 seconds into the new swegment
  this.mediaSource.tech_.seeking = function() {
    return true;
  };
  currentTime = 10 + 7;
  this.mediaSource.tech_.trigger('seeking');
  sourceBuffer.appendBuffer(new Uint8Array(10));
  this.swfCalls.length = 0;
  timers.runAll();

  QUnit.equal(this.swfCalls.length, 0, 'dropped all tags and made no swf calls');
});

QUnit.test('seek targeting accounts for changing timestampOffsets', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let i = 10;
  let tags_ = [];
  let currentTime;

  this.mediaSource.tech_.currentTime = function() {
    return currentTime;
  };

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  let dataMessage = createDataMessage([{
    pts: 19 * 1000,
    bytes: new Uint8Array(1)
  }]);

  // push a tag into the buffer to establish the starting PTS value
  currentTime = 0;
  sourceBuffer.transmuxer_.onmessage(dataMessage);

  timers.runAll();

  // to seek across a discontinuity:
  // 1. set the timestamp offset to the media timeline position for
  //    the start of the segment
  // 2. set currentTime to the desired media timeline position
  sourceBuffer.timestampOffset = 22;
  currentTime = sourceBuffer.timestampOffset + 3.5;
  this.mediaSource.tech_.seeking = function() {
    return true;
  };

  // the new segment FLV tags are at disjoint PTS positions
  while (i--) {
    tags_.unshift({
      // (101 * 1000) !== the old PTS offset
      pts: (i * 1000) + (101 * 1000),
      bytes: new Uint8Array([i + sourceBuffer.timestampOffset])
    });
  }

  dataMessage = createDataMessage(tags_);
  // mock gop start at seek point
  dataMessage.data.segment.tags.videoTags[3].keyFrame = true;

  sourceBuffer.transmuxer_.onmessage(dataMessage);

  this.mediaSource.tech_.trigger('seeking');
  this.swfCalls.length = 0;
  timers.runAll();

  QUnit.equal(this.swfCalls[0].value, 25, 'adjusted current time');
  QUnit.deepEqual(this.swfCalls[1].arguments[0],
            [25, 26, 27, 28, 29, 30, 31],
            'filtered the appended tags');
});

QUnit.test('calling endOfStream sets mediaSource readyState to ended', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  /* eslint-disable camelcase */
  this.mediaSource.swfObj.vjs_endOfStream = () => {
    this.swfCalls.push('endOfStream');
  };
  /* eslint-enable camelcase */
  sourceBuffer.addEventListener('updateend', () => {
    this.mediaSource.endOfStream();
  });

  this.swfCalls.length = 0;
  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));

  timers.runAll();

  QUnit.strictEqual(sourceBuffer.mediaSource_.readyState,
    'ended',
    'readyState is \'ended\'');
  QUnit.strictEqual(this.swfCalls.length, 2, 'made two calls to swf');
  QUnit.deepEqual(this.swfCalls.shift().arguments[0],
            [0, 1],
            'contains the data');

  QUnit.ok(this.swfCalls.shift().indexOf('endOfStream') === 0,
      'the second call should be for the updateend');

  QUnit.strictEqual(timers.length, 0, 'no more appends are scheduled');
});

QUnit.test('opens the stream on sourceBuffer.appendBuffer after endOfStream', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let foo = () => {
    this.mediaSource.endOfStream();
    sourceBuffer.removeEventListener('updateend', foo);
  };

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  /* eslint-disable camelcase */
  this.mediaSource.swfObj.vjs_endOfStream = () => {
    this.swfCalls.push('endOfStream');
  };
  /* eslint-enable camelcase */
  sourceBuffer.addEventListener('updateend', foo);

  this.swfCalls.length = 0;
  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));

  timers.runAll();

  QUnit.strictEqual(this.swfCalls.length, 2, 'made two calls to swf');
  QUnit.deepEqual(this.swfCalls.shift().arguments[0],
            [0, 1],
            'contains the data');

  QUnit.equal(this.swfCalls.shift(),
        'endOfStream',
        'the second call should be for the updateend');

  sourceBuffer.appendBuffer(new Uint8Array([2, 3]));
  // remove previous video pts save because mock appends don't have actual timing data
  sourceBuffer.videoBufferEnd_ = NaN;
  timers.runAll();

  QUnit.strictEqual(this.swfCalls.length, 1, 'made one more append');
  QUnit.deepEqual(this.swfCalls.shift().arguments[0],
            [2, 3],
            'contains the third and fourth bytes');
  QUnit.strictEqual(
    sourceBuffer.mediaSource_.readyState,
    'open',
    'The streams should be open if more bytes are appended to an "ended" stream'
  );
  QUnit.strictEqual(timers.length, 0, 'no more appends are scheduled');
});

QUnit.test('abort() clears any buffered input', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  this.swfCalls.length = 0;
  sourceBuffer.appendBuffer(new Uint8Array([0]));
  sourceBuffer.abort();

  timers.pop()();
  QUnit.strictEqual(this.swfCalls.length, 1, 'called the swf');
  QUnit.strictEqual(this.swfCalls[0], 'abort', 'invoked abort');
});
// requestAnimationFrame is heavily throttled or unscheduled when
// the browser tab running contrib-media-sources is in a background
// tab. If that happens, video data can continuously build up in
// memory and cause the tab or browser to crash.
QUnit.test('does not use requestAnimationFrame', function() {
  let oldRFA = window.requestAnimationFrame;
  let requests = 0;
  let sourceBuffer;

  window.requestAnimationFrame = function() {
    requests++;
  };

  sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  sourceBuffer.transmuxer_.postMessage = postMessage_;
  sourceBuffer.appendBuffer(new Uint8Array([0, 1, 2, 3]));
  while (timers.length) {
    timers.pop()();
  }
  QUnit.equal(requests, 0, 'no calls to requestAnimationFrame were made');
  window.requestAnimationFrame = oldRFA;
});
QUnit.test('updating is true while an append is in progress', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let ended = false;

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  sourceBuffer.addEventListener('updateend', function() {
    ended = true;
  });

  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));

  QUnit.equal(sourceBuffer.updating, true, 'updating is set');

  while (!ended) {
    timers.pop()();
  }
  QUnit.equal(sourceBuffer.updating, false, 'updating is unset');
});

QUnit.test('throws an error if append is called while updating', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');

  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  sourceBuffer.transmuxer_.postMessage = postMessage_;

  QUnit.throws(function() {
    sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  }, function(e) {
    return e.name === 'InvalidStateError' &&
      e.code === window.DOMException.INVALID_STATE_ERR;
  }, 'threw an InvalidStateError');
});

QUnit.test('stops updating if abort is called', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let updateEnds = 0;

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  sourceBuffer.addEventListener('updateend', function() {
    updateEnds++;
  });
  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));

  sourceBuffer.abort();
  QUnit.equal(sourceBuffer.updating, false, 'no longer updating');
  QUnit.equal(updateEnds, 1, 'triggered updateend');
});

QUnit.test('forwards duration overrides to the SWF', function() {
  /* eslint-disable no-unused-vars */
  let ignored = this.mediaSource.duration;
  /* eslint-enable no-unused-vars */

  QUnit.deepEqual(this.swfCalls[1], {
    attr: 'duration'
  }, 'requests duration from the SWF');

  this.mediaSource.duration = 101.3;
  // Setting a duration results in two calls to the swf
  // Ignore the first call (this.swfCalls[2]) as it was just to get the
  // current duration
  QUnit.deepEqual(this.swfCalls[3], {
    attr: 'duration', value: 101.3
  }, 'set the duration override');

});

QUnit.test('returns NaN for duration before the SWF is ready', function() {
  this.mediaSource.swfObj = null;

  QUnit.ok(isNaN(this.mediaSource.duration), 'duration is NaN');
});

QUnit.test('calculates the base PTS for the media', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let tags_ = [];

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  // seek to 15 seconds
  this.player.tech_.seeking = function() {
    return true;
  };
  this.player.tech_.currentTime = function() {
    return 15;
  };
  // FLV tags for this segment start at 10 seconds in the media
  // timeline
  tags_.push(
    // zero in the media timeline is PTS 3
    { pts: (10 + 3) * 1000, bytes: new Uint8Array([10]) },
    { pts: (15 + 3) * 1000, bytes: new Uint8Array([15]) }
  );

  let dataMessage = createDataMessage(tags_);

  // mock gop start at seek point
  dataMessage.data.segment.tags.videoTags[1].keyFrame = true;
  sourceBuffer.transmuxer_.onmessage(dataMessage);

  // let the source buffer know the segment start time
  sourceBuffer.timestampOffset = 10;

  this.swfCalls.length = 0;
  timers.runAll();

  QUnit.equal(this.swfCalls.length, 1, 'made a SWF call');
  QUnit.deepEqual(this.swfCalls[0].arguments[0], [15], 'dropped the early tag');
});

QUnit.test('remove fires update events', function() {
  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
  let events = [];

  sourceBuffer.transmuxer_.postMessage = postMessage_;
  sourceBuffer.on(['update', 'updateend'], function(event) {
    events.push(event.type);
  });

  sourceBuffer.remove(0, 1);
  QUnit.deepEqual(events, ['update', 'updateend'], 'fired update events');
  QUnit.equal(sourceBuffer.updating, false, 'finished updating');
});

QUnit.test('passes endOfStream network errors to the tech', function() {
  this.mediaSource.readyState = 'ended';
  this.mediaSource.endOfStream('network');
  QUnit.equal(this.player.tech_.error().code, 2, 'set a network error');
});

QUnit.test('passes endOfStream decode errors to the tech', function() {
  this.mediaSource.readyState = 'ended';
  this.mediaSource.endOfStream('decode');

  QUnit.equal(this.player.tech_.error().code, 3, 'set a decode error');
});

QUnit.test('has addSeekableRange()', function() {
  QUnit.ok(this.mediaSource.addSeekableRange_, 'has addSeekableRange_');
});

QUnit.test('fires loadedmetadata after first segment append', function() {
  let loadedmetadataCount = 0;

  this.mediaSource.tech_.on('loadedmetadata', () => loadedmetadataCount++);

  let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');

  sourceBuffer.transmuxer_.postMessage = postMessage_;

  QUnit.equal(loadedmetadataCount, 0, 'loadedmetadata not called on buffer creation');
  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  QUnit.equal(loadedmetadataCount, 0, 'loadedmetadata not called on segment append');
  timers.runAll();
  QUnit.equal(loadedmetadataCount, 1, 'loadedmetadata fires after first append');
  sourceBuffer.appendBuffer(new Uint8Array([0, 1]));
  timers.runAll();
  QUnit.equal(loadedmetadataCount, 1, 'loadedmetadata does not fire after second append');
});
