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

// Tests using testGenerator are expected to define it themselves.
// Testing functions are expected to call testSteps and its type should either
// be GeneratorFunction or AsyncFunction
/* global testGenerator, testSteps:false */

var { classes: Cc, interfaces: Ci, utils: Cu } = Components;

if (!("self" in this)) {
  this.self = this;
}

var bufferCache = [];

function is(a, b, msg) {
  Assert.equal(a, b, msg);
}

function ok(cond, msg) {
  Assert.ok(!!cond, msg);
}

function isnot(a, b, msg) {
  Assert.notEqual(a, b, msg);
}

function todo(condition) {
  todo_check_true(condition);
}

function run_test() {
  runTest();
}

if (!this.runTest) {
  this.runTest = function () {
    if (SpecialPowers.isMainProcess()) {
      // XPCShell does not get a profile by default.
      do_get_profile();

      enableTesting();
      enableExperimental();
    }

    // In order to support converting tests to using async functions from using
    // generator functions, we detect async functions by checking the name of
    // function's constructor.
    Assert.ok(
      typeof testSteps === "function",
      "There should be a testSteps function"
    );
    if (testSteps.constructor.name === "AsyncFunction") {
      // Do run our existing cleanup function that would normally be called by
      // the generator's call to finishTest().
      registerCleanupFunction(function () {
        if (SpecialPowers.isMainProcess()) {
          resetTesting();
        }
      });

      add_task(testSteps);

      // Since we defined run_test, we must invoke run_next_test() to start the
      // async test.
      run_next_test();
    } else {
      Assert.ok(
        testSteps.constructor.name === "GeneratorFunction",
        "Unsupported function type"
      );

      do_test_pending();
      testGenerator.next();
    }
  };
}

function finishTest() {
  if (SpecialPowers.isMainProcess()) {
    resetExperimental();
    resetTesting();
  }

  SpecialPowers.removeFiles();

  executeSoon(function () {
    do_test_finished();
  });
}

function grabEventAndContinueHandler(event) {
  testGenerator.next(event);
}

function continueToNextStep() {
  executeSoon(function () {
    testGenerator.next();
  });
}

function errorHandler(event) {
  try {
    dump("indexedDB error: " + event.target.error.name);
  } catch (e) {
    dump("indexedDB error: " + e);
  }
  Assert.ok(false);
  finishTest();
}

function unexpectedSuccessHandler() {
  Assert.ok(false);
  finishTest();
}

function expectedErrorHandler(name) {
  return function (event) {
    Assert.equal(event.type, "error");
    Assert.equal(event.target.error.name, name);
    event.preventDefault();
    grabEventAndContinueHandler(event);
  };
}

function expectUncaughtException() {
  // This is dummy for xpcshell test.
}

function ExpectError(name, preventDefault) {
  this._name = name;
  this._preventDefault = preventDefault;
}
ExpectError.prototype = {
  handleEvent(event) {
    Assert.equal(event.type, "error");
    Assert.equal(this._name, event.target.error.name);
    if (this._preventDefault) {
      event.preventDefault();
      event.stopPropagation();
    }
    grabEventAndContinueHandler(event);
  },
};

function continueToNextStepSync() {
  testGenerator.next();
}

// TODO compareKeys is duplicated in ../helpers.js, can we import that here?
// the same applies to many other functions in this file
// this duplication should be avoided (bug 1565986)
function compareKeys(k1, k2) {
  let t = typeof k1;
  if (t != typeof k2) {
    return false;
  }

  if (t !== "object") {
    return k1 === k2;
  }

  if (k1 instanceof Date) {
    return k2 instanceof Date && k1.getTime() === k2.getTime();
  }

  if (k1 instanceof Array) {
    if (!(k2 instanceof Array) || k1.length != k2.length) {
      return false;
    }

    for (let i = 0; i < k1.length; ++i) {
      if (!compareKeys(k1[i], k2[i])) {
        return false;
      }
    }

    return true;
  }

  if (k1 instanceof ArrayBuffer) {
    if (!(k2 instanceof ArrayBuffer)) {
      return false;
    }

    function arrayBuffersAreEqual(a, b) {
      if (a.byteLength != b.byteLength) {
        return false;
      }
      let ui8b = new Uint8Array(b);
      return new Uint8Array(a).every((val, i) => val === ui8b[i]);
    }

    return arrayBuffersAreEqual(k1, k2);
  }

  return false;
}

function addPermission() {
  throw new Error("addPermission");
}

function removePermission() {
  throw new Error("removePermission");
}

function allowIndexedDB() {
  throw new Error("allowIndexedDB");
}

function disallowIndexedDB() {
  throw new Error("disallowIndexedDB");
}

function enableExperimental() {
  SpecialPowers.setBoolPref("dom.indexedDB.experimental", true);
}

function resetExperimental() {
  SpecialPowers.clearUserPref("dom.indexedDB.experimental");
}

function enableTesting() {
  SpecialPowers.setBoolPref("dom.indexedDB.testing", true);
}

function resetTesting() {
  SpecialPowers.clearUserPref("dom.indexedDB.testing");
}

function gc() {
  Cu.forceGC();
  Cu.forceCC();
}

function scheduleGC() {
  SpecialPowers.exactGC(continueToNextStep);
}

function setTimeout(fun, timeout) {
  let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  var event = {
    notify() {
      fun();
    },
  };
  timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
  return timer;
}

function initStorage() {
  return Services.qms.init();
}

function initPersistentOrigin(principal) {
  return Services.qms.initializePersistentOrigin(principal);
}

function resetOrClearAllDatabases(callback, clear) {
  if (!SpecialPowers.isMainProcess()) {
    throw new Error("clearAllDatabases not implemented for child processes!");
  }

  const quotaPref = "dom.quotaManager.testing";

  let oldPrefValue;
  if (Services.prefs.prefHasUserValue(quotaPref)) {
    oldPrefValue = SpecialPowers.getBoolPref(quotaPref);
  }

  SpecialPowers.setBoolPref(quotaPref, true);

  let request;

  try {
    if (clear) {
      request = Services.qms.clear();
    } else {
      request = Services.qms.reset();
    }
  } catch (e) {
    if (oldPrefValue !== undefined) {
      SpecialPowers.setBoolPref(quotaPref, oldPrefValue);
    } else {
      SpecialPowers.clearUserPref(quotaPref);
    }
    throw e;
  }

  request.callback = callback;

  return request;
}

function resetAllDatabases(callback) {
  return resetOrClearAllDatabases(callback, false);
}

function clearAllDatabases(callback) {
  return resetOrClearAllDatabases(callback, true);
}

function installPackagedProfile(packageName) {
  let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);

  let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);

  let packageFile = currentDir.clone();
  packageFile.append(packageName + ".zip");

  let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(
    Ci.nsIZipReader
  );
  zipReader.open(packageFile);

  let entryNames = [];
  for (let entry of zipReader.findEntries(null)) {
    if (entry != "create_db.html") {
      entryNames.push(entry);
    }
  }
  entryNames.sort();

  for (let entryName of entryNames) {
    let zipentry = zipReader.getEntry(entryName);

    let file = profileDir.clone();
    let split = entryName.split("/");
    for (let i = 0; i < split.length; i++) {
      file.append(split[i]);
    }

    if (zipentry.isDirectory) {
      file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
    } else {
      let istream = zipReader.getInputStream(entryName);

      var ostream = Cc[
        "@mozilla.org/network/file-output-stream;1"
      ].createInstance(Ci.nsIFileOutputStream);
      ostream.init(file, -1, parseInt("0644", 8), 0);

      let bostream = Cc[
        "@mozilla.org/network/buffered-output-stream;1"
      ].createInstance(Ci.nsIBufferedOutputStream);
      bostream.init(ostream, 32768);

      bostream.writeFrom(istream, istream.available());

      istream.close();
      bostream.close();
    }
  }

  zipReader.close();
}

function getChromeFilesDir() {
  let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);

  let idbDir = profileDir.clone();
  idbDir.append("storage");
  idbDir.append("permanent");
  idbDir.append("chrome");
  idbDir.append("idb");

  let idbEntries = idbDir.directoryEntries;
  while (idbEntries.hasMoreElements()) {
    let file = idbEntries.nextFile;
    if (file.isDirectory()) {
      return file;
    }
  }

  throw new Error("files directory doesn't exist!");
}

function getView(size) {
  let buffer = new ArrayBuffer(size);
  let view = new Uint8Array(buffer);
  is(buffer.byteLength, size, "Correct byte length");
  return view;
}

function getRandomView(size) {
  let view = getView(size);
  for (let i = 0; i < size; i++) {
    view[i] = parseInt(Math.random() * 255);
  }
  return view;
}

function getBlob(str) {
  return new Blob([str], { type: "type/text" });
}

function getFile(name, type, str) {
  return new File([str], name, { type });
}

function isWasmSupported() {
  let testingFunctions = Cu.getJSTestingFunctions();
  return testingFunctions.wasmIsSupported();
}

function getWasmModule(binary) {
  let module = new WebAssembly.Module(binary);
  return module;
}

function compareBuffers(buffer1, buffer2) {
  if (buffer1.byteLength != buffer2.byteLength) {
    return false;
  }

  let view1 = buffer1 instanceof Uint8Array ? buffer1 : new Uint8Array(buffer1);
  let view2 = buffer2 instanceof Uint8Array ? buffer2 : new Uint8Array(buffer2);
  for (let i = 0; i < buffer1.byteLength; i++) {
    if (view1[i] != view2[i]) {
      return false;
    }
  }
  return true;
}

function verifyBuffers(buffer1, buffer2) {
  ok(compareBuffers(buffer1, buffer2), "Correct buffer data");
}

function verifyBlob(blob1, blob2) {
  is(Blob.isInstance(blob1), true, "Instance of nsIDOMBlob");
  is(File.isInstance(blob1), File.isInstance(blob2), "Instance of DOM File");
  is(blob1.size, blob2.size, "Correct size");
  is(blob1.type, blob2.type, "Correct type");
  if (File.isInstance(blob2)) {
    is(blob1.name, blob2.name, "Correct name");
  }

  let buffer1;
  let buffer2;

  for (let i = 0; i < bufferCache.length; i++) {
    if (bufferCache[i].blob == blob2) {
      buffer2 = bufferCache[i].buffer;
      break;
    }
  }

  if (!buffer2) {
    let reader = new FileReader();
    reader.readAsArrayBuffer(blob2);
    reader.onload = function (event) {
      buffer2 = event.target.result;
      bufferCache.push({ blob: blob2, buffer: buffer2 });
      if (buffer1) {
        verifyBuffers(buffer1, buffer2);
        testGenerator.next();
      }
    };
  }

  let reader = new FileReader();
  reader.readAsArrayBuffer(blob1);
  reader.onload = function (event) {
    buffer1 = event.target.result;
    if (buffer2) {
      verifyBuffers(buffer1, buffer2);
      testGenerator.next();
    }
  };
}

function verifyView(view1, view2) {
  is(view1.byteLength, view2.byteLength, "Correct byteLength");
  verifyBuffers(view1, view2);
  continueToNextStep();
}

function grabFileUsageAndContinueHandler(request) {
  testGenerator.next(request.result.fileUsage);
}

function getCurrentUsage(usageHandler) {
  let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
    Ci.nsIPrincipal
  );
  Services.qms.getUsageForPrincipal(principal, usageHandler);
}

function setTemporaryStorageLimit(limit) {
  const pref = "dom.quotaManager.temporaryStorage.fixedLimit";
  if (limit) {
    info("Setting temporary storage limit to " + limit);
    SpecialPowers.setIntPref(pref, limit);
  } else {
    info("Removing temporary storage limit");
    SpecialPowers.clearUserPref(pref);
  }
}

function setDataThreshold(threshold) {
  info("Setting data threshold to " + threshold);
  SpecialPowers.setIntPref("dom.indexedDB.dataThreshold", threshold);
}

function resetDataThreshold() {
  info("Clearing data threshold pref");
  SpecialPowers.clearUserPref("dom.indexedDB.dataThreshold");
}

function setMaxSerializedMsgSize(aSize) {
  info("Setting maximal size of a serialized message to " + aSize);
  SpecialPowers.setIntPref("dom.indexedDB.maxSerializedMsgSize", aSize);
}

function enablePreprocessing() {
  info("Setting preprocessing pref");
  SpecialPowers.setBoolPref("dom.indexedDB.preprocessing", true);
}

function resetPreprocessing() {
  info("Clearing preprocessing pref");
  SpecialPowers.clearUserPref("dom.indexedDB.preprocessing");
}

function getSystemPrincipal() {
  return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
}

function getPrincipal(url) {
  let uri = Services.io.newURI(url);
  return Services.scriptSecurityManager.createContentPrincipal(uri, {});
}

class RequestError extends Error {
  constructor(resultCode, resultName) {
    super(`Request failed (code: ${resultCode}, name: ${resultName})`);
    this.name = "RequestError";
    this.resultCode = resultCode;
    this.resultName = resultName;
  }
}

async function requestFinished(request) {
  await new Promise(function (resolve) {
    request.callback = function () {
      resolve();
    };
  });

  if (request.resultCode !== Cr.NS_OK) {
    throw new RequestError(request.resultCode, request.resultName);
  }

  return request.result;
}

// TODO: Rename to openDBRequestSucceeded ?
function expectingSuccess(request) {
  return new Promise(function (resolve, reject) {
    request.onerror = function (event) {
      ok(false, "indexedDB error, '" + event.target.error.name + "'");
      reject(event);
    };
    request.onsuccess = function (event) {
      resolve(event);
    };
    request.onupgradeneeded = function (event) {
      ok(false, "Got upgrade, but did not expect it!");
      reject(event);
    };
  });
}

// TODO: Rename to openDBRequestUpgradeNeeded ?
function expectingUpgrade(request) {
  return new Promise(function (resolve, reject) {
    request.onerror = function (event) {
      ok(false, "indexedDB error, '" + event.target.error.name + "'");
      reject(event);
    };
    request.onupgradeneeded = function (event) {
      resolve(event);
    };
    request.onsuccess = function (event) {
      ok(false, "Got success, but did not expect it!");
      reject(event);
    };
  });
}

function requestSucceeded(request) {
  return new Promise(function (resolve, reject) {
    request.onerror = function (event) {
      ok(false, "indexedDB error, '" + event.target.error.name + "'");
      reject(event);
    };
    request.onsuccess = function (event) {
      resolve(event);
    };
  });
}

// Given a "/"-delimited path relative to the profile directory,
// return an nsIFile representing the path.  This does not test
// for the existence of the file or parent directories.
// It is safe even on Windows where the directory separator is not "/",
// but make sure you're not passing in a "\"-delimited path.
function getRelativeFile(relativePath) {
  let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);

  let file = profileDir.clone();
  relativePath.split("/").forEach(function (component) {
    file.append(component);
  });

  return file;
}

var SpecialPowers = {
  isMainProcess() {
    return (
      Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT
    );
  },
  notifyObservers(subject, topic, data) {
    Services.obs.notifyObservers(subject, topic, data);
  },
  notifyObserversInParentProcess(subject, topic, data) {
    if (subject) {
      throw new Error("Can't send subject to another process!");
    }
    return this.notifyObservers(subject, topic, data);
  },
  getBoolPref(prefName) {
    return Services.prefs.getBoolPref(prefName);
  },
  setBoolPref(prefName, value) {
    Services.prefs.setBoolPref(prefName, value);
  },
  setIntPref(prefName, value) {
    Services.prefs.setIntPref(prefName, value);
  },
  clearUserPref(prefName) {
    Services.prefs.clearUserPref(prefName);
  },
  // Copied (and slightly adjusted) from testing/specialpowers/api.js
  exactGC(callback) {
    let count = 0;

    function doPreciseGCandCC() {
      function scheduledGCCallback() {
        Cu.forceCC();

        if (++count < 3) {
          doPreciseGCandCC();
        } else {
          callback();
        }
      }

      Cu.schedulePreciseGC(scheduledGCCallback);
    }

    doPreciseGCandCC();
  },

  get Cc() {
    return Cc;
  },

  get Ci() {
    return Ci;
  },

  get Cu() {
    return Cu;
  },

  // Based on SpecialPowersObserver.prototype.receiveMessage
  createFiles(requests, callback) {
    let filePaths = [];
    if (!this._createdFiles) {
      this._createdFiles = [];
    }
    let createdFiles = this._createdFiles;
    let promises = [];
    requests.forEach(function (request) {
      const filePerms = 0o666;
      let testFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
      if (request.name) {
        testFile.append(request.name);
      } else {
        testFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, filePerms);
      }
      let outStream = Cc[
        "@mozilla.org/network/file-output-stream;1"
      ].createInstance(Ci.nsIFileOutputStream);
      outStream.init(
        testFile,
        0x02 | 0x08 | 0x20, // PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE
        filePerms,
        0
      );
      if (request.data) {
        outStream.write(request.data, request.data.length);
        outStream.close();
      }
      promises.push(
        File.createFromFileName(testFile.path, request.options).then(function (
          file
        ) {
          filePaths.push(file);
        })
      );
      createdFiles.push(testFile);
    });

    Promise.all(promises).then(function () {
      setTimeout(function () {
        callback(filePaths);
      }, 0);
    });
  },

  removeFiles() {
    if (this._createdFiles) {
      this._createdFiles.forEach(function (testFile) {
        try {
          testFile.remove(false);
        } catch (e) {}
      });
      this._createdFiles = null;
    }
  },
};
