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

// All of the database dumps referenced below were originally made from the
// "Heliocentrism" content in our demo app.
// https://storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd
// The dumps were made with test/test/util/canned_idb.js
const compatibilityTestsMetadata = [
  {
    // This was our original (v1) storage format for Shaka Player v2.0,
    // deprecated in v2.3.  This is not the same as the storage format from
    // Shaka Player v1, which is no longer supported.
    name: 'v1',
    dbImagePath: '/base/test/test/assets/db-dump-v1.json',
    manifestKey: 0,
    readOnly: true,
    makeCell: (connection) => new shaka.offline.indexeddb.V1StorageCell(
        connection,
        /* segmentStore= */ 'segment',
        /* manifestStore= */ 'manifest'),
  },
  {
    // Two variants of v2 exist in the field.  This is the initial version of
    // v2, as upgraded from v1 databases.  It was broken in a way that prevented
    // new records from being added.  This format was introduced in Shaka Player
    // v2.3 and deprecated in v2.3.2.
    name: 'v2-broken',
    dbImagePath: '/base/test/test/assets/db-dump-v2-broken.json',
    manifestKey: 0,
    readOnly: true,
    makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
        connection,
        /* segmentStore= */ 'segment-v2',
        /* manifestStore= */ 'manifest-v2'),
  },
  {
    // This is the "clean" version of the v2 database format, as created from
    // scratch, to which new records could be added.  This format was introduced
    // in Shaka Player v2.3 and deprecated in v2.3.2.
    name: 'v2-clean',
    dbImagePath: '/base/test/test/assets/db-dump-v2-clean.json',
    manifestKey: 1,
    readOnly: true,
    makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
        connection,
        /* segmentStore= */ 'segment-v2',
        /* manifestStore= */ 'manifest-v2'),
  },
  {
    // This is the v3 version of the database, which is actually identical to
    // the "clean" version of the v2 database.  The version number was
    // incremented to overcome the "broken" v2 databases.  This format was
    // introduced in v2.3.2 and deprecated in v3.0.
    name: 'v3',
    dbImagePath: '/base/test/test/assets/db-dump-v3.json',
    manifestKey: 1,
    readOnly: true,
    makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
        connection,
        /* segmentStore= */ 'segment-v3',
        /* manifestStore= */ 'manifest-v3'),
  },
  {
    // This is the v4 version of the database as written by v2.5.0 - v2.5.9.  A
    // bug in v2.5 caused the stream metadata from all periods to be written to
    // each period.  This was corrected in v2.5.10.
    // See https://github.com/google/shaka-player/issues/2389
    name: 'v4-broken',
    dbImagePath: '/base/test/test/assets/db-dump-v4-broken.json',
    manifestKey: 1,
    readOnly: true,
    makeCell: (connection) => new shaka.offline.indexeddb.V2StorageCell(
        connection,
        // V4 of the database still used the V3 store names and structures.
        /* segmentStore= */ 'segment-v3',
        /* manifestStore= */ 'manifest-v3'),
  },
  {
    // This is the v5 version of the database, introduced in v3.0.
    name: 'v5',
    dbImagePath: '/base/test/test/assets/db-dump-v5.json',
    manifestKey: 1,
    readOnly: false,
    makeCell: (connection) => new shaka.offline.indexeddb.V5StorageCell(
        connection,
        /* segmentStore= */ 'segment-v5',
        /* manifestStore= */ 'manifest-v5'),
  },
];

filterDescribe('Storage Compatibility', () => window.indexedDB, () => {
  for (const metadata of compatibilityTestsMetadata) {
    describe(metadata.name, () => {
      makeTests(metadata);
    });
  }

  function makeTests(metadata) {
    const CannedIDB = shaka.test.CannedIDB;
    const ContentType = shaka.util.ManifestParserUtils.ContentType;
    const Util = shaka.test.Util;

    /** @type {?shaka.extern.StorageCell} */
    let cell = null;

    /** @type {?IDBDatabase} */
    let connection = null;

    /** @type {string} */
    let dbImageAsString;

    beforeAll(async () => {
      const data = await shaka.test.Util.fetch(metadata.dbImagePath);
      dbImageAsString = shaka.util.StringUtils.fromUTF8(data);
    });

    beforeEach(async () => {
      const dbName = 'shaka-storage-cell-test';

      // Load the canned database image.
      await CannedIDB.restoreJSON(
          dbName, dbImageAsString, /* wipeDatabase= */ true);

      // Track the connection so that we can close it when the test is over.
      connection = await shaka.test.IndexedDBUtils.open(dbName);

      // Create a storage cell.
      cell = metadata.makeCell(connection);
    });

    afterEach(async () => {
      // Destroy the cell before killing the connection.
      if (cell) {
        await cell.destroy();
      }
      cell = null;

      if (connection) {
        connection.close();
      }
      connection = null;
    });

    if (metadata.readOnly) {
      it('cannot add new manifests', async () => {
        const expected = Util.jasmineError(new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.STORAGE,
            shaka.util.Error.Code.NEW_KEY_OPERATION_NOT_SUPPORTED,
            jasmine.any(String)));

        // There should be one manifest.
        const manifests = await cell.getAllManifests();
        const manifest = manifests.get(metadata.manifestKey);
        expect(manifest).toBeTruthy();

        // Make sure that the request fails.
        await expectAsync(
            cell.addManifests([manifest])).toBeRejectedWith(expected);
      });

      it('cannot add new segment', async () => {
        const expected = Util.jasmineError(new shaka.util.Error(
            shaka.util.Error.Severity.CRITICAL,
            shaka.util.Error.Category.STORAGE,
            shaka.util.Error.Code.NEW_KEY_OPERATION_NOT_SUPPORTED,
            jasmine.any(String)));

        // Update the key to what should be a free key.
        const segment = {data: new ArrayBuffer(16)};

        // Make sure that the request fails.
        await expectAsync(
            cell.addSegments([segment])).toBeRejectedWith(expected);
      });
    }  // if (metadata.readOnly)

    it('can get all manifests', async () => {
      // There should be one manifest.
      const map = await cell.getAllManifests();
      expect(map).toBeTruthy();
      expect(map.size).toBe(1);
      expect(map.get(metadata.manifestKey)).toBeTruthy();
    });

    it('can get manifest and all segments', async () => {
      // There should be one manifest.
      const manifests = await cell.getManifests([metadata.manifestKey]);
      const manifest = manifests[0];
      expect(manifest).toBeTruthy();

      // Collect all the keys for each segment.
      const dataKeys = getAllSegmentKeys(manifest);

      // Check that each segment was successfully retrieved.
      const segmentData = await cell.getSegments(dataKeys);
      expect(segmentData.length).not.toBe(0);

      for (const segment of segmentData) {
        expect(segment).toBeTruthy();
      }
    });

    it('can update expiration', async () => {
      const oldExpiration = Infinity;
      const newExpiration = 1000;

      const original = await cell.getManifests([metadata.manifestKey]);
      expect(original).toBeTruthy();
      expect(original[0]).toBeTruthy();
      expect(original[0].expiration).toBe(oldExpiration);

      await cell.updateManifestExpiration(metadata.manifestKey, newExpiration);

      const updated = await cell.getManifests([metadata.manifestKey]);
      expect(updated).toBeTruthy();
      expect(updated[0]).toBeTruthy();
      expect(updated[0].expiration).toBe(newExpiration);
    });

    it('can remove manifests and segments', async () => {
      /** @type {!Array.<number>} */
      const manifestKeys = [];
      /** @type {!Array.<number>} */
      const segmentKeys = [];

      const manifests = await cell.getAllManifests();
      manifests.forEach((manifest, manifestKey) => {
        manifestKeys.push(manifestKey);

        for (const key of getAllSegmentKeys(manifest)) {
          segmentKeys.push(key);
        }
      });

      expect(manifestKeys.length).toBe(1);
      expect(segmentKeys.length).not.toBe(0);

      // Remove all the segments.
      const noop = () => {};
      await cell.removeManifests(manifestKeys, noop);
      await cell.removeSegments(segmentKeys, noop);

      const expected = Util.jasmineError(new shaka.util.Error(
          shaka.util.Error.Severity.CRITICAL,
          shaka.util.Error.Category.STORAGE,
          shaka.util.Error.Code.KEY_NOT_FOUND,
          jasmine.any(String)));
      const checkMissingSegment = async (key) => {
        await expectAsync(cell.getSegments([key])).toBeRejectedWith(expected);
      };

      const checkMissingManifest = async (key) => {
        await expectAsync(cell.getManifests([key])).toBeRejectedWith(expected);
      };

      // Need to check each key on its own to ensure that each key is missing
      // and not just one of the keys is missing.
      const checkMissingSegments = (keys) => {
        return Promise.all(keys.map((key) => checkMissingSegment(key)));
      };
      const checkMissingManifests = (keys) => {
        return Promise.all(keys.map((key) => checkMissingManifest(key)));
      };

      await checkMissingSegments(segmentKeys);
      await checkMissingManifests(manifestKeys);
    });

    it('correctly converts to the current manifest format', async () => {
      // There should be one manifest.
      const manifestDb = (await cell.getManifests([metadata.manifestKey]))[0];
      const converter = new shaka.offline.ManifestConverter(
          'mechanism', 'cell');
      const actual = converter.fromManifestDB(manifestDb);

      const expected = shaka.test.ManifestGenerator.generate((manifest) => {
        manifest.anyTimeline();
        manifest.minBufferTime = 2;

        manifest.addPartialVariant((variant) => {
          variant.addPartialStream(ContentType.VIDEO, (stream) => {
            stream.frameRate = 29.97;
            stream.mime('video/webm', 'vp9');
            stream.size(640, 480);
          });
        });
      });

      expect(actual).toEqual(expected);

      const segmentIndex = actual.variants[0].video.segmentIndex;
      goog.asserts.assert(segmentIndex != null, 'Null segmentIndex!');
      const [segment0, segment1, segment2] = Array.from(segmentIndex);

      expect(segment0).toEqual(jasmine.objectContaining({
        startTime: 0,
        endTime: Util.closeTo(2.06874),
        timestampOffset: 0,
        appendWindowStart: 0,
        appendWindowEnd: Util.closeTo(2.06874),
      }));
      expect(segment1).toEqual(jasmine.objectContaining({
        startTime: Util.closeTo(2.06874),
        endTime: Util.closeTo(4.20413),
        timestampOffset: Util.closeTo(2.06874),
        appendWindowStart: Util.closeTo(2.06874),
        appendWindowEnd: Util.closeTo(4.20413),
      }));
      expect(segment2).toEqual(jasmine.objectContaining({
        startTime: Util.closeTo(4.20413),
        endTime: Util.closeTo(4.904831),
        timestampOffset: Util.closeTo(4.20413),
        appendWindowStart: Util.closeTo(4.20413),
        appendWindowEnd: Util.closeTo(4.904831),
      }));
    });

    /**
     * Get the keys for each segment. This will include the init segments.
     *
     * @param {shaka.extern.ManifestDB} manifest
     * @return {!Array.<number>}
     */
    function getAllSegmentKeys(manifest) {
      const keys = new Set();

      for (const stream of manifest.streams) {
        for (const segment of stream.segments) {
          if (segment.initSegmentKey != null) {
            keys.add(segment.initSegmentKey);
          }

          keys.add(segment.dataKey);
        }
      }

      return Array.from(keys);
    }
  }  // makeTests
});
