/**
 * Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/
 */
"use strict";

var disableWorkerTest = "This test uses SpecialPowers";

var self = this;

var testGenerator = testSteps();

function testSteps()
{
  const dbName = self.window ? window.location.pathname : "test_lowDiskSpace";
  const dbVersion = 1;

  const objectStoreName = "foo";
  const objectStoreOptions = { keyPath: "foo" };

  const indexName = "bar";
  const indexOptions = { unique: true };

  const dbData = [
    { foo: 0, bar: 0 },
    { foo: 1, bar: 10 },
    { foo: 2, bar: 20 },
    { foo: 3, bar: 30 },
    { foo: 4, bar: 40 },
    { foo: 5, bar: 50 },
    { foo: 6, bar: 60 },
    { foo: 7, bar: 70 },
    { foo: 8, bar: 80 },
    { foo: 9, bar: 90 }
  ];

  let lowDiskMode = false;
  function setLowDiskMode(val) {
    let data = val ? "full" : "free";

    if (val == lowDiskMode) {
      info("Low disk mode is: " + data);
    }
    else {
      info("Changing low disk mode to: " + data);
      SpecialPowers.notifyObserversInParentProcess(null, "disk-space-watcher",
                                                   data);
      lowDiskMode = val;
    }
  }

  { // Make sure opening works from the beginning.
    info("Test 1");

    setLowDiskMode(false);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = errorHandler;
    request.onsuccess = grabEventAndContinueHandler;
    let event = yield undefined;

    is(event.type, "success", "Opened database without setting low disk mode");

    let db = event.target.result;
    db.close();
  }

  { // Make sure delete works in low disk mode.
    info("Test 2");

    setLowDiskMode(true);

    let request = indexedDB.deleteDatabase(dbName);
    request.onerror = errorHandler;
    request.onsuccess = grabEventAndContinueHandler;
    let event = yield undefined;

    is(event.type, "success", "Deleted database after setting low disk mode");
  }

  { // Make sure creating a db in low disk mode fails.
    info("Test 3");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = expectedErrorHandler("QuotaExceededError");
    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = unexpectedSuccessHandler;
    let event = yield undefined;

    is(event.type, "error", "Didn't create new database in low disk mode");
  }

  { // Make sure opening an already-existing db in low disk mode succeeds.
    info("Test 4");

    setLowDiskMode(false);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;
    let event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    let db = event.target.result;
    db.onerror = errorHandler;

    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Created database");
    ok(event.target.result === db, "Got the same database");

    db.close();

    setLowDiskMode(true);

    request = indexedDB.open(dbName);
    request.onerror = errorHandler;
    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Opened existing database in low disk mode");

    db = event.target.result;
    db.close();
  }

  { // Make sure upgrading an already-existing db in low disk mode succeeds.
    info("Test 5");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion + 1);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;

    let event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    let db = event.target.result;
    db.onerror = errorHandler;

    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Created database");
    ok(event.target.result === db, "Got the same database");

    db.close();
  }

  { // Make sure creating objectStores in low disk mode fails.
    info("Test 6");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion + 2);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;

    let event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    let db = event.target.result;
    db.onerror = errorHandler;

    let txn = event.target.transaction;
    txn.onerror = expectedErrorHandler("AbortError");
    txn.onabort = grabEventAndContinueHandler;

    let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions);

    request.onupgradeneeded = unexpectedSuccessHandler;
    event = yield undefined;

    is(event.type, "abort", "Got correct event type");
    is(event.target.error.name, "QuotaExceededError", "Got correct error type");

    request.onerror = expectedErrorHandler("AbortError");
    event = yield undefined;
  }

  { // Make sure creating indexes in low disk mode fails.
    info("Test 7");

    setLowDiskMode(false);

    let request = indexedDB.open(dbName, dbVersion + 2);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;

    let event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    let db = event.target.result;
    db.onerror = errorHandler;

    let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions);

    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Upgraded database");
    ok(event.target.result === db, "Got the same database");

    db.close();

    setLowDiskMode(true);

    request = indexedDB.open(dbName, dbVersion + 3);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;
    event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    db = event.target.result;
    db.onerror = errorHandler;
    let txn = event.target.transaction;
    txn.onerror = expectedErrorHandler("AbortError");
    txn.onabort = grabEventAndContinueHandler;

    objectStore = event.target.transaction.objectStore(objectStoreName);
    let index = objectStore.createIndex(indexName, indexName, indexOptions);

    request.onupgradeneeded = unexpectedSuccessHandler;
    event = yield undefined;

    is(event.type, "abort", "Got correct event type");
    is(event.target.error.name, "QuotaExceededError", "Got correct error type");

    request.onerror = expectedErrorHandler("AbortError");
    event = yield undefined;
  }

  { // Make sure deleting indexes in low disk mode succeeds.
    info("Test 8");

    setLowDiskMode(false);

    let request = indexedDB.open(dbName, dbVersion + 3);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;

    let event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    let db = event.target.result;
    db.onerror = errorHandler;

    let objectStore = event.target.transaction.objectStore(objectStoreName);
    let index = objectStore.createIndex(indexName, indexName, indexOptions);

    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Upgraded database");
    ok(event.target.result === db, "Got the same database");

    db.close();

    setLowDiskMode(true);

    request = indexedDB.open(dbName, dbVersion + 4);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;
    event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    db = event.target.result;
    db.onerror = errorHandler;

    objectStore = event.target.transaction.objectStore(objectStoreName);
    objectStore.deleteIndex(indexName);

    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Upgraded database");
    ok(event.target.result === db, "Got the same database");

    db.close();
  }

  { // Make sure deleting objectStores in low disk mode succeeds.
    info("Test 9");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion + 5);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;

    let event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    let db = event.target.result;
    db.onerror = errorHandler;

    db.deleteObjectStore(objectStoreName);

    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Upgraded database");
    ok(event.target.result === db, "Got the same database");

    db.close();

    // Reset everything.
    indexedDB.deleteDatabase(dbName);
  }


  { // Add data that the rest of the tests will use.
    info("Adding test data");

    setLowDiskMode(false);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = errorHandler;
    request.onupgradeneeded = grabEventAndContinueHandler;
    request.onsuccess = unexpectedSuccessHandler;
    let event = yield undefined;

    is(event.type, "upgradeneeded", "Upgrading database");

    let db = event.target.result;
    db.onerror = errorHandler;

    let objectStore = db.createObjectStore(objectStoreName, objectStoreOptions);
    let index = objectStore.createIndex(indexName, indexName, indexOptions);

    for (let data of dbData) {
      objectStore.add(data);
    }

    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "success", "Upgraded database");
    ok(event.target.result === db, "Got the same database");

    db.close();
  }

  { // Make sure read operations in readonly transactions succeed in low disk
    // mode.
    info("Test 10");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = errorHandler;
    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    let event = yield undefined;

    let db = event.target.result;
    db.onerror = errorHandler;

    let transaction = db.transaction(objectStoreName);
    let objectStore = transaction.objectStore(objectStoreName);
    let index = objectStore.index(indexName);

    let data = dbData[0];

    let requestCounter = new RequestCounter();

    objectStore.get(data.foo).onsuccess = requestCounter.handler();
    objectStore.mozGetAll().onsuccess = requestCounter.handler();
    objectStore.count().onsuccess = requestCounter.handler();
    index.get(data.bar).onsuccess = requestCounter.handler();
    index.mozGetAll().onsuccess = requestCounter.handler();
    index.getKey(data.bar).onsuccess = requestCounter.handler();
    index.mozGetAllKeys().onsuccess = requestCounter.handler();
    index.count().onsuccess = requestCounter.handler();

    let objectStoreDataCount = 0;

    request = objectStore.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        objectStoreDataCount++;
        objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(objectStoreDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    let indexDataCount = 0;

    request = index.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        indexDataCount++;
        indexDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(indexDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    let indexKeyDataCount = 0;

    request = index.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        indexKeyDataCount++;
        indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(indexKeyDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    // Wait for all requests.
    yield undefined;

    transaction.oncomplete = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "complete", "Transaction succeeded");

    db.close();
  }

  { // Make sure read operations in readwrite transactions succeed in low disk
    // mode.
    info("Test 11");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = errorHandler;
    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    let event = yield undefined;

    let db = event.target.result;
    db.onerror = errorHandler;

    let transaction = db.transaction(objectStoreName, "readwrite");
    let objectStore = transaction.objectStore(objectStoreName);
    let index = objectStore.index(indexName);

    let data = dbData[0];

    let requestCounter = new RequestCounter();

    objectStore.get(data.foo).onsuccess = requestCounter.handler();
    objectStore.mozGetAll().onsuccess = requestCounter.handler();
    objectStore.count().onsuccess = requestCounter.handler();
    index.get(data.bar).onsuccess = requestCounter.handler();
    index.mozGetAll().onsuccess = requestCounter.handler();
    index.getKey(data.bar).onsuccess = requestCounter.handler();
    index.mozGetAllKeys().onsuccess = requestCounter.handler();
    index.count().onsuccess = requestCounter.handler();

    let objectStoreDataCount = 0;

    request = objectStore.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        objectStoreDataCount++;
        objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(objectStoreDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    let indexDataCount = 0;

    request = index.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        indexDataCount++;
        indexDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(indexDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    let indexKeyDataCount = 0;

    request = index.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        indexKeyDataCount++;
        indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(indexKeyDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    // Wait for all requests.
    yield undefined;

    transaction.oncomplete = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "complete", "Transaction succeeded");

    db.close();
  }

  { // Make sure write operations in readwrite transactions fail in low disk
    // mode.
    info("Test 12");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = errorHandler;
    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    let event = yield undefined;

    let db = event.target.result;
    db.onerror = errorHandler;

    let transaction = db.transaction(objectStoreName, "readwrite");
    let objectStore = transaction.objectStore(objectStoreName);
    let index = objectStore.index(indexName);

    let data = dbData[0];
    let newData = { foo: 999, bar: 999 };

    let requestCounter = new RequestCounter();

    objectStore.add(newData).onerror = requestCounter.errorHandler();
    objectStore.put(newData).onerror = requestCounter.errorHandler();

    objectStore.get(data.foo).onsuccess = requestCounter.handler();
    objectStore.mozGetAll().onsuccess = requestCounter.handler();
    objectStore.count().onsuccess = requestCounter.handler();
    index.get(data.bar).onsuccess = requestCounter.handler();
    index.mozGetAll().onsuccess = requestCounter.handler();
    index.getKey(data.bar).onsuccess = requestCounter.handler();
    index.mozGetAllKeys().onsuccess = requestCounter.handler();
    index.count().onsuccess = requestCounter.handler();

    let objectStoreDataCount = 0;

    request = objectStore.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        objectStoreDataCount++;
        cursor.update(cursor.value).onerror = requestCounter.errorHandler();
        objectStoreDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(objectStoreDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    let indexDataCount = 0;

    request = index.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        indexDataCount++;
        cursor.update(cursor.value).onerror = requestCounter.errorHandler();
        indexDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(indexDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    let indexKeyDataCount = 0;

    request = index.openCursor();
    request.onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        indexKeyDataCount++;
        cursor.update(cursor.value).onerror = requestCounter.errorHandler();
        indexKeyDataCount % 2 ? cursor.continue() : cursor.advance(1);
      }
      else {
        is(indexKeyDataCount, dbData.length, "Saw all data");
        requestCounter.decr();
      }
    };
    requestCounter.incr();

    // Wait for all requests.
    yield undefined;

    transaction.oncomplete = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "complete", "Transaction succeeded");

    db.close();
  }

  { // Make sure deleting operations in readwrite transactions succeed in low
    // disk mode.
    info("Test 13");

    setLowDiskMode(true);

    let request = indexedDB.open(dbName, dbVersion);
    request.onerror = errorHandler;
    request.onupgradeneeded = unexpectedSuccessHandler;
    request.onsuccess = grabEventAndContinueHandler;
    let event = yield undefined;

    let db = event.target.result;
    db.onerror = errorHandler;

    let transaction = db.transaction(objectStoreName, "readwrite");
    let objectStore = transaction.objectStore(objectStoreName);
    let index = objectStore.index(indexName);

    let dataIndex = 0;
    let data = dbData[dataIndex++];

    let requestCounter = new RequestCounter();

    objectStore.delete(data.foo).onsuccess = requestCounter.handler();

    objectStore.openCursor().onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        cursor.delete().onsuccess = requestCounter.handler();
      }
      requestCounter.decr();
    };
    requestCounter.incr();

    index.openCursor(null, "prev").onsuccess = function(event) {
      let cursor = event.target.result;
      if (cursor) {
        cursor.delete().onsuccess = requestCounter.handler();
      }
      requestCounter.decr();
    };
    requestCounter.incr();

    yield undefined;

    objectStore.count().onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.target.result, dbData.length - 3, "Actually deleted something");

    objectStore.clear();
    objectStore.count().onsuccess = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.target.result, 0, "Actually cleared");

    transaction.oncomplete = grabEventAndContinueHandler;
    event = yield undefined;

    is(event.type, "complete", "Transaction succeeded");

    db.close();
  }

  finishTest();
  yield undefined;
}

function RequestCounter(expectedType) {
  this._counter = 0;
}
RequestCounter.prototype = {
  incr: function() {
    this._counter++;
  },

  decr: function() {
    if (!--this._counter) {
      continueToNextStepSync();
    }
  },

  handler: function(type, preventDefault) {
    this.incr();
    return function(event) {
      is(event.type, type || "success", "Correct type");
      this.decr();
    }.bind(this);
  },

  errorHandler: function(eventType, errorName) {
    this.incr();
    return function(event) {
      is(event.type, eventType || "error", "Correct type");
      is(event.target.error.name, errorName || "QuotaExceededError",
          "Correct error name");
      event.preventDefault();
      event.stopPropagation();
      this.decr();
    }.bind(this);
  }
};