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

"use strict";

const { SQLiteKeyValueService, KeyValueImporter } = ChromeUtils.importESModule(
  "moz-src:///toolkit/components/kvstore/kvstore.sys.mjs"
);

add_setup(async function setup() {
  do_get_profile();
});

async function makeDatabaseDir(name) {
  const databaseDir = PathUtils.join(PathUtils.profileDir, name);
  await IOUtils.makeDirectory(databaseDir);
  return databaseDir;
}

async function allEntries(db) {
  return Array.from(await db.enumerate(), ({ key, value }) => ({ key, value }));
}

async function allKeys(db) {
  let entries = await allEntries(db);
  return entries.map(({ key }) => key);
}

async function copyRkvFixture(fixtureName, destDir) {
  const src = do_get_file("data/" + fixtureName + ".bin");
  await IOUtils.copy(src.path, PathUtils.join(destDir, "data.safe.bin"));
}

add_task(async function getOrCreate() {
  const databaseDir = await makeDatabaseDir("getOrCreate");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");
  Assert.ok(database);
  await database.close();

  // Test creating a database with a nonexistent path.
  const nonexistentDir = PathUtils.join(PathUtils.profileDir, "nonexistent");
  await Assert.rejects(
    SQLiteKeyValueService.getOrCreate(nonexistentDir),
    /storage dir/
  );

  // Test creating a database with a non-normalized but fully-qualified path.
  let nonNormalizedDir = await makeDatabaseDir("non-normalized");
  nonNormalizedDir = [nonNormalizedDir, "..", ".", "non-normalized"].join(
    Services.appinfo.OS === "WINNT" ? "\\" : "/"
  );
  const nonNormalizedRepository = await SQLiteKeyValueService.getOrCreate(
    nonNormalizedDir,
    "db"
  );
  Assert.ok(nonNormalizedRepository);
  await nonNormalizedRepository.close();
});

add_task(async function putGetHasDelete() {
  const databaseDir = await makeDatabaseDir("putGetHasDelete");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  // Getting key/value pairs that don't exist (yet) returns default values
  // or null, depending on whether you specify a default value.
  Assert.strictEqual(await database.get("int-key", 1), 1);
  Assert.strictEqual(await database.get("double-key", 1.1), 1.1);
  Assert.strictEqual(await database.get("string-key", ""), "");
  Assert.strictEqual(await database.get("bool-key", false), false);
  Assert.strictEqual(await database.get("int-key"), undefined);
  Assert.strictEqual(await database.get("double-key"), undefined);
  Assert.strictEqual(await database.get("string-key"), undefined);
  Assert.strictEqual(await database.get("bool-key"), undefined);

  // The put method succeeds without returning a value.
  Assert.strictEqual(await database.put("int-key", 1234), undefined);
  Assert.strictEqual(await database.put("double-key", 56.78), undefined);
  Assert.strictEqual(
    await database.put("string-key", "Héllo, wőrld!"),
    undefined
  );
  Assert.strictEqual(await database.put("bool-key", true), undefined);

  // Getting key/value pairs that exist returns the expected values.
  Assert.strictEqual(await database.get("int-key", 1), 1234);
  Assert.strictEqual(await database.get("double-key", 1.1), 56.78);
  Assert.strictEqual(await database.get("string-key", ""), "Héllo, wőrld!");
  Assert.strictEqual(await database.get("bool-key", false), true);
  Assert.strictEqual(await database.get("int-key"), 1234);
  Assert.strictEqual(await database.get("double-key"), 56.78);
  Assert.strictEqual(await database.get("string-key"), "Héllo, wőrld!");
  Assert.strictEqual(await database.get("bool-key"), true);

  // The has() method works as expected for both existing and non-existent keys.
  Assert.strictEqual(await database.has("int-key"), true);
  Assert.strictEqual(await database.has("double-key"), true);
  Assert.strictEqual(await database.has("string-key"), true);
  Assert.strictEqual(await database.has("bool-key"), true);
  Assert.strictEqual(await database.has("nonexistent-key"), false);

  // The delete() method succeeds without returning a value.
  Assert.strictEqual(await database.delete("int-key"), undefined);
  Assert.strictEqual(await database.delete("double-key"), undefined);
  Assert.strictEqual(await database.delete("string-key"), undefined);
  Assert.strictEqual(await database.delete("bool-key"), undefined);

  // The has() method works as expected for a deleted key.
  Assert.strictEqual(await database.has("int-key"), false);
  Assert.strictEqual(await database.has("double-key"), false);
  Assert.strictEqual(await database.has("string-key"), false);
  Assert.strictEqual(await database.has("bool-key"), false);

  // Getting key/value pairs that were deleted returns default values.
  Assert.strictEqual(await database.get("int-key", 1), 1);
  Assert.strictEqual(await database.get("double-key", 1.1), 1.1);
  Assert.strictEqual(await database.get("string-key", ""), "");
  Assert.strictEqual(await database.get("bool-key", false), false);
  Assert.strictEqual(await database.get("int-key"), undefined);
  Assert.strictEqual(await database.get("double-key"), undefined);
  Assert.strictEqual(await database.get("string-key"), undefined);
  Assert.strictEqual(await database.get("bool-key"), undefined);
});

add_task(async function putWithResizing() {
  const databaseDir = await makeDatabaseDir("putWithResizing");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  // The default store size is 1MB, putting key/value pairs bigger than that
  // would trigger auto resizing.
  const base = "A humongous string in 32 bytes!!";
  const val1M = base.repeat(32768);
  const val2M = val1M.repeat(2);
  Assert.strictEqual(await database.put("A-1M-value", val1M), undefined);
  Assert.strictEqual(await database.put("A-2M-value", val2M), undefined);
  Assert.strictEqual(await database.put("A-32B-value", base), undefined);

  Assert.strictEqual(await database.get("A-1M-value"), val1M);
  Assert.strictEqual(await database.get("A-2M-value"), val2M);
  Assert.strictEqual(await database.get("A-32B-value"), base);
});

add_task(async function largeNumbers() {
  const databaseDir = await makeDatabaseDir("largeNumbers");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  const MAX_INT_VARIANT = Math.pow(2, 31) - 1;
  const MIN_DOUBLE_VARIANT = Math.pow(2, 31);

  await database.put("max-int-variant", MAX_INT_VARIANT);
  await database.put("min-double-variant", MIN_DOUBLE_VARIANT);
  await database.put("max-safe-integer", Number.MAX_SAFE_INTEGER);
  await database.put("min-safe-integer", Number.MIN_SAFE_INTEGER);
  await database.put("max-value", Number.MAX_VALUE);
  await database.put("min-value", Number.MIN_VALUE);

  Assert.strictEqual(await database.get("max-int-variant"), MAX_INT_VARIANT);
  Assert.strictEqual(
    await database.get("min-double-variant"),
    MIN_DOUBLE_VARIANT
  );
  Assert.strictEqual(
    await database.get("max-safe-integer"),
    Number.MAX_SAFE_INTEGER
  );
  Assert.strictEqual(
    await database.get("min-safe-integer"),
    Number.MIN_SAFE_INTEGER
  );
  Assert.strictEqual(await database.get("max-value"), Number.MAX_VALUE);
  Assert.strictEqual(await database.get("min-value"), Number.MIN_VALUE);
});

add_task(async function extendedCharacterKey() {
  const databaseDir = await makeDatabaseDir("extendedCharacterKey");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  // Ensure that we can use extended character (i.e. non-ASCII) strings as keys.

  await database.put("Héllo, wőrld!", 1);
  Assert.strictEqual(await database.has("Héllo, wőrld!"), true);
  Assert.strictEqual(await database.get("Héllo, wőrld!"), 1);

  const enumerator = await database.enumerate();
  const { key } = enumerator.getNext();
  Assert.strictEqual(key, "Héllo, wőrld!");

  await database.delete("Héllo, wőrld!");
});

add_task(async function deleteRange() {
  const databaseDir = await makeDatabaseDir("deleteRange");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  await database.writeMany({
    "bool/a": true,
    "bool/b": true,
    "double/a": 12.34,
    "double/b": 56.78,
    "int/a": 12,
    "int/b": 34,
    "int/c": 56,
    "int/d": 78,
    "string/a": "Héllo",
    "string/b": "Wőrld",
  });

  await database.deleteRange(null, "double/a");
  Assert.deepEqual(await allKeys(database), [
    "double/a",
    "double/b",
    "int/a",
    "int/b",
    "int/c",
    "int/d",
    "string/a",
    "string/b",
  ]);

  await database.deleteRange("int/a", "string");
  Assert.deepEqual(await allKeys(database), [
    "double/a",
    "double/b",
    "string/a",
    "string/b",
  ]);

  await database.deleteRange("string/b", "string/a");
  Assert.deepEqual(await allKeys(database), [
    "double/a",
    "double/b",
    "string/a",
    "string/b",
  ]);

  await database.deleteRange("string");
  Assert.deepEqual(await allKeys(database), ["double/a", "double/b"]);

  await database.deleteRange("string");
  Assert.deepEqual(await allKeys(database), ["double/a", "double/b"]);

  await database.deleteRange();
  Assert.deepEqual(await allKeys(database), []);
});

add_task(async function clear() {
  const databaseDir = await makeDatabaseDir("clear");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  await database.put("int-key", 1234);
  await database.put("double-key", 56.78);
  await database.put("string-key", "Héllo, wőrld!");
  await database.put("bool-key", true);

  Assert.strictEqual(await database.clear(), undefined);
  Assert.strictEqual(await database.has("int-key"), false);
  Assert.strictEqual(await database.has("double-key"), false);
  Assert.strictEqual(await database.has("string-key"), false);
  Assert.strictEqual(await database.has("bool-key"), false);
});

add_task(async function writeManyFailureCases() {
  const databaseDir = await makeDatabaseDir("writeManyFailureCases");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  Assert.throws(() => database.writeMany(), /unexpected argument/);
  Assert.throws(() => database.writeMany("foo"), /unexpected argument/);
  Assert.throws(() => database.writeMany(["foo"]), /unexpected argument/);
});

add_task(async function writeManyPutOnly() {
  const databaseDir = await makeDatabaseDir("writeMany");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  async function test_helper(pairs) {
    Assert.strictEqual(await database.writeMany(pairs), undefined);
    Assert.strictEqual(await database.get("int-key"), 1234);
    Assert.strictEqual(await database.get("double-key"), 56.78);
    Assert.strictEqual(await database.get("string-key"), "Héllo, wőrld!");
    Assert.strictEqual(await database.get("bool-key"), true);
    await database.clear();
  }

  // writeMany with an empty object is OK
  Assert.strictEqual(await database.writeMany({}), undefined);

  // writeMany with an object
  const pairs = {
    "int-key": 1234,
    "double-key": 56.78,
    "string-key": "Héllo, wőrld!",
    "bool-key": true,
  };
  await test_helper(pairs);

  // writeMany with an array of pairs
  const arrayPairs = [
    ["int-key", 1234],
    ["double-key", 56.78],
    ["string-key", "Héllo, wőrld!"],
    ["bool-key", true],
  ];
  await test_helper(arrayPairs);

  // writeMany with a key/value generator
  function* pairMaker() {
    yield ["int-key", 1234];
    yield ["double-key", 56.78];
    yield ["string-key", "Héllo, wőrld!"];
    yield ["bool-key", true];
  }
  await test_helper(pairMaker());

  // writeMany with a map
  const mapPairs = new Map(arrayPairs);
  await test_helper(mapPairs);
});

add_task(async function writeManyLargePairsWithResizing() {
  const databaseDir = await makeDatabaseDir("writeManyWithResizing");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  // The default store size is 1MB, putting key/value pairs bigger than that
  // would trigger auto resizing.
  const base = "A humongous string in 32 bytes!!";
  const val1M = base.repeat(32768);
  const val2M = val1M.repeat(2);

  // writeMany with an object
  const pairs = {
    "A-1M-value": val1M,
    "A-32B-value": base,
    "A-2M-value": val2M,
  };

  Assert.strictEqual(await database.writeMany(pairs), undefined);

  Assert.strictEqual(await database.get("A-1M-value"), val1M);
  Assert.strictEqual(await database.get("A-2M-value"), val2M);
  Assert.strictEqual(await database.get("A-32B-value"), base);
});

add_task(async function writeManySmallPairsWithResizing() {
  const databaseDir = await makeDatabaseDir("writeManyWithResizing");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  // The default store size is 1MB, putting key/value pairs bigger than that
  // would trigger auto resizing.
  const base = "A humongous string in 32 bytes!!";
  const val1K = base.repeat(32);
  // writeMany with a key/value generator
  function* pairMaker() {
    for (let i = 0; i < 1024; i++) {
      yield [`key-${i}`, val1K];
    }
  }

  Assert.strictEqual(await database.writeMany(pairMaker()), undefined);
  for (let i = 0; i < 1024; i++) {
    Assert.ok(await database.has(`key-${i}`));
  }
});

add_task(async function writeManyDeleteOnly() {
  const databaseDir = await makeDatabaseDir("writeManyDeletesOnly");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  // writeMany with an object
  const pairs = {
    "int-key": 1234,
    "double-key": 56.78,
    "string-key": "Héllo, wőrld!",
    "bool-key": true,
  };

  async function test_helper(deletes) {
    Assert.strictEqual(await database.writeMany(pairs), undefined);
    Assert.strictEqual(await database.writeMany(deletes), undefined);
    Assert.strictEqual(await database.get("int-key"), undefined);
    Assert.strictEqual(await database.get("double-key"), undefined);
    Assert.strictEqual(await database.get("string-key"), undefined);
    Assert.strictEqual(await database.get("bool-key"), undefined);
  }

  // writeMany with an empty object is OK
  Assert.strictEqual(await database.writeMany({}), undefined);

  // writeMany with an object
  await test_helper({
    "int-key": null,
    "double-key": null,
    "string-key": null,
    "bool-key": null,
  });

  // writeMany with an array of pairs
  const arrayPairs = [
    ["int-key", null],
    ["double-key", null],
    ["string-key", null],
    ["bool-key", null],
  ];
  await test_helper(arrayPairs);

  // writeMany with a key/value generator
  function* pairMaker() {
    yield ["int-key", null];
    yield ["double-key", null];
    yield ["string-key", null];
    yield ["bool-key", null];
  }
  await test_helper(pairMaker());

  // writeMany with a map
  const mapPairs = new Map(arrayPairs);
  await test_helper(mapPairs);
});

add_task(async function writeManyPutDelete() {
  const databaseDir = await makeDatabaseDir("writeManyPutDelete");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  await database.writeMany([
    ["key1", "val1"],
    ["key3", "val3"],
    ["key4", "val4"],
    ["key5", "val5"],
  ]);

  await database.writeMany([
    ["key2", "val2"],
    ["key4", null],
    ["key5", null],
  ]);

  Assert.strictEqual(await database.get("key1"), "val1");
  Assert.strictEqual(await database.get("key2"), "val2");
  Assert.strictEqual(await database.get("key3"), "val3");
  Assert.strictEqual(await database.get("key4"), undefined);
  Assert.strictEqual(await database.get("key5"), undefined);

  await database.clear();

  await database.writeMany([
    ["key1", "val1"],
    ["key1", null],
    ["key1", "val11"],
    ["key1", null],
    ["key2", null],
    ["key2", "val2"],
  ]);

  Assert.strictEqual(await database.get("key1"), undefined);
  Assert.strictEqual(await database.get("key2"), "val2");
});

add_task(async function getOrCreateNamedDatabases() {
  const databaseDir = await makeDatabaseDir("getOrCreateNamedDatabases");

  let fooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  Assert.ok(fooDB, "retrieval of first named database works");

  let barDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "bar");
  Assert.ok(barDB, "retrieval of second named database works");

  let bazDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "baz");
  Assert.ok(bazDB, "retrieval of third named database works");

  // Key/value pairs that are put into a database don't exist in others.
  await bazDB.put("key", 1);
  Assert.ok(!(await fooDB.has("key")), "the foo DB still doesn't have the key");
  await fooDB.put("key", 2);
  Assert.ok(!(await barDB.has("key")), "the bar DB still doesn't have the key");
  await barDB.put("key", 3);
  Assert.strictEqual(
    await bazDB.get("key", 0),
    1,
    "the baz DB has its KV pair"
  );
  Assert.strictEqual(
    await fooDB.get("key", 0),
    2,
    "the foo DB has its KV pair"
  );
  Assert.strictEqual(
    await barDB.get("key", 0),
    3,
    "the bar DB has its KV pair"
  );

  // Key/value pairs that are deleted from a database still exist in other DBs.
  await bazDB.delete("key");
  Assert.strictEqual(
    await fooDB.get("key", 0),
    2,
    "the foo DB still has its KV pair"
  );
  await fooDB.delete("key");
  Assert.strictEqual(
    await barDB.get("key", 0),
    3,
    "the bar DB still has its KV pair"
  );
  await barDB.delete("key");
});

add_task(async function inMemory() {
  const database = await SQLiteKeyValueService.getOrCreate(":memory:", "db");
  await database.put("int-key", 1234);
  await database.put("double-key", 56.78);
  Assert.deepEqual(await allEntries(database), [
    {
      key: "double-key",
      value: 56.78,
    },
    {
      key: "int-key",
      value: 1234,
    },
  ]);

  const otherDatabase = await SQLiteKeyValueService.getOrCreate(
    ":memory:",
    "db"
  );
  await otherDatabase.put("string-key", "Héllo, wőrld!");
  await otherDatabase.put("bool-key", true);
  Assert.deepEqual(await allEntries(otherDatabase), [
    {
      key: "bool-key",
      value: true,
    },
    {
      key: "string-key",
      value: "Héllo, wőrld!",
    },
  ]);
});

add_task(async function enumeration() {
  const databaseDir = await makeDatabaseDir("enumeration");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  await database.put("int-key", 1234);
  await database.put("double-key", 56.78);
  await database.put("string-key", "Héllo, wőrld!");
  await database.put("bool-key", true);

  async function test(fromKey, toKey, pairs) {
    const enumerator = await database.enumerate(fromKey, toKey);

    for (const pair of pairs) {
      Assert.strictEqual(enumerator.hasMoreElements(), true);
      const element = enumerator.getNext();
      Assert.ok(element);
      Assert.strictEqual(element.key, pair[0]);
      Assert.strictEqual(element.value, pair[1]);
    }

    Assert.strictEqual(enumerator.hasMoreElements(), false);
    Assert.throws(() => enumerator.getNext(), /NS_ERROR_FAILURE/);
  }

  // Test enumeration without specifying "from" and "to" keys, which should
  // enumerate all of the pairs in the database.  This test does so explicitly
  // by passing "null", "undefined" or "" (empty string) arguments
  // for those parameters. The iterator test below also tests this implicitly
  // by not specifying arguments for those parameters.
  await test(null, null, [
    ["bool-key", true],
    ["double-key", 56.78],
    ["int-key", 1234],
    ["string-key", "Héllo, wőrld!"],
  ]);
  await test(undefined, undefined, [
    ["bool-key", true],
    ["double-key", 56.78],
    ["int-key", 1234],
    ["string-key", "Héllo, wőrld!"],
  ]);

  // The implementation doesn't distinguish between a null/undefined value
  // and an empty string, so enumerating pairs from "" to "" has the same effect
  // as enumerating pairs without specifying from/to keys: it enumerates
  // all of the pairs in the database.
  await test("", "", [
    ["bool-key", true],
    ["double-key", 56.78],
    ["int-key", 1234],
    ["string-key", "Héllo, wőrld!"],
  ]);

  // Test enumeration from a key that doesn't exist and is lexicographically
  // less than the least key in the database, which should enumerate
  // all of the pairs in the database.
  await test("aaaaa", null, [
    ["bool-key", true],
    ["double-key", 56.78],
    ["int-key", 1234],
    ["string-key", "Héllo, wőrld!"],
  ]);

  // Test enumeration from a key that doesn't exist and is lexicographically
  // greater than the first key in the database, which should enumerate pairs
  // whose key is greater than or equal to the specified key.
  await test("ccccc", null, [
    ["double-key", 56.78],
    ["int-key", 1234],
    ["string-key", "Héllo, wőrld!"],
  ]);

  // Test enumeration from a key that does exist, which should enumerate pairs
  // whose key is greater than or equal to that key.
  await test("int-key", null, [
    ["int-key", 1234],
    ["string-key", "Héllo, wőrld!"],
  ]);

  // Test enumeration from a key that doesn't exist and is lexicographically
  // greater than the greatest test key in the database, which should enumerate
  // none of the pairs in the database.
  await test("zzzzz", null, []);

  // Test enumeration to a key that doesn't exist and is lexicographically
  // greater than the greatest test key in the database, which should enumerate
  // all of the pairs in the database.
  await test(null, "zzzzz", [
    ["bool-key", true],
    ["double-key", 56.78],
    ["int-key", 1234],
    ["string-key", "Héllo, wőrld!"],
  ]);

  // Test enumeration to a key that doesn't exist and is lexicographically
  // less than the greatest test key in the database, which should enumerate
  // pairs whose key is less than the specified key.
  await test(null, "ppppp", [
    ["bool-key", true],
    ["double-key", 56.78],
    ["int-key", 1234],
  ]);

  // Test enumeration to a key that does exist, which should enumerate pairs
  // whose key is less than that key.
  await test(null, "int-key", [
    ["bool-key", true],
    ["double-key", 56.78],
  ]);

  // Test enumeration to a key that doesn't exist and is lexicographically
  // less than the least key in the database, which should enumerate
  // none of the pairs in the database.
  await test(null, "aaaaa", []);

  // Test enumeration between intermediate keys that don't exist, which should
  // enumerate the pairs whose keys lie in between them.
  await test("ggggg", "ppppp", [["int-key", 1234]]);

  // Test enumeration from a key that exists to the same key, which shouldn't
  // enumerate any pairs, because the "to" key is exclusive.
  await test("int-key", "int-key", []);

  // Test enumeration from a greater key to a lesser one, which should
  // enumerate none of the pairs in the database, even if the reverse ordering
  // would enumerate some pairs.  Consumers are responsible for ordering
  // the "from" and "to" keys such that "from" is less than or equal to "to".
  await test("ppppp", "ccccc", []);
  await test("int-key", "ccccc", []);
  await test("ppppp", "int-key", []);

  const actual = {};
  for (const { key, value } of await database.enumerate()) {
    actual[key] = value;
  }
  Assert.deepEqual(actual, {
    "bool-key": true,
    "double-key": 56.78,
    "int-key": 1234,
    "string-key": "Héllo, wőrld!",
  });

  await database.delete("int-key");
  await database.delete("double-key");
  await database.delete("string-key");
  await database.delete("bool-key");
});

add_task(async function keysWithWhitespace() {
  const databaseDir = await makeDatabaseDir("keysWithWhitespace");
  const database = await SQLiteKeyValueService.getOrCreate(databaseDir, "db");

  await database.put(" leading", 1234);
  await database.writeMany({
    "trailing ": 5678,
    " both ": 9123,
  });

  Assert.strictEqual(await database.get(" leading"), 1234);
  Assert.strictEqual(await database.get("trailing "), 5678);
  Assert.strictEqual(await database.get(" both "), 9123);

  Assert.strictEqual(await database.has(" leading"), true);
  Assert.strictEqual(await database.has(" nonexistent "), false);

  Assert.deepEqual(await allEntries(database), [
    { key: " both ", value: 9123 },
    { key: " leading", value: 1234 },
    { key: "trailing ", value: 5678 },
  ]);

  Assert.strictEqual(await database.deleteRange(" ", "t"));

  Assert.deepEqual(await allEntries(database), [
    { key: "trailing ", value: 5678 },
  ]);

  await database.delete("trailing ");

  Assert.ok(await database.isEmpty());
});

add_task(async function importFromRkv() {
  const databaseDir = await makeDatabaseDir("importFromRkv");
  await copyRkvFixture("rkv-foo-bar", databaseDir);

  const newFooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  const newBarDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "bar");

  {
    const importer = SQLiteKeyValueService.createImporter(
      SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
      databaseDir
    );
    Assert.strictEqual(importer.path, databaseDir);
    Assert.strictEqual(
      importer.type,
      SQLiteKeyValueService.Importer.RKV_SAFE_MODE
    );

    importer.addDatabase("foo");

    await importer.import();

    Assert.deepEqual(await allEntries(newFooDB), [
      { key: "bool-key", value: true },
      { key: "double-key", value: 56.78 },
    ]);
    Assert.strictEqual(await newBarDB.isEmpty(), true);
  }

  {
    const importer = SQLiteKeyValueService.createImporter(
      SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
      databaseDir
    );

    importer.addDatabase("bar");

    await importer.import();

    Assert.deepEqual(await allEntries(newBarDB), [
      { key: "int-key", value: 1234 },
      { key: "string-key", value: "Héllo, wőrld!" },
    ]);
  }

  await newFooDB.close();
  await newBarDB.close();
});

add_task(async function importAllFromRkv() {
  const databaseDir = await makeDatabaseDir("importAllFromRkv");
  await copyRkvFixture("rkv-foo-bar", databaseDir);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    databaseDir
  );

  importer.addAllDatabases();

  await importer.import();

  const newFooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  Assert.deepEqual(await allEntries(newFooDB), [
    { key: "bool-key", value: true },
    { key: "double-key", value: 56.78 },
  ]);

  const newBarDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "bar");
  Assert.deepEqual(await allEntries(newBarDB), [
    { key: "int-key", value: 1234 },
    { key: "string-key", value: "Héllo, wőrld!" },
  ]);

  await newFooDB.close();
  await newBarDB.close();
});

add_task(async function importOrErrorFromRkv() {
  const databaseDir = await makeDatabaseDir("importOrErrorFromRkv");
  await copyRkvFixture("rkv-foo-bool-int", databaseDir);

  // Pre-populate the SQLite database with the same values as the rkv fixture.
  const newFooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  await newFooDB.put("bool-key", true);
  await newFooDB.put("int-key", 1234);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    databaseDir
  );

  importer
    .addDatabase("foo")
    .setConflictPolicy(KeyValueImporter.ConflictPolicy.ERROR);

  // Both databases contain identical values, so this import should succeed.
  await importer.import();

  await newFooDB.put("int-key", 5678);

  // The values are different now, so this import should fail.
  await Assert.rejects(importer.import(), /conflict: 'int-key'/);

  await newFooDB.close();
});

add_task(async function importOrIgnoreFromRkv() {
  const databaseDir = await makeDatabaseDir("importOrIgnoreFromRkv");
  await copyRkvFixture("rkv-foo-int", databaseDir);

  // Pre-populate the SQLite database with a different value.
  const newFooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  await newFooDB.put("int-key", 5678);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    databaseDir
  );

  importer
    .addDatabase("foo")
    .setConflictPolicy(KeyValueImporter.ConflictPolicy.IGNORE);

  await importer.import();

  Assert.deepEqual(await allEntries(newFooDB), [
    { key: "int-key", value: 5678 },
  ]);

  await newFooDB.close();
});

add_task(async function importOrReplaceFromRkv() {
  const databaseDir = await makeDatabaseDir("importOrReplaceFromRkv");
  await copyRkvFixture("rkv-foo-int", databaseDir);

  // Pre-populate the SQLite database with a different value.
  const newFooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  await newFooDB.put("int-key", 5678);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    databaseDir
  );

  importer
    .addDatabase("foo")
    .setConflictPolicy(KeyValueImporter.ConflictPolicy.REPLACE);

  await importer.import();

  Assert.deepEqual(await allEntries(newFooDB), [
    { key: "int-key", value: 1234 },
  ]);

  await newFooDB.close();
});

add_task(async function importFromRkvAndKeep() {
  const databaseDir = await makeDatabaseDir("importFromRkvAndKeep");
  await copyRkvFixture("rkv-foo-bar", databaseDir);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    databaseDir
  );

  importer.addAllDatabases();

  await importer.import();

  // Verify data was imported correctly.
  const fooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  Assert.deepEqual(await allEntries(fooDB), [
    { key: "bool-key", value: true },
    { key: "double-key", value: 56.78 },
  ]);
  await fooDB.close();

  // Verify the rkv source data was kept by re-importing into a fresh
  // destination directory.
  const destDir = await makeDatabaseDir("importFromRkvAndKeep-verify");
  const reimporter = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    destDir
  );
  reimporter.addPath(databaseDir).addAllDatabases();
  await reimporter.import();

  const verifyFooDB = await SQLiteKeyValueService.getOrCreate(destDir, "foo");
  Assert.deepEqual(await allEntries(verifyFooDB), [
    { key: "bool-key", value: true },
    { key: "double-key", value: 56.78 },
  ]);
  const verifyBarDB = await SQLiteKeyValueService.getOrCreate(destDir, "bar");
  Assert.deepEqual(await allEntries(verifyBarDB), [
    { key: "int-key", value: 1234 },
    { key: "string-key", value: "Héllo, wőrld!" },
  ]);
  await verifyFooDB.close();
  await verifyBarDB.close();
});

add_task(async function importFromRkvAndDelete() {
  const databaseDir = await makeDatabaseDir("importFromRkvAndDelete");
  await copyRkvFixture("rkv-foo-bar", databaseDir);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    databaseDir
  );

  importer
    .addAllDatabases()
    .setCleanupPolicy(KeyValueImporter.CleanupPolicy.DELETE);

  await importer.import();

  // Verify data was imported correctly.
  const fooDB = await SQLiteKeyValueService.getOrCreate(databaseDir, "foo");
  Assert.deepEqual(await allEntries(fooDB), [
    { key: "bool-key", value: true },
    { key: "double-key", value: 56.78 },
  ]);
  await fooDB.close();

  // Verify the rkv source data was deleted by re-importing into a fresh
  // destination directory. The rkv databases should be empty after cleanup.
  const destDir = await makeDatabaseDir("importFromRkvAndDelete-verify");
  const reimporter = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    destDir
  );
  reimporter.addPath(databaseDir).addAllDatabases();
  await reimporter.import();

  const verifyFooDB = await SQLiteKeyValueService.getOrCreate(destDir, "foo");
  Assert.deepEqual(await allEntries(verifyFooDB), []);
  const verifyBarDB = await SQLiteKeyValueService.getOrCreate(destDir, "bar");
  Assert.deepEqual(await allEntries(verifyBarDB), []);
  await verifyFooDB.close();
  await verifyBarDB.close();
});

add_task(async function importFromMultipleRkvDirs() {
  const destinationDir = await makeDatabaseDir("importFromMultipleRkvDirs");

  const fooDatabaseDir = PathUtils.join(destinationDir, "foo");
  await IOUtils.makeDirectory(fooDatabaseDir);
  const barDatabaseDir = PathUtils.join(destinationDir, "bar");
  await IOUtils.makeDirectory(barDatabaseDir);

  await copyRkvFixture("rkv-baz-bool", destinationDir);
  await copyRkvFixture("rkv-qux-double-blarg-int", fooDatabaseDir);
  await copyRkvFixture("rkv-bork-string", barDatabaseDir);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    destinationDir
  );

  // Configure the importer to import four databases from three different
  // directories. Delete the imported keys from the old `qux` and `bork
  // databases, but keep them in the old `baz` and `blarg` databases.

  importer.addDatabase("baz");

  let fooSpec = importer.addPath(fooDatabaseDir);
  fooSpec
    .addDatabase("qux")
    .setCleanupPolicy(KeyValueImporter.CleanupPolicy.DELETE);
  fooSpec.addDatabase("blarg");

  let barSpec = importer.addPath(barDatabaseDir);
  barSpec
    .addAllDatabases()
    .setCleanupPolicy(KeyValueImporter.CleanupPolicy.DELETE);

  await importer.import();

  const newBazDB = await SQLiteKeyValueService.getOrCreate(
    destinationDir,
    "baz"
  );
  const newQuxDB = await SQLiteKeyValueService.getOrCreate(
    destinationDir,
    "qux"
  );
  const newBlargDB = await SQLiteKeyValueService.getOrCreate(
    destinationDir,
    "blarg"
  );
  const newBorkDB = await SQLiteKeyValueService.getOrCreate(
    destinationDir,
    "bork"
  );

  Assert.deepEqual(await allEntries(newBazDB), [
    { key: "bool-key", value: true },
  ]);
  Assert.deepEqual(await allEntries(newQuxDB), [
    { key: "double-key", value: 56.78 },
  ]);
  Assert.deepEqual(await allEntries(newBlargDB), [
    { key: "int-key", value: 1234 },
  ]);
  Assert.deepEqual(await allEntries(newBorkDB), [
    { key: "string-key", value: "Héllo, wőrld!" },
  ]);

  // Verify cleanup policies: re-import from the rkv sources into a fresh
  // destination. baz (KEEP) and blarg (KEEP) should still have data,
  // while qux (DELETE) and bork (DELETE) should be empty.
  const verifyDir = await makeDatabaseDir("importFromMultipleRkvDirs-verify");
  const reimporter = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    verifyDir
  );
  reimporter.addPath(destinationDir).addAllDatabases();
  reimporter.addPath(fooDatabaseDir).addAllDatabases();
  reimporter.addPath(barDatabaseDir).addAllDatabases();
  await reimporter.import();

  const vBazDB = await SQLiteKeyValueService.getOrCreate(verifyDir, "baz");
  Assert.deepEqual(await allKeys(vBazDB), ["bool-key"]);
  const vQuxDB = await SQLiteKeyValueService.getOrCreate(verifyDir, "qux");
  Assert.deepEqual(await allKeys(vQuxDB), []);
  const vBlargDB = await SQLiteKeyValueService.getOrCreate(verifyDir, "blarg");
  Assert.deepEqual(await allKeys(vBlargDB), ["int-key"]);
  const vBorkDB = await SQLiteKeyValueService.getOrCreate(verifyDir, "bork");
  Assert.deepEqual(await allKeys(vBorkDB), []);

  await newBazDB.close();
  await newQuxDB.close();
  await newBlargDB.close();
  await newBorkDB.close();
  await vBazDB.close();
  await vQuxDB.close();
  await vBlargDB.close();
  await vBorkDB.close();
});

add_task(async function importOrErrorFromMultipleRkvDirs() {
  const destinationDir = await makeDatabaseDir(
    "importOrErrorFromMultipleRkvDirs"
  );

  const fooDatabaseDir = PathUtils.join(destinationDir, "foo");
  await IOUtils.makeDirectory(fooDatabaseDir);
  const barDatabaseDir = PathUtils.join(destinationDir, "bar");
  await IOUtils.makeDirectory(barDatabaseDir);

  await copyRkvFixture("rkv-baz-bool-double", fooDatabaseDir);
  await copyRkvFixture("rkv-baz-double-string", barDatabaseDir);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    destinationDir
  );

  // `fooDatabaseDir` and `barDatabaseDir` both contain databases named `baz`,
  // with conflicting values for `double-key`. Since we add `fooDatabaseDir`
  // first, we'll import its `baz` first, then fail when we try to import
  // `baz` from `barDatabaseDir`.

  importer.addPath(fooDatabaseDir).addAllDatabases();
  importer
    .addPath(barDatabaseDir)
    .addAllDatabases()
    .setConflictPolicy(KeyValueImporter.ConflictPolicy.ERROR);

  await Assert.rejects(importer.import(), /conflict: 'double-key'/);

  const newBazDB = await SQLiteKeyValueService.getOrCreate(
    destinationDir,
    "baz"
  );
  Assert.deepEqual(await allEntries(newBazDB), [
    { key: "bool-key", value: true },
    { key: "double-key", value: 56.78 },
  ]);

  await newBazDB.close();
});

add_task(async function importOrIgnoreFromMultipleRkvDirs() {
  const destinationDir = await makeDatabaseDir(
    "importOrIgnoreFromMultipleRkvDirs"
  );

  const fooDatabaseDir = PathUtils.join(destinationDir, "foo");
  await IOUtils.makeDirectory(fooDatabaseDir);
  const barDatabaseDir = PathUtils.join(destinationDir, "bar");
  await IOUtils.makeDirectory(barDatabaseDir);

  await copyRkvFixture("rkv-baz-bool-double-int", fooDatabaseDir);
  await copyRkvFixture("rkv-baz-double-int-string", barDatabaseDir);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    destinationDir
  );

  // `fooDatabaseDir` and `barDatabaseDir` both contain databases named `baz`,
  // with conflicting values for `double-key` and `int-key`. We'll import
  // from `fooDatabaseDir` first, then `barDatabaseDir`; ignoring the
  // conflicting pairs from the latter.

  importer.addPath(fooDatabaseDir).addAllDatabases();
  importer
    .addPath(barDatabaseDir)
    .addAllDatabases()
    .setConflictPolicy(KeyValueImporter.ConflictPolicy.IGNORE);

  await importer.import();

  const newBazDB = await SQLiteKeyValueService.getOrCreate(
    destinationDir,
    "baz"
  );
  Assert.deepEqual(await allEntries(newBazDB), [
    { key: "bool-key", value: true },
    { key: "double-key", value: 56.78 },
    { key: "int-key", value: 1234 },
    { key: "string-key", value: "Héllo, wőrld!" },
  ]);

  await newBazDB.close();
});

add_task(async function importOrReplaceFromMultipleRkvDirs() {
  const destinationDir = await makeDatabaseDir(
    "importOrReplaceFromMultipleRkvDirs"
  );

  const fooDatabaseDir = PathUtils.join(destinationDir, "foo");
  await IOUtils.makeDirectory(fooDatabaseDir);
  const barDatabaseDir = PathUtils.join(destinationDir, "bar");
  await IOUtils.makeDirectory(barDatabaseDir);

  await copyRkvFixture("rkv-baz-bool-double-int", fooDatabaseDir);
  await copyRkvFixture("rkv-baz-double-int-string", barDatabaseDir);

  const importer = SQLiteKeyValueService.createImporter(
    SQLiteKeyValueService.Importer.RKV_SAFE_MODE,
    destinationDir
  );

  // `fooDatabaseDir` and `barDatabaseDir` both contain databases named `baz`,
  // with conflicting values for `double-key` and `int-key`. We'll import
  // from `fooDatabaseDir` first, then `barDatabaseDir`; replacing the
  // conflicting pairs with the pairs from `barDatabaseDir`.

  importer.addPath(fooDatabaseDir).addAllDatabases();
  importer
    .addPath(barDatabaseDir)
    .addAllDatabases()
    .setConflictPolicy(KeyValueImporter.ConflictPolicy.REPLACE);

  await importer.import();

  const newBazDB = await SQLiteKeyValueService.getOrCreate(
    destinationDir,
    "baz"
  );
  Assert.deepEqual(await allEntries(newBazDB), [
    { key: "bool-key", value: true },
    { key: "double-key", value: 12.34 },
    { key: "int-key", value: 5678 },
    { key: "string-key", value: "Héllo, wőrld!" },
  ]);

  await newBazDB.close();
});

add_task(async function stats() {
  const database = await SQLiteKeyValueService.getOrCreate(":memory:", "db");
  Assert.strictEqual(await database.isEmpty(), true);
  Assert.strictEqual(await database.count(), 0);
  Assert.strictEqual(await database.size(), 0);

  await database.put("int-key", 1234); // 12 (7 + 5) bytes.
  Assert.strictEqual(await database.isEmpty(), false);
  Assert.strictEqual(await database.count(), 1);
  Assert.strictEqual(await database.size(), 12);

  await database.put("double-key", 56.78); // 16 (10 + 6) bytes.
  Assert.strictEqual(await database.isEmpty(), false);
  Assert.strictEqual(await database.count(), 2);
  Assert.strictEqual(await database.size(), 28);

  await database.put("string-key", "Héllo, wőrld!"); // 27 (10 + 17) bytes.
  Assert.strictEqual(await database.isEmpty(), false);
  Assert.strictEqual(await database.count(), 3);
  Assert.strictEqual(await database.size(), 55);

  await database.put("bool-key", true); // 9 (8 + 1) bytes.
  Assert.strictEqual(await database.isEmpty(), false);
  Assert.strictEqual(await database.count(), 4);
  Assert.strictEqual(await database.size(), 64);
});
