/**
 * @license
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

describe('CastUtils', function() {
  const CastUtils = shaka.cast.CastUtils;
  const FakeEvent = shaka.util.FakeEvent;

  it('includes every Player member', function() {
    let ignoredMembers = [
      'constructor',  // JavaScript added field
      'getNetworkingEngine',  // Handled specially
      'getMediaElement',  // Handled specially
      'setMaxHardwareResolution',
      'destroy',  // Should use CastProxy.destroy instead
      'getManifest', // Too large to proxy

      // Test helper methods (not @export'd)
      'createDrmEngine',
      'createNetworkingEngine',
      'createPlayhead',
      'createPlayheadObserver',
      'createMediaSource',
      'createMediaSourceEngine',
      'createStreamingEngine',
    ];

    let castMembers = CastUtils.PlayerVoidMethods
                          .concat(CastUtils.PlayerPromiseMethods);
    for (let name in CastUtils.PlayerGetterMethods) {
      castMembers.push(name);
    }
    for (let name in CastUtils.PlayerGetterMethodsThatRequireLive) {
      castMembers.push(name);
    }
    let playerMembers = Object.keys(shaka.Player.prototype).filter(
        function(name) {
          // Private members end with _.
          return ignoredMembers.indexOf(name) < 0 &&
              name.substr(name.length - 1) != '_';
        });

    // To make debugging easier, don't check that they are equal; instead check
    // that neither has any extra entries.
    let extraCastMembers = castMembers.filter(function(name) {
      return playerMembers.indexOf(name) < 0;
    });
    let extraPlayerMembers = playerMembers.filter(function(name) {
      return castMembers.indexOf(name) < 0;
    });
    expect(extraCastMembers).toEqual([]);
    expect(extraPlayerMembers).toEqual([]);
  });

  describe('serialize/deserialize', function() {
    it('transfers infinite values and NaN', function() {
      let orig = {
        'nan': NaN,
        'positive_infinity': Infinity,
        'negative_infinity': -Infinity,
        'null': null,
        'true': true,
        'false': false,
        'one': 1,
        'string': 'a string',
      };

      let serialized = CastUtils.serialize(orig);
      // The object is turned into a string.
      expect(typeof serialized).toBe('string');

      // The deserialized object matches the original.
      let deserialized = CastUtils.deserialize(serialized);
      for (let k in orig) {
        expect(deserialized[k]).toEqual(orig[k]);
      }
    });

    it('transfers real Events', function() {
      // new Event() is not usable on IE11:
      let event =
          /** @type {!CustomEvent} */ (document.createEvent('CustomEvent'));
      event.initCustomEvent('myEventType', false, false, null);

      // Properties that can definitely be transferred.
      let nativeProperties = [
        'bubbles',
        'type',
        'cancelable',
        'defaultPrevented',
      ];
      let extraProperties = {
        'key': 'value',
        'true': true,
        'one': 1,
      };

      for (let k in extraProperties) {
        event[k] = extraProperties[k];
      }

      // The event is turned into a string.
      let serialized = CastUtils.serialize(event);
      expect(typeof serialized).toBe('string');

      // The string is turned back into an object.
      let deserialized = CastUtils.deserialize(serialized);
      expect(typeof deserialized).toBe('object');

      // The object can be used to construct a FakeEvent.
      let fakeEvent = new FakeEvent(deserialized['type'], deserialized);

      // The fake event has the same type and properties as the original.
      nativeProperties.forEach(function(k) {
        expect(fakeEvent[k]).toEqual(event[k]);
      });
      for (let k in extraProperties) {
        expect(fakeEvent[k]).toEqual(event[k]);
      }
    });

    it('transfers dispatched FakeEvents', function(done) {
      let event = new FakeEvent('custom');

      // Properties that can definitely be transferred.
      let nativeProperties = [
        'bubbles',
        'type',
        'cancelable',
        'defaultPrevented',
      ];
      let extraProperties = {
        'key': 'value',
        'true': true,
        'one': 1,
      };

      for (let k in extraProperties) {
        event[k] = extraProperties[k];
      }

      let target = new shaka.util.FakeEventTarget();
      target.addEventListener(event.type, function() {
        try {
          // The event is turned into a string.
          let serialized = CastUtils.serialize(event);
          expect(typeof serialized).toBe('string');

          // The string is turned back into an object.
          let deserialized = CastUtils.deserialize(serialized);
          expect(typeof deserialized).toBe('object');

          // The deserialized event has the same type and properties as the
          // original.
          nativeProperties.forEach(function(k) {
            expect(deserialized[k]).toEqual(event[k]);
          });
          for (let k in extraProperties) {
            expect(deserialized[k]).toEqual(event[k]);
          }
        } catch (exception) {
          fail(exception);
        }
        done();
      });
      target.dispatchEvent(event);
    });

    describe('TimeRanges', function() {
      /** @type {!HTMLVideoElement} */
      let video;
      /** @type {!shaka.util.EventManager} */
      let eventManager;
      /** @type {!shaka.media.MediaSourceEngine} */
      let mediaSourceEngine;

      beforeAll(function() {
        video =
            /** @type {!HTMLVideoElement} */ (document.createElement('video'));
        document.body.appendChild(video);
      });

      beforeEach(function(done) {
        // The TimeRanges constructor cannot be used directly, so we load a clip
        // to get ranges to use.
        let fakeVideoStream = {
          mimeType: 'video/mp4',
          codecs: 'avc1.42c01e',
        };
        let initSegmentUrl = '/base/test/test/assets/sintel-video-init.mp4';
        let videoSegmentUrl = '/base/test/test/assets/sintel-video-segment.mp4';

        // Wait for the media source to be open.
        eventManager = new shaka.util.EventManager();
        eventManager.listen(video, 'error', onError);

        function onError() {
          fail('Error code ' + (video.error ? video.error.code : 0));
        }

        mediaSourceEngine = new shaka.media.MediaSourceEngine(video);

        // Create empty object first and initialize the fields through
        // [] to allow field names to be expressions.
        let initObject = {};
        const ContentType = shaka.util.ManifestParserUtils.ContentType;
        initObject[ContentType.VIDEO] = fakeVideoStream;

        mediaSourceEngine.init(initObject, false).then(function() {
          return shaka.test.Util.fetch(initSegmentUrl);
        }).then(function(data) {
          return mediaSourceEngine.appendBuffer(ContentType.VIDEO, data,
                                                null, null);
        }).then(function() {
          return shaka.test.Util.fetch(videoSegmentUrl);
        }).then(function(data) {
          return mediaSourceEngine.appendBuffer(ContentType.VIDEO, data,
                                                null, null);
        }).catch(fail).then(done);
      });

      afterEach(function(done) {
        eventManager.destroy().then(function() {
          if (mediaSourceEngine) {
            return mediaSourceEngine.destroy();
          }
        }).then(function() {
          video.removeAttribute('src');
          video.load();
          done();
        });
      });

      afterAll(function() {
        document.body.removeChild(video);
      });

      quarantinedIt('deserialize into equivalent objects', function() {
        let buffered = video.buffered;

        // The test is less interesting if the ranges are empty.
        expect(buffered.length).toBeGreaterThan(0);

        // The TimeRanges object is turned into a string.
        let serialized = CastUtils.serialize(buffered);
        expect(typeof serialized).toBe('string');

        // Expect the deserialized version to look like the original.
        let deserialized = CastUtils.deserialize(serialized);
        expect(deserialized.length).toEqual(buffered.length);
        expect(deserialized.start).toEqual(jasmine.any(Function));
        expect(deserialized.end).toEqual(jasmine.any(Function));

        for (let i = 0; i < deserialized.length; ++i) {
          // Not exact because of the possibility of rounding errors.
          expect(deserialized.start(i)).toBeCloseTo(buffered.start(i));
          expect(deserialized.end(i)).toBeCloseTo(buffered.end(i));
        }
      });
    });
  });
});
