/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * This file tests the functionality of mozIStorageBaseStatement::executeAsync
 * for both mozIStorageStatement and mozIStorageAsyncStatement.
 */

// This file uses the internal _quit from testing/xpcshell/head.js */
/* global _quit */

const INTEGER = 1;
const TEXT = "this is test text";
const REAL = 3.23;
const BLOB = [1, 2];

/**
 * Execute the given statement asynchronously, spinning an event loop until the
 * async statement completes.
 *
 * @param aStmt
 *        The statement to execute.
 * @param [aOptions={}]
 * @param [aOptions.error=false]
 *        If true we should expect an error whose code we do not care about.  If
 *        a numeric value, that's the error code we expect and require.  If we
 *        are expecting an error, we expect a completion reason of REASON_ERROR.
 *        Otherwise we expect no error notification and a completion reason of
 *        REASON_FINISHED.
 * @param [aOptions.cancel]
 *        If true we cancel the pending statement and additionally return the
 *        pending statement in case you want to further manipulate it.
 * @param [aOptions.returnPending=false]
 *        If true we keep the pending statement around and return it to you.  We
 *        normally avoid doing this to try and minimize the amount of time a
 *        reference is held to the returned pending statement.
 * @param [aResults]
 *        If omitted, we assume no results rows are expected.  If it is a
 *        number, we assume it is the number of results rows expected.  If it is
 *        a function, we assume it is a function that takes the 1) result row
 *        number, 2) result tuple, 3) call stack for the original call to
 *        execAsync as arguments.  If it is a list, we currently assume it is a
 *        list of functions where each function is intended to evaluate the
 *        result row at that ordinal position and takes the result tuple and
 *        the call stack for the original call.
 */
function execAsync(aStmt, aOptions, aResults) {
  let caller = Components.stack.caller;
  if (aOptions == null) {
    aOptions = {};
  }

  let resultsExpected;
  let resultsChecker;
  if (aResults == null) {
    resultsExpected = 0;
  } else if (typeof aResults == "number") {
    resultsExpected = aResults;
  } else if (typeof aResults == "function") {
    resultsChecker = aResults;
  } else {
    // array
    resultsExpected = aResults.length;
    resultsChecker = function (aResultNum, aTup, aCaller) {
      aResults[aResultNum](aTup, aCaller);
    };
  }
  let resultsSeen = 0;

  let errorCodeExpected = false;
  let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED;
  let altReasonExpected = null;
  if ("error" in aOptions) {
    errorCodeExpected = aOptions.error;
    if (errorCodeExpected) {
      reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR;
    }
  }
  let errorCodeSeen = false;

  if ("cancel" in aOptions && aOptions.cancel) {
    altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED;
  }

  let completed = false;

  let listener = {
    handleResult(aResultSet) {
      let row,
        resultsSeenThisCall = 0;
      while ((row = aResultSet.getNextRow()) != null) {
        if (resultsChecker) {
          resultsChecker(resultsSeen, row, caller);
        }
        resultsSeen++;
        resultsSeenThisCall++;
      }

      if (!resultsSeenThisCall) {
        do_throw("handleResult invoked with 0 result rows!");
      }
    },
    handleError(aError) {
      if (errorCodeSeen) {
        do_throw("handleError called when we already had an error!");
      }
      errorCodeSeen = aError.result;
    },
    handleCompletion(aReason) {
      if (completed) {
        // paranoia check
        do_throw("Received a second handleCompletion notification!", caller);
      }

      if (resultsSeen != resultsExpected) {
        do_throw(
          "Expected " +
            resultsExpected +
            " rows of results but " +
            "got " +
            resultsSeen +
            " rows!",
          caller
        );
      }

      if (errorCodeExpected && !errorCodeSeen) {
        do_throw("Expected an error, but did not see one.", caller);
      } else if (errorCodeExpected != errorCodeSeen) {
        do_throw(
          "Expected error code " +
            errorCodeExpected +
            " but got " +
            errorCodeSeen,
          caller
        );
      }

      if (aReason != reasonExpected && aReason != altReasonExpected) {
        do_throw(
          "Expected reason " +
            reasonExpected +
            (altReasonExpected ? " or " + altReasonExpected : "") +
            " but got " +
            aReason,
          caller
        );
      }

      completed = true;
    },
  };

  let pending;
  // Only get a pending reference if we're supposed to do.
  // (note: This does not stop XPConnect from holding onto one currently.)
  if (
    ("cancel" in aOptions && aOptions.cancel) ||
    ("returnPending" in aOptions && aOptions.returnPending)
  ) {
    pending = aStmt.executeAsync(listener);
  } else {
    aStmt.executeAsync(listener);
  }

  if ("cancel" in aOptions && aOptions.cancel) {
    pending.cancel();
  }

  Services.tm.spinEventLoopUntil(
    "Test(test_statement_executeAsync.js:execAsync)",
    () => completed || _quit
  );

  return pending;
}

/**
 * Make sure that illegal SQL generates the expected runtime error and does not
 * result in any crashes.  Async-only since the synchronous case generates the
 * error synchronously (and is tested elsewhere).
 */
function test_illegal_sql_async_deferred() {
  let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);

  // gibberish
  let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY.");
  execAsync(stmt, { error: Ci.mozIStorageError.ERROR });
  stmt.finalize();

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.failure,
    1
  );
  histogram.clear();

  // legal SQL syntax, but with semantics issues.
  stmt = makeTestStatement("SELECT destination FROM funkytown");
  execAsync(stmt, { error: Ci.mozIStorageError.ERROR });
  stmt.finalize();

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.failure,
    1
  );
  histogram.clear();

  run_next_test();
}
test_illegal_sql_async_deferred.asyncOnly = true;

function test_create_table() {
  // Ensure our table doesn't exist
  Assert.ok(!getOpenedDatabase().tableExists("test"));

  let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);

  var stmt = makeTestStatement(
    "CREATE TABLE test (" +
      "id INTEGER, " +
      "string TEXT, " +
      "number REAL, " +
      "nuller NULL, " +
      "blober BLOB" +
      ")"
  );
  execAsync(stmt);
  stmt.finalize();

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.success,
    1
  );
  histogram.clear();

  // Check that the table has been created
  Assert.ok(getOpenedDatabase().tableExists("test"));

  histogram.clear();

  // Verify that it's created correctly (this will throw if it wasn't)
  let checkStmt = getOpenedDatabase().createStatement(
    "SELECT id, string, number, nuller, blober FROM test"
  );
  checkStmt.finalize();

  // Nothing has executed so the histogram should be empty.
  Assert.ok(!histogram.snapshot().values);

  run_next_test();
}

function test_add_data() {
  let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);

  var stmt = makeTestStatement(
    "INSERT INTO test (id, string, number, nuller, blober) " +
      "VALUES (?, ?, ?, ?, ?)"
  );
  stmt.bindBlobByIndex(4, BLOB, BLOB.length);
  stmt.bindByIndex(3, null);
  stmt.bindByIndex(2, REAL);
  stmt.bindByIndex(1, TEXT);
  stmt.bindByIndex(0, INTEGER);

  execAsync(stmt);
  stmt.finalize();

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.success,
    1
  );
  histogram.clear();

  // Check that the result is in the table
  verifyQuery(
    "SELECT string, number, nuller, blober FROM test WHERE id = ?",
    INTEGER,
    [TEXT, REAL, null, BLOB]
  );

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.success,
    1
  );
  histogram.clear();

  run_next_test();
}

function test_get_data() {
  let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);

  var stmt = makeTestStatement(
    "SELECT string, number, nuller, blober, id FROM test WHERE id = ?"
  );
  stmt.bindByIndex(0, INTEGER);
  execAsync(stmt, {}, [
    function (tuple) {
      Assert.notEqual(null, tuple);

      // Check that it's what we expect
      Assert.ok(!tuple.getIsNull(0));
      Assert.equal(tuple.getResultByName("string"), tuple.getResultByIndex(0));
      Assert.equal(TEXT, tuple.getResultByName("string"));
      Assert.equal(
        Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
        tuple.getTypeOfIndex(0)
      );

      Assert.ok(!tuple.getIsNull(1));
      Assert.equal(tuple.getResultByName("number"), tuple.getResultByIndex(1));
      Assert.equal(REAL, tuple.getResultByName("number"));
      Assert.equal(
        Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
        tuple.getTypeOfIndex(1)
      );

      Assert.ok(tuple.getIsNull(2));
      Assert.equal(tuple.getResultByName("nuller"), tuple.getResultByIndex(2));
      Assert.equal(null, tuple.getResultByName("nuller"));
      Assert.equal(
        Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
        tuple.getTypeOfIndex(2)
      );

      Assert.ok(!tuple.getIsNull(3));
      var blobByName = tuple.getResultByName("blober");
      Assert.equal(BLOB.length, blobByName.length);
      var blobByIndex = tuple.getResultByIndex(3);
      Assert.equal(BLOB.length, blobByIndex.length);
      for (let i = 0; i < BLOB.length; i++) {
        Assert.equal(BLOB[i], blobByName[i]);
        Assert.equal(BLOB[i], blobByIndex[i]);
      }
      var count = { value: 0 };
      var blob = { value: null };
      tuple.getBlob(3, count, blob);
      Assert.equal(BLOB.length, count.value);
      for (let i = 0; i < BLOB.length; i++) {
        Assert.equal(BLOB[i], blob.value[i]);
      }
      Assert.equal(
        Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
        tuple.getTypeOfIndex(3)
      );

      Assert.ok(!tuple.getIsNull(4));
      Assert.equal(tuple.getResultByName("id"), tuple.getResultByIndex(4));
      Assert.equal(INTEGER, tuple.getResultByName("id"));
      Assert.equal(
        Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
        tuple.getTypeOfIndex(4)
      );
    },
  ]);
  stmt.finalize();

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.success,
    1
  );
  histogram.clear();

  run_next_test();
}

function test_tuple_out_of_bounds() {
  let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);

  var stmt = makeTestStatement("SELECT string FROM test");
  execAsync(stmt, {}, [
    function (tuple) {
      Assert.notEqual(null, tuple);

      // Check all out of bounds - should throw
      var methods = [
        "getTypeOfIndex",
        "getInt32",
        "getInt64",
        "getDouble",
        "getUTF8String",
        "getString",
        "getIsNull",
      ];
      for (var i in methods) {
        try {
          tuple[methods[i]](tuple.numEntries);
          do_throw("did not throw :(");
        } catch (e) {
          Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
        }
      }

      // getBlob requires more args...
      try {
        var blob = { value: null };
        var size = { value: 0 };
        tuple.getBlob(tuple.numEntries, blob, size);
        do_throw("did not throw :(");
      } catch (e) {
        Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
      }
    },
  ]);
  stmt.finalize();

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.success,
    1
  );
  histogram.clear();

  run_next_test();
}

function test_no_listener_works_on_success() {
  var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
  stmt.bindByIndex(0, 0);
  stmt.executeAsync();
  stmt.finalize();

  // Run the next test.
  run_next_test();
}

function test_no_listener_works_on_results() {
  var stmt = makeTestStatement("SELECT ?");
  stmt.bindByIndex(0, 1);
  stmt.executeAsync();
  stmt.finalize();

  // Run the next test.
  run_next_test();
}

function test_no_listener_works_on_error() {
  // commit without a transaction will trigger an error
  var stmt = makeTestStatement("COMMIT");
  stmt.executeAsync();
  stmt.finalize();

  // Run the next test.
  run_next_test();
}

function test_partial_listener_works() {
  var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
  stmt.bindByIndex(0, 0);
  stmt.executeAsync({
    handleResult(aResultSet) {},
  });
  stmt.executeAsync({
    handleError(aError) {},
  });
  stmt.executeAsync({
    handleCompletion(aReason) {},
  });
  stmt.finalize();

  // Run the next test.
  run_next_test();
}

/**
 * Dubious cancellation test that depends on system loading may or may not
 * succeed in canceling things.  It does at least test if calling cancel blows
 * up.  test_AsyncCancellation in test_true_async.cpp is our test that canceling
 * actually works correctly.
 */
function test_immediate_cancellation() {
  var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
  stmt.bindByIndex(0, 0);
  execAsync(stmt, { cancel: true });
  stmt.finalize();
  run_next_test();
}

/**
 * Test that calling cancel twice throws the second time.
 */
function test_double_cancellation() {
  var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
  stmt.bindByIndex(0, 0);
  let pendingStatement = execAsync(stmt, { cancel: true });
  // And cancel again - expect an exception
  expectError(Cr.NS_ERROR_UNEXPECTED, () => pendingStatement.cancel());

  stmt.finalize();
  run_next_test();
}

/**
 * Verify that nothing untoward happens if we try and cancel something after it
 * has fully run to completion.
 */
function test_cancellation_after_execution() {
  var stmt = makeTestStatement("DELETE FROM test WHERE id = ?");
  stmt.bindByIndex(0, 0);
  let pendingStatement = execAsync(stmt, { returnPending: true });
  // (the statement has fully executed at this point)
  // canceling after the statement has run to completion should not throw!
  pendingStatement.cancel();

  stmt.finalize();
  run_next_test();
}

/**
 * Verifies that a single statement can be executed more than once.  Might once
 * have been intended to also ensure that callback notifications were not
 * incorrectly interleaved, but that part was brittle (it's totally fine for
 * handleResult to get called multiple times) and not comprehensive.
 */
function test_double_execute() {
  let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM);

  var stmt = makeTestStatement("SELECT 1");
  execAsync(stmt, null, 1);
  execAsync(stmt, null, 1);
  stmt.finalize();

  TelemetryTestUtils.assertKeyedHistogramValue(
    histogram,
    TEST_DB_NAME,
    TELEMETRY_VALUES.success,
    2
  );
  histogram.clear();

  run_next_test();
}

function test_finalized_statement_does_not_crash() {
  var stmt = makeTestStatement("SELECT * FROM TEST");
  stmt.finalize();
  // we are concerned about a crash here; an error is fine.
  try {
    stmt.executeAsync();
  } catch (ex) {
    // Do nothing.
  }

  // Run the next test.
  run_next_test();
}

/**
 * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index.
 */
function test_bind_direct_binding_params_by_index() {
  var stmt = makeTestStatement(
    "INSERT INTO test (id, string, number, nuller, blober) " +
      "VALUES (?, ?, ?, ?, ?)"
  );
  let insertId = nextUniqueId++;
  stmt.bindByIndex(0, insertId);
  stmt.bindByIndex(1, TEXT);
  stmt.bindByIndex(2, REAL);
  stmt.bindByIndex(3, null);
  stmt.bindBlobByIndex(4, BLOB, BLOB.length);
  execAsync(stmt);
  stmt.finalize();
  verifyQuery(
    "SELECT string, number, nuller, blober FROM test WHERE id = ?",
    insertId,
    [TEXT, REAL, null, BLOB]
  );
  run_next_test();
}

/**
 * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name.
 */
function test_bind_direct_binding_params_by_name() {
  var stmt = makeTestStatement(
    "INSERT INTO test (id, string, number, nuller, blober) " +
      "VALUES (:int, :text, :real, :null, :blob)"
  );
  let insertId = nextUniqueId++;
  stmt.bindByName("int", insertId);
  stmt.bindByName("text", TEXT);
  stmt.bindByName("real", REAL);
  stmt.bindByName("null", null);
  stmt.bindBlobByName("blob", BLOB);
  execAsync(stmt);
  stmt.finalize();
  verifyQuery(
    "SELECT string, number, nuller, blober FROM test WHERE id = ?",
    insertId,
    [TEXT, REAL, null, BLOB]
  );
  run_next_test();
}

function test_bind_js_params_helper_by_index() {
  var stmt = makeTestStatement(
    "INSERT INTO test (id, string, number, nuller, blober) " +
      "VALUES (?, ?, ?, ?, NULL)"
  );
  let insertId = nextUniqueId++;
  // we cannot bind blobs this way; no blober
  stmt.params[3] = null;
  stmt.params[2] = REAL;
  stmt.params[1] = TEXT;
  stmt.params[0] = insertId;
  execAsync(stmt);
  stmt.finalize();
  verifyQuery(
    "SELECT string, number, nuller FROM test WHERE id = ?",
    insertId,
    [TEXT, REAL, null]
  );
  run_next_test();
}

function test_bind_js_params_helper_by_name() {
  var stmt = makeTestStatement(
    "INSERT INTO test (id, string, number, nuller, blober) " +
      "VALUES (:int, :text, :real, :null, NULL)"
  );
  let insertId = nextUniqueId++;
  // we cannot bind blobs this way; no blober
  stmt.params.null = null;
  stmt.params.real = REAL;
  stmt.params.text = TEXT;
  stmt.params.int = insertId;
  execAsync(stmt);
  stmt.finalize();
  verifyQuery(
    "SELECT string, number, nuller FROM test WHERE id = ?",
    insertId,
    [TEXT, REAL, null]
  );
  run_next_test();
}

function test_bind_multiple_rows_by_index() {
  const AMOUNT_TO_ADD = 5;
  var stmt = makeTestStatement(
    "INSERT INTO test (id, string, number, nuller, blober) " +
      "VALUES (?, ?, ?, ?, ?)"
  );
  var array = stmt.newBindingParamsArray();
  for (let i = 0; i < AMOUNT_TO_ADD; i++) {
    let bp = array.newBindingParams();
    bp.bindByIndex(0, INTEGER);
    bp.bindByIndex(1, TEXT);
    bp.bindByIndex(2, REAL);
    bp.bindByIndex(3, null);
    bp.bindBlobByIndex(4, BLOB, BLOB.length);
    array.addParams(bp);
    Assert.equal(array.length, i + 1);
  }
  stmt.bindParameters(array);

  let rowCount = getTableRowCount("test");
  execAsync(stmt);
  Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
  stmt.finalize();
  run_next_test();
}

function test_bind_multiple_rows_by_name() {
  const AMOUNT_TO_ADD = 5;
  var stmt = makeTestStatement(
    "INSERT INTO test (id, string, number, nuller, blober) " +
      "VALUES (:int, :text, :real, :null, :blob)"
  );
  var array = stmt.newBindingParamsArray();
  for (let i = 0; i < AMOUNT_TO_ADD; i++) {
    let bp = array.newBindingParams();
    bp.bindByName("int", INTEGER);
    bp.bindByName("text", TEXT);
    bp.bindByName("real", REAL);
    bp.bindByName("null", null);
    bp.bindBlobByName("blob", BLOB);
    array.addParams(bp);
    Assert.equal(array.length, i + 1);
  }
  stmt.bindParameters(array);

  let rowCount = getTableRowCount("test");
  execAsync(stmt);
  Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test"));
  stmt.finalize();
  run_next_test();
}

/**
 * Verify that a mozIStorageStatement instance throws immediately when we
 * try and bind to an illegal index.
 */
function test_bind_out_of_bounds_sync_immediate() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();

  // Check variant binding.
  expectError(Cr.NS_ERROR_INVALID_ARG, () => bp.bindByIndex(1, INTEGER));
  // Check blob binding.
  expectError(Cr.NS_ERROR_INVALID_ARG, () =>
    bp.bindBlobByIndex(1, BLOB, BLOB.length)
  );

  stmt.finalize();
  run_next_test();
}
test_bind_out_of_bounds_sync_immediate.syncOnly = true;

/**
 * Verify that a mozIStorageAsyncStatement reports an error asynchronously when
 * we bind to an illegal index.
 */
function test_bind_out_of_bounds_async_deferred() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();

  // There is no difference between variant and blob binding for async purposes.
  bp.bindByIndex(1, INTEGER);
  array.addParams(bp);
  stmt.bindParameters(array);
  execAsync(stmt, { error: Ci.mozIStorageError.RANGE });

  stmt.finalize();
  run_next_test();
}
test_bind_out_of_bounds_async_deferred.asyncOnly = true;

function test_bind_no_such_name_sync_immediate() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();

  // Check variant binding.
  expectError(Cr.NS_ERROR_INVALID_ARG, () =>
    bp.bindByName("doesnotexist", INTEGER)
  );
  // Check blob binding.
  expectError(Cr.NS_ERROR_INVALID_ARG, () =>
    bp.bindBlobByName("doesnotexist", BLOB)
  );

  stmt.finalize();
  run_next_test();
}
test_bind_no_such_name_sync_immediate.syncOnly = true;

function test_bind_no_such_name_async_deferred() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();

  bp.bindByName("doesnotexist", INTEGER);
  array.addParams(bp);
  stmt.bindParameters(array);
  execAsync(stmt, { error: Ci.mozIStorageError.RANGE });

  stmt.finalize();
  run_next_test();
}
test_bind_no_such_name_async_deferred.asyncOnly = true;

function test_bind_bogus_type_by_index() {
  // We try to bind a JS Object here that should fail to bind.
  let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (?)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();
  Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/);

  stmt.finalize();
  run_next_test();
}

function test_bind_bogus_type_by_name() {
  // We try to bind a JS Object here that should fail to bind.
  let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (:blob)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();
  Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/);

  stmt.finalize();
  run_next_test();
}

function test_bind_params_already_locked() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();
  bp.bindByName("int", INTEGER);
  array.addParams(bp);

  // We should get an error after we call addParams and try to bind again.
  expectError(Cr.NS_ERROR_UNEXPECTED, () => bp.bindByName("int", INTEGER));

  stmt.finalize();
  run_next_test();
}

function test_bind_params_array_already_locked() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");

  let array = stmt.newBindingParamsArray();
  let bp1 = array.newBindingParams();
  bp1.bindByName("int", INTEGER);
  array.addParams(bp1);
  let bp2 = array.newBindingParams();
  stmt.bindParameters(array);
  bp2.bindByName("int", INTEGER);

  // We should get an error after we have bound the array to the statement.
  expectError(Cr.NS_ERROR_UNEXPECTED, () => array.addParams(bp2));

  stmt.finalize();
  run_next_test();
}

function test_no_binding_params_from_locked_array() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");

  let array = stmt.newBindingParamsArray();
  let bp = array.newBindingParams();
  bp.bindByName("int", INTEGER);
  array.addParams(bp);
  stmt.bindParameters(array);

  // We should not be able to get a new BindingParams object after we have bound
  // to the statement.
  expectError(Cr.NS_ERROR_UNEXPECTED, () => array.newBindingParams());

  stmt.finalize();
  run_next_test();
}

function test_not_right_owning_array() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");

  let array1 = stmt.newBindingParamsArray();
  let array2 = stmt.newBindingParamsArray();
  let bp = array1.newBindingParams();
  bp.bindByName("int", INTEGER);

  // We should not be able to add bp to array2 since it was created from array1.
  expectError(Cr.NS_ERROR_UNEXPECTED, () => array2.addParams(bp));

  stmt.finalize();
  run_next_test();
}

function test_not_right_owning_statement() {
  let stmt1 = makeTestStatement("INSERT INTO test (id) VALUES (:int)");
  let stmt2 = makeTestStatement("INSERT INTO test (id) VALUES (:int)");

  let array1 = stmt1.newBindingParamsArray();
  stmt2.newBindingParamsArray();
  let bp = array1.newBindingParams();
  bp.bindByName("int", INTEGER);
  array1.addParams(bp);

  // We should not be able to bind array1 since it was created from stmt1.
  expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt2.bindParameters(array1));

  stmt1.finalize();
  stmt2.finalize();
  run_next_test();
}

function test_bind_empty_array() {
  let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)");

  let paramsArray = stmt.newBindingParamsArray();

  // We should not be able to bind this array to the statement because it is
  // empty.
  expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt.bindParameters(paramsArray));

  stmt.finalize();
  run_next_test();
}

function test_multiple_results() {
  let expectedResults = getTableRowCount("test");
  // Sanity check - we should have more than one result, but let's be sure.
  Assert.ok(expectedResults > 1);

  // Now check that we get back two rows of data from our async query.
  let stmt = makeTestStatement("SELECT * FROM test");
  execAsync(stmt, {}, expectedResults);

  stmt.finalize();
  run_next_test();
}

// Test Runner

const TEST_PASS_SYNC = 0;
const TEST_PASS_ASYNC = 1;
/**
 * We run 2 passes against the test.  One where makeTestStatement generates
 * synchronous (mozIStorageStatement) statements and one where it generates
 * asynchronous (mozIStorageAsyncStatement) statements.
 *
 * Because of differences in the ability to know the number of parameters before
 * dispatching, some tests are sync/async specific.  These functions are marked
 * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do.
 */
var testPass = TEST_PASS_SYNC;

/**
 * Create a statement of the type under test per testPass.
 *
 * @param aSQL
 *        The SQL string from which to build a statement.
 * @return a statement of the type under test per testPass.
 */
function makeTestStatement(aSQL) {
  if (testPass == TEST_PASS_SYNC) {
    return getOpenedDatabase().createStatement(aSQL);
  }
  return getOpenedDatabase().createAsyncStatement(aSQL);
}

var tests = [
  test_illegal_sql_async_deferred,
  test_create_table,
  test_add_data,
  test_get_data,
  test_tuple_out_of_bounds,
  test_no_listener_works_on_success,
  test_no_listener_works_on_results,
  test_no_listener_works_on_error,
  test_partial_listener_works,
  test_immediate_cancellation,
  test_double_cancellation,
  test_cancellation_after_execution,
  test_double_execute,
  test_finalized_statement_does_not_crash,
  test_bind_direct_binding_params_by_index,
  test_bind_direct_binding_params_by_name,
  test_bind_js_params_helper_by_index,
  test_bind_js_params_helper_by_name,
  test_bind_multiple_rows_by_index,
  test_bind_multiple_rows_by_name,
  test_bind_out_of_bounds_sync_immediate,
  test_bind_out_of_bounds_async_deferred,
  test_bind_no_such_name_sync_immediate,
  test_bind_no_such_name_async_deferred,
  test_bind_bogus_type_by_index,
  test_bind_bogus_type_by_name,
  test_bind_params_already_locked,
  test_bind_params_array_already_locked,
  test_bind_empty_array,
  test_no_binding_params_from_locked_array,
  test_not_right_owning_array,
  test_not_right_owning_statement,
  test_multiple_results,
];
var index = 0;

const STARTING_UNIQUE_ID = 2;
var nextUniqueId = STARTING_UNIQUE_ID;

function run_next_test() {
  function _run_next_test() {
    // use a loop so we can skip tests...
    while (index < tests.length) {
      let test = tests[index++];
      // skip tests not appropriate to the current test pass
      if (
        (testPass == TEST_PASS_SYNC && "asyncOnly" in test) ||
        (testPass == TEST_PASS_ASYNC && "syncOnly" in test)
      ) {
        continue;
      }

      // Asynchronous tests means that exceptions don't kill the test.
      try {
        print("****** Running the next test: " + test.name);
        test();
        return;
      } catch (e) {
        do_throw(e);
      }
    }

    // if we only completed the first pass, move to the next pass
    if (testPass == TEST_PASS_SYNC) {
      print("********* Beginning mozIStorageAsyncStatement pass.");
      testPass++;
      index = 0;
      // a new pass demands a new database
      asyncCleanup();
      nextUniqueId = STARTING_UNIQUE_ID;
      _run_next_test();
      return;
    }

    // we did some async stuff; we need to clean up.
    asyncCleanup();
    do_test_finished();
  }

  // Don't actually schedule another test if we're quitting.
  if (!_quit) {
    // For saner stacks, we execute this code RSN.
    executeSoon(_run_next_test);
  }
}

function run_test() {
  // Thunderbird doesn't have one or more of the probes used in this test.
  // Ensure the data is collected anyway.
  Services.prefs.setBoolPref(
    "toolkit.telemetry.testing.overrideProductsCheck",
    true
  );

  cleanup();

  do_test_pending();
  run_next_test();
}
