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

describe('MpdUtils', () => {
  const MpdUtils = shaka.dash.MpdUtils;

  describe('fillUriTemplate', () => {
    it('handles a single RepresentationID identifier', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$RepresentationID$.mp4',
              '100', null, null, null).toString()).toBe('/example/100.mp4');

      // RepresentationID cannot use a width specifier.
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$RepresentationID%01d$.mp4',
              '100', null, null, null).toString()).toBe('/example/100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$RepresentationID$.mp4',
              null, null, null, null).toString())
          .toBe('/example/$RepresentationID$.mp4');
    });

    it('handles a single Number identifier', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Number$.mp4',
              null, 100, null, null).toString()).toBe('/example/100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Number%05d$.mp4',
              null, 100, null, null).toString()).toBe('/example/00100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Number$.mp4',
              null, null, null, null).toString())
          .toBe('/example/$Number$.mp4');
    });

    it('handles a single Bandwidth identifier', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Bandwidth$.mp4',
              null, null, 100, null).toString()).toBe('/example/100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Bandwidth%05d$.mp4',
              null, null, 100, null).toString()).toBe('/example/00100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Bandwidth$.mp4',
              null, null, null, null).toString())
          .toBe('/example/$Bandwidth$.mp4');
    });

    it('handles a single Time identifier', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Time$.mp4',
              null, null, null, 100).toString()).toBe('/example/100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Time%05d$.mp4',
              null, null, null, 100).toString()).toBe('/example/00100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Time$.mp4',
              null, null, null, null).toString())
          .toBe('/example/$Time$.mp4');
    });

    it('handles rounding errors for calculated Times', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Time$.mp4',
              null, null, null, 100.0001).toString()).toBe('/example/100.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Time%05d$.mp4',
              null, null, null, 99.9999).toString()).toBe('/example/00100.mp4');
    });

    it('handles multiple identifiers', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$RepresentationID$_$Number$_$Bandwidth$_$Time$.mp4',
              '1', 2, 3, 4).toString()).toBe('/example/1_2_3_4.mp4');

      // No spaces.
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$RepresentationID$$Number$$Bandwidth$$Time$.mp4',
              '1', 2, 3, 4).toString()).toBe('/example/1234.mp4');

      // Different order.
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Bandwidth$_$Time$_$RepresentationID$_$Number$.mp4',
              '1', 2, 3, 4).toString()).toBe('/example/3_4_1_2.mp4');

      // Single width.
      expect(
          MpdUtils.fillUriTemplate(
              '$RepresentationID$_$Number%01d$_$Bandwidth%01d$_$Time%01d$',
              '1', 2, 3, 400).toString()).toBe('1_2_3_400');

      // Different widths.
      expect(
          MpdUtils.fillUriTemplate(
              '$RepresentationID$_$Number%02d$_$Bandwidth%02d$_$Time%02d$',
              '1', 2, 3, 4).toString()).toBe('1_02_03_04');

      // Double $$.
      expect(
          MpdUtils.fillUriTemplate(
              '$$/$RepresentationID$$$$Number$$$$Bandwidth$$$$Time$$$.$$',
              '1', 2, 3, 4).toString()).toBe('$/1$2$3$4$.$');
    });

    it('handles invalid identifiers', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Garbage$.mp4',
              '1', 2, 3, 4).toString()).toBe('/example/$Garbage$.mp4');

      expect(
          MpdUtils.fillUriTemplate(
              '/example/$Time.mp4',
              '1', 2, 3, 4).toString()).toBe('/example/$Time.mp4');
    });

    it('handles non-decimal format specifiers', () => {
      expect(
          MpdUtils.fillUriTemplate(
              '/$Number%05x$_$Number%01X$_$Number%01u$_$Number%01o$.mp4',
              '', 180, 0, 0).toString()).toBe('/000b4_B4_180_264.mp4');
    });
  });

  describe('createTimeline', () => {
    it('works in normal case', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, 0),
        createTimePoint(20, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 30},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles null start time', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(null, 10, 0),
        createTimePoint(null, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 30},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles gaps', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(15, 10, 0),
      ];
      const result = [
        {start: 0, end: 15},
        {start: 15, end: 25},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles overlap', () => {
      const timePoints = [
        createTimePoint(0, 15, 0),
        createTimePoint(10, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles repetitions', () => {
      const timePoints = [
        createTimePoint(0, 10, 5),
        createTimePoint(60, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 30},
        {start: 30, end: 40},
        {start: 40, end: 50},
        {start: 50, end: 60},
        {start: 60, end: 70},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles null repeat', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, null),
        createTimePoint(20, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 30},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles repetitions with gap', () => {
      const timePoints = [
        createTimePoint(0, 10, 2),
        createTimePoint(35, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 35},
        {start: 35, end: 45},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles negative repetitions', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, -1),
        createTimePoint(40, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 30},
        {start: 30, end: 40},
        {start: 40, end: 50},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles negative repetitions with uneven border', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, -1),
        createTimePoint(45, 5, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 30},
        {start: 30, end: 40},
        {start: 40, end: 45},
        {start: 45, end: 50},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles negative repetitions w/ bad next start time', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, -1),
        createTimePoint(5, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles negative repetitions w/ null next start time', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, -1),
        createTimePoint(null, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles negative repetitions at end', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 5, -1),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 15},
        {start: 15, end: 20},
        {start: 20, end: 25},
      ];
      checkTimePoints(timePoints, result, 1, 0, 25);
    });

    it('handles negative repetitions at end w/o Period length', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 5, -1),
      ];
      const result = [
        {start: 0, end: 10},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('handles negative repetitions at end w/ bad Period length', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, 0),
        createTimePoint(25, 5, -1),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
      ];
      checkTimePoints(timePoints, result, 1, 0, 20);
    });

    it('ignores elements after null duration', () => {
      const timePoints = [
        createTimePoint(0, 10, 0),
        createTimePoint(10, 10, 0),
        createTimePoint(20, null, 0),
        createTimePoint(30, 10, 0),
        createTimePoint(40, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
      ];
      checkTimePoints(timePoints, result, 1, 0, Infinity);
    });

    it('adjust start with presentationTimeOffset', () => {
      const timePoints = [
        createTimePoint(10, 10, 0),
        createTimePoint(20, 10, 0),
        createTimePoint(30, 10, 0),
        createTimePoint(40, 10, 0),
      ];
      const result = [
        {start: 0, end: 10},
        {start: 10, end: 20},
        {start: 20, end: 30},
        {start: 30, end: 40},
      ];
      checkTimePoints(timePoints, result, 1, 10, Infinity);
    });

    it('adjust start time w/ t missing', () => {
      // No S@t is equivalent to t=0, which should use PTO to make negative.
      // See https://github.com/google/shaka-player/issues/2590
      const timePoints = [
        createTimePoint(null, 10, 0),
        createTimePoint(10, 10, 0),
      ];
      const result = [
        {start: -5, end: 5},
        {start: 5, end: 15},
      ];
      checkTimePoints(timePoints, result, 1, 5, Infinity);
    });

    /**
     * Creates a new TimePoint.
     *
     * @param {?number} t
     * @param {?number} d
     * @param {?number} r
     * @return {{t: ?number, d: ?number, r: ?number}}
     */
    function createTimePoint(t, d, r) {
      return {t: t, d: d, r: r};
    }

    /**
     * Checks that the createTimeline works with the given timePoints and the
     * given expected results.
     *
     * @param {!Array.<{t: ?number, d: ?number, r: ?number}>} points
     * @param {!Array.<{start: number, end: number}>} expected
     * @param {number} timescale
     * @param {number} presentationTimeOffset
     * @param {number} periodDuration
     */
    function checkTimePoints(points, expected, timescale,
        presentationTimeOffset, periodDuration) {
      // Construct a SegmentTimeline Node object.
      const xmlLines = ['<?xml version="1.0"?>', '<SegmentTimeline>'];
      for (const p of points) {
        xmlLines.push('<S' +
                      (p.t != null ? ' t="' + p.t + '"' : '') +
                      (p.d != null ? ' d="' + p.d + '"' : '') +
                      (p.r != null ? ' r="' + p.r + '"' : '') +
                      ' />');
      }
      xmlLines.push('</SegmentTimeline>');
      const parser = new DOMParser();
      const xml =
          parser.parseFromString(xmlLines.join('\n'), 'application/xml');
      const segmentTimeline = xml.documentElement;
      console.assert(segmentTimeline);

      const timeline = MpdUtils.createTimeline(
          segmentTimeline, timescale, presentationTimeOffset, periodDuration);
      expect(timeline).toEqual(
          expected.map((c) => jasmine.objectContaining(c)));
    }
  });

  describe('processXlinks', () => {
    const Error = shaka.util.Error;

    /** @type {!shaka.test.FakeNetworkingEngine} */
    let fakeNetEngine;
    /** @type {shaka.extern.RetryParameters} */
    let retry;
    /** @type {!DOMParser} */
    let parser;
    /** @type {boolean} */
    let failGracefully;

    beforeEach(() => {
      failGracefully = false;
      retry = shaka.net.NetworkingEngine.defaultRetryParameters();
      fakeNetEngine = new shaka.test.FakeNetworkingEngine();
      parser = new DOMParser();
    });

    it('will replace elements and children', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" xlink:actuate="onLoad" />');
      const xlinkXMLString = '<ToReplace variable="1"><Contents /></ToReplace>';
      const desiredXMLString = inBaseContainer(
          '<ToReplace variable="1"><Contents /></ToReplace>');

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testSucceeds(baseXMLString, desiredXMLString, 1);
    });

    it('preserves non-xlink attributes', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace otherVariable="q" xlink:href="https://xlink1" ' +
          'xlink:actuate="onLoad" />');
      const xlinkXMLString = '<ToReplace variable="1"><Contents /></ToReplace>';
      const desiredXMLString = inBaseContainer(
          '<ToReplace otherVariable="q" variable="1"><Contents /></ToReplace>');

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testSucceeds(baseXMLString, desiredXMLString, 1);
    });

    it('preserves text', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" xlink:actuate="onLoad" />');
      const xlinkXMLString =
          '<ToReplace variable="1">TEXT CONTAINED WITHIN</ToReplace>';
      const desiredXMLString = inBaseContainer(
          '<ToReplace variable="1">TEXT CONTAINED WITHIN</ToReplace>');

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testSucceeds(baseXMLString, desiredXMLString, 1);
    });

    it('supports multiple replacements', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" xlink:actuate="onLoad" />',
          '<ToReplace xlink:href="https://xlink2" xlink:actuate="onLoad" />');
      const xlinkXMLString1 = makeRecursiveXMLString(1, 'https://xlink3');
      const xlinkXMLString2 =
          '<ToReplace variable="2"><Contents /></ToReplace>';
      const xlinkXMLString3 = '<ToReplace otherVariable="blue" />';
      const desiredXMLString = inBaseContainer(
          '<ToReplace xmlns="urn:mpeg:dash:schema:mpd:2011" ' +
          'xmlns:xlink="http://www.w3.org/1999/xlink" variable="1">' +
          '<ToReplace otherVariable="blue" /></ToReplace>',
          '<ToReplace variable="2"><Contents /></ToReplace>');

      fakeNetEngine
          .setResponseText('https://xlink1', xlinkXMLString1)
          .setResponseText('https://xlink2', xlinkXMLString2)
          .setResponseText('https://xlink3', xlinkXMLString3);
      await testSucceeds(baseXMLString, desiredXMLString, 3);
    });

    it('fails if loaded file is invalid xml', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" xlink:actuate="onLoad" />');
      // Note this does not have a close angle bracket.
      const xlinkXMLString = '<ToReplace></ToReplace';
      const expectedError = new shaka.util.Error(
          Error.Severity.CRITICAL, Error.Category.MANIFEST,
          Error.Code.DASH_INVALID_XML, 'https://xlink1');

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testFails(baseXMLString, expectedError, 1);
    });

    it('fails if it recurses too many times', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink0" xlink:actuate="onLoad" />');
      // Create a large but finite number of links, so this won't
      // infinitely recurse if there isn't a depth limit.
      for (const i of shaka.util.Iterables.range(20)) {
        const key = 'https://xlink' + i;
        const value = makeRecursiveXMLString(0, 'https://xlink' + (i + 1));

        fakeNetEngine.setResponseText(key, value);
      }
      const expectedError = new shaka.util.Error(
          Error.Severity.CRITICAL, Error.Category.MANIFEST,
          Error.Code.DASH_XLINK_DEPTH_LIMIT);

      await testFails(baseXMLString, expectedError, 5);
    });

    it('preserves url parameters', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1?parameter" ' +
          'xlink:actuate="onLoad" />');
      const xlinkXMLString = '<ToReplace variable="1"><Contents /></ToReplace>';
      const desiredXMLString = inBaseContainer(
          '<ToReplace variable="1"><Contents /></ToReplace>');

      fakeNetEngine.setResponseText(
          'https://xlink1?parameter', xlinkXMLString);
      await testSucceeds(baseXMLString, desiredXMLString, 1);
    });

    it('replaces existing contents', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" xlink:actuate="onLoad">' +
          '<Unwanted /></ToReplace>');
      const xlinkXMLString = '<ToReplace variable="1"><Contents /></ToReplace>';
      const desiredXMLString = inBaseContainer(
          '<ToReplace variable="1"><Contents /></ToReplace>');

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testSucceeds(baseXMLString, desiredXMLString, 1);
    });

    it('handles relative links', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="xlink1" xlink:actuate="onLoad" />',
          '<ToReplace xlink:href="xlink2" xlink:actuate="onLoad" />');
      const xlinkXMLString1 = // This is loaded relative to base.
          makeRecursiveXMLString(1, 'xlink3');
      const xlinkXMLString2 = // This is loaded relative to base.
          '<ToReplace variable="2"><Contents /></ToReplace>';
      const xlinkXMLString3 = // This is loaded relative to string1.
          '<ToReplace variable="3" />';
      fakeNetEngine
          .setResponseText('https://base/xlink1', xlinkXMLString1)
          .setResponseText('https://base/xlink2', xlinkXMLString2)
          .setResponseText('https://base/xlink3', xlinkXMLString3);

      const desiredXMLString = inBaseContainer(
          '<ToReplace xmlns="urn:mpeg:dash:schema:mpd:2011" ' +
          'xmlns:xlink="http://www.w3.org/1999/xlink" variable="1">' +
          '<ToReplace variable="3" /></ToReplace>',
          '<ToReplace variable="2"><Contents /></ToReplace>');

      await testSucceeds(baseXMLString, desiredXMLString, 3);
    });

    it('fails for actuate=onRequest', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" ' +
          'xlink:actuate="onRequest" />');
      const xlinkXMLString = '<ToReplace variable="1"><Contents /></ToReplace>';
      const expectedError = new shaka.util.Error(
          Error.Severity.CRITICAL, Error.Category.MANIFEST,
          Error.Code.DASH_UNSUPPORTED_XLINK_ACTUATE);

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testFails(baseXMLString, expectedError, 0);
    });

    it('fails for no actuate', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" />');
      const xlinkXMLString = '<ToReplace variable="1"><Contents /></ToReplace>';
      const expectedError = new shaka.util.Error(
          Error.Severity.CRITICAL, Error.Category.MANIFEST,
          Error.Code.DASH_UNSUPPORTED_XLINK_ACTUATE);

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testFails(baseXMLString, expectedError, 0);
    });

    it('removes elements with resolve-to-zero', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="urn:mpeg:dash:resolve-to-zero:2013" />');
      const desiredXMLString = inBaseContainer();

      await testSucceeds(baseXMLString, desiredXMLString, 0);
    });

    it('needs the top-level to match the link\'s tagName', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" xlink:actuate="onLoad" />');
      const xlinkXMLString = '<BadTagName</BadTagName>';

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testFails(baseXMLString, null, 1);
    });

    it('doesn\'t error when set to fail gracefully', async () => {
      failGracefully = true;
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink1" xlink:actuate="onLoad">' +
          '<DefaultContents />' +
          '</ToReplace>');
      const xlinkXMLString = '<BadTagName</BadTagName>';
      const desiredXMLString = inBaseContainer(
          '<ToReplace><DefaultContents /></ToReplace>');

      fakeNetEngine.setResponseText('https://xlink1', xlinkXMLString);
      await testSucceeds(baseXMLString, desiredXMLString, 1);
    });

    it('interrupts requests on abort', async () => {
      const baseXMLString = inBaseContainer(
          '<ToReplace xlink:href="https://xlink0" xlink:actuate="onLoad" />');
      // Create a few links.  This is few enough that it would succeed if we
      // didn't abort it.
      for (const i of shaka.util.Iterables.range(4)) {
        const key = 'https://xlink' + i;
        const value = makeRecursiveXMLString(0, 'https://xlink' + (i + 1));

        fakeNetEngine.setResponseText(key, value);
      }
      /** @type {!shaka.util.PublicPromise} */
      const continuePromise = fakeNetEngine.delayNextRequest();

      const xml = parser.parseFromString(baseXMLString, 'text/xml')
          .documentElement;
      /** @type {!shaka.extern.IAbortableOperation} */
      const operation = MpdUtils.processXlinks(
          xml, retry, failGracefully, 'https://base', fakeNetEngine);

      const abort = async () => {
        await shaka.test.Util.shortDelay();
        // Only one request has been made so far.
        expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
        continuePromise.resolve();

        // Abort the operation.
        operation.abort();
      };

      // The operation was aborted.
      const expected = shaka.test.Util.jasmineError(new shaka.util.Error(
          Error.Severity.CRITICAL,
          Error.Category.PLAYER,
          Error.Code.OPERATION_ABORTED));
      const p = expectAsync(operation.promise).toBeRejectedWith(expected);

      await Promise.all([abort(), p]);
      // Still only one request has been made.
      expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
    });

    it('ignores SegmentTimeline children', async () => {
      const baseXMLString = inBaseContainer(
          '<SegmentTimeline>' +
          '  <ToReplace xlink:href="https://xlink1" ' +
          '     xlink:actuate="onRequest" />' +
          '</SegmentTimeline>');
      await testSucceeds(baseXMLString, baseXMLString, 0);
    });

    async function testSucceeds(
        baseXMLString, desiredXMLString, desiredNetCalls) {
      const desiredXML = parser.parseFromString(desiredXMLString, 'text/xml')
          .documentElement;
      const finalXML = await testRequest(baseXMLString);
      expect(fakeNetEngine.request).toHaveBeenCalledTimes(desiredNetCalls);
      expect(finalXML).toEqualElement(desiredXML);
    }

    async function testFails(baseXMLString, desiredError, desiredNetCalls) {
      if (desiredError) {
        await expectAsync(testRequest(baseXMLString))
            .toBeRejectedWith(shaka.test.Util.jasmineError(desiredError));
      } else {
        await expectAsync(testRequest(baseXMLString)).toBeRejected();
      }
      expect(fakeNetEngine.request).toHaveBeenCalledTimes(desiredNetCalls);
    }

    /**
    * Creates an XML string with an xlink link to another URL,
    * for use in testing recursive chains of xlink links.
    * @param {number} variable
    * @param {string} link
    * @return {string}
    * @private
    */
    function makeRecursiveXMLString(variable, link) {
      const format =
          '<ToReplace xmlns="urn:mpeg:dash:schema:mpd:2011" ' +
          'xmlns:xlink="http://www.w3.org/1999/xlink" variable="%(let)s">' +
          '<ToReplace xlink:href="%(link)s" xlink:actuate="onLoad" />' +
          '</ToReplace>';
      return sprintf(format, {'let': variable, 'link': link});
    }

    /**
     * @param {string=} toReplaceOne
     * @param {string=} toReplaceTwo
     * @return {string}
     * @private
     */
    function inBaseContainer(toReplaceOne = '', toReplaceTwo = '') {
      const format =
          '<Container xmlns="urn:mpeg:dash:schema:mpd:2011" ' +
          'xmlns:xlink="http://www.w3.org/1999/xlink">' +
          '<Thing>' +
          '%(toReplaceOne)s' +
          '</Thing>' +
          '%(toReplaceTwo)s' +
          '</Container>';
      return sprintf(format, {
        toReplaceOne: toReplaceOne,
        toReplaceTwo: toReplaceTwo});
    }

    function testRequest(baseXMLString) {
      const xml = parser.parseFromString(baseXMLString, 'text/xml')
          .documentElement;
      return MpdUtils.processXlinks(xml, retry, failGracefully, 'https://base',
          fakeNetEngine).promise;
    }
  });
});
