// META: title=IndexedDB: transactions with large request results are aborted correctly
// META: global=window,worker
// META: script=resources/support-promises.js
// META: timeout=long

// Spec: https://w3c.github.io/IndexedDB/#abort-transaction

'use strict';

// Should be large enough to trigger large value handling in the IndexedDB
// engines that have special code paths for large values.
const wrapThreshold = 128 * 1024;

function populateStore(store) {
  store.put({id: 1, key: 'k1', value: largeValue(wrapThreshold, 1)});
  store.put({id: 2, key: 'k2', value: ['small-2']});
  store.put({id: 3, key: 'k3', value: largeValue(wrapThreshold, 3)});
  store.put({id: 4, key: 'k4', value: ['small-4']});
}

// Opens index cursors for operations that require open cursors.
//
// onsuccess is called if all cursors are opened successfully. Otherwise,
// onerror will be called at least once.
function openCursors(testCase, index, operations, onerror, onsuccess) {
  let pendingCursors = 0;

  for (let operation of operations) {
    const opcode = operation[0];
    const primaryKey = operation[1];
    let request;
    switch (opcode) {
      case 'continue':
        request =
            index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey - 1}`));
        break;
      case 'continue-empty':
        // k4 is the last key in the data set, so calling continue() will get
        // the cursor past the end of the store.
        request = index.openCursor(IDBKeyRange.lowerBound('k4'));
        break;
      default:
        continue;
    }

    operation[2] = request;
    ++pendingCursors;

    request.onsuccess = testCase.step_func(() => {
      --pendingCursors;
      if (!pendingCursors)
        onsuccess();
    });
    request.onerror = testCase.step_func(onerror);
  }

  if (!pendingCursors)
    onsuccess();
}

function doOperation(testCase, store, index, operation, requestId, results) {
  const opcode = operation[0];
  const primaryKey = operation[1];
  const cursor = operation[2];

  return new Promise((resolve, reject) => {
    let request;
    switch (opcode) {
      case 'add':  // Tests returning a primary key.
        request =
            store.add({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
        break;
      case 'put':  // Tests returning a primary key.
        request =
            store.put({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
        break;
      case 'put-with-id':  // Tests returning success or a primary key.
        request = store.put({
          key: `k${primaryKey}`,
          value: [`small-${primaryKey}`],
          id: primaryKey
        });
        break;
      case 'get':        // Tests returning a value.
      case 'get-empty':  // Tests returning undefined.
        request = store.get(primaryKey);
        break;
      case 'getall':  // Tests returning an array of values.
        request = store.getAll();
        break;
      case 'error':  // Tests returning an error.
        request =
            store.put({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
        break;
      case 'continue':        // Tests returning a key, primary key, and value.
      case 'continue-empty':  // Tests returning null.
        request = cursor;
        cursor.result.continue();
        break;
      case 'open':  // Tests returning a cursor, key, primary key, and value.
      case 'open-empty':  // Tests returning null.
        request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`));
        break;
      case 'count':  // Tests returning a numeric result.
        request = index.count();
        break;
    };

    request.onsuccess = testCase.step_func(() => {
      reject(new Error(
          'requests should not succeed after the transaction is aborted'));
    });
    request.onerror = testCase.step_func(event => {
      event.preventDefault();
      results.push([requestId, request.error]);
      resolve();
    });
  });
}

function abortTest(label, operations) {
  promise_test(testCase => {
    return createDatabase(
               testCase,
               (database, transaction) => {
                 const store = database.createObjectStore(
                     'test-store', {autoIncrement: true, keyPath: 'id'});
                 store.createIndex('test-index', 'key', {unique: true});
                 populateStore(store);
               })
        .then(database => {
          const transaction = database.transaction(['test-store'], 'readwrite');
          const store = transaction.objectStore('test-store');
          const index = store.index('test-index');
          return new Promise((resolve, reject) => {
            openCursors(testCase, index, operations, reject, () => {
              const results = [];
              const promises = [];
              for (let i = 0; i < operations.length; ++i) {
                const promise = doOperation(
                    testCase, store, index, operations[i], i, results);
                promises.push(promise);
              };
              transaction.abort();
              resolve(Promise.all(promises).then(() => results));
            });
          });
        })
        .then(results => {
          assert_equals(
              results.length, operations.length,
              'Promise.all should resolve after all sub-promises resolve');
          for (let i = 0; i < operations.length; ++i) {
            assert_equals(
                results[i][0], i,
                'error event order should match request order');
            assert_equals(
                results[i][1].name, 'AbortError',
                'transaction aborting should result in AbortError on all requests');
          }
        });
  }, label);
}

abortTest('small values', [
  ['get', 2],
  ['count', null],
  ['continue-empty', null],
  ['get-empty', 5],
  ['add', 5],
  ['open', 2],
  ['continue', 2],
  ['get', 4],
  ['get-empty', 6],
  ['count', null],
  ['put-with-id', 5],
  ['put', 6],
  ['error', 3],
  ['continue', 4],
  ['count', null],
  ['get-empty', 7],
  ['open', 4],
  ['open-empty', 7],
  ['add', 7],
]);

abortTest('large values', [
  ['open', 1],
  ['get', 1],
  ['getall', 4],
  ['get', 3],
  ['continue', 3],
  ['open', 3],
]);

abortTest('large value followed by small values', [
  ['get', 1],
  ['getall', null],
  ['open', 2],
  ['continue-empty', null],
  ['get', 2],
  ['get-empty', 5],
  ['count', null],
  ['continue-empty', null],
  ['open-empty', 5],
  ['add', 5],
  ['error', 1],
  ['continue', 2],
  ['get-empty', 6],
  ['put-with-id', 5],
  ['put', 6],
]);

abortTest('large values mixed with small values', [
  ['get', 1],
  ['get', 2],
  ['get-empty', 5],
  ['count', null],
  ['continue-empty', null],
  ['open', 1],
  ['continue', 2],
  ['open-empty', 5],
  ['getall', 4],
  ['open', 2],
  ['continue-empty', null],
  ['add', 5],
  ['get', 3],
  ['count', null],
  ['get-empty', 6],
  ['put-with-id', 5],
  ['getall', null],
  ['continue', 3],
  ['open-empty', 6],
  ['put', 6],
  ['error', 1],
  ['continue', 2],
  ['open', 4],
  ['get-empty', 7],
  ['count', null],
  ['continue', 3],
  ['add', 7],
  ['getall', null],
  ['error', 3],
  ['count', null],
]);
