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

const { CryptoWrapper, WBORecord } = ChromeUtils.importESModule(
  "resource://services-sync/record.sys.mjs"
);
const { Service } = ChromeUtils.importESModule(
  "resource://services-sync/service.sys.mjs"
);

add_task(async function v4_upgrade() {
  enableValidationPrefs();

  let clients = new ServerCollection();
  let meta_global = new ServerWBO("global");

  // Tracking info/collections.
  let collectionsHelper = track_collections_helper();
  let upd = collectionsHelper.with_updated_collection;
  let collections = collectionsHelper.collections;

  let keysWBO = new ServerWBO("keys");
  let server = httpd_setup({
    // Special.
    "/1.1/johndoe/info/collections": collectionsHelper.handler,
    "/1.1/johndoe/storage/crypto/keys": upd("crypto", keysWBO.handler()),
    "/1.1/johndoe/storage/meta/global": upd("meta", meta_global.handler()),

    // Track modified times.
    "/1.1/johndoe/storage/clients": upd("clients", clients.handler()),
    "/1.1/johndoe/storage/tabs": upd("tabs", new ServerCollection().handler()),

    // Just so we don't get 404s in the logs.
    "/1.1/johndoe/storage/bookmarks": new ServerCollection().handler(),
    "/1.1/johndoe/storage/forms": new ServerCollection().handler(),
    "/1.1/johndoe/storage/history": new ServerCollection().handler(),
    "/1.1/johndoe/storage/passwords": new ServerCollection().handler(),
    "/1.1/johndoe/storage/prefs": new ServerCollection().handler(),
  });

  try {
    Service.status.resetSync();

    _("Logging in.");

    await configureIdentity({ username: "johndoe" }, server);

    await Service.login();
    Assert.ok(Service.isLoggedIn);
    await Service.verifyAndFetchSymmetricKeys();
    Assert.ok(await Service._remoteSetup());

    async function test_out_of_date() {
      _("Old meta/global: " + JSON.stringify(meta_global));
      meta_global.payload = JSON.stringify({
        syncID: "foooooooooooooooooooooooooo",
        storageVersion: STORAGE_VERSION + 1,
      });
      collections.meta = Date.now() / 1000;
      _("New meta/global: " + JSON.stringify(meta_global));
      Service.recordManager.set(Service.metaURL, meta_global);
      try {
        await Service.sync();
      } catch (ex) {}
      Assert.equal(Service.status.sync, VERSION_OUT_OF_DATE);
    }

    // See what happens when we bump the storage version.
    _("Syncing after server has been upgraded.");
    await test_out_of_date();

    // Same should happen after a wipe.
    _("Syncing after server has been upgraded and wiped.");
    await Service.wipeServer();
    await test_out_of_date();

    // Now's a great time to test what happens when keys get replaced.
    _("Syncing afresh...");
    Service.logout();
    Service.collectionKeys.clear();
    meta_global.payload = JSON.stringify({
      syncID: "foooooooooooooobbbbbbbbbbbb",
      storageVersion: STORAGE_VERSION,
    });
    collections.meta = Date.now() / 1000;
    Service.recordManager.set(Service.metaURL, meta_global);
    await Service.login();
    Assert.ok(Service.isLoggedIn);
    await Service.sync();
    Assert.ok(Service.isLoggedIn);

    let serverDecrypted;
    let serverKeys;
    let serverResp;

    async function retrieve_server_default() {
      serverKeys = serverResp = serverDecrypted = null;

      serverKeys = new CryptoWrapper("crypto", "keys");
      serverResp = (
        await serverKeys.fetch(Service.resource(Service.cryptoKeysURL))
      ).response;
      Assert.ok(serverResp.success);

      serverDecrypted = await serverKeys.decrypt(
        Service.identity.syncKeyBundle
      );
      _("Retrieved WBO:       " + JSON.stringify(serverDecrypted));
      _("serverKeys:          " + JSON.stringify(serverKeys));

      return serverDecrypted.default;
    }

    async function retrieve_and_compare_default(should_succeed) {
      let serverDefault = await retrieve_server_default();
      let localDefault = Service.collectionKeys.keyForCollection().keyPairB64;

      _("Retrieved keyBundle: " + JSON.stringify(serverDefault));
      _("Local keyBundle:     " + JSON.stringify(localDefault));

      if (should_succeed) {
        Assert.equal(
          JSON.stringify(serverDefault),
          JSON.stringify(localDefault)
        );
      } else {
        Assert.notEqual(
          JSON.stringify(serverDefault),
          JSON.stringify(localDefault)
        );
      }
    }

    // Uses the objects set above.
    async function set_server_keys(pair) {
      serverDecrypted.default = pair;
      serverKeys.cleartext = serverDecrypted;
      await serverKeys.encrypt(Service.identity.syncKeyBundle);
      await serverKeys.upload(Service.resource(Service.cryptoKeysURL));
    }

    _("Checking we have the latest keys.");
    await retrieve_and_compare_default(true);

    _("Update keys on server.");
    await set_server_keys([
      "KaaaaaaaaaaaHAtfmuRY0XEJ7LXfFuqvF7opFdBD/MY=",
      "aaaaaaaaaaaapxMO6TEWtLIOv9dj6kBAJdzhWDkkkis=",
    ]);

    _("Checking that we no longer have the latest keys.");
    await retrieve_and_compare_default(false);

    _("Indeed, they're what we set them to...");
    Assert.equal(
      "KaaaaaaaaaaaHAtfmuRY0XEJ7LXfFuqvF7opFdBD/MY=",
      (await retrieve_server_default())[0]
    );

    _("Sync. Should download changed keys automatically.");
    let oldClientsModified = collections.clients;
    let oldTabsModified = collections.tabs;

    await Service.login();
    await Service.sync();
    _("New key should have forced upload of data.");
    _("Tabs: " + oldTabsModified + " < " + collections.tabs);
    _("Clients: " + oldClientsModified + " < " + collections.clients);
    Assert.ok(collections.clients > oldClientsModified);
    Assert.ok(collections.tabs > oldTabsModified);

    _("... and keys will now match.");
    await retrieve_and_compare_default(true);

    // Clean up.
    await Service.startOver();
  } finally {
    Svc.Prefs.resetBranch("");
    await promiseStopServer(server);
  }
});

add_task(async function v5_upgrade() {
  enableValidationPrefs();

  // Tracking info/collections.
  let collectionsHelper = track_collections_helper();
  let upd = collectionsHelper.with_updated_collection;

  let keysWBO = new ServerWBO("keys");
  let bulkWBO = new ServerWBO("bulk");
  let clients = new ServerCollection();
  let meta_global = new ServerWBO("global");

  let server = httpd_setup({
    // Special.
    "/1.1/johndoe/storage/meta/global": upd("meta", meta_global.handler()),
    "/1.1/johndoe/info/collections": collectionsHelper.handler,
    "/1.1/johndoe/storage/crypto/keys": upd("crypto", keysWBO.handler()),
    "/1.1/johndoe/storage/crypto/bulk": upd("crypto", bulkWBO.handler()),

    // Track modified times.
    "/1.1/johndoe/storage/clients": upd("clients", clients.handler()),
    "/1.1/johndoe/storage/tabs": upd("tabs", new ServerCollection().handler()),
  });

  try {
    Service.status.resetSync();

    Service.clusterURL = server.baseURI + "/";

    await configureIdentity({ username: "johndoe" }, server);

    // Test an upgrade where the contents of the server would cause us to error
    // -- keys decrypted with a different sync key, for example.
    _("Testing v4 -> v5 (or similar) upgrade.");
    async function update_server_keys(syncKeyBundle, wboName, collWBO) {
      await generateNewKeys(Service.collectionKeys);
      let serverKeys = Service.collectionKeys.asWBO("crypto", wboName);
      await serverKeys.encrypt(syncKeyBundle);
      let res = Service.resource(Service.storageURL + collWBO);
      Assert.ok((await serverKeys.upload(res)).success);
    }

    _("Bumping version.");
    // Bump version on the server.
    let m = new WBORecord("meta", "global");
    m.payload = {
      syncID: "foooooooooooooooooooooooooo",
      storageVersion: STORAGE_VERSION + 1,
    };
    await m.upload(Service.resource(Service.metaURL));

    _("New meta/global: " + JSON.stringify(meta_global));

    // Fill the keys with bad data.
    let badKeys = new BulkKeyBundle("crypto");
    await badKeys.generateRandom();
    await update_server_keys(badKeys, "keys", "crypto/keys"); // v4
    await update_server_keys(badKeys, "bulk", "crypto/bulk"); // v5

    _("Generating new keys.");
    await generateNewKeys(Service.collectionKeys);

    // Now sync and see what happens. It should be a version fail, not a crypto
    // fail.

    _("Logging in.");
    try {
      await Service.login();
    } catch (e) {
      _("Exception: " + e);
    }
    _("Status: " + Service.status);
    Assert.ok(!Service.isLoggedIn);
    Assert.equal(VERSION_OUT_OF_DATE, Service.status.sync);

    // Clean up.
    await Service.startOver();
  } finally {
    Svc.Prefs.resetBranch("");
    await promiseStopServer(server);
  }
});

function run_test() {
  Log.repository.rootLogger.addAppender(new Log.DumpAppender());

  run_next_test();
}
