/*! @license
 * Shaka Player
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

// These tests are for testing Shaka Player's integration with
// |HTMLMediaElement.src=|. These tests are to verify that all |shaka.Player|
// public methods behaviour correctly when playing content video |src=|.
describe('Player Src Equals', () => {
  const Util = shaka.test.Util;

  const SMALL_MP4_CONTENT_URI = '/base/test/test/assets/small.mp4';

  /** @type {!HTMLVideoElement} */
  let video;
  /** @type {!shaka.Player} */
  let player;
  /** @type {!shaka.util.EventManager} */
  let eventManager;

  beforeAll(() => {
    video = shaka.test.UiUtils.createVideoElement();
    document.body.appendChild(video);
  });

  beforeEach(() => {
    player = new shaka.Player();
    player.addEventListener('error', fail);
    eventManager = new shaka.util.EventManager();
  });

  afterEach(async () => {
    await player.destroy();

    eventManager.release();
  });

  afterAll(() => {
    document.body.removeChild(video);
  });

  // This test verifies that we can successfully load content that requires us
  // to use |src=|.
  it('loads content', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
  });

  // This test verifys that we can successfully unload content that required
  // |src=| to load.
  it('unloads content', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    await player.unload(/* initMediaSource= */ false);
  });

  it('can get asset uri after loading', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(player.getAssetUri()).toBe(SMALL_MP4_CONTENT_URI);
  });

  // TODO: test an HLS live stream on platforms supporting native HLS
  it('considers simple mp4 content to be VOD"', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(player.isLive()).toBeFalsy();
    expect(player.isInProgress()).toBeFalsy();
  });

  // TODO: test an audio-only mp4
  // TODO: test audio-only HLS on platforms with native HLS
  it('considers audio-video mp4 content to be audio-video', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(player.isAudioOnly()).toBeFalsy();
  });

  it('allow load with startTime', async () => {
    const startTime = 5;
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, startTime);

    // For some reason, the delta on IE & Edge can be 0.1 for this content and
    // this start time.  It may be rounded to a key frame or something.
    const delta = Math.abs(video.currentTime - startTime);
    expect(delta).toBeLessThan(0.2);
  });

  // Since we don't have any manifest data, we must assume that we can seek
  // anywhere in the presentation; end-time will come from the media element.
  it('allows seeking throughout the presentation', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // For src=, the seekRange is based on video.seekable, so wait for this
    // event before proceeding to check seekRange.
    await new Promise((resolve) => {
      eventManager.listenOnce(video, 'canplay', resolve);
    });

    // The seek range should match the duration of the content.
    const seekRange = player.seekRange();
    expect(seekRange.start).toBeCloseTo(0);
    expect(seekRange.end).toBeCloseTo(video.duration);
    expect(video.duration).not.toBeCloseTo(0);

    // Start playback and wait for the playhead to move.
    video.play();
    await Util.waitForMovementOrFailOnTimeout(
        eventManager, video, /* timeout= */10);

    // Make sure the playhead is roughly where we expect it to be before
    // seeking.
    expect(video.currentTime).toBeGreaterThan(0);
    expect(video.currentTime).toBeLessThan(2.0);

    // Trigger a seek and then wait for the seek to take effect.
    // This seek target is very close to the duration of the video.
    video.currentTime = 10;
    await Util.waitForMovementOrFailOnTimeout(
        eventManager, video, /* timeout= */10);

    // Make sure the playhead is roughly where we expect it to be after
    // seeking.
    expect(video.currentTime).toBeGreaterThan(9.5);
    expect(video.currentTime).toBeLessThan(10.5);
  });

  // TODO: test src= with DRM
  // TODO: test HLS without DRM on platforms with native HLS
  // TODO: test HLS with DRM on platforms with native HLS
  it('considers simple content to be clear ', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    expect(player.keySystem()).toBe('');
    expect(player.drmInfo()).toBe(null);
    expect(player.getExpiration()).toBe(Infinity);
  });

  // Compared to media source, when loading content with src=, we will have less
  // accurate information. However we can still report what the media element
  // surfaces.
  it('reports buffering information', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // For playback to begin so that we have some content buffered.
    video.play();
    await Util.waitForMovementOrFailOnTimeout(
        eventManager, video, /* timeout= */10);

    const buffered = player.getBufferedInfo();

    // We don't have per-stream insights.
    expect(buffered.audio).toEqual([]);
    expect(buffered.video).toEqual([]);
    expect(buffered.text).toEqual([]);

    // We should have an overall view of buffering. We waited for playback,
    // so we should have some content buffered.
    expect(buffered.total).toBeTruthy();
    expect(buffered.total.length).toBe(1);
    expect(buffered.total[0].start).toBeCloseTo(0);
    expect(buffered.total[0].end).toBeGreaterThan(0);
  });

  // When we load content via src=, can we use the trick play controls to
  // control the playback rate.
  it('can control trick play rate', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // Let playback run for a little.
    video.play();
    await Util.waitForMovementOrFailOnTimeout(
        eventManager, video, /* timeout= */10);

    let videoRateChange = false;
    let playerRateChange = false;
    eventManager.listen(video, 'ratechange', () => {
      videoRateChange = true;
    });
    eventManager.listen(player, 'ratechange', () => {
      playerRateChange = true;
    });

    // Enabling trick play should change our playback rate to the same rate.
    player.trickPlay(2);
    expect(video.playbackRate).toBe(2);

    // It should also have fired a 'ratechange' event on both video and player.
    // We may have to delay a short time to see the events, though.
    await shaka.test.Util.shortDelay();
    expect(videoRateChange).toBe(true);
    expect(playerRateChange).toBe(true);

    // Let playback continue playing for a bit longer.
    await shaka.test.Util.delay(2);

    // Cancelling trick play should return our playback rate to normal.
    player.cancelTrickPlay();
    expect(video.playbackRate).toBe(1);
  });

  // TODO: test audio-video mp4 content on platforms with audioTracks API
  it('reports variant tracks for video-only mp4 content', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // On platforms with audioTracks, such as Safari, we get one track here.
    if (video.audioTracks) {
      expect(player.getVariantTracks().length).toBe(1);
    } else {
      expect(player.getVariantTracks().length).toBe(0);
    }
  });

  // TODO: test HLS on platforms with native HLS
  it('allows selecting variant tracks', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // We can only get a variant track here on certain browsers.
    const tracks = player.getVariantTracks();

    // If we have tracks, we should be able to select them.
    if (tracks.length) {
      // The test fails if this throws.
      player.selectVariantTrack(tracks[0]);
    }
  });

  // TODO: test HLS with text tracks on platforms with native HLS
  it('reports no text tracks for simple mp4 content', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(player.getTextTracks()).toEqual([]);
  });

  // TODO: test HLS on platforms with native HLS
  it('allows selecting text tracks', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // We can only get a text track here on certain browsers.
    const tracks = player.getTextTracks();

    // If we have tracks, we should be able to select them.
    if (tracks.length) {
      // The test fails if this throws.
      player.selectTextTrack(tracks[0]);
    }
  });

  it('ignores extra text track on the video element', async () => {
    // The extra text track with label "Shaka Player TextTrack" should not be
    // listed.
    video.addTextTrack('subtitles', /* label= */ shaka.Player.TextTrackLabel);

    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(player.getTextTracks()).toEqual([]);
  });


  // TODO: test HLS on platforms with native HLS
  it('returns no languages or roles for simple mp4 content', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // On platforms with audioTracks, such as Safari, we get one track, with
    // language set to whatever is in the mp4.
    if (video.audioTracks) {
      expect(player.getAudioLanguages()).toEqual(['en']);
      // Note that some browsers, such as Safari, say this is the 'main'
      // role, while others, such as Edge, do not.  For the purposes of this
      // test, it doesn't matter what the role is.
      expect(player.getAudioLanguagesAndRoles()).toEqual(
          [{language: 'en', role: jasmine.any(String)}]);
    } else {
      expect(player.getAudioLanguages()).toEqual([]);
      expect(player.getAudioLanguagesAndRoles()).toEqual([]);
    }

    expect(player.getTextLanguages()).toEqual([]);
    expect(player.getTextLanguagesAndRoles()).toEqual([]);
  });

  // Even though we loaded content using |src=| we should still be able to get
  // the playhead position as normal.
  it('can get the playhead position', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(video.readyState).toBeGreaterThan(0);

    expect(video.currentTime).toBeCloseTo(0);

    // Start playback and wait. We should see the playhead move.
    video.play();
    await Util.waitForMovementOrFailOnTimeout(
        eventManager, video, /* timeout= */10);
    await shaka.test.Util.delay(1.5);

    // When checking if the playhead moved, check for less progress than time we
    // delayed. This will allow for some latency between |play| and playback
    // starting.
    expect(video.currentTime).toBeGreaterThan(1);
  });

  // Even though we are not using all the internals, we should still get some
  // meaningful statistics.
  it('can get stats', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    // Wait some time for playback to start so that we will have a load latency
    // value.
    video.play();
    await Util.waitForMovementOrFailOnTimeout(
        eventManager, video, /* timeout= */10);

    // Get the stats and check that some stats have been filled in.
    const stats = player.getStats();
    expect(stats).toBeTruthy();
    expect(stats.loadLatency).toBeGreaterThan(0);
    expect(stats.manifestTimeSeconds).toBeNaN(); // There's no manifest.
    expect(stats.drmTimeSeconds).toBeNaN(); // There's no DRM.
  });

  // Because we have no manifest, we can't add text tracks.
  it('cannot add text tracks', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);

    expect(() => {
      player.addTextTrack(
          'test:need-a-uri-for-text',
          'en-US',
          'main',
          'text/mp4');
    }).toThrow();
  });

  // Since we are not in-charge of streaming, calling |retryStreaming| should
  // have no effect.
  it('requesting streaming retry does nothing', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(player.retryStreaming()).toBeFalsy();
  });

  // Since we are not loading a manifest, we can't return a manifest.
  // |getManifest| should return |null|.
  it('has no manifest to return', async () => {
    await loadWithSrcEquals(SMALL_MP4_CONTENT_URI, /* startTime= */ null);
    expect(player.getManifest()).toBeFalsy();
  });

  /**
   * @param {string} contentUri
   * @param {?number} startTime
   * @return {!Promise}
   */
  async function loadWithSrcEquals(contentUri, startTime) {
    /** @type {!shaka.util.EventManager} */
    const eventManager = new shaka.util.EventManager();

    const ready = new Promise((resolve) => {
      eventManager.listenOnce(video, 'loadeddata', resolve);
    });

    await player.attach(video, /* initMediaSource= */ false);
    await player.load(contentUri, startTime);

    // Wait until the media element is ready with content. Waiting until this
    // point ensures it is safe to interact with the media element.
    await ready;

    // The initial seek is triggered about the same time this ready promise
    // resolves.  Wait (with timeout) for movement, so that the initial-seek
    // promise chain has time to resolve before we test our expectations.
    if (startTime != null) {
      const waiter = new shaka.test.Waiter(eventManager);
      if (video.currentTime == 0) {
        // A one-second timeout is too short for Chromecast, but a longer
        // timeout doesn't hurt anyone.  This will always resolve as fast as
        // playback can actually start.
        await waiter.timeoutAfter(5).failOnTimeout(true).waitForMovement(video);
      }
    }

    eventManager.release();
  }
});
