/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var testnum = 0;
var dbConnection; // used for deleted table tests

async function countDeletedEntries(expected) {
  let stmt = "SELECT COUNT(*) AS numEntries FROM moz_deleted_formhistory";
  try {
    let requiredRow = await dbConnection.executeCached(stmt);
    Assert.equal(expected, requiredRow[0].getResultByName("numEntries"));
  } catch (error) {
    do_throw("Error occurred counting deleted entries: " + error);
  }
}

async function checkTimeDeleted(guid, checkFunction) {
  let stmt =
    "SELECT timeDeleted FROM moz_deleted_formhistory WHERE guid = :guid";
  let params = { guid };

  try {
    let requiredRow = await dbConnection.executeCached(stmt, params);
    checkFunction(requiredRow[0].getResultByName("timeDeleted"));
  } catch (error) {
    do_throw("Error occurred getting deleted entries: " + error);
  }
}

function promiseUpdateEntry(op, name, value) {
  let change = { op };
  if (name !== null) {
    change.fieldname = name;
  }
  if (value !== null) {
    change.value = value;
  }
  return promiseUpdate(change);
}

add_task(async function () {
  let oldSupportsDeletedTable = FormHistory._supportsDeletedTable;
  FormHistory._supportsDeletedTable = true;

  try {
    // ===== test init =====
    let testfile = do_get_file("formhistory_apitest.sqlite");
    let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile);

    // Cleanup from any previous tests or failures.
    let destFile = profileDir.clone();
    destFile.append("formhistory.sqlite");
    if (destFile.exists()) {
      destFile.remove(false);
    }

    testfile.copyTo(profileDir, "formhistory.sqlite");

    function checkExists(num) {
      Assert.ok(num > 0);
    }
    function checkNotExists(num) {
      Assert.ok(num == 0);
    }

    // ===== 1 =====
    // Check initial state is as expected
    testnum++;
    await promiseCountEntries("name-A", null, checkExists);
    await promiseCountEntries("name-B", null, checkExists);
    await promiseCountEntries("name-C", null, checkExists);
    await promiseCountEntries("name-D", null, checkExists);
    await promiseCountEntries("name-A", "value-A", checkExists);
    await promiseCountEntries("name-B", "value-B1", checkExists);
    await promiseCountEntries("name-B", "value-B2", checkExists);
    await promiseCountEntries("name-C", "value-C", checkExists);
    await promiseCountEntries("name-D", "value-D", checkExists);
    // time-A/B/C/D checked below.

    // Delete anything from the deleted table
    let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile).clone();
    dbFile.append("formhistory.sqlite");

    dbConnection = await Sqlite.openConnection({
      path: dbFile.path,
      sharedMemoryCache: false,
    });

    let stmt = "DELETE FROM moz_deleted_formhistory";
    try {
      await dbConnection.executeCached(stmt);
    } catch (error) {
      do_throw("Error occurred counting deleted all entries: " + error);
    }

    // ===== 2 =====
    // Test looking for nonexistent / bogus data.
    testnum++;
    await promiseCountEntries("blah", null, checkNotExists);
    await promiseCountEntries("", null, checkNotExists);
    await promiseCountEntries("name-A", "blah", checkNotExists);
    await promiseCountEntries("name-A", "", checkNotExists);
    await promiseCountEntries("name-A", null, checkExists);
    await promiseCountEntries("blah", "value-A", checkNotExists);
    await promiseCountEntries("", "value-A", checkNotExists);
    await promiseCountEntries(null, "value-A", checkExists);

    // Cannot use promiseCountEntries when name and value are null
    // because it treats null values as not set
    // and here a search should be done explicity for null.
    let count = await FormHistory.count({ fieldname: null, value: null });
    checkNotExists(count);

    // ===== 3 =====
    // Test removeEntriesForName with a single matching value
    testnum++;
    await promiseUpdateEntry("remove", "name-A", null);

    await promiseCountEntries("name-A", "value-A", checkNotExists);
    await promiseCountEntries("name-B", "value-B1", checkExists);
    await promiseCountEntries("name-B", "value-B2", checkExists);
    await promiseCountEntries("name-C", "value-C", checkExists);
    await promiseCountEntries("name-D", "value-D", checkExists);
    await countDeletedEntries(1);

    // ===== 4 =====
    // Test removeEntriesForName with multiple matching values
    testnum++;
    await promiseUpdateEntry("remove", "name-B", null);

    await promiseCountEntries("name-A", "value-A", checkNotExists);
    await promiseCountEntries("name-B", "value-B1", checkNotExists);
    await promiseCountEntries("name-B", "value-B2", checkNotExists);
    await promiseCountEntries("name-C", "value-C", checkExists);
    await promiseCountEntries("name-D", "value-D", checkExists);
    await countDeletedEntries(3);

    // ===== 5 =====
    // Test removing by time range (single entry, not surrounding entries)
    testnum++;
    await promiseCountEntries("time-A", null, checkExists); // firstUsed=1000, lastUsed=1000
    await promiseCountEntries("time-B", null, checkExists); // firstUsed=1000, lastUsed=1099
    await promiseCountEntries("time-C", null, checkExists); // firstUsed=1099, lastUsed=1099
    await promiseCountEntries("time-D", null, checkExists); // firstUsed=2001, lastUsed=2001
    await promiseUpdate({
      op: "remove",
      firstUsedStart: 1050,
      firstUsedEnd: 2000,
    });

    await promiseCountEntries("time-A", null, checkExists);
    await promiseCountEntries("time-B", null, checkExists);
    await promiseCountEntries("time-C", null, checkNotExists);
    await promiseCountEntries("time-D", null, checkExists);
    await countDeletedEntries(4);

    // ===== 6 =====
    // Test removing by time range (multiple entries)
    testnum++;
    await promiseUpdate({
      op: "remove",
      firstUsedStart: 1000,
      firstUsedEnd: 2000,
    });

    await promiseCountEntries("time-A", null, checkNotExists);
    await promiseCountEntries("time-B", null, checkNotExists);
    await promiseCountEntries("time-C", null, checkNotExists);
    await promiseCountEntries("time-D", null, checkExists);
    await countDeletedEntries(6);

    // ===== 7 =====
    // test removeAllEntries
    testnum++;
    await promiseUpdateEntry("remove", null, null);

    await promiseCountEntries("name-C", null, checkNotExists);
    await promiseCountEntries("name-D", null, checkNotExists);
    await promiseCountEntries("name-C", "value-C", checkNotExists);
    await promiseCountEntries("name-D", "value-D", checkNotExists);

    await promiseCountEntries(null, null, checkNotExists);
    await countDeletedEntries(6);

    // ===== 8 =====
    // Add a single entry back
    testnum++;
    await promiseUpdateEntry("add", "newname-A", "newvalue-A");
    await promiseCountEntries("newname-A", "newvalue-A", checkExists);

    // ===== 9 =====
    // Remove the single entry
    testnum++;
    await promiseUpdateEntry("remove", "newname-A", "newvalue-A");
    await promiseCountEntries("newname-A", "newvalue-A", checkNotExists);

    // ===== 10 =====
    // Add a single entry
    testnum++;
    await promiseUpdateEntry("add", "field1", "value1");
    await promiseCountEntries("field1", "value1", checkExists);

    let processFirstResult = function processResults(results) {
      // Only handle the first result
      if (results.length) {
        let result = results[0];
        return [
          result.timesUsed,
          result.firstUsed,
          result.lastUsed,
          result.guid,
        ];
      }
      return undefined;
    };

    let results = await FormHistory.search(
      ["timesUsed", "firstUsed", "lastUsed"],
      { fieldname: "field1", value: "value1" }
    );
    let [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
    Assert.equal(1, timesUsed);
    Assert.ok(firstUsed > 0);
    Assert.ok(lastUsed > 0);
    await promiseCountEntries(null, null, num => Assert.equal(num, 1));

    // ===== 11 =====
    // Add another single entry
    testnum++;
    await promiseUpdateEntry("add", "field1", "value1b");
    await promiseCountEntries("field1", "value1", checkExists);
    await promiseCountEntries("field1", "value1b", checkExists);
    await promiseCountEntries(null, null, num => Assert.equal(num, 2));

    // ===== 12 =====
    // Update a single entry
    testnum++;

    results = await FormHistory.search(["guid"], {
      fieldname: "field1",
      value: "value1",
    });
    let guid = processFirstResult(results)[3];

    await promiseUpdate({ op: "update", guid, value: "modifiedValue" });
    await promiseCountEntries("field1", "modifiedValue", checkExists);
    await promiseCountEntries("field1", "value1", checkNotExists);
    await promiseCountEntries("field1", "value1b", checkExists);
    await promiseCountEntries(null, null, num => Assert.equal(num, 2));

    // ===== 13 =====
    // Add a single entry with times
    testnum++;
    await promiseUpdate({
      op: "add",
      fieldname: "field2",
      value: "value2",
      timesUsed: 20,
      firstUsed: 100,
      lastUsed: 500,
    });

    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], {
      fieldname: "field2",
      value: "value2",
    });
    [timesUsed, firstUsed, lastUsed] = processFirstResult(results);

    Assert.equal(20, timesUsed);
    Assert.equal(100, firstUsed);
    Assert.equal(500, lastUsed);
    await promiseCountEntries(null, null, num => Assert.equal(num, 3));

    // ===== 14 =====
    // Bump an entry, which updates its lastUsed field
    testnum++;
    await promiseUpdate({
      op: "bump",
      fieldname: "field2",
      value: "value2",
      timesUsed: 20,
      firstUsed: 100,
      lastUsed: 500,
    });
    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], {
      fieldname: "field2",
      value: "value2",
    });
    [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
    Assert.equal(21, timesUsed);
    Assert.equal(100, firstUsed);
    Assert.ok(lastUsed > 500);
    await promiseCountEntries(null, null, num => Assert.equal(num, 3));

    // ===== 15 =====
    // Bump an entry that does not exist
    testnum++;
    await promiseUpdate({
      op: "bump",
      fieldname: "field3",
      value: "value3",
      timesUsed: 10,
      firstUsed: 50,
      lastUsed: 400,
    });
    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], {
      fieldname: "field3",
      value: "value3",
    });
    [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
    Assert.equal(10, timesUsed);
    Assert.equal(50, firstUsed);
    Assert.equal(400, lastUsed);
    await promiseCountEntries(null, null, num => Assert.equal(num, 4));

    // ===== 16 =====
    // Bump an entry with a guid
    testnum++;
    results = await FormHistory.search(["guid"], {
      fieldname: "field3",
      value: "value3",
    });
    guid = processFirstResult(results)[3];
    await promiseUpdate({
      op: "bump",
      guid,
      timesUsed: 20,
      firstUsed: 55,
      lastUsed: 400,
    });
    results = await FormHistory.search(["timesUsed", "firstUsed", "lastUsed"], {
      fieldname: "field3",
      value: "value3",
    });
    [timesUsed, firstUsed, lastUsed] = processFirstResult(results);
    Assert.equal(11, timesUsed);
    Assert.equal(50, firstUsed);
    Assert.ok(lastUsed > 400);
    await promiseCountEntries(null, null, num => Assert.equal(num, 4));

    // ===== 17 =====
    // Remove an entry
    testnum++;
    await countDeletedEntries(7);

    results = await FormHistory.search(["guid"], {
      fieldname: "field1",
      value: "value1b",
    });
    guid = processFirstResult(results)[3];

    await promiseUpdate({ op: "remove", guid });
    await promiseCountEntries("field1", "modifiedValue", checkExists);
    await promiseCountEntries("field1", "value1b", checkNotExists);
    await promiseCountEntries(null, null, num => Assert.equal(num, 3));

    await countDeletedEntries(8);
    await checkTimeDeleted(guid, timeDeleted => Assert.ok(timeDeleted > 10000));

    // ===== 18 =====
    // Add yet another single entry
    testnum++;
    await promiseUpdate({
      op: "add",
      fieldname: "field4",
      value: "value4",
      timesUsed: 5,
      firstUsed: 230,
      lastUsed: 600,
    });
    await promiseCountEntries(null, null, num => Assert.equal(num, 4));

    // ===== 19 =====
    // Remove an entry by time
    testnum++;
    await promiseUpdate({
      op: "remove",
      firstUsedStart: 60,
      firstUsedEnd: 250,
    });
    await promiseCountEntries("field1", "modifiedValue", checkExists);
    await promiseCountEntries("field2", "value2", checkNotExists);
    await promiseCountEntries("field3", "value3", checkExists);
    await promiseCountEntries("field4", "value4", checkNotExists);
    await promiseCountEntries(null, null, num => Assert.equal(num, 2));
    await countDeletedEntries(10);

    // ===== 20 =====
    // Bump multiple existing entries at once
    testnum++;

    await promiseUpdate([
      {
        op: "add",
        fieldname: "field5",
        value: "value5",
        timesUsed: 5,
        firstUsed: 230,
        lastUsed: 600,
      },
      {
        op: "add",
        fieldname: "field6",
        value: "value6",
        timesUsed: 12,
        firstUsed: 430,
        lastUsed: 700,
      },
    ]);
    await promiseCountEntries(null, null, num => Assert.equal(num, 4));

    await promiseUpdate([
      { op: "bump", fieldname: "field5", value: "value5" },
      { op: "bump", fieldname: "field6", value: "value6" },
    ]);
    results = await FormHistory.search(
      ["fieldname", "timesUsed", "firstUsed", "lastUsed"],
      {}
    );

    Assert.equal(6, results[2].timesUsed);
    Assert.equal(13, results[3].timesUsed);
    Assert.equal(230, results[2].firstUsed);
    Assert.equal(430, results[3].firstUsed);
    Assert.ok(results[2].lastUsed > 600);
    Assert.ok(results[3].lastUsed > 700);

    await promiseCountEntries(null, null, num => Assert.equal(num, 4));

    // ===== 21 =====
    // Check update fails if form history is disabled and the operation is not a
    // pure removal.
    testnum++;
    Services.prefs.setBoolPref("browser.formfill.enable", false);

    // Cannot use arrow functions, see bug 1237961.
    await Assert.rejects(
      promiseUpdate({ op: "bump", fieldname: "field5", value: "value5" }),
      /Form history is disabled, only remove operations are allowed/,
      "bumping when form history is disabled should fail"
    );
    await Assert.rejects(
      promiseUpdate({ op: "add", fieldname: "field5", value: "value5" }),
      /Form history is disabled, only remove operations are allowed/,
      "Adding when form history is disabled should fail"
    );
    await Assert.rejects(
      promiseUpdate([
        { op: "update", fieldname: "field5", value: "value5" },
        { op: "remove", fieldname: "field5", value: "value5" },
      ]),
      /Form history is disabled, only remove operations are allowed/,
      "mixed operations when form history is disabled should fail"
    );
    await Assert.rejects(
      promiseUpdate([
        null,
        undefined,
        "",
        1,
        {},
        { op: "remove", fieldname: "field5", value: "value5" },
      ]),
      /Form history is disabled, only remove operations are allowed/,
      "Invalid entries when form history is disabled should fail"
    );

    // Remove should work though.
    await promiseUpdate([
      { op: "remove", fieldname: "field5", value: null },
      { op: "remove", fieldname: null, value: null },
    ]);
    Services.prefs.clearUserPref("browser.formfill.enable");
  } catch (e) {
    throw new Error(`FAILED in test #${testnum} -- ${e}`);
  } finally {
    FormHistory._supportsDeletedTable = oldSupportsDeletedTable;
    await dbConnection.close(do_test_finished);
  }
});

function run_test() {
  return run_next_test();
}
