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

const NS_OK = Cr.NS_OK;
const NS_ERROR_FAILURE = Cr.NS_ERROR_FAILURE;
const NS_ERROR_UNEXPECTED = Cr.NS_ERROR_UNEXPECTED;
const NS_ERROR_FILE_NO_DEVICE_SPACE = Cr.NS_ERROR_FILE_NO_DEVICE_SPACE;

const loggingEnabled = false;

var testGenerator;

loadScript("dom/quota/test/common/xpcshell.js");

function log(msg) {
  if (loggingEnabled) {
    info(msg);
  }
}

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

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

function todo(cond, msg) {
  todo_check_true(cond);
}

function run_test() {
  runTest();
}

if (!this.runTest) {
  this.runTest = function () {
    do_get_profile();

    enableStorageTesting();
    enableTesting();

    // 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.strictEqual(
      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 () {
        resetStorageTesting();
        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.strictEqual(
        testSteps.constructor.name,
        "GeneratorFunction",
        "Unsupported function type"
      );

      do_test_pending();

      testGenerator = testSteps();
      testGenerator.next();
    }
  };
}

function finishTest() {
  resetStorageTesting();
  resetTesting();

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

function grabArgAndContinueHandler(arg) {
  testGenerator.next(arg);
}

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

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

function enableTesting() {
  SpecialPowers.setBoolPref(
    "dom.storage.enable_unsupported_legacy_implementation",
    false
  );
}

function resetTesting() {
  SpecialPowers.clearUserPref(
    "dom.storage.enable_unsupported_legacy_implementation"
  );
}

function setGlobalLimit(globalLimit) {
  SpecialPowers.setIntPref(
    "dom.quotaManager.temporaryStorage.fixedLimit",
    globalLimit
  );
}

function resetGlobalLimit() {
  SpecialPowers.clearUserPref("dom.quotaManager.temporaryStorage.fixedLimit");
}

function storageInitialized(callback) {
  let request = SpecialPowers._getQuotaManager().storageInitialized();
  request.callback = callback;

  return request;
}

function persistentStorageInitialized(callback) {
  let request = SpecialPowers._getQuotaManager().persistentStorageInitialized();
  request.callback = callback;

  return request;
}

function temporaryStorageInitialized(callback) {
  let request = SpecialPowers._getQuotaManager().temporaryStorageInitialized();
  request.callback = callback;

  return request;
}

function persistentOriginInitialized(principal, callback) {
  let request =
    SpecialPowers._getQuotaManager().persistentOriginInitialized(principal);
  request.callback = callback;

  return request;
}

function temporaryOriginInitialized(persistence, principal, callback) {
  let request = SpecialPowers._getQuotaManager().temporaryOriginInitialized(
    persistence,
    principal
  );
  request.callback = callback;

  return request;
}

function init(callback) {
  let request = SpecialPowers._getQuotaManager().init();
  request.callback = callback;

  return request;
}

function initializePersistentStorage(callback) {
  let request = SpecialPowers._getQuotaManager().initializePersistentStorage();
  request.callback = callback;

  return request;
}

function initTemporaryStorage(callback) {
  let request = SpecialPowers._getQuotaManager().initTemporaryStorage();
  request.callback = callback;

  return request;
}

function initPersistentOrigin(principal, callback) {
  let request =
    SpecialPowers._getQuotaManager().initializePersistentOrigin(principal);
  request.callback = callback;

  return request;
}

function initTemporaryOrigin(
  persistence,
  principal,
  createIfNonExistent = true,
  callback
) {
  let request = SpecialPowers._getQuotaManager().initializeTemporaryOrigin(
    persistence,
    principal,
    createIfNonExistent
  );
  request.callback = callback;

  return request;
}

function initPersistentClient(principal, client, callback) {
  let request = SpecialPowers._getQuotaManager().initializePersistentClient(
    principal,
    client
  );
  request.callback = callback;

  return request;
}

function initTemporaryClient(
  persistence,
  principal,
  client,
  createIfNonExistent = true,
  callback
) {
  let request = SpecialPowers._getQuotaManager().initializeTemporaryClient(
    persistence,
    principal,
    client,
    createIfNonExistent
  );
  request.callback = callback;

  return request;
}

function getFullOriginMetadata(persistence, principal, callback) {
  const request = SpecialPowers._getQuotaManager().getFullOriginMetadata(
    persistence,
    principal
  );
  request.callback = callback;

  return request;
}

function clearClient(principal, client, persistence, callback) {
  let request = SpecialPowers._getQuotaManager().clearStoragesForClient(
    principal,
    client,
    persistence
  );
  request.callback = callback;

  return request;
}

function clearOrigin(principal, persistence, callback) {
  let request = SpecialPowers._getQuotaManager().clearStoragesForPrincipal(
    principal,
    persistence
  );
  request.callback = callback;

  return request;
}

function clearOriginsByPrefix(principal, persistence, callback) {
  let request = SpecialPowers._getQuotaManager().clearStoragesForOriginPrefix(
    principal,
    persistence
  );
  request.callback = callback;

  return request;
}

function clearPrivateBrowsing(callback) {
  let request =
    SpecialPowers._getQuotaManager().clearStoragesForPrivateBrowsing();
  request.callback = callback;

  return request;
}

function resetClient(principal, client) {
  let request = Services.qms.resetStoragesForClient(
    principal,
    client,
    "default"
  );

  return request;
}

function persist(principal, callback) {
  let request = SpecialPowers._getQuotaManager().persist(principal);
  request.callback = callback;

  return request;
}

function persisted(principal, callback) {
  let request = SpecialPowers._getQuotaManager().persisted(principal);
  request.callback = callback;

  return request;
}

function estimateOrigin(principal, callback) {
  let request = SpecialPowers._getQuotaManager().estimate(principal);
  request.callback = callback;

  return request;
}

function listOrigins(callback) {
  let request = SpecialPowers._getQuotaManager().listOrigins(callback);
  request.callback = callback;

  return request;
}

function getPersistedFromMetadata(readBuffer) {
  const persistedPosition = 8; // Persisted state is stored in the 9th byte
  let view =
    readBuffer instanceof Uint8Array ? readBuffer : new Uint8Array(readBuffer);

  return !!view[persistedPosition];
}

function grabResultAndContinueHandler(request) {
  testGenerator.next(request.result);
}

function grabUsageAndContinueHandler(request) {
  testGenerator.next(request.result.usage);
}

function getUsage(usageHandler, getAll) {
  let request = SpecialPowers._getQuotaManager().getUsage(usageHandler, getAll);

  return request;
}

function getOriginUsage(principal) {
  let request = Services.qms.getUsageForPrincipal(principal, function () {});

  return request;
}

function getCachedOriginUsage(principal) {
  let request = Services.qms.getCachedUsageForPrincipal(
    principal,
    function () {}
  );

  return request;
}

function getCachedOriginUsage(principal) {
  let request = Services.qms.getCachedUsageForPrincipal(principal);

  return request;
}

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

  return request;
}

function getPrincipal(url, attr = {}) {
  let uri = Cc["@mozilla.org/network/io-service;1"]
    .getService(Ci.nsIIOService)
    .newURI(url);
  let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
    Ci.nsIScriptSecurityManager
  );
  return ssm.createContentPrincipal(uri, attr);
}

var SpecialPowers = {
  getBoolPref(prefName) {
    return this._getPrefs().getBoolPref(prefName);
  },

  setBoolPref(prefName, value) {
    this._getPrefs().setBoolPref(prefName, value);
  },

  setIntPref(prefName, value) {
    this._getPrefs().setIntPref(prefName, value);
  },

  clearUserPref(prefName) {
    this._getPrefs().clearUserPref(prefName);
  },

  _getPrefs() {
    let prefService = Cc["@mozilla.org/preferences-service;1"].getService(
      Ci.nsIPrefService
    );
    return prefService.getBranch(null);
  },

  _getQuotaManager() {
    return Cc["@mozilla.org/dom/quota-manager-service;1"].getService(
      Ci.nsIQuotaManagerService
    );
  },
};

function installPackages(packageRelativePaths) {
  if (packageRelativePaths.length != 2) {
    throw new Error("Unsupported number of package relative paths");
  }

  for (const packageRelativePath of packageRelativePaths) {
    installPackage(packageRelativePath);
  }
}

// Take current storage structure on disk and compare it with the expected
// structure. The expected structure is defined in JSON and consists of a per
// test package definition and a shared package definition. The shared package
// definition should contain unknown stuff which needs to be properly handled
// in all situations.
function verifyStorage(packageDefinitionRelativePaths, key, sharedKey) {
  if (packageDefinitionRelativePaths.length != 2) {
    throw new Error("Unsupported number of package definition relative paths");
  }

  function verifyEntries(entries, name, indent = "") {
    log(`${indent}Verifying ${name} entries`);

    indent += "  ";

    for (const entry of entries) {
      const maybeName = entry.name;

      log(`${indent}Verifying entry ${maybeName}`);

      let hasName = false;
      let hasDir = false;
      let hasEntries = false;

      for (const property in entry) {
        switch (property) {
          case "note":
          case "todo":
            break;

          case "name":
            hasName = true;
            break;

          case "dir":
            hasDir = true;
            break;

          case "entries":
            hasEntries = true;
            break;

          default:
            throw new Error(`Unknown property ${property}`);
        }
      }

      if (!hasName) {
        throw new Error("An entry must have the name property");
      }

      if (!hasDir) {
        throw new Error("An entry must have the dir property");
      }

      if (hasEntries && !entry.dir) {
        throw new Error("An entry can't have entries if it's not a directory");
      }

      if (hasEntries) {
        verifyEntries(entry.entries, entry.name, indent);
      }
    }
  }

  function getCurrentEntries() {
    log("Getting current entries");

    function getEntryForFile(file) {
      let entry = {
        name: file.leafName,
        dir: file.isDirectory(),
      };

      if (file.isDirectory()) {
        const enumerator = file.directoryEntries;
        let nextFile;
        while ((nextFile = enumerator.nextFile)) {
          if (!entry.entries) {
            entry.entries = [];
          }
          entry.entries.push(getEntryForFile(nextFile));
        }
      }

      return entry;
    }

    let entries = [];

    let file = getRelativeFile("indexedDB");
    if (file.exists()) {
      entries.push(getEntryForFile(file));
    }

    file = getRelativeFile("storage");
    if (file.exists()) {
      entries.push(getEntryForFile(file));
    }

    file = getRelativeFile("storage.sqlite");
    if (file.exists()) {
      entries.push(getEntryForFile(file));
    }

    verifyEntries(entries, "current");

    return entries;
  }

  function getEntriesFromPackageDefinition(
    packageDefinitionRelativePath,
    lookupKey
  ) {
    log(`Getting ${lookupKey} entries from ${packageDefinitionRelativePath}`);

    const currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
    const file = getRelativeFile(
      packageDefinitionRelativePath + ".json",
      currentDir
    );

    const fileInputStream = Cc[
      "@mozilla.org/network/file-input-stream;1"
    ].createInstance(Ci.nsIFileInputStream);
    fileInputStream.init(file, -1, -1, 0);

    const scriptableInputStream = Cc[
      "@mozilla.org/scriptableinputstream;1"
    ].createInstance(Ci.nsIScriptableInputStream);
    scriptableInputStream.init(fileInputStream);

    const data = scriptableInputStream.readBytes(
      scriptableInputStream.available()
    );

    const obj = JSON.parse(data);

    const result = obj.find(({ key: elementKey }) => elementKey == lookupKey);

    if (!result) {
      throw new Error("The file doesn't contain an element for given key");
    }

    if (!result.entries) {
      throw new Error("The element doesn't have the entries property");
    }

    verifyEntries(result.entries, lookupKey);

    return result.entries;
  }

  function addSharedEntries(expectedEntries, sharedEntries, name, indent = "") {
    log(`${indent}Checking common ${name} entries`);

    indent += "  ";

    for (const sharedEntry of sharedEntries) {
      const expectedEntry = expectedEntries.find(
        ({ name: elementName }) => elementName == sharedEntry.name
      );

      if (expectedEntry) {
        log(`${indent}Checking common entry ${sharedEntry.name}`);

        if (!expectedEntry.dir || !sharedEntry.dir) {
          throw new Error("A common entry must be a directory");
        }

        if (!expectedEntry.entries && !sharedEntry.entries) {
          throw new Error("A common entry must not be a leaf");
        }

        if (sharedEntry.entries) {
          if (!expectedEntry.entries) {
            expectedEntry.entries = [];
          }

          addSharedEntries(
            expectedEntry.entries,
            sharedEntry.entries,
            sharedEntry.name,
            indent
          );
        }
      } else {
        log(`${indent}Adding entry ${sharedEntry.name}`);
        expectedEntries.push(sharedEntry);
      }
    }
  }

  function compareEntries(currentEntries, expectedEntries, name, indent = "") {
    log(`${indent}Comparing ${name} entries`);

    indent += "  ";

    if (currentEntries.length != expectedEntries.length) {
      throw new Error("Entries must have the same length");
    }

    for (const currentEntry of currentEntries) {
      log(`${indent}Comparing entry ${currentEntry.name}`);

      const expectedEntry = expectedEntries.find(
        ({ name: elementName }) => elementName == currentEntry.name
      );

      if (!expectedEntry) {
        throw new Error("Cannot find a matching entry");
      }

      if (expectedEntry.dir != currentEntry.dir) {
        throw new Error("The dir property doesn't match");
      }

      if (
        (expectedEntry.entries && !currentEntry.entries) ||
        (!expectedEntry.entries && currentEntry.entries)
      ) {
        throw new Error("The entries property doesn't match");
      }

      if (expectedEntry.entries) {
        compareEntries(
          currentEntry.entries,
          expectedEntry.entries,
          currentEntry.name,
          indent
        );
      }
    }
  }

  const currentEntries = getCurrentEntries();

  log("Stringified current entries: " + JSON.stringify(currentEntries));

  const expectedEntries = getEntriesFromPackageDefinition(
    packageDefinitionRelativePaths[0],
    key
  );
  const sharedEntries = getEntriesFromPackageDefinition(
    packageDefinitionRelativePaths[1],
    sharedKey ? sharedKey : key
  );

  addSharedEntries(expectedEntries, sharedEntries, key);

  log("Stringified expected entries: " + JSON.stringify(expectedEntries));

  compareEntries(currentEntries, expectedEntries, key);
}

async function verifyInitializationStatus(
  expectStorageIsInitialized,
  expectPersistentStorageIsInitialized,
  expectTemporaryStorageIsInitialized
) {
  if (!expectStorageIsInitialized && expectPersistentStorageIsInitialized) {
    throw new Error("Invalid expectation");
  }

  if (!expectStorageIsInitialized && expectTemporaryStorageIsInitialized) {
    throw new Error("Invalid expectation");
  }

  let request = storageInitialized();
  await requestFinished(request);

  const storageIsInitialized = request.result;

  request = persistentStorageInitialized();
  await requestFinished(request);

  const persistentStorageIsInitialized = request.result;

  request = temporaryStorageInitialized();
  await requestFinished(request);

  const temporaryStorageIsInitialized = request.result;

  ok(
    !(!storageIsInitialized && persistentStorageIsInitialized),
    "Initialization status is consistent"
  );

  ok(
    !(!storageIsInitialized && temporaryStorageIsInitialized),
    "Initialization status is consistent"
  );

  if (expectStorageIsInitialized) {
    ok(storageIsInitialized, "Storage is initialized");
  } else {
    ok(!storageIsInitialized, "Storage is not initialized");
  }

  if (expectPersistentStorageIsInitialized) {
    ok(persistentStorageIsInitialized, "Persistent storage is initialized");
  } else {
    ok(
      !persistentStorageIsInitialized,
      "Persistent storage is not initialized"
    );
  }

  if (expectTemporaryStorageIsInitialized) {
    ok(temporaryStorageIsInitialized, "Temporary storage is initialized");
  } else {
    ok(!temporaryStorageIsInitialized, "Temporary storage is not initialized");
  }
}
