/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Storage Test Code.
 *
 * The Initial Developer of the Original Code is
 * Mozilla Corporation.
 * Portions created by the Initial Developer are Copyright (C) 2008
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
 *   Andrew Sutherland <asutherland@asutherland.org>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

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

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: function(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: function(aError)
    {
      if (errorCodeSeen != false)
        do_throw("handleError called when we already had an error!");
      errorCodeSeen = aError.result;
    },
    handleCompletion: function(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 == true && errorCodeSeen == false)
        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();

  let curThread = Components.classes["@mozilla.org/thread-manager;1"]
                            .getService().currentThread;
  while (!completed && !_quit)
    curThread.processNextEvent(true);

  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()
{
  // gibberish
  let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY.");
  execAsync(stmt, {error: Ci.mozIStorageError.ERROR});
  stmt.finalize();

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

  run_next_test();
}
test_illegal_sql_async_deferred.asyncOnly = true;

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

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

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

  // 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();
  run_next_test();
}

function test_add_data()
{
  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();

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

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

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

      do_check_false(tuple.getIsNull(1));
      do_check_eq(tuple.getResultByName("number"), tuple.getResultByIndex(1));
      do_check_eq(REAL, tuple.getResultByName("number"));
      do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
                  tuple.getTypeOfIndex(1));

      do_check_true(tuple.getIsNull(2));
      do_check_eq(tuple.getResultByName("nuller"), tuple.getResultByIndex(2));
      do_check_eq(null, tuple.getResultByName("nuller"));
      do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
                  tuple.getTypeOfIndex(2));

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

      do_check_false(tuple.getIsNull(4));
      do_check_eq(tuple.getResultByName("id"), tuple.getResultByIndex(4));
      do_check_eq(INTEGER, tuple.getResultByName("id"));
      do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
                  tuple.getTypeOfIndex(4));
    }]);
  stmt.finalize();
  run_next_test();
}

function test_tuple_out_of_bounds()
{
  var stmt = makeTestStatement(
    "SELECT string FROM test"
  );
  execAsync(stmt, {}, [
    function(tuple) {
      do_check_neq(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) {
          do_check_eq(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) {
        do_check_eq(Cr.NS_ERROR_ILLEGAL_VALUE, e.result);
      }
    }]);
  stmt.finalize();
  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: function(aResultSet)
    {
    }
  });
  stmt.executeAsync({
    handleError: function(aError)
    {
    }
  });
  stmt.executeAsync({
    handleCompletion: function(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,
              function() 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()
{
  var stmt = makeTestStatement(
    "SELECT 1"
  );
  execAsync(stmt, null, 1);
  execAsync(stmt, null, 1);
  stmt.finalize();
  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) {}

  // 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, BLOB.length);
  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);
    do_check_eq(array.length, i + 1);
  }
  stmt.bindParameters(array);

  let rowCount = getTableRowCount("test");
  execAsync(stmt);
  do_check_eq(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, BLOB.length);
    array.addParams(bp);
    do_check_eq(array.length, i + 1);
  }
  stmt.bindParameters(array);

  let rowCount = getTableRowCount("test");
  execAsync(stmt);
  do_check_eq(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,
              function() bp.bindByIndex(1, INTEGER));
  // Check blob binding.
  expectError(Cr.NS_ERROR_INVALID_ARG,
              function() 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,
              function() bp.bindByName("doesnotexist", INTEGER));
  // Check blob binding.
  expectError(Cr.NS_ERROR_INVALID_ARG,
              function() bp.bindBlobByName("doesnotexist", BLOB, BLOB.length));

  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();
  // We get an error after calling executeAsync, not when we bind.
  bp.bindByIndex(0, run_test);
  array.addParams(bp);
  stmt.bindParameters(array);

  execAsync(stmt, {error: Ci.mozIStorageError.MISMATCH});

  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();
  // We get an error after calling executeAsync, not when we bind.
  bp.bindByName("blob", run_test);
  array.addParams(bp);
  stmt.bindParameters(array);

  execAsync(stmt, {error: Ci.mozIStorageError.MISMATCH});

  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,
              function() 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,
              function() 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,
              function() 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,
              function() 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();
  let array2 = 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,
              function() 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,
              function() 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.
  do_check_true(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.
 */
let 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);
  else
    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,
];
let index = 0;

const STARTING_UNIQUE_ID = 2;
let 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.
    do_execute_soon(_run_next_test);
  }
}

function run_test()
{
  cleanup();

  do_test_pending();
  run_next_test();
}
