/* 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"
);
const { Status } = ChromeUtils.importESModule(
  "resource://services-sync/status.sys.mjs"
);
const { FileUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/FileUtils.sys.mjs"
);

const fakeServer = new SyncServer();
fakeServer.start();

registerCleanupFunction(function () {
  return promiseStopServer(fakeServer).finally(() => {
    for (const pref of Svc.PrefBranch.getChildList("")) {
      Svc.PrefBranch.clearUserPref(pref);
    }
  });
});

const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"]);
logsdir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);

function removeLogFiles() {
  let entries = logsdir.directoryEntries;
  while (entries.hasMoreElements()) {
    let logfile = entries.getNext().QueryInterface(Ci.nsIFile);
    logfile.remove(false);
  }
}

function getLogFiles() {
  let result = [];
  let entries = logsdir.directoryEntries;
  while (entries.hasMoreElements()) {
    result.push(entries.getNext().QueryInterface(Ci.nsIFile));
  }
  return result;
}

let engine;
add_task(async function setup() {
  await Service.engineManager.clear();
  await Service.engineManager.register(EHTestsCommon.CatapultEngine);
  engine = Service.engineManager.get("catapult");
});

async function clean() {
  let promiseLogReset = promiseOneObserver("weave:service:reset-file-log");
  await Service.startOver();
  await promiseLogReset;
  Status.resetSync();
  Status.resetBackoff();
  removeLogFiles();
  // Move log levels back to trace (startOver will have reversed this), sicne
  syncTestLogging();
}

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

  Status.resetSync();
  // Test crypto/keys server maintenance errors are not reported.
  let server = await EHTestsCommon.sync_httpd_setup();
  await EHTestsCommon.setUp(server);

  await configureIdentity({ username: "broken.keys" }, server);

  // Force re-download of keys
  Service.collectionKeys.clear();

  let backoffInterval;
  Svc.Obs.add(
    "weave:service:backoff:interval",
    function observe(subject, data) {
      Svc.Obs.remove("weave:service:backoff:interval", observe);
      backoffInterval = subject;
    }
  );

  Assert.ok(!Status.enforceBackoff);
  Assert.equal(Status.service, STATUS_OK);

  let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
  await Service.sync();
  await promiseObserved;

  Assert.ok(Status.enforceBackoff);
  Assert.equal(backoffInterval, 42);
  Assert.equal(Status.service, LOGIN_FAILED);
  Assert.equal(Status.login, SERVER_MAINTENANCE);

  await clean();
  await promiseStopServer(server);
});

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

  // Test info/collections prolonged server maintenance errors are reported.
  let server = await EHTestsCommon.sync_httpd_setup();
  await EHTestsCommon.setUp(server);

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

  // Do an initial sync that we expect to be successful.
  let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
  await sync_and_validate_telem();
  await promiseObserved;

  Assert.equal(Status.service, STATUS_OK);
  Assert.equal(Status.sync, SYNC_SUCCEEDED);

  let lastSync = Svc.PrefBranch.getCharPref("lastSync");

  Assert.ok(lastSync);

  // Report server maintenance on info/collections requests
  server.registerPathHandler(
    "/1.1/johndoe/info/collections",
    EHTestsCommon.service_unavailable
  );

  promiseObserved = promiseOneObserver("weave:service:reset-file-log");
  await sync_and_validate_telem(() => {});
  await promiseObserved;

  Assert.equal(Status.sync, SERVER_MAINTENANCE);
  Assert.equal(Status.service, SYNC_FAILED);

  // We shouldn't update lastSync on complete failure.
  Assert.equal(lastSync, Svc.PrefBranch.getCharPref("lastSync"));

  await clean();
  await promiseStopServer(server);
});

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

    // Test server maintenance errors are reported
    // when calling syncAndReportErrors.
    let server = await EHTestsCommon.sync_httpd_setup();
    await EHTestsCommon.setUp(server);

    const BACKOFF = 42;
    engine.enabled = true;
    engine.exception = { status: 503, headers: { "retry-after": BACKOFF } };

    Assert.equal(Status.service, STATUS_OK);

    let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
    await Service.sync();
    await promiseObserved;

    Assert.equal(Status.service, SYNC_FAILED_PARTIAL);
    Assert.equal(Status.sync, SERVER_MAINTENANCE);

    await clean();
    await promiseStopServer(server);
  }
);

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

    // Test info/collections server maintenance errors are reported
    // when calling syncAndReportErrors.
    let server = await EHTestsCommon.sync_httpd_setup();
    await EHTestsCommon.setUp(server);

    await configureIdentity({ username: "broken.info" }, server);

    let backoffInterval;
    Svc.Obs.add(
      "weave:service:backoff:interval",
      function observe(subject, data) {
        Svc.Obs.remove("weave:service:backoff:interval", observe);
        backoffInterval = subject;
      }
    );

    Assert.ok(!Status.enforceBackoff);
    Assert.equal(Status.service, STATUS_OK);

    let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
    await Service.sync();
    await promiseObserved;

    Assert.ok(Status.enforceBackoff);
    Assert.equal(backoffInterval, 42);
    Assert.equal(Status.service, LOGIN_FAILED);
    Assert.equal(Status.login, SERVER_MAINTENANCE);

    await clean();
    await promiseStopServer(server);
  }
);

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

    // Test meta/global server maintenance errors are reported
    // when calling syncAndReportErrors.
    let server = await EHTestsCommon.sync_httpd_setup();
    await EHTestsCommon.setUp(server);

    await configureIdentity({ username: "broken.meta" }, server);

    let backoffInterval;
    Svc.Obs.add(
      "weave:service:backoff:interval",
      function observe(subject, data) {
        Svc.Obs.remove("weave:service:backoff:interval", observe);
        backoffInterval = subject;
      }
    );

    Assert.ok(!Status.enforceBackoff);
    Assert.equal(Status.service, STATUS_OK);

    let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
    await Service.sync();
    await promiseObserved;

    Assert.ok(Status.enforceBackoff);
    Assert.equal(backoffInterval, 42);
    Assert.equal(Status.service, LOGIN_FAILED);
    Assert.equal(Status.login, SERVER_MAINTENANCE);

    await clean();
    await promiseStopServer(server);
  }
);

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

    // Test crypto/keys server maintenance errors are reported
    // when calling syncAndReportErrors.
    let server = await EHTestsCommon.sync_httpd_setup();
    await EHTestsCommon.setUp(server);

    await configureIdentity({ username: "broken.keys" }, server);
    // Force re-download of keys
    Service.collectionKeys.clear();

    let backoffInterval;
    Svc.Obs.add(
      "weave:service:backoff:interval",
      function observe(subject, data) {
        Svc.Obs.remove("weave:service:backoff:interval", observe);
        backoffInterval = subject;
      }
    );

    Assert.ok(!Status.enforceBackoff);
    Assert.equal(Status.service, STATUS_OK);

    let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
    await Service.sync();
    await promiseObserved;

    Assert.ok(Status.enforceBackoff);
    Assert.equal(backoffInterval, 42);
    Assert.equal(Status.service, LOGIN_FAILED);
    Assert.equal(Status.login, SERVER_MAINTENANCE);

    await clean();
    await promiseStopServer(server);
  }
);

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

    // Test crypto/keys server maintenance errors are reported
    // when calling syncAndReportErrors.
    let server = await EHTestsCommon.sync_httpd_setup();

    // Start off with an empty account, do not upload a key.
    await configureIdentity({ username: "broken.keys" }, server);

    let backoffInterval;
    Svc.Obs.add(
      "weave:service:backoff:interval",
      function observe(subject, data) {
        Svc.Obs.remove("weave:service:backoff:interval", observe);
        backoffInterval = subject;
      }
    );

    Assert.ok(!Status.enforceBackoff);
    Assert.equal(Status.service, STATUS_OK);

    let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
    await Service.sync();
    await promiseObserved;

    Assert.ok(Status.enforceBackoff);
    Assert.equal(backoffInterval, 42);
    Assert.equal(Status.service, LOGIN_FAILED);
    Assert.equal(Status.login, SERVER_MAINTENANCE);

    await clean();
    await promiseStopServer(server);
  }
);

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

    // Test crypto/keys server maintenance errors are reported
    // when calling syncAndReportErrors.
    let server = await EHTestsCommon.sync_httpd_setup();

    // Start off with an empty account, do not upload a key.
    await configureIdentity({ username: "broken.wipe" }, server);

    let backoffInterval;
    Svc.Obs.add(
      "weave:service:backoff:interval",
      function observe(subject, data) {
        Svc.Obs.remove("weave:service:backoff:interval", observe);
        backoffInterval = subject;
      }
    );

    Assert.ok(!Status.enforceBackoff);
    Assert.equal(Status.service, STATUS_OK);

    let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
    await Service.sync();
    await promiseObserved;

    Assert.ok(Status.enforceBackoff);
    Assert.equal(backoffInterval, 42);
    Assert.equal(Status.service, LOGIN_FAILED);
    Assert.equal(Status.login, SERVER_MAINTENANCE);

    await clean();
    await promiseStopServer(server);
  }
);

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

    // Test that we report prolonged server maintenance errors that occur whilst
    // wiping all remote devices.
    let server = await EHTestsCommon.sync_httpd_setup();

    await configureIdentity({ username: "broken.wipe" }, server);
    await EHTestsCommon.generateAndUploadKeys();

    engine.exception = null;
    engine.enabled = true;

    let backoffInterval;
    Svc.Obs.add(
      "weave:service:backoff:interval",
      function observe(subject, data) {
        Svc.Obs.remove("weave:service:backoff:interval", observe);
        backoffInterval = subject;
      }
    );

    Assert.ok(!Status.enforceBackoff);
    Assert.equal(Status.service, STATUS_OK);

    Svc.PrefBranch.setCharPref("firstSync", "wipeRemote");

    let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
    await Service.sync();
    await promiseObserved;

    Assert.ok(Status.enforceBackoff);
    Assert.equal(backoffInterval, 42);
    Assert.equal(Status.service, SYNC_FAILED);
    Assert.equal(Status.sync, SERVER_MAINTENANCE);
    Assert.equal(Svc.PrefBranch.getCharPref("firstSync"), "wipeRemote");

    await clean();
    await promiseStopServer(server);
  }
);

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

  equal(getLogFiles().length, 0);

  let server = await EHTestsCommon.sync_httpd_setup();
  engine.enabled = true;
  engine.sync = async function sync() {
    Svc.Obs.notify("weave:engine:sync:error", ENGINE_UNKNOWN_FAIL, "catapult");
  };
  let lastSync = Svc.PrefBranch.getCharPref("lastSync", null);
  let log = Log.repository.getLogger("Sync.ErrorHandler");
  Svc.PrefBranch.setBoolPref("log.appender.file.logOnError", true);

  Assert.equal(Status.engines.catapult, undefined);

  let promiseObserved = new Promise(res => {
    Svc.Obs.add("weave:engine:sync:finish", function onEngineFinish() {
      Svc.Obs.remove("weave:engine:sync:finish", onEngineFinish);

      log.info("Adding reset-file-log observer.");
      Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
        Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
        res();
      });
    });
  });

  Assert.ok(await EHTestsCommon.setUp(server));
  await sync_and_validate_telem(ping => {
    deepEqual(ping.status.service, SYNC_FAILED_PARTIAL);
    deepEqual(ping.engines.find(e => e.status).status, ENGINE_UNKNOWN_FAIL);
  });

  await promiseObserved;

  _("Status.engines: " + JSON.stringify(Status.engines));
  Assert.equal(Status.engines.catapult, ENGINE_UNKNOWN_FAIL);
  Assert.equal(Status.service, SYNC_FAILED_PARTIAL);

  // lastSync should update on partial failure.
  Assert.notEqual(lastSync, Svc.PrefBranch.getCharPref("lastSync"));

  // Test Error log was written on SYNC_FAILED_PARTIAL.
  let logFiles = getLogFiles();
  equal(logFiles.length, 1);
  Assert.ok(
    logFiles[0].leafName.startsWith("error-sync-"),
    logFiles[0].leafName
  );

  await clean();

  await promiseStopServer(server);
});

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

  _(
    "Ensure that an error is still logged when weave:service:sync:error " +
      "is notified, despite shouldReportError returning false."
  );

  let log = Log.repository.getLogger("Sync.ErrorHandler");
  Svc.PrefBranch.setBoolPref("log.appender.file.logOnError", true);
  log.info("TESTING");

  // Ensure that we report no error.
  Status.login = MASTER_PASSWORD_LOCKED;

  let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
  Svc.Obs.notify("weave:service:sync:error", {});
  await promiseObserved;

  // Test that error log was written.
  let logFiles = getLogFiles();
  equal(logFiles.length, 1);
  Assert.ok(
    logFiles[0].leafName.startsWith("error-sync-"),
    logFiles[0].leafName
  );

  await clean();
});

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

  _(
    "Ensure that an error is still logged when weave:service:login:error " +
      "is notified, despite shouldReportError returning false."
  );

  let log = Log.repository.getLogger("Sync.ErrorHandler");
  Svc.PrefBranch.setBoolPref("log.appender.file.logOnError", true);
  log.info("TESTING");

  // Ensure that we report no error.
  Status.login = MASTER_PASSWORD_LOCKED;

  let promiseObserved = promiseOneObserver("weave:service:reset-file-log");
  Svc.Obs.notify("weave:service:login:error", {});
  await promiseObserved;

  // Test that error log was written.
  let logFiles = getLogFiles();
  equal(logFiles.length, 1);
  Assert.ok(
    logFiles[0].leafName.startsWith("error-sync-"),
    logFiles[0].leafName
  );

  await clean();
});

// This test should be the last one since it monkeypatches the engine object
// and we should only have one engine object throughout the file (bug 629664).
add_task(async function test_engine_applyFailed() {
  enableValidationPrefs();

  let server = await EHTestsCommon.sync_httpd_setup();

  engine.enabled = true;
  delete engine.exception;
  engine.sync = async function sync() {
    Svc.Obs.notify("weave:engine:sync:applied", { newFailed: 1 }, "catapult");
  };

  Svc.PrefBranch.setBoolPref("log.appender.file.logOnError", true);

  let promiseObserved = promiseOneObserver("weave:service:reset-file-log");

  Assert.equal(Status.engines.catapult, undefined);
  Assert.ok(await EHTestsCommon.setUp(server));
  await Service.sync();
  await promiseObserved;

  Assert.equal(Status.engines.catapult, ENGINE_APPLY_FAIL);
  Assert.equal(Status.service, SYNC_FAILED_PARTIAL);

  // Test Error log was written on SYNC_FAILED_PARTIAL.
  let logFiles = getLogFiles();
  equal(logFiles.length, 1);
  Assert.ok(
    logFiles[0].leafName.startsWith("error-sync-"),
    logFiles[0].leafName
  );

  await clean();
  await promiseStopServer(server);
});
