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

describe('SimpleAbrManager', () => {
  const sufficientBWMultiplier = 1.06;
  const defaultBandwidthEstimate = 500e3; // 500kbps
  const oldDateNow = Date.now;

  /** @type {shaka.extern.AbrConfiguration} */
  let config;
  /** @type {!jasmine.Spy} */
  let switchCallback;
  /** @type {!shaka.abr.SimpleAbrManager} */
  let abrManager;
  /** @type {shaka.extern.Manifest} */
  let manifest;
  /** @type {!Array.<shaka.extern.Variant>} */
  let variants;

  beforeEach(() => {
    Date.now = () => 0;
    switchCallback = jasmine.createSpy('switchCallback');

    // Keep unsorted.
    manifest = shaka.test.ManifestGenerator.generate((manifest) => {
      manifest.addVariant(100, (variant) => {
        variant.bandwidth = 4e5;  // 400 kbps
        variant.addAudio(0);
        variant.addVideo(1);
      });
      manifest.addVariant(101, (variant) => {
        variant.bandwidth = 1e6;  // 1000 kbps
        variant.addAudio(2);
        variant.addVideo(3);
      });
      manifest.addVariant(102, (variant) => {
        variant.bandwidth = 5e5;  // 500 kbps
        variant.addAudio(4);
        variant.addVideo(5);
      });
      manifest.addVariant(103, (variant) => {
        variant.bandwidth = 2e6;
        variant.addAudio(6);
        variant.addVideo(7);
      });
      manifest.addVariant(104, (variant) => {
        variant.bandwidth = 2e6;  // Identical on purpose.
        variant.addAudio(8);
        variant.addVideo(9);
      });
      manifest.addVariant(105, (variant) => {
        variant.bandwidth = 6e5;
        variant.addAudio(10);
        variant.addVideo(11);
      });
      manifest.addTextStream(20);
      manifest.addTextStream(21);
    });

    config = shaka.util.PlayerConfiguration.createDefault().abr;
    config.defaultBandwidthEstimate = defaultBandwidthEstimate;

    variants = manifest.variants;

    abrManager = new shaka.abr.SimpleAbrManager();
    abrManager.init(shaka.test.Util.spyFunc(switchCallback));
    abrManager.configure(config);
    abrManager.setVariants(variants);
  });

  afterEach(() => {
    abrManager.stop();
    Date.now = oldDateNow;
  });

  it('can choose audio and video Streams right away', () => {
    const chosen = abrManager.chooseVariant();
    expect(chosen).not.toBe(null);
  });

  it('uses custom default estimate', () => {
    config.defaultBandwidthEstimate = 3e6;
    config.useNetworkInformation = false;
    abrManager.configure(config);
    const chosen = abrManager.chooseVariant();
    expect(chosen.id).toBe(104);
  });

  it('can handle empty variants', () => {
    abrManager.setVariants([]);
    const chosen = abrManager.chooseVariant();
    expect(chosen).toBe(null);
  });

  it('can choose from audio only variants', () => {
    manifest = shaka.test.ManifestGenerator.generate((manifest) => {
      manifest.addVariant(0, (variant) => {
        variant.bandwidth = 4e5;
        variant.addAudio(0);
      });
      manifest.addVariant(1, (variant) => {
        variant.bandwidth = 1e6;
        variant.addAudio(2);
      });
    });

    abrManager.setVariants(manifest.variants);
    const chosen = abrManager.chooseVariant();
    expect(chosen).not.toBe(null);
    expect(chosen.audio).not.toBe(null);
    expect(chosen.video).toBe(null);
  });

  it('can choose from video only variants', () => {
    manifest = shaka.test.ManifestGenerator.generate((manifest) => {
      manifest.addVariant(0, (variant) => {
        variant.bandwidth = 4e5;
        variant.addVideo(0);
      });
      manifest.addVariant(1, (variant) => {
        variant.bandwidth = 1e6;
        variant.addVideo(2);
      });
    });

    abrManager.setVariants(manifest.variants);
    const chosen = abrManager.chooseVariant();
    expect(chosen).not.toBe(null);
    expect(chosen.audio).toBe(null);
    expect(chosen.video).not.toBe(null);
  });

  for (const bandwidth of [5e5, 6e5]) {
    // Simulate some segments being downloaded just above the desired
    // bandwidth.
    const bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    const bandwidthKbps = bandwidth / 1000.0;
    const description = 'picks correct Variant at ' + bandwidthKbps + ' kbps';

    it(description, () => {
      abrManager.setVariants(variants);
      abrManager.chooseVariant();

      abrManager.segmentDownloaded(1000, bytesPerSecond);
      abrManager.segmentDownloaded(1000, bytesPerSecond);

      abrManager.enable();

      // Make another call to segmentDownloaded() so switchCallback() is
      // called.
      abrManager.segmentDownloaded(1000, bytesPerSecond);

      // Expect variants 2 to be chosen for bandwidth = 5e5
      // and variant 5 - for bandwidth = 6e5
      const expectedVariant = (bandwidth == 6e5) ? variants[5] : variants[2];

      expect(switchCallback).toHaveBeenCalledWith(expectedVariant);
    });
  }

  it('can handle 0 duration segments', () => {
    // Makes sure bandwidth estimate doesn't get set to NaN
    // when a 0 duration segment is encountered.
    // https://github.com/google/shaka-player/issues/582
    const bandwidth = 5e5;
    const bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    abrManager.setVariants(variants);
    abrManager.chooseVariant();

    // 0 duration segment shouldn't cause us to get stuck on the lowest variant
    abrManager.segmentDownloaded(0, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    abrManager.enable();

    abrManager.segmentDownloaded(1000, bytesPerSecond);

    expect(abrManager.getBandwidthEstimate()).toBeTruthy();
  });

  it('picks lowest variant when there is insufficient bandwidth', () => {
    const bandwidth = 2e6;

    abrManager.setVariants(variants);
    abrManager.chooseVariant();

    // Simulate some segments being downloaded just above the desired
    // bandwidth.
    const bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    abrManager.enable();

    // Make another call to segmentDownloaded() so switchCallback() is
    // called.
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    // Expect variants 4 to be chosen
    const expectedVariant = variants[4];

    expect(switchCallback).toHaveBeenCalledWith(expectedVariant);
  });

  it('does not call switchCallback() if not enabled', () => {
    const bandwidth = 5e5;
    const bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    abrManager.setVariants(variants);
    abrManager.chooseVariant();

    // Don't enable AbrManager.
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    expect(switchCallback).not.toHaveBeenCalled();
  });

  it('does not call switchCallback() in switch interval', () => {
    let bandwidth = 5e5;
    let bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    abrManager.setVariants(variants);
    abrManager.chooseVariant();

    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    abrManager.enable();

    abrManager.segmentDownloaded(1000, bytesPerSecond);
    expect(switchCallback).toHaveBeenCalled();
    switchCallback.calls.reset();

    // Simulate drop in bandwidth.
    bandwidth = 2e6;
    bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    // Stay inside switch interval.
    Date.now = () => (config.switchInterval - 2) * 1e3;
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    expect(switchCallback).not.toHaveBeenCalled();

    // Move outside switch interval.
    Date.now = () => (config.switchInterval + 2) * 1e3;
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    expect(switchCallback).toHaveBeenCalled();
  });

  it('does not clear the buffer on upgrade', () => {
    // Simulate some segments being downloaded at a high rate, to trigger an
    // upgrade.
    const bandwidth = 5e5;
    const bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    abrManager.setVariants(variants);
    abrManager.chooseVariant();

    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    abrManager.enable();

    // Make another call to segmentDownloaded(). switchCallback() will be
    // called to upgrade.
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    // The second parameter is missing to indicate that the buffer should not be
    // cleared.
    expect(switchCallback).toHaveBeenCalledWith(jasmine.any(Object));
  });

  it('does not clear the buffer on downgrade', () => {
    // Simulate some segments being downloaded at a low rate, to trigger a
    // downgrade.
    const bandwidth = 5e5;
    const bytesPerSecond = sufficientBWMultiplier * bandwidth / 8.0;

    // Set the default high so that the initial choice will be high-quality.
    config.defaultBandwidthEstimate = 4e6;
    abrManager.configure(config);

    abrManager.setVariants(variants);
    abrManager.chooseVariant();

    abrManager.segmentDownloaded(1000, bytesPerSecond);
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    abrManager.enable();

    // Make another call to segmentDownloaded(). switchCallback() will be
    // called to downgrade.
    abrManager.segmentDownloaded(1000, bytesPerSecond);

    // The second parameter is missing to indicate that the buffer should not be
    // cleared.
    expect(switchCallback).toHaveBeenCalledWith(jasmine.any(Object));
  });

  it('will respect restrictions', () => {
    manifest = shaka.test.ManifestGenerator.generate((manifest) => {
      manifest.addVariant(10, (variant) => {
        variant.bandwidth = 1e5;
        variant.addVideo(0, (stream) => {
          stream.size(50, 50);
        });
      });
      manifest.addVariant(11, (variant) => {
        variant.bandwidth = 2e5;
        variant.addVideo(1, (stream) => {
          stream.size(200, 200);
        });
      });
    });

    abrManager.setVariants(manifest.variants);
    let chosen = abrManager.chooseVariant();
    expect(chosen.id).toBe(11);

    config.restrictions.maxWidth = 100;
    abrManager.configure(config);

    chosen = abrManager.chooseVariant();
    expect(chosen.id).toBe(10);
  });

  it('uses lowest-bandwidth variant when restrictions cannot be met', () => {
    manifest = shaka.test.ManifestGenerator.generate((manifest) => {
      manifest.addVariant(10, (variant) => {
        variant.bandwidth = 1e5;
        variant.addVideo(0, (stream) => {
          stream.size(50, 50);
        });
      });
      manifest.addVariant(11, (variant) => {
        variant.bandwidth = 2e5;
        variant.addVideo(1, (stream) => {
          stream.size(200, 200);
        });
      });
    });

    abrManager.setVariants(manifest.variants);
    let chosen = abrManager.chooseVariant();
    expect(chosen.id).toBe(11);

    // This restriction cannot be met, but we shouldn't fail.
    config.restrictions.maxWidth = 1;
    abrManager.configure(config);

    chosen = abrManager.chooseVariant();
    expect(chosen.id).toBe(10);
  });
});
