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

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  Service: "resource://services-sync/service.sys.mjs",
  extensionStorageSync: "resource://gre/modules/ExtensionStorageSync.sys.mjs",
});

const { ExtensionStorageEngineBridge, ExtensionStorageEngineKinto } =
  ChromeUtils.importESModule(
    "resource://services-sync/engines/extension-storage.sys.mjs"
  );

const { BridgeWrapperXPCOM } = ChromeUtils.importESModule(
  "resource://services-sync/bridged_engine.sys.mjs"
);

Services.prefs.setStringPref("webextensions.storage.sync.log.level", "debug");

add_task(async function test_switching_between_kinto_and_bridged() {
  function assertUsingKinto(message) {
    let kintoEngine = Service.engineManager.get("extension-storage");
    Assert.ok(kintoEngine instanceof ExtensionStorageEngineKinto, message);
  }
  function assertUsingBridged(message) {
    let bridgedEngine = Service.engineManager.get("extension-storage");
    Assert.ok(bridgedEngine instanceof ExtensionStorageEngineBridge, message);
  }

  let isUsingKinto = Services.prefs.getBoolPref(
    "webextensions.storage.sync.kinto",
    false
  );
  if (isUsingKinto) {
    assertUsingKinto("Should use Kinto engine before flipping pref");
  } else {
    assertUsingBridged("Should use bridged engine before flipping pref");
  }

  _("Flip pref");
  Services.prefs.setBoolPref("webextensions.storage.sync.kinto", !isUsingKinto);
  await Service.engineManager.switchAlternatives();

  if (isUsingKinto) {
    assertUsingBridged("Should use bridged engine after flipping pref");
  } else {
    assertUsingKinto("Should use Kinto engine after flipping pref");
  }

  _("Clean up");
  Services.prefs.clearUserPref("webextensions.storage.sync.kinto");
  await Service.engineManager.switchAlternatives();
});

add_task(async function test_enable() {
  const PREF = "services.sync.engine.extension-storage.force";

  let addonsEngine = Service.engineManager.get("addons");
  let extensionStorageEngine = Service.engineManager.get("extension-storage");

  try {
    Assert.ok(
      addonsEngine.enabled,
      "Add-ons engine should be enabled by default"
    );
    Assert.ok(
      extensionStorageEngine.enabled,
      "Extension storage engine should be enabled by default"
    );

    addonsEngine.enabled = false;
    Assert.ok(
      !extensionStorageEngine.enabled,
      "Disabling add-ons should disable extension storage"
    );

    extensionStorageEngine.enabled = true;
    Assert.ok(
      !extensionStorageEngine.enabled,
      "Enabling extension storage without override pref shouldn't work"
    );

    Services.prefs.setBoolPref(PREF, true);
    Assert.ok(
      extensionStorageEngine.enabled,
      "Setting override pref should enable extension storage"
    );

    extensionStorageEngine.enabled = false;
    Assert.ok(
      !extensionStorageEngine.enabled,
      "Disabling extension storage engine with override pref should work"
    );

    extensionStorageEngine.enabled = true;
    Assert.ok(
      extensionStorageEngine.enabled,
      "Enabling extension storage with override pref should work"
    );
  } finally {
    addonsEngine.enabled = true;
    Services.prefs.clearUserPref(PREF);
  }
});

add_task(async function test_notifyPendingChanges() {
  let engine = new ExtensionStorageEngineBridge(Service);

  let extension = { id: "ext-1" };
  let expectedChange = {
    a: "b",
    c: "d",
  };

  let lastSync = 0;
  let syncID = Utils.makeGUID();
  let error = null;
  engine.component = {
    QueryInterface: ChromeUtils.generateQI([
      "mozIBridgedSyncEngine",
      "mozIExtensionStorageArea",
      "mozISyncedExtensionStorageArea",
    ]),
    ensureCurrentSyncId(id, callback) {
      if (syncID != id) {
        syncID = id;
        lastSync = 0;
      }
      callback.handleSuccess(id);
    },
    resetSyncId(callback) {
      callback.handleSuccess(syncID);
    },
    syncStarted(callback) {
      callback.handleSuccess();
    },
    getLastSync(callback) {
      callback.handleSuccess(lastSync);
    },
    setLastSync(lastSyncMillis, callback) {
      lastSync = lastSyncMillis;
      callback.handleSuccess();
    },
    apply(callback) {
      callback.handleSuccess([]);
    },
    fetchPendingSyncChanges(callback) {
      if (error) {
        callback.handleError(Cr.NS_ERROR_FAILURE, error.message);
      } else {
        callback.onChanged(extension.id, JSON.stringify(expectedChange));
        callback.handleSuccess();
      }
    },
    setUploaded(modified, ids, callback) {
      callback.handleSuccess();
    },
    syncFinished(callback) {
      callback.handleSuccess();
    },
    takeMigrationInfo(callback) {
      callback.handleSuccess(null);
    },
  };

  engine._bridge = new BridgeWrapperXPCOM(engine.component);

  let server = await serverForFoo(engine);

  let actualChanges = [];
  let listener = changes => actualChanges.push(changes);
  extensionStorageSync.addOnChangedListener(extension, listener);

  try {
    await SyncTestingInfrastructure(server);

    info("Sync engine; notify about changes");
    await sync_engine_and_validate_telem(engine, false);
    deepEqual(
      actualChanges,
      [expectedChange],
      "Should notify about changes during sync"
    );

    error = new Error("oops!");
    actualChanges = [];
    await sync_engine_and_validate_telem(engine, false);
    deepEqual(
      actualChanges,
      [],
      "Should finish syncing even if notifying about changes fails"
    );
  } finally {
    extensionStorageSync.removeOnChangedListener(extension, listener);
    await promiseStopServer(server);
    await engine.finalize();
  }
});

// It's difficult to know what to test - there's already tests for the bridged
// engine etc - so we just try and check that this engine conforms to the
// mozIBridgedSyncEngine interface guarantees.
add_task(async function test_engine() {
  // Forcibly set the bridged engine in the engine manager. the reason we do
  // this, unlike the other tests where we just create the engine, is so that
  // telemetry can get at the engine's `overrideTelemetryName`, which it gets
  // through the engine manager.
  await Service.engineManager.unregister("extension-storage");
  await Service.engineManager.register(ExtensionStorageEngineBridge);
  let engine = Service.engineManager.get("extension-storage");
  Assert.equal(engine.version, 1);

  Assert.deepEqual(await engine.getSyncID(), null);
  await engine.resetLocalSyncID();
  Assert.notEqual(await engine.getSyncID(), null);

  Assert.equal(await engine.getLastSync(), 0);
  // lastSync is seconds on this side of the world, but milli-seconds on the other.
  await engine.setLastSync(1234.567);
  // should have 2 digit precision.
  Assert.equal(await engine.getLastSync(), 1234.57);
  await engine.setLastSync(0);

  // Set some data.
  await extensionStorageSync.set({ id: "ext-2" }, { ext_2_key: "ext_2_value" });
  // Now do a sync with out regular test server.
  let server = await serverForFoo(engine);
  try {
    await SyncTestingInfrastructure(server);

    info("Add server records");
    let foo = server.user("foo");
    let collection = foo.collection("extension-storage");
    let now = new_timestamp();

    collection.insert(
      "fakeguid0000",
      encryptPayload({
        id: "fakeguid0000",
        extId: "ext-1",
        data: JSON.stringify({ foo: "bar" }),
      }),
      now
    );

    info("Sync the engine");

    let ping = await sync_engine_and_validate_telem(engine, false);
    Assert.ok(ping.engines.find(e => e.name == "rust-webext-storage"));
    Assert.equal(
      ping.engines.find(e => e.name == "extension-storage"),
      null
    );

    // We should have applied the data from the existing collection record.
    Assert.deepEqual(await extensionStorageSync.get({ id: "ext-1" }, null), {
      foo: "bar",
    });

    // should now be 2 records on the server.
    let payloads = collection.payloads();
    Assert.equal(payloads.length, 2);
    // find the new one we wrote.
    let newPayload =
      payloads[0].id == "fakeguid0000" ? payloads[1] : payloads[0];
    Assert.equal(newPayload.data, `{"ext_2_key":"ext_2_value"}`);
    // should have updated the timestamp.
    greater(await engine.getLastSync(), 0, "Should update last sync time");
  } finally {
    await promiseStopServer(server);
    await engine.finalize();
  }
});
