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

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

// Track HMAC error counts.
var hmacErrorCount = 0;
(function () {
  let hHE = Service.handleHMACEvent;
  Service.handleHMACEvent = async function () {
    hmacErrorCount++;
    return hHE.call(Service);
  };
})();

async function shared_setup() {
  enableValidationPrefs();
  syncTestLogging();

  hmacErrorCount = 0;

  let clientsEngine = Service.clientsEngine;
  let clientsSyncID = await clientsEngine.resetLocalSyncID();

  // Make sure RotaryEngine is the only one we sync.
  let { engine, syncID, tracker } = await registerRotaryEngine();
  await engine.setLastSync(123); // Needs to be non-zero so that tracker is queried.
  engine._store.items = {
    flying: "LNER Class A3 4472",
    scotsman: "Flying Scotsman",
  };
  await tracker.addChangedID("scotsman", 0);
  Assert.equal(1, Service.engineManager.getEnabled().length);

  let engines = {
    rotary: { version: engine.version, syncID },
    clients: { version: clientsEngine.version, syncID: clientsSyncID },
  };

  // Common server objects.
  let global = new ServerWBO("global", { engines });
  let keysWBO = new ServerWBO("keys");
  let rotaryColl = new ServerCollection({}, true);
  let clientsColl = new ServerCollection({}, true);

  return [engine, rotaryColl, clientsColl, keysWBO, global, tracker];
}

add_task(async function hmac_error_during_404() {
  _("Attempt to replicate the HMAC error setup.");
  let [engine, rotaryColl, clientsColl, keysWBO, global, tracker] =
    await shared_setup();

  // Hand out 404s for crypto/keys.
  let keysHandler = keysWBO.handler();
  let key404Counter = 0;
  let keys404Handler = function (request, response) {
    if (key404Counter > 0) {
      let body = "Not Found";
      response.setStatusLine(request.httpVersion, 404, body);
      response.bodyOutputStream.write(body, body.length);
      key404Counter--;
      return;
    }
    keysHandler(request, response);
  };

  let collectionsHelper = track_collections_helper();
  let upd = collectionsHelper.with_updated_collection;
  let handlers = {
    "/1.1/foo/info/collections": collectionsHelper.handler,
    "/1.1/foo/storage/meta/global": upd("meta", global.handler()),
    "/1.1/foo/storage/crypto/keys": upd("crypto", keys404Handler),
    "/1.1/foo/storage/clients": upd("clients", clientsColl.handler()),
    "/1.1/foo/storage/rotary": upd("rotary", rotaryColl.handler()),
  };

  let server = sync_httpd_setup(handlers);
  // Do not instantiate SyncTestingInfrastructure; we need real crypto.
  await configureIdentity({ username: "foo" }, server);
  await Service.login();

  try {
    _("Syncing.");
    await sync_and_validate_telem();

    _(
      "Partially resetting client, as if after a restart, and forcing redownload."
    );
    Service.collectionKeys.clear();
    await engine.setLastSync(0); // So that we redownload records.
    key404Counter = 1;
    _("---------------------------");
    await sync_and_validate_telem();
    _("---------------------------");

    // Two rotary items, one client record... no errors.
    Assert.equal(hmacErrorCount, 0);
  } finally {
    await tracker.clearChangedIDs();
    await Service.engineManager.unregister(engine);
    for (const pref of Svc.PrefBranch.getChildList("")) {
      Svc.PrefBranch.clearUserPref(pref);
    }
    Service.recordManager.clearCache();
    await promiseStopServer(server);
  }
});

add_task(async function hmac_error_during_node_reassignment() {
  _("Attempt to replicate an HMAC error during node reassignment.");
  let [engine, rotaryColl, clientsColl, keysWBO, global, tracker] =
    await shared_setup();

  let collectionsHelper = track_collections_helper();
  let upd = collectionsHelper.with_updated_collection;

  // We'll provide a 401 mid-way through the sync. This function
  // simulates shifting to a node which has no data.
  function on401() {
    _("Deleting server data...");
    global.delete();
    rotaryColl.delete();
    keysWBO.delete();
    clientsColl.delete();
    delete collectionsHelper.collections.rotary;
    delete collectionsHelper.collections.crypto;
    delete collectionsHelper.collections.clients;
    _("Deleted server data.");
  }

  let should401 = false;
  function upd401(coll, handler) {
    return function (request, response) {
      if (should401 && request.method != "DELETE") {
        on401();
        should401 = false;
        let body = '"reassigned!"';
        response.setStatusLine(request.httpVersion, 401, "Node reassignment.");
        response.bodyOutputStream.write(body, body.length);
        return;
      }
      handler(request, response);
    };
  }

  let handlers = {
    "/1.1/foo/info/collections": collectionsHelper.handler,
    "/1.1/foo/storage/meta/global": upd("meta", global.handler()),
    "/1.1/foo/storage/crypto/keys": upd("crypto", keysWBO.handler()),
    "/1.1/foo/storage/clients": upd401("clients", clientsColl.handler()),
    "/1.1/foo/storage/rotary": upd("rotary", rotaryColl.handler()),
  };

  let server = sync_httpd_setup(handlers);
  // Do not instantiate SyncTestingInfrastructure; we need real crypto.
  await configureIdentity({ username: "foo" }, server);

  _("Syncing.");
  // First hit of clients will 401. This will happen after meta/global and
  // keys -- i.e., in the middle of the sync, but before RotaryEngine.
  should401 = true;

  // Use observers to perform actions when our sync finishes.
  // This allows us to observe the automatic next-tick sync that occurs after
  // an abort.
  function onSyncError() {
    do_throw("Should not get a sync error!");
  }
  let onSyncFinished = function () {};
  let obs = {
    observe: function observe(subject, topic, data) {
      switch (topic) {
        case "weave:service:sync:error":
          onSyncError();
          break;
        case "weave:service:sync:finish":
          onSyncFinished();
          break;
      }
    },
  };

  Svc.Obs.add("weave:service:sync:finish", obs);
  Svc.Obs.add("weave:service:sync:error", obs);

  // This kicks off the actual test. Split into a function here to allow this
  // source file to broadly follow actual execution order.
  async function onwards() {
    _("== Invoking first sync.");
    await Service.sync();
    _("We should not simultaneously have data but no keys on the server.");
    let hasData = rotaryColl.wbo("flying") || rotaryColl.wbo("scotsman");
    let hasKeys = keysWBO.modified;

    _("We correctly handle 401s by aborting the sync and starting again.");
    Assert.ok(!hasData == !hasKeys);

    _("Be prepared for the second (automatic) sync...");
  }

  _("Make sure that syncing again causes recovery.");
  let callbacksPromise = new Promise(resolve => {
    onSyncFinished = function () {
      _("== First sync done.");
      _("---------------------------");
      onSyncFinished = function () {
        _("== Second (automatic) sync done.");
        let hasData = rotaryColl.wbo("flying") || rotaryColl.wbo("scotsman");
        let hasKeys = keysWBO.modified;
        Assert.ok(!hasData == !hasKeys);

        // Kick off another sync. Can't just call it, because we're inside the
        // lock...
        (async () => {
          await Async.promiseYield();
          _("Now a fresh sync will get no HMAC errors.");
          _(
            "Partially resetting client, as if after a restart, and forcing redownload."
          );
          Service.collectionKeys.clear();
          await engine.setLastSync(0);
          hmacErrorCount = 0;

          onSyncFinished = async function () {
            // Two rotary items, one client record... no errors.
            Assert.equal(hmacErrorCount, 0);

            Svc.Obs.remove("weave:service:sync:finish", obs);
            Svc.Obs.remove("weave:service:sync:error", obs);

            await tracker.clearChangedIDs();
            await Service.engineManager.unregister(engine);
            for (const pref of Svc.PrefBranch.getChildList("")) {
              Svc.PrefBranch.clearUserPref(pref);
            }
            Service.recordManager.clearCache();
            server.stop(resolve);
          };

          Service.sync();
        })().catch(console.error);
      };
    };
  });
  await onwards();
  await callbacksPromise;
});
