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

Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/engines.js");
Cu.import("resource://services-sync/engines/clients.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");

Svc.Prefs.set("registerEngines", "");
Cu.import("resource://services-sync/service.js");

let scheduler;
let clientsEngine;

function sync_httpd_setup() {
  let global = new ServerWBO("global", {
    syncID: Service.syncID,
    storageVersion: STORAGE_VERSION,
    engines: {clients: {version: clientsEngine.version,
                        syncID: clientsEngine.syncID}}
  });
  let clientsColl = new ServerCollection({}, true);

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

  return httpd_setup({
    "/1.1/johndoe/storage/meta/global": upd("meta", global.handler()),
    "/1.1/johndoe/info/collections": collectionsHelper.handler,
    "/1.1/johndoe/storage/crypto/keys":
      upd("crypto", (new ServerWBO("keys")).handler()),
    "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler())
  });
}

async function setUp(server) {
  await configureIdentity({username: "johndoe"}, server);
  await generateNewKeys(Service.collectionKeys);
  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
  await serverKeys.encrypt(Service.identity.syncKeyBundle);
  await serverKeys.upload(Service.resource(Service.cryptoKeysURL));
}

add_task(async function setup() {
  initTestLogging("Trace");

  Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
  Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;

  scheduler = Service.scheduler;
  clientsEngine = Service.clientsEngine;

  // Don't remove stale clients when syncing. This is a test-only workaround
  // that lets us add clients directly to the store, without losing them on
  // the next sync.
  clientsEngine._removeRemoteClient = async (id) => {};
});

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

  _("Test successful sync calling adjustSyncInterval");
  let syncSuccesses = 0;
  function onSyncFinish() {
    _("Sync success.");
    syncSuccesses++;
  }
  Svc.Obs.add("weave:service:sync:finish", onSyncFinish);

  let server = sync_httpd_setup();
  await setUp(server);

  // Confirm defaults
  Assert.ok(!scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
  Assert.ok(!scheduler.hasIncomingItems);

  _("Test as long as numClients <= 1 our sync interval is SINGLE_USER.");
  // idle == true && numClients <= 1 && hasIncomingItems == false
  scheduler.idle = true;
  await Service.sync();
  Assert.equal(syncSuccesses, 1);
  Assert.ok(scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  // idle == false && numClients <= 1 && hasIncomingItems == false
  scheduler.idle = false;
  await Service.sync();
  Assert.equal(syncSuccesses, 2);
  Assert.ok(!scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  // idle == false && numClients <= 1 && hasIncomingItems == true
  scheduler.hasIncomingItems = true;
  await Service.sync();
  Assert.equal(syncSuccesses, 3);
  Assert.ok(!scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  // idle == true && numClients <= 1 && hasIncomingItems == true
  scheduler.idle = true;
  await Service.sync();
  Assert.equal(syncSuccesses, 4);
  Assert.ok(scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  _("Test as long as idle && numClients > 1 our sync interval is idleInterval.");
  // idle == true && numClients > 1 && hasIncomingItems == true
  await Service.clientsEngine._store.create({ id: "foo", cleartext: { name: "bar", type: "mobile" } });
  await Service.sync();
  Assert.equal(syncSuccesses, 5);
  Assert.ok(scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.idleInterval);

  // idle == true && numClients > 1 && hasIncomingItems == false
  scheduler.hasIncomingItems = false;
  await Service.sync();
  Assert.equal(syncSuccesses, 6);
  Assert.ok(scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.idleInterval);

  _("Test non-idle, numClients > 1, no incoming items => activeInterval.");
  // idle == false && numClients > 1 && hasIncomingItems == false
  scheduler.idle = false;
  await Service.sync();
  Assert.equal(syncSuccesses, 7);
  Assert.ok(!scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);

  _("Test non-idle, numClients > 1, incoming items => immediateInterval.");
  // idle == false && numClients > 1 && hasIncomingItems == true
  scheduler.hasIncomingItems = true;
  await Service.sync();
  Assert.equal(syncSuccesses, 8);
  Assert.ok(!scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems); // gets reset to false
  Assert.equal(scheduler.syncInterval, scheduler.immediateInterval);

  Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
  await Service.startOver();
  await promiseStopServer(server);
});

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

  _("Test unsuccessful sync calling adjustSyncInterval");

  let syncFailures = 0;
  function onSyncError() {
    _("Sync error.");
    syncFailures++;
  }
  Svc.Obs.add("weave:service:sync:error", onSyncError);

  _("Test unsuccessful sync calls adjustSyncInterval");
  // Force sync to fail.
  Svc.Prefs.set("firstSync", "notReady");

  let server = sync_httpd_setup();
  await setUp(server);

  // Confirm defaults
  Assert.ok(!scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
  Assert.ok(!scheduler.hasIncomingItems);

  _("Test as long as numClients <= 1 our sync interval is SINGLE_USER.");
  // idle == true && numClients <= 1 && hasIncomingItems == false
  scheduler.idle = true;
  await Service.sync();
  Assert.equal(syncFailures, 1);
  Assert.ok(scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  // idle == false && numClients <= 1 && hasIncomingItems == false
  scheduler.idle = false;
  await Service.sync();
  Assert.equal(syncFailures, 2);
  Assert.ok(!scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  // idle == false && numClients <= 1 && hasIncomingItems == true
  scheduler.hasIncomingItems = true;
  await Service.sync();
  Assert.equal(syncFailures, 3);
  Assert.ok(!scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  // idle == true && numClients <= 1 && hasIncomingItems == true
  scheduler.idle = true;
  await Service.sync();
  Assert.equal(syncFailures, 4);
  Assert.ok(scheduler.idle);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.ok(scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  _("Test as long as idle && numClients > 1 our sync interval is idleInterval.");
  // idle == true && numClients > 1 && hasIncomingItems == true
  Svc.Prefs.set("clients.devices.mobile", 2);
  scheduler.updateClientMode();

  await Service.sync();
  Assert.equal(syncFailures, 5);
  Assert.ok(scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.idleInterval);

  // idle == true && numClients > 1 && hasIncomingItems == false
  scheduler.hasIncomingItems = false;
  await Service.sync();
  Assert.equal(syncFailures, 6);
  Assert.ok(scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.idleInterval);

  _("Test non-idle, numClients > 1, no incoming items => activeInterval.");
  // idle == false && numClients > 1 && hasIncomingItems == false
  scheduler.idle = false;
  await Service.sync();
  Assert.equal(syncFailures, 7);
  Assert.ok(!scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems);
  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);

  _("Test non-idle, numClients > 1, incoming items => immediateInterval.");
  // idle == false && numClients > 1 && hasIncomingItems == true
  scheduler.hasIncomingItems = true;
  await Service.sync();
  Assert.equal(syncFailures, 8);
  Assert.ok(!scheduler.idle);
  Assert.ok(scheduler.numClients > 1);
  Assert.ok(!scheduler.hasIncomingItems); // gets reset to false
  Assert.equal(scheduler.syncInterval, scheduler.immediateInterval);

  await Service.startOver();
  Svc.Obs.remove("weave:service:sync:error", onSyncError);
  await promiseStopServer(server);
});

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

  let server = sync_httpd_setup();
  await setUp(server);

  // Single device: no sync triggered.
  scheduler.idle = true;
  scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
  Assert.ok(!scheduler.idle);

  // Multiple devices: sync is triggered.
  Svc.Prefs.set("clients.devices.mobile", 2);
  scheduler.updateClientMode();

  let promiseDone = promiseOneObserver("weave:service:sync:finish");

  scheduler.idle = true;
  scheduler.observe(null, "active", Svc.Prefs.get("scheduler.idleTime"));
  Assert.ok(!scheduler.idle);
  await promiseDone;

  Service.recordManager.clearCache();
  Svc.Prefs.resetBranch("");
  scheduler.setDefaults();
  await clientsEngine.resetClient();

  await Service.startOver();
  await promiseStopServer(server);
});

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

  let server = sync_httpd_setup();
  await setUp(server);

  let syncFailures = 0;
  function onSyncError() {
    _("Sync error.");
    syncFailures++;
  }
  Svc.Obs.add("weave:service:sync:error", onSyncError);

  _("Test unsuccessful sync updates client mode & sync intervals");
  // Force a sync fail.
  Svc.Prefs.set("firstSync", "notReady");

  Assert.equal(syncFailures, 0);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  Svc.Prefs.set("clients.devices.mobile", 2);
  await Service.sync();

  Assert.equal(syncFailures, 1);
  Assert.ok(scheduler.numClients > 1);
  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);

  Svc.Obs.remove("weave:service:sync:error", onSyncError);
  await Service.startOver();
  await promiseStopServer(server);
});

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

  // Test scenario similar to bug 671378. This bug appeared when a score
  // update occurred that wasn't large enough to trigger a sync so
  // scheduleNextSync() was called without a time interval parameter,
  // setting nextSync to a non-zero value and preventing the timer from
  // being adjusted in the next call to scheduleNextSync().
  let server = sync_httpd_setup();
  await setUp(server);

  let syncSuccesses = 0;
  function onSyncFinish() {
    _("Sync success.");
    syncSuccesses++;
  }
  Svc.Obs.add("weave:service:sync:finish", onSyncFinish);

  // After first sync call, syncInterval & syncTimer are singleDeviceInterval.
  await Service.sync();
  Assert.equal(syncSuccesses, 1);
  Assert.equal(false, scheduler.numClients > 1);
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
  Assert.equal(scheduler.syncTimer.delay, scheduler.singleDeviceInterval);

  let promiseDone = new Promise(resolve => {
    // Wrap scheduleNextSync so we are notified when it is finished.
    scheduler._scheduleNextSync = scheduler.scheduleNextSync;
    scheduler.scheduleNextSync = function() {
      scheduler._scheduleNextSync();

      // Check on sync:finish scheduleNextSync sets the appropriate
      // syncInterval and syncTimer values.
      if (syncSuccesses == 2) {
        Assert.notEqual(scheduler.nextSync, 0);
        Assert.equal(scheduler.syncInterval, scheduler.activeInterval);
        Assert.ok(scheduler.syncTimer.delay <= scheduler.activeInterval);

        scheduler.scheduleNextSync = scheduler._scheduleNextSync;
        Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
        Service.startOver().then(() => {
          server.stop(resolve);
        });
      }
    };
  });

  // Set nextSync != 0
  // syncInterval still hasn't been set by call to updateClientMode.
  // Explicitly trying to invoke scheduleNextSync during a sync
  // (to immitate a score update that isn't big enough to trigger a sync).
  Svc.Obs.add("weave:service:sync:start", function onSyncStart() {
    // Wait for other sync:start observers to be called so that
    // nextSync is set to 0.
    CommonUtils.nextTick(function() {
      Svc.Obs.remove("weave:service:sync:start", onSyncStart);

      scheduler.scheduleNextSync();
      Assert.notEqual(scheduler.nextSync, 0);
      Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);
      Assert.equal(scheduler.syncTimer.delay, scheduler.singleDeviceInterval);
    });
  });

  await Service.clientsEngine._store.create({ id: "foo", cleartext: { name: "bar", type: "mobile" } });
  await Service.sync();
  await promiseDone;
});

add_task(async function test_adjust_timer_larger_syncInterval() {
  _("Test syncInterval > current timout period && nextSync != 0, syncInterval is NOT used.");
  Svc.Prefs.set("clients.devices.mobile", 2);
  scheduler.updateClientMode();
  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);

  scheduler.scheduleNextSync();

  // Ensure we have a small interval.
  Assert.notEqual(scheduler.nextSync, 0);
  Assert.equal(scheduler.syncTimer.delay, scheduler.activeInterval);

  // Make interval large again
  await clientsEngine._wipeClient();
  Svc.Prefs.reset("clients.devices.mobile");
  scheduler.updateClientMode();
  Assert.equal(scheduler.syncInterval, scheduler.singleDeviceInterval);

  scheduler.scheduleNextSync();

  // Ensure timer delay remains as the small interval.
  Assert.notEqual(scheduler.nextSync, 0);
  Assert.ok(scheduler.syncTimer.delay <= scheduler.activeInterval);

  // SyncSchedule.
  await Service.startOver();
});

add_task(async function test_adjust_timer_smaller_syncInterval() {
  _("Test current timout > syncInterval period && nextSync != 0, syncInterval is used.");
  scheduler.scheduleNextSync();

  // Ensure we have a large interval.
  Assert.notEqual(scheduler.nextSync, 0);
  Assert.equal(scheduler.syncTimer.delay, scheduler.singleDeviceInterval);

  // Make interval smaller
  Svc.Prefs.set("clients.devices.mobile", 2);
  scheduler.updateClientMode();
  Assert.equal(scheduler.syncInterval, scheduler.activeInterval);

  scheduler.scheduleNextSync();

  // Ensure smaller timer delay is used.
  Assert.notEqual(scheduler.nextSync, 0);
  Assert.ok(scheduler.syncTimer.delay <= scheduler.activeInterval);

  // SyncSchedule.
  await Service.startOver();
});
