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

"use strict";

const { AddonsReconciler, CHANGE_INSTALLED, CHANGE_UNINSTALLED } =
  ChromeUtils.importESModule(
    "resource://services-sync/addonsreconciler.sys.mjs"
  );
const { AddonsEngine } = ChromeUtils.importESModule(
  "resource://services-sync/engines/addons.sys.mjs"
);
const { Service } = ChromeUtils.importESModule(
  "resource://services-sync/service.sys.mjs"
);

AddonTestUtils.init(this);
AddonTestUtils.createAppInfo(
  "xpcshell@tests.mozilla.org",
  "XPCShell",
  "1",
  "1.9.2"
);
AddonTestUtils.overrideCertDB();

const ADDON_ID = "addon1@tests.mozilla.org";
const XPI = AddonTestUtils.createTempWebExtensionFile({
  manifest: {
    name: "Test 1",
    description: "Test Description",
    browser_specific_settings: { gecko: { id: ADDON_ID } },
  },
});

function makeAddonsReconciler() {
  const log = Service.engineManager.get("addons")._log;
  const queueCaller = Async.asyncQueueCaller(log);
  return new AddonsReconciler(queueCaller);
}

add_task(async function setup() {
  await AddonTestUtils.promiseStartupManager();
  Svc.Prefs.set("engine.addons", true);
  await Service.engineManager.register(AddonsEngine);
});

add_task(async function test_defaults() {
  _("Ensure new objects have reasonable defaults.");

  let reconciler = makeAddonsReconciler();
  await reconciler.ensureStateLoaded();

  Assert.ok(!reconciler._listening);
  Assert.equal("object", typeof reconciler.addons);
  Assert.equal(0, Object.keys(reconciler.addons).length);
  Assert.equal(0, reconciler._changes.length);
  Assert.equal(0, reconciler._listeners.length);
});

add_task(async function test_load_state_empty_file() {
  _("Ensure loading from a missing file results in defaults being set.");

  let reconciler = makeAddonsReconciler();
  await reconciler.ensureStateLoaded();

  let loaded = await reconciler.loadState();
  Assert.ok(!loaded);

  Assert.equal("object", typeof reconciler.addons);
  Assert.equal(0, Object.keys(reconciler.addons).length);
  Assert.equal(0, reconciler._changes.length);
});

add_task(async function test_install_detection() {
  _("Ensure that add-on installation results in appropriate side-effects.");

  let reconciler = makeAddonsReconciler();
  await reconciler.ensureStateLoaded();
  reconciler.startListening();

  let before = new Date();
  let addon = await installAddon(XPI);
  let after = new Date();

  Assert.equal(1, Object.keys(reconciler.addons).length);
  Assert.ok(addon.id in reconciler.addons);
  let record = reconciler.addons[ADDON_ID];

  const KEYS = [
    "id",
    "guid",
    "enabled",
    "installed",
    "modified",
    "type",
    "scope",
    "foreignInstall",
  ];
  for (let key of KEYS) {
    Assert.ok(key in record);
    Assert.notEqual(null, record[key]);
  }

  Assert.equal(addon.id, record.id);
  Assert.equal(addon.syncGUID, record.guid);
  Assert.ok(record.enabled);
  Assert.ok(record.installed);
  Assert.ok(record.modified >= before && record.modified <= after);
  Assert.equal("extension", record.type);
  Assert.ok(!record.foreignInstall);

  Assert.equal(1, reconciler._changes.length);
  let change = reconciler._changes[0];
  Assert.ok(change[0] >= before && change[1] <= after);
  Assert.equal(CHANGE_INSTALLED, change[1]);
  Assert.equal(addon.id, change[2]);

  await uninstallAddon(addon);
});

add_task(async function test_uninstall_detection() {
  _("Ensure that add-on uninstallation results in appropriate side-effects.");

  let reconciler = makeAddonsReconciler();
  await reconciler.ensureStateLoaded();
  reconciler.startListening();

  reconciler._addons = {};
  reconciler._changes = [];

  let addon = await installAddon(XPI);
  let id = addon.id;

  reconciler._changes = [];
  await uninstallAddon(addon, reconciler);

  Assert.equal(1, Object.keys(reconciler.addons).length);
  Assert.ok(id in reconciler.addons);

  let record = reconciler.addons[id];
  Assert.ok(!record.installed);

  Assert.equal(1, reconciler._changes.length);
  let change = reconciler._changes[0];
  Assert.equal(CHANGE_UNINSTALLED, change[1]);
  Assert.equal(id, change[2]);
});

add_task(async function test_load_state_future_version() {
  _("Ensure loading a file from a future version results in no data loaded.");

  const FILENAME = "TEST_LOAD_STATE_FUTURE_VERSION";

  let reconciler = makeAddonsReconciler();
  await reconciler.ensureStateLoaded();

  // First we populate our new file.
  let state = { version: 100, addons: { foo: {} }, changes: [[1, 1, "foo"]] };

  // jsonSave() expects an object with ._log, so we give it a reconciler
  // instance.
  await Utils.jsonSave(FILENAME, reconciler, state);

  let loaded = await reconciler.loadState(FILENAME);
  Assert.ok(!loaded);

  Assert.equal("object", typeof reconciler.addons);
  Assert.equal(0, Object.keys(reconciler.addons).length);
  Assert.equal(0, reconciler._changes.length);
});

add_task(async function test_prune_changes_before_date() {
  _("Ensure that old changes are pruned properly.");

  let reconciler = makeAddonsReconciler();
  await reconciler.ensureStateLoaded();
  reconciler._changes = [];

  let now = new Date();
  const HOUR_MS = 1000 * 60 * 60;

  _("Ensure pruning an empty changes array works.");
  reconciler.pruneChangesBeforeDate(now);
  Assert.equal(0, reconciler._changes.length);

  let old = new Date(now.getTime() - HOUR_MS);
  let young = new Date(now.getTime() - 1000);
  reconciler._changes.push([old, CHANGE_INSTALLED, "foo"]);
  reconciler._changes.push([young, CHANGE_INSTALLED, "bar"]);
  Assert.equal(2, reconciler._changes.length);

  _("Ensure pruning with an old time won't delete anything.");
  let threshold = new Date(old.getTime() - 1);
  reconciler.pruneChangesBeforeDate(threshold);
  Assert.equal(2, reconciler._changes.length);

  _("Ensure pruning a single item works.");
  threshold = new Date(young.getTime() - 1000);
  reconciler.pruneChangesBeforeDate(threshold);
  Assert.equal(1, reconciler._changes.length);
  Assert.notEqual(undefined, reconciler._changes[0]);
  Assert.equal(young, reconciler._changes[0][0]);
  Assert.equal("bar", reconciler._changes[0][2]);

  _("Ensure pruning all changes works.");
  reconciler._changes.push([old, CHANGE_INSTALLED, "foo"]);
  reconciler.pruneChangesBeforeDate(now);
  Assert.equal(0, reconciler._changes.length);
});
