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

Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/policies.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://testing-common/services/sync/utils.js");

function login_handling(handler) {
  return function (request, response) {
    if (basic_auth_matches(request, "johndoe", "ilovejane") ||
        basic_auth_matches(request, "janedoe", "ilovejohn")) {
      handler(request, response);
    } else {
      let body = "Unauthorized";
      response.setStatusLine(request.httpVersion, 401, "Unauthorized");
      response.setHeader("Content-Type", "text/plain");
      response.bodyOutputStream.write(body, body.length);
    }
  };
}

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

  run_next_test();
}

add_test(function test_offline() {
  try {
    _("The right bits are set when we're offline.");
    Services.io.offline = true;
    do_check_false(!!Service.login());
    do_check_eq(Service.status.login, LOGIN_FAILED_NETWORK_ERROR);
    Services.io.offline = false;
  } finally {
    Svc.Prefs.resetBranch("");
    run_next_test();
  }
});

function setup() {
  let janeHelper = track_collections_helper();
  let janeU      = janeHelper.with_updated_collection;
  let janeColls  = janeHelper.collections;
  let johnHelper = track_collections_helper();
  let johnU      = johnHelper.with_updated_collection;
  let johnColls  = johnHelper.collections;

  let server = httpd_setup({
    "/1.1/johndoe/info/collections": login_handling(johnHelper.handler),
    "/1.1/janedoe/info/collections": login_handling(janeHelper.handler),

    // We need these handlers because we test login, and login
    // is where keys are generated or fetched.
    // TODO: have Jane fetch her keys, not generate them...
    "/1.1/johndoe/storage/crypto/keys": johnU("crypto", new ServerWBO("keys").handler()),
    "/1.1/johndoe/storage/meta/global": johnU("meta",   new ServerWBO("global").handler()),
    "/1.1/janedoe/storage/crypto/keys": janeU("crypto", new ServerWBO("keys").handler()),
    "/1.1/janedoe/storage/meta/global": janeU("meta",   new ServerWBO("global").handler())
  });

  Service.serverURL = server.baseURI;
  return server;
}

add_test(function test_login_logout() {
  let server = setup();

  try {
    _("Force the initial state.");
    ensureLegacyIdentityManager();
    Service.status.service = STATUS_OK;
    do_check_eq(Service.status.service, STATUS_OK);

    _("Try logging in. It won't work because we're not configured yet.");
    Service.login();
    do_check_eq(Service.status.service, CLIENT_NOT_CONFIGURED);
    do_check_eq(Service.status.login, LOGIN_FAILED_NO_USERNAME);
    do_check_false(Service.isLoggedIn);

    _("Try again with username and password set.");
    Service.identity.account = "johndoe";
    Service.identity.basicPassword = "ilovejane";
    Service.login();
    do_check_eq(Service.status.service, CLIENT_NOT_CONFIGURED);
    do_check_eq(Service.status.login, LOGIN_FAILED_NO_PASSPHRASE);
    do_check_false(Service.isLoggedIn);

    _("Success if passphrase is set.");
    Service.identity.syncKey = "foo";
    Service.login();
    do_check_eq(Service.status.service, STATUS_OK);
    do_check_eq(Service.status.login, LOGIN_SUCCEEDED);
    do_check_true(Service.isLoggedIn);

    _("We can also pass username, password and passphrase to login().");
    Service.login("janedoe", "incorrectpassword", "bar");
    setBasicCredentials("janedoe", "incorrectpassword", "bar");
    do_check_eq(Service.status.service, LOGIN_FAILED);
    do_check_eq(Service.status.login, LOGIN_FAILED_LOGIN_REJECTED);
    do_check_false(Service.isLoggedIn);

    _("Try again with correct password.");
    Service.login("janedoe", "ilovejohn");
    do_check_eq(Service.status.service, STATUS_OK);
    do_check_eq(Service.status.login, LOGIN_SUCCEEDED);
    do_check_true(Service.isLoggedIn);

    _("Calling login() with parameters when the client is unconfigured sends notification.");
    let notified = false;
    Svc.Obs.add("weave:service:setup-complete", function() {
      notified = true;
    });
    setBasicCredentials(null, null, null);
    Service.login("janedoe", "ilovejohn", "bar");
    do_check_true(notified);
    do_check_eq(Service.status.service, STATUS_OK);
    do_check_eq(Service.status.login, LOGIN_SUCCEEDED);
    do_check_true(Service.isLoggedIn);

    _("Logout.");
    Service.logout();
    do_check_false(Service.isLoggedIn);

    _("Logging out again won't do any harm.");
    Service.logout();
    do_check_false(Service.isLoggedIn);

  } finally {
    Svc.Prefs.resetBranch("");
    server.stop(run_next_test);
  }
});

add_test(function test_login_on_sync() {
  let server = setup();
  setBasicCredentials("johndoe", "ilovejane", "bar");

  try {
    _("Sync calls login.");
    let oldLogin = Service.login;
    let loginCalled = false;
    Service.login = function() {
      loginCalled = true;
      Service.status.login = LOGIN_SUCCEEDED;
      this._loggedIn = false;           // So that sync aborts.
      return true;
    };

    Service.sync();

    do_check_true(loginCalled);
    Service.login = oldLogin;

    // Stub mpLocked.
    let mpLockedF = Utils.mpLocked;
    let mpLocked = true;
    Utils.mpLocked = () => mpLocked;

    // Stub scheduleNextSync. This gets called within checkSyncStatus if we're
    // ready to sync, so use it as an indicator.
    let scheduleNextSyncF = Service.scheduler.scheduleNextSync;
    let scheduleCalled = false;
    Service.scheduler.scheduleNextSync = function(wait) {
      scheduleCalled = true;
      scheduleNextSyncF.call(this, wait);
    };

    // Autoconnect still tries to connect in the background (useful behavior:
    // for non-MP users and unlocked MPs, this will detect version expiry
    // earlier).
    //
    // Consequently, non-MP users will be logged in as in the pre-Bug 543784 world,
    // and checkSyncStatus reflects that by waiting for login.
    //
    // This process doesn't apply if your MP is still locked, so we make
    // checkSyncStatus accept a locked MP in place of being logged in.
    //
    // This test exercises these two branches.

    _("We're ready to sync if locked.");
    Service.enabled = true;
    Services.io.offline = false;
    Service.scheduler.checkSyncStatus();
    do_check_true(scheduleCalled);

    _("... and also if we're not locked.");
    scheduleCalled = false;
    mpLocked = false;
    Service.scheduler.checkSyncStatus();
    do_check_true(scheduleCalled);
    Service.scheduler.scheduleNextSync = scheduleNextSyncF;

    // TODO: need better tests around master password prompting. See Bug 620583.

    mpLocked = true;

    // Testing exception handling if master password dialog is canceled.
    // Do this by monkeypatching.
    let oldGetter = Service.identity.__lookupGetter__("syncKey");
    let oldSetter = Service.identity.__lookupSetter__("syncKey");
    _("Old passphrase function is " + oldGetter);
    Service.identity.__defineGetter__("syncKey",
                           function() {
                             throw "User canceled Master Password entry";
                           });

    let oldClearSyncTriggers = Service.scheduler.clearSyncTriggers;
    let oldLockedSync = Service._lockedSync;

    let cSTCalled = false;
    let lockedSyncCalled = false;

    Service.scheduler.clearSyncTriggers = function() { cSTCalled = true; };
    Service._lockedSync = function() { lockedSyncCalled = true; };

    _("If master password is canceled, login fails and we report lockage.");
    do_check_false(!!Service.login());
    do_check_eq(Service.status.login, MASTER_PASSWORD_LOCKED);
    do_check_eq(Service.status.service, LOGIN_FAILED);
    _("Locked? " + Utils.mpLocked());
    _("checkSync reports the correct term.");
    do_check_eq(Service._checkSync(), kSyncMasterPasswordLocked);

    _("Sync doesn't proceed and clears triggers if MP is still locked.");
    Service.sync();

    do_check_true(cSTCalled);
    do_check_false(lockedSyncCalled);

    Service.identity.__defineGetter__("syncKey", oldGetter);
    Service.identity.__defineSetter__("syncKey", oldSetter);

    // N.B., a bunch of methods are stubbed at this point. Be careful putting
    // new tests after this point!

  } finally {
    Svc.Prefs.resetBranch("");
    server.stop(run_next_test);
  }
});
