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

ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
ChromeUtils.import("resource://services-sync/browserid_identity.js");
ChromeUtils.import("resource://services-sync/resource.js");
ChromeUtils.import("resource://services-sync/util.js");
ChromeUtils.import("resource://services-common/utils.js");
ChromeUtils.import("resource://services-crypto/utils.js");
ChromeUtils.import("resource://testing-common/services/sync/fxa_utils.js");
ChromeUtils.import("resource://services-common/hawkclient.js");
ChromeUtils.import("resource://gre/modules/FxAccounts.jsm");
ChromeUtils.import("resource://gre/modules/FxAccountsClient.jsm");
ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
ChromeUtils.import("resource://services-sync/service.js");
ChromeUtils.import("resource://services-sync/status.js");
ChromeUtils.import("resource://services-sync/constants.js");
ChromeUtils.import("resource://services-common/tokenserverclient.js");

const SECOND_MS = 1000;
const MINUTE_MS = SECOND_MS * 60;
const HOUR_MS = MINUTE_MS * 60;

var globalIdentityConfig = makeIdentityConfig();
var globalBrowseridManager = new BrowserIDManager();
configureFxAccountIdentity(globalBrowseridManager, globalIdentityConfig);

/**
 * Mock client clock and skew vs server in FxAccounts signed-in user module and
 * API client.  browserid_identity.js queries these values to construct HAWK
 * headers.  We will use this to test clock skew compensation in these headers
 * below.
 */
var MockFxAccountsClient = function() {
  FxAccountsClient.apply(this);
};
MockFxAccountsClient.prototype = {
  __proto__: FxAccountsClient.prototype,
  accountStatus() {
    return Promise.resolve(true);
  }
};

function MockFxAccounts() {
  let fxa = new FxAccounts({
    _now_is: Date.now(),

    now() {
      return this._now_is;
    },

    fxAccountsClient: new MockFxAccountsClient()
  });
  fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
    this.cert = {
      validUntil: fxa.internal.now() + CERT_LIFETIME,
      cert: "certificate",
    };
    return Promise.resolve(this.cert.cert);
  };
  return fxa;
}

add_test(function test_initial_state() {
    _("Verify initial state");
    Assert.ok(!globalBrowseridManager._token);
    Assert.ok(!globalBrowseridManager.hasValidToken());
    run_next_test();
  }
);

add_task(async function test_initialializeWithCurrentIdentity() {
    _("Verify start after initializeWithCurrentIdentity");
    globalBrowseridManager.initializeWithCurrentIdentity();
    await globalBrowseridManager.whenReadyToAuthenticate.promise;
    Assert.ok(!!globalBrowseridManager._token);
    Assert.ok(globalBrowseridManager.hasValidToken());
    Assert.deepEqual(getLoginTelemetryScalar(), {SUCCESS: 1});
  }
);

add_task(async function test_initialializeWithAuthErrorAndDeletedAccount() {
    _("Verify sync unpair after initializeWithCurrentIdentity with auth error + account deleted");

    var identityConfig = makeIdentityConfig();
    var browseridManager = new BrowserIDManager();

    // Use the real `_getAssertion` method that calls
    // `mockFxAClient.signCertificate`.
    let fxaInternal = makeFxAccountsInternalMock(identityConfig);
    delete fxaInternal._getAssertion;

    configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
    browseridManager._fxaService.internal.initialize();

    let signCertificateCalled = false;
    let accountStatusCalled = false;

    let AuthErrorMockFxAClient = function() {
      FxAccountsClient.apply(this);
    };
    AuthErrorMockFxAClient.prototype = {
      __proto__: FxAccountsClient.prototype,
      signCertificate() {
        signCertificateCalled = true;
        return Promise.reject({
          code: 401,
          errno: ERRNO_INVALID_AUTH_TOKEN,
        });
      },
      accountStatus() {
        accountStatusCalled = true;
        return Promise.resolve(false);
      }
    };

    let mockFxAClient = new AuthErrorMockFxAClient();
    browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;

    await browseridManager.initializeWithCurrentIdentity();
    await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                     "should reject due to an auth error");

    Assert.ok(signCertificateCalled);
    Assert.ok(accountStatusCalled);
    Assert.ok(!browseridManager._token);
    Assert.ok(!browseridManager.hasValidToken());
    Assert.deepEqual(getLoginTelemetryScalar(), {REJECTED: 1});
});

add_task(async function test_initialializeWithNoKeys() {
    _("Verify start after initializeWithCurrentIdentity without kSync, kXCS, kExtSync, kExtKbHash or keyFetchToken");
    let identityConfig = makeIdentityConfig();
    delete identityConfig.fxaccount.user.kSync;
    delete identityConfig.fxaccount.user.kXCS;
    delete identityConfig.fxaccount.user.kExtSync;
    delete identityConfig.fxaccount.user.kExtKbHash;
    // there's no keyFetchToken by default, so the initialize should fail.
    configureFxAccountIdentity(globalBrowseridManager, identityConfig);

    await globalBrowseridManager.initializeWithCurrentIdentity();
    await globalBrowseridManager.whenReadyToAuthenticate.promise;
    Assert.equal(Status.login, LOGIN_SUCCEEDED, "login succeeded even without keys");
    Assert.equal(globalBrowseridManager._token, null, "we don't have a token");
});

add_task(async function test_getResourceAuthenticator() {
    _("BrowserIDManager supplies a Resource Authenticator callback which returns a Hawk header.");
    configureFxAccountIdentity(globalBrowseridManager);
    let authenticator = globalBrowseridManager.getResourceAuthenticator();
    Assert.ok(!!authenticator);
    let req = {uri: CommonUtils.makeURI(
      "https://example.net/somewhere/over/the/rainbow"),
               method: "GET"};
    let output = await authenticator(req, "GET");
    Assert.ok("headers" in output);
    Assert.ok("authorization" in output.headers);
    Assert.ok(output.headers.authorization.startsWith("Hawk"));
    _("Expected internal state after successful call.");
    Assert.equal(globalBrowseridManager._token.uid, globalIdentityConfig.fxaccount.token.uid);
  }
);

add_task(async function test_resourceAuthenticatorSkew() {
  _("BrowserIDManager Resource Authenticator compensates for clock skew in Hawk header.");

  // Clock is skewed 12 hours into the future
  // We pick a date in the past so we don't risk concealing bugs in code that
  // uses new Date() instead of our given date.
  let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
  let browseridManager = new BrowserIDManager();
  let hawkClient = new HawkClient("https://example.net/v1", "/foo");

  // mock fxa hawk client skew
  hawkClient.now = function() {
    dump("mocked client now: " + now + "\n");
    return now;
  };
  // Imagine there's already been one fxa request and the hawk client has
  // already detected skew vs the fxa auth server.
  let localtimeOffsetMsec = -1 * 12 * HOUR_MS;
  hawkClient._localtimeOffsetMsec = localtimeOffsetMsec;

  let fxaClient = new MockFxAccountsClient();
  fxaClient.hawk = hawkClient;

  // Sanity check
  Assert.equal(hawkClient.now(), now);
  Assert.equal(hawkClient.localtimeOffsetMsec, localtimeOffsetMsec);

  // Properly picked up by the client
  Assert.equal(fxaClient.now(), now);
  Assert.equal(fxaClient.localtimeOffsetMsec, localtimeOffsetMsec);

  let identityConfig = makeIdentityConfig();
  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
  fxaInternal._now_is = now;
  fxaInternal.fxAccountsClient = fxaClient;

  // Mocks within mocks...
  configureFxAccountIdentity(browseridManager, globalIdentityConfig, fxaInternal);

  Assert.equal(browseridManager._fxaService.internal.now(), now);
  Assert.equal(browseridManager._fxaService.internal.localtimeOffsetMsec,
      localtimeOffsetMsec);

  Assert.equal(browseridManager._fxaService.now(), now);
  Assert.equal(browseridManager._fxaService.localtimeOffsetMsec,
      localtimeOffsetMsec);

  let request = new Resource("https://example.net/i/like/pie/");
  let authenticator = browseridManager.getResourceAuthenticator();
  let output = await authenticator(request, "GET");
  dump("output" + JSON.stringify(output));
  let authHeader = output.headers.authorization;
  Assert.ok(authHeader.startsWith("Hawk"));

  // Skew correction is applied in the header and we're within the two-minute
  // window.
  Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS);
  Assert.ok(
      (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
});

add_task(async function test_RESTResourceAuthenticatorSkew() {
  _("BrowserIDManager REST Resource Authenticator compensates for clock skew in Hawk header.");

  // Clock is skewed 12 hours into the future from our arbitary date
  let now = new Date("Fri Apr 09 2004 00:00:00 GMT-0700").valueOf() + 12 * HOUR_MS;
  let browseridManager = new BrowserIDManager();
  let hawkClient = new HawkClient("https://example.net/v1", "/foo");

  // mock fxa hawk client skew
  hawkClient.now = function() {
    return now;
  };
  // Imagine there's already been one fxa request and the hawk client has
  // already detected skew vs the fxa auth server.
  hawkClient._localtimeOffsetMsec = -1 * 12 * HOUR_MS;

  let fxaClient = new MockFxAccountsClient();
  fxaClient.hawk = hawkClient;

  let identityConfig = makeIdentityConfig();
  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
  fxaInternal._now_is = now;
  fxaInternal.fxAccountsClient = fxaClient;

  configureFxAccountIdentity(browseridManager, globalIdentityConfig, fxaInternal);

  Assert.equal(browseridManager._fxaService.internal.now(), now);

  let request = new Resource("https://example.net/i/like/pie/");
  let authenticator = browseridManager.getResourceAuthenticator();
  let output = await authenticator(request, "GET");
  dump("output" + JSON.stringify(output));
  let authHeader = output.headers.authorization;
  Assert.ok(authHeader.startsWith("Hawk"));

  // Skew correction is applied in the header and we're within the two-minute
  // window.
  Assert.equal(getTimestamp(authHeader), now - 12 * HOUR_MS);
  Assert.ok(
      (getTimestampDelta(authHeader, now) - 12 * HOUR_MS) < 2 * MINUTE_MS);
});

add_task(async function test_ensureLoggedIn() {
  configureFxAccountIdentity(globalBrowseridManager);
  await globalBrowseridManager.initializeWithCurrentIdentity();
  await globalBrowseridManager.whenReadyToAuthenticate.promise;
  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original initialize worked");
  await globalBrowseridManager.ensureLoggedIn();
  Assert.equal(Status.login, LOGIN_SUCCEEDED, "original ensureLoggedIn worked");
  Assert.ok(globalBrowseridManager._shouldHaveSyncKeyBundle,
            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");

  // arrange for no logged in user.
  let fxa = globalBrowseridManager._fxaService;
  let signedInUser = fxa.internal.currentAccountState.storageManager.accountData;
  fxa.internal.currentAccountState.storageManager.accountData = null;
  globalBrowseridManager.initializeWithCurrentIdentity();
  Assert.ok(!globalBrowseridManager._shouldHaveSyncKeyBundle,
            "_shouldHaveSyncKeyBundle should be false so we know we are testing what we think we are.");
  Status.login = LOGIN_FAILED_NO_USERNAME;
  await Assert.rejects(globalBrowseridManager.ensureLoggedIn(), "expecting rejection due to no user");
  Assert.ok(globalBrowseridManager._shouldHaveSyncKeyBundle,
            "_shouldHaveSyncKeyBundle should always be true after ensureLogin completes.");
  // Restore the logged in user to what it was.
  fxa.internal.currentAccountState.storageManager.accountData = signedInUser;
  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
  await Assert.rejects(globalBrowseridManager.ensureLoggedIn(),
                       "LOGIN_FAILED_LOGIN_REJECTED should have caused immediate rejection");
  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED,
               "status should remain LOGIN_FAILED_LOGIN_REJECTED");
  Status.login = LOGIN_FAILED_NETWORK_ERROR;
  await globalBrowseridManager.ensureLoggedIn();
  Assert.equal(Status.login, LOGIN_SUCCEEDED, "final ensureLoggedIn worked");
});

add_task(async function test_tokenExpiration() {
    _("BrowserIDManager notices token expiration:");
    let bimExp = new BrowserIDManager();
    configureFxAccountIdentity(bimExp, globalIdentityConfig);

    let authenticator = bimExp.getResourceAuthenticator();
    Assert.ok(!!authenticator);
    let req = {uri: CommonUtils.makeURI(
      "https://example.net/somewhere/over/the/rainbow"),
               method: "GET"};
    await authenticator(req, "GET");

    // Mock the clock.
    _("Forcing the token to expire ...");
    Object.defineProperty(bimExp, "_now", {
      value: function customNow() {
        return (Date.now() + 3000001);
      },
      writable: true,
    });
    Assert.ok(bimExp._token.expiration < bimExp._now());
    _("... means BrowserIDManager knows to re-fetch it on the next call.");
    Assert.ok(!bimExp.hasValidToken());
  }
);

add_task(async function test_getTokenErrors() {
  _("BrowserIDManager correctly handles various failures to get a token.");

  _("Arrange for a 401 - Sync should reflect an auth error.");
  initializeIdentityWithTokenServerResponse({
    status: 401,
    headers: {"content-type": "application/json"},
    body: JSON.stringify({}),
  });
  let browseridManager = Service.identity;

  await browseridManager.initializeWithCurrentIdentity();
  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                       "should reject due to 401");
  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");

  // XXX - other interesting responses to return?

  // And for good measure, some totally "unexpected" errors - we generally
  // assume these problems are going to magically go away at some point.
  _("Arrange for an empty body with a 200 response - should reflect a network error.");
  initializeIdentityWithTokenServerResponse({
    status: 200,
    headers: [],
    body: "",
  });
  browseridManager = Service.identity;
  await browseridManager.initializeWithCurrentIdentity();
  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                       "should reject due to non-JSON response");
  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
});

add_task(async function test_refreshCertificateOn401() {
  _("BrowserIDManager refreshes the FXA certificate after a 401.");
  var identityConfig = makeIdentityConfig();
  var browseridManager = new BrowserIDManager();
  // Use the real `_getAssertion` method that calls
  // `mockFxAClient.signCertificate`.
  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
  delete fxaInternal._getAssertion;
  configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
  browseridManager._fxaService.internal.initialize();

  let getCertCount = 0;

  let CheckSignMockFxAClient = function() {
    FxAccountsClient.apply(this);
  };
  CheckSignMockFxAClient.prototype = {
    __proto__: FxAccountsClient.prototype,
    signCertificate() {
      ++getCertCount;
    }
  };

  let mockFxAClient = new CheckSignMockFxAClient();
  browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;

  let didReturn401 = false;
  let didReturn200 = false;
  let mockTSC = mockTokenServer(() => {
    if (getCertCount <= 1) {
      didReturn401 = true;
      return {
        status: 401,
        headers: {"content-type": "application/json"},
        body: JSON.stringify({}),
      };
    }
    didReturn200 = true;
    return {
      status: 200,
      headers: {"content-type": "application/json"},
      body: JSON.stringify({
        id:           "id",
        key:          "key",
        api_endpoint: "http://example.com/",
        uid:          "uid",
        duration:     300,
      })
    };
  });

  browseridManager._tokenServerClient = mockTSC;

  await browseridManager.initializeWithCurrentIdentity();
  await browseridManager.whenReadyToAuthenticate.promise;

  Assert.equal(getCertCount, 2);
  Assert.ok(didReturn401);
  Assert.ok(didReturn200);
  Assert.ok(browseridManager._token);
  Assert.ok(browseridManager.hasValidToken());
});



add_task(async function test_getTokenErrorWithRetry() {
  _("tokenserver sends an observer notification on various backoff headers.");

  // Set Sync's backoffInterval to zero - after we simulated the backoff header
  // it should reflect the value we sent.
  Status.backoffInterval = 0;
  _("Arrange for a 503 with a Retry-After header.");
  initializeIdentityWithTokenServerResponse({
    status: 503,
    headers: {"content-type": "application/json",
              "retry-after": "100"},
    body: JSON.stringify({}),
  });
  let browseridManager = Service.identity;

  await browseridManager.initializeWithCurrentIdentity();
  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                       "should reject due to 503");

  // The observer should have fired - check it got the value in the response.
  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
  // Sync will have the value in ms with some slop - so check it is at least that.
  Assert.ok(Status.backoffInterval >= 100000);

  _("Arrange for a 200 with an X-Backoff header.");
  Status.backoffInterval = 0;
  initializeIdentityWithTokenServerResponse({
    status: 503,
    headers: {"content-type": "application/json",
              "x-backoff": "200"},
    body: JSON.stringify({}),
  });
  browseridManager = Service.identity;

  await browseridManager.initializeWithCurrentIdentity();
  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                       "should reject due to no token in response");

  // The observer should have fired - check it got the value in the response.
  Assert.ok(Status.backoffInterval >= 200000);
});

add_task(async function test_getKeysErrorWithBackoff() {
  _("Auth server (via hawk) sends an observer notification on backoff headers.");

  // Set Sync's backoffInterval to zero - after we simulated the backoff header
  // it should reflect the value we sent.
  Status.backoffInterval = 0;
  _("Arrange for a 503 with a X-Backoff header.");

  let config = makeIdentityConfig();
  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
  delete config.fxaccount.user.kSync;
  delete config.fxaccount.user.kXCS;
  delete config.fxaccount.user.kExtSync;
  delete config.fxaccount.user.kExtKbHash;
  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
  await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
    Assert.equal(method, "get");
    Assert.equal(uri, "http://mockedserver:9999/account/keys");
    return {
      status: 503,
      headers: {"content-type": "application/json",
                "x-backoff": "100"},
      body: "{}",
    };
  });

  let browseridManager = Service.identity;
  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                       "should reject due to 503");

  // The observer should have fired - check it got the value in the response.
  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
  // Sync will have the value in ms with some slop - so check it is at least that.
  Assert.ok(Status.backoffInterval >= 100000);
});

add_task(async function test_getKeysErrorWithRetry() {
  _("Auth server (via hawk) sends an observer notification on retry headers.");

  // Set Sync's backoffInterval to zero - after we simulated the backoff header
  // it should reflect the value we sent.
  Status.backoffInterval = 0;
  _("Arrange for a 503 with a Retry-After header.");

  let config = makeIdentityConfig();
  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
  delete config.fxaccount.user.kSync;
  delete config.fxaccount.user.kXCS;
  delete config.fxaccount.user.kExtSync;
  delete config.fxaccount.user.kExtKbHash;
  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
  await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
    Assert.equal(method, "get");
    Assert.equal(uri, "http://mockedserver:9999/account/keys");
    return {
      status: 503,
      headers: {"content-type": "application/json",
                "retry-after": "100"},
      body: "{}",
    };
  });

  let browseridManager = Service.identity;
  await Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                       "should reject due to 503");

  // The observer should have fired - check it got the value in the response.
  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login was rejected");
  // Sync will have the value in ms with some slop - so check it is at least that.
  Assert.ok(Status.backoffInterval >= 100000);
});

add_task(async function test_getHAWKErrors() {
  _("BrowserIDManager correctly handles various HAWK failures.");

  _("Arrange for a 401 - Sync should reflect an auth error.");
  let config = makeIdentityConfig();
  await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
    Assert.equal(method, "post");
    Assert.equal(uri, "http://mockedserver:9999/certificate/sign");
    return {
      status: 401,
      headers: {"content-type": "application/json"},
      body: JSON.stringify({}),
    };
  });
  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");

  // XXX - other interesting responses to return?

  // And for good measure, some totally "unexpected" errors - we generally
  // assume these problems are going to magically go away at some point.
  _("Arrange for an empty body with a 200 response - should reflect a network error.");
  await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
    Assert.equal(method, "post");
    Assert.equal(uri, "http://mockedserver:9999/certificate/sign");
    return {
      status: 200,
      headers: [],
      body: "",
    };
  });
  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
});

add_task(async function test_getGetKeysFailing401() {
  _("BrowserIDManager correctly handles 401 responses fetching keys.");

  _("Arrange for a 401 - Sync should reflect an auth error.");
  let config = makeIdentityConfig();
  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
  delete config.fxaccount.user.kSync;
  delete config.fxaccount.user.kXCS;
  delete config.fxaccount.user.kExtSync;
  delete config.fxaccount.user.kExtKbHash;
  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
  await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
    Assert.equal(method, "get");
    Assert.equal(uri, "http://mockedserver:9999/account/keys");
    return {
      status: 401,
      headers: {"content-type": "application/json"},
      body: "{}",
    };
  });
  Assert.equal(Status.login, LOGIN_FAILED_LOGIN_REJECTED, "login was rejected");
});

add_task(async function test_getGetKeysFailing503() {
  _("BrowserIDManager correctly handles 5XX responses fetching keys.");

  _("Arrange for a 503 - Sync should reflect a network error.");
  let config = makeIdentityConfig();
  // We want no kSync, kXCS, kExtSync or kExtKbHash so we attempt to fetch them.
  delete config.fxaccount.user.kSync;
  delete config.fxaccount.user.kXCS;
  delete config.fxaccount.user.kExtSync;
  delete config.fxaccount.user.kExtKbHash;
  config.fxaccount.user.keyFetchToken = "keyfetchtoken";
  await initializeIdentityWithHAWKResponseFactory(config, function(method, data, uri) {
    Assert.equal(method, "get");
    Assert.equal(uri, "http://mockedserver:9999/account/keys");
    return {
      status: 503,
      headers: {"content-type": "application/json"},
      body: "{}",
    };
  });
  Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "state reflects network error");
});

add_task(async function test_getKeysMissing() {
  _("BrowserIDManager correctly handles getKeys succeeding but not returning keys.");

  let browseridManager = new BrowserIDManager();
  let identityConfig = makeIdentityConfig();
  // our mock identity config already has kSync, kXCS, kExtSync and kExtKbHash - remove them or we never
  // try and fetch them.
  delete identityConfig.fxaccount.user.kSync;
  delete identityConfig.fxaccount.user.kXCS;
  delete identityConfig.fxaccount.user.kExtSync;
  delete identityConfig.fxaccount.user.kExtKbHash;
  identityConfig.fxaccount.user.keyFetchToken = "keyFetchToken";

  configureFxAccountIdentity(browseridManager, identityConfig);

  // Mock a fxAccounts object that returns no keys
  let fxa = new FxAccounts({
    fetchAndUnwrapKeys() {
      return Promise.resolve({});
    },
    fxAccountsClient: new MockFxAccountsClient(),
    newAccountState(credentials) {
      // We only expect this to be called with null indicating the (mock)
      // storage should be read.
      if (credentials) {
        throw new Error("Not expecting to have credentials passed");
      }
      let storageManager = new MockFxaStorageManager();
      storageManager.initialize(identityConfig.fxaccount.user);
      return new AccountState(storageManager);
    },
  });

  // Add a mock to the currentAccountState object.
  fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
    this.cert = {
      validUntil: fxa.internal.now() + CERT_LIFETIME,
      cert: "certificate",
    };
    return Promise.resolve(this.cert.cert);
  };

  browseridManager._fxaService = fxa;

  await browseridManager.initializeWithCurrentIdentity();

  let ex;
  try {
    await browseridManager.whenReadyToAuthenticate.promise;
  } catch (e) {
    ex = e;
  }

  Assert.equal(ex.message, "user data missing: kSync, kXCS, kExtSync, kExtKbHash");
});

add_task(async function test_signedInUserMissing() {
  _("BrowserIDManager detects getSignedInUser returning incomplete account data");

  let browseridManager = new BrowserIDManager();
  // Delete stored keys and the key fetch token.
  delete globalIdentityConfig.fxaccount.user.kSync;
  delete globalIdentityConfig.fxaccount.user.kXCS;
  delete globalIdentityConfig.fxaccount.user.kExtSync;
  delete globalIdentityConfig.fxaccount.user.kExtKbHash;
  delete globalIdentityConfig.fxaccount.user.keyFetchToken;

  configureFxAccountIdentity(browseridManager, globalIdentityConfig);

  let fxa = new FxAccounts({
    fetchAndUnwrapKeys() {
      return Promise.resolve({});
    },
    fxAccountsClient: new MockFxAccountsClient(),
    newAccountState(credentials) {
      // We only expect this to be called with null indicating the (mock)
      // storage should be read.
      if (credentials) {
        throw new Error("Not expecting to have credentials passed");
      }
      let storageManager = new MockFxaStorageManager();
      storageManager.initialize(globalIdentityConfig.fxaccount.user);
      return new AccountState(storageManager);
    },
  });

  browseridManager._fxaService = fxa;

  let status = await browseridManager.unlockAndVerifyAuthState();
  Assert.equal(status, LOGIN_FAILED_LOGIN_REJECTED);
});

// End of tests
// Utility functions follow

// Create a new browserid_identity object and initialize it with a
// hawk mock that simulates HTTP responses.
// The callback function will be called each time the mocked hawk server wants
// to make a request.  The result of the callback should be the mock response
// object that will be returned to hawk.
// A token server mock will be used that doesn't hit a server, so we move
// directly to a hawk request.
async function initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
  // A mock request object.
  function MockRESTRequest(uri, credentials, extra) {
    this._uri = uri;
    this._credentials = credentials;
    this._extra = extra;
  }
  MockRESTRequest.prototype = {
    setHeader() {},
    post(data, callback) {
      this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra);
      callback.call(this);
    },
    get(callback) {
      // Skip /status requests (browserid_identity checks if the account still
      // exists after an auth error)
      if (this._uri.startsWith("http://mockedserver:9999/account/status")) {
        this.response = {
          status: 200,
          headers: {"content-type": "application/json"},
          body: JSON.stringify({exists: true}),
        };
      } else {
        this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra);
      }
      callback.call(this);
    }
  };

  // The hawk client.
  function MockedHawkClient() {}
  MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999");
  MockedHawkClient.prototype.constructor = MockedHawkClient;
  MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) {
    return new MockRESTRequest(uri, credentials, extra);
  };
  // Arrange for the same observerPrefix as FxAccountsClient uses
  MockedHawkClient.prototype.observerPrefix = "FxA:hawk";

  // tie it all together - configureFxAccountIdentity isn't useful here :(
  let fxaClient = new MockFxAccountsClient();
  fxaClient.hawk = new MockedHawkClient();
  let internal = {
    fxAccountsClient: fxaClient,
    newAccountState(credentials) {
      // We only expect this to be called with null indicating the (mock)
      // storage should be read.
      if (credentials) {
        throw new Error("Not expecting to have credentials passed");
      }
      let storageManager = new MockFxaStorageManager();
      storageManager.initialize(config.fxaccount.user);
      return new AccountState(storageManager);
    },
  };
  let fxa = new FxAccounts(internal);

  globalBrowseridManager._fxaService = fxa;
  globalBrowseridManager._signedInUser = null;
  await globalBrowseridManager.initializeWithCurrentIdentity();
  await Assert.rejects(globalBrowseridManager.whenReadyToAuthenticate.promise,
                       "expecting rejection due to hawk error");
}


function getTimestamp(hawkAuthHeader) {
  return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
}

function getTimestampDelta(hawkAuthHeader, now = Date.now()) {
  return Math.abs(getTimestamp(hawkAuthHeader) - now);
}

function mockTokenServer(func) {
  let requestLog = Log.repository.getLogger("testing.mock-rest");
  if (!requestLog.appenders.length) { // might as well see what it says :)
    requestLog.addAppender(new Log.DumpAppender());
    requestLog.level = Log.Level.Trace;
  }
  function MockRESTRequest(url) {}
  MockRESTRequest.prototype = {
    _log: requestLog,
    setHeader() {},
    get(callback) {
      this.response = func();
      callback.call(this);
    }
  };
  // The mocked TokenServer client which will get the response.
  function MockTSC() { }
  MockTSC.prototype = new TokenServerClient();
  MockTSC.prototype.constructor = MockTSC;
  MockTSC.prototype.newRESTRequest = function(url) {
    return new MockRESTRequest(url);
  };
  // Arrange for the same observerPrefix as browserid_identity uses.
  MockTSC.prototype.observerPrefix = "weave:service";
  return new MockTSC();
}
