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

const { Weave } = ChromeUtils.importESModule(
  "resource://services-sync/main.sys.mjs"
);
const { HistoryEngine } = ChromeUtils.importESModule(
  "resource://services-sync/engines/history.sys.mjs"
);
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 test_locally_changed_keys() {
  enableValidationPrefs();

  let hmacErrorCount = 0;
  function counting(f) {
    return async function () {
      hmacErrorCount++;
      return f.call(this);
    };
  }

  Service.handleHMACEvent = counting(Service.handleHMACEvent);

  let server = new SyncServer();
  let johndoe = server.registerUser("johndoe", "password");
  johndoe.createContents({
    meta: {},
    crypto: {},
    clients: {},
  });
  server.start();

  try {
    Svc.PrefBranch.setCharPref("registerEngines", "Tab");

    await configureIdentity({ username: "johndoe" }, server);
    // We aren't doing a .login yet, so fudge the cluster URL.
    Service.clusterURL = Service.identity._token.endpoint;

    await Service.engineManager.register(HistoryEngine);
    // Disable addon sync because AddonManager won't be initialized here.
    await Service.engineManager.unregister("addons");
    await Service.engineManager.unregister("extension-storage");

    async function corrupt_local_keys() {
      Service.collectionKeys._default.keyPair = [
        await Weave.Crypto.generateRandomKey(),
        await Weave.Crypto.generateRandomKey(),
      ];
    }

    _("Setting meta.");

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

    _(
      "New meta/global: " +
        JSON.stringify(johndoe.collection("meta").wbo("global"))
    );

    // Upload keys.
    await generateNewKeys(Service.collectionKeys);
    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
    await serverKeys.encrypt(Service.identity.syncKeyBundle);
    Assert.ok(
      (await serverKeys.upload(Service.resource(Service.cryptoKeysURL))).success
    );

    // Check that login works.
    Assert.ok(await Service.login());
    Assert.ok(Service.isLoggedIn);

    // Sync should upload records.
    await sync_and_validate_telem();

    // Tabs exist.
    _("Tabs modified: " + johndoe.modified("tabs"));
    Assert.ok(johndoe.modified("tabs") > 0);

    // Let's create some server side history records.
    let liveKeys = Service.collectionKeys.keyForCollection("history");
    _("Keys now: " + liveKeys.keyPair);
    let visitType = Ci.nsINavHistoryService.TRANSITION_LINK;
    let history = johndoe.createCollection("history");
    for (let i = 0; i < 5; i++) {
      let id = "record-no--" + i;
      let modified = Date.now() / 1000 - 60 * (i + 10);

      let w = new CryptoWrapper("history", "id");
      w.cleartext = {
        id,
        histUri: "http://foo/bar?" + id,
        title: id,
        sortindex: i,
        visits: [{ date: (modified - 5) * 1000000, type: visitType }],
        deleted: false,
      };
      await w.encrypt(liveKeys);

      let payload = { ciphertext: w.ciphertext, IV: w.IV, hmac: w.hmac };
      history.insert(id, payload, modified);
    }

    history.timestamp = Date.now() / 1000;
    let old_key_time = johndoe.modified("crypto");
    _("Old key time: " + old_key_time);

    // Check that we can decrypt one.
    let rec = new CryptoWrapper("history", "record-no--0");
    await rec.fetch(
      Service.resource(Service.storageURL + "history/record-no--0")
    );
    _(JSON.stringify(rec));
    Assert.ok(!!(await rec.decrypt(liveKeys)));

    Assert.equal(hmacErrorCount, 0);

    // Fill local key cache with bad data.
    await corrupt_local_keys();
    _(
      "Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair
    );

    Assert.equal(hmacErrorCount, 0);

    _("HMAC error count: " + hmacErrorCount);
    // Now syncing should succeed, after one HMAC error.
    await sync_and_validate_telem(ping => {
      Assert.equal(
        ping.engines.find(e => e.name == "history").incoming.applied,
        5
      );
    });

    Assert.equal(hmacErrorCount, 1);
    _(
      "Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair
    );

    // And look! We downloaded history!
    Assert.ok(
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--0")
    );
    Assert.ok(
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--1")
    );
    Assert.ok(
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--2")
    );
    Assert.ok(
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--3")
    );
    Assert.ok(
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--4")
    );
    Assert.equal(hmacErrorCount, 1);

    _("Busting some new server values.");
    // Now what happens if we corrupt the HMAC on the server?
    for (let i = 5; i < 10; i++) {
      let id = "record-no--" + i;
      let modified = 1 + Date.now() / 1000;

      let w = new CryptoWrapper("history", "id");
      w.cleartext = {
        id,
        histUri: "http://foo/bar?" + id,
        title: id,
        sortindex: i,
        visits: [{ date: (modified - 5) * 1000000, type: visitType }],
        deleted: false,
      };
      await w.encrypt(Service.collectionKeys.keyForCollection("history"));
      w.hmac = w.hmac.toUpperCase();

      let payload = { ciphertext: w.ciphertext, IV: w.IV, hmac: w.hmac };
      history.insert(id, payload, modified);
    }
    history.timestamp = Date.now() / 1000;

    _("Server key time hasn't changed.");
    Assert.equal(johndoe.modified("crypto"), old_key_time);

    _("Resetting HMAC error timer.");
    Service.lastHMACEvent = 0;

    _("Syncing...");
    await sync_and_validate_telem(ping => {
      Assert.equal(
        ping.engines.find(e => e.name == "history").incoming.failed,
        5
      );
    });

    _(
      "Keys now: " + Service.collectionKeys.keyForCollection("history").keyPair
    );
    _(
      "Server keys have been updated, and we skipped over 5 more HMAC errors without adjusting history."
    );
    Assert.ok(johndoe.modified("crypto") > old_key_time);
    Assert.equal(hmacErrorCount, 6);
    Assert.equal(
      false,
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--5")
    );
    Assert.equal(
      false,
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--6")
    );
    Assert.equal(
      false,
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--7")
    );
    Assert.equal(
      false,
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--8")
    );
    Assert.equal(
      false,
      await PlacesUtils.history.hasVisits("http://foo/bar?record-no--9")
    );
  } finally {
    for (const pref of Svc.PrefBranch.getChildList("")) {
      Svc.PrefBranch.clearUserPref(pref);
    }
    await promiseStopServer(server);
  }
});

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

  run_next_test();
}
