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

"use strict";

Cu.import("resource://gre/modules/FxAccountsCommon.js");
const { FxAccountsWebChannel, FxAccountsWebChannelHelpers } =
    Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm", {});

const URL_STRING = "https://example.com";

const mockSendingContext = {
  browser: {},
  principal: {},
  eventTarget: {}
};

add_test(function () {
  validationHelper(undefined,
  "Error: Missing configuration options");

  validationHelper({
    channel_id: WEBCHANNEL_ID
  },
  "Error: Missing 'content_uri' option");

  validationHelper({
    content_uri: 'bad uri',
    channel_id: WEBCHANNEL_ID
  },
  /NS_ERROR_MALFORMED_URI/);

  validationHelper({
    content_uri: URL_STRING
  },
  'Error: Missing \'channel_id\' option');

  run_next_test();
});

add_task(function* test_rejection_reporting() {
  let mockMessage = {
    command: 'fxaccounts:login',
    messageId: '1234',
    data: { email: 'testuser@testuser.com' },
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING,
    helpers: {
      login(accountData) {
        equal(accountData.email, 'testuser@testuser.com',
          'Should forward incoming message data to the helper');
        return Promise.reject(new Error('oops'));
      },
    },
  });

  let promiseSend = new Promise(resolve => {
    channel._channel.send = (message, context) => {
      resolve({ message, context });
    };
  });

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);

  let { message, context } = yield promiseSend;

  equal(context, mockSendingContext, 'Should forward the original context');
  equal(message.command, 'fxaccounts:login',
    'Should include the incoming command');
  equal(message.messageId, '1234', 'Should include the message ID');
  equal(message.data.error.message, 'Error: oops',
    'Should convert the error message to a string');
  notStrictEqual(message.data.error.stack, null,
    'Should include the stack for JS error rejections');
});

add_test(function test_exception_reporting() {
  let mockMessage = {
    command: 'fxaccounts:sync_preferences',
    messageId: '5678',
    data: { entryPoint: 'fxa:verification_complete' }
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING,
    helpers: {
      openSyncPreferences(browser, entryPoint) {
        equal(entryPoint, 'fxa:verification_complete',
          'Should forward incoming message data to the helper');
        throw new TypeError('splines not reticulated');
      },
    },
  });

  channel._channel.send = (message, context) => {
    equal(context, mockSendingContext, 'Should forward the original context');
    equal(message.command, 'fxaccounts:sync_preferences',
      'Should include the incoming command');
    equal(message.messageId, '5678', 'Should include the message ID');
    equal(message.data.error.message, 'TypeError: splines not reticulated',
      'Should convert the exception to a string');
    notStrictEqual(message.data.error.stack, null,
      'Should include the stack for JS exceptions');

    run_next_test();
  };

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});

add_test(function test_profile_image_change_message() {
  var mockMessage = {
    command: "profile:change",
    data: { uid: "foo" }
  };

  makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
    do_check_eq(data, "foo");
    run_next_test();
  });

  var channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING
  });

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});

add_test(function test_login_message() {
  let mockMessage = {
    command: 'fxaccounts:login',
    data: { email: 'testuser@testuser.com' }
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING,
    helpers: {
      login: function (accountData) {
        do_check_eq(accountData.email, 'testuser@testuser.com');
        run_next_test();
        return Promise.resolve();
      }
    }
  });

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});

add_test(function test_logout_message() {
  let mockMessage = {
    command: 'fxaccounts:logout',
    data: { uid: "foo" }
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING,
    helpers: {
      logout: function (uid) {
        do_check_eq(uid, 'foo');
        run_next_test();
        return Promise.resolve();
      }
    }
  });

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});

add_test(function test_delete_message() {
  let mockMessage = {
    command: 'fxaccounts:delete',
    data: { uid: "foo" }
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING,
    helpers: {
      logout: function (uid) {
        do_check_eq(uid, 'foo');
        run_next_test();
        return Promise.resolve();
      }
    }
  });

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});

add_test(function test_can_link_account_message() {
  let mockMessage = {
    command: 'fxaccounts:can_link_account',
    data: { email: 'testuser@testuser.com' }
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING,
    helpers: {
      shouldAllowRelink: function (email) {
        do_check_eq(email, 'testuser@testuser.com');
        run_next_test();
      }
    }
  });

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});

add_test(function test_sync_preferences_message() {
  let mockMessage = {
    command: 'fxaccounts:sync_preferences',
    data: { entryPoint: 'fxa:verification_complete' }
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING,
    helpers: {
      openSyncPreferences: function (browser, entryPoint) {
        do_check_eq(entryPoint, 'fxa:verification_complete');
        do_check_eq(browser, mockSendingContext.browser);
        run_next_test();
      }
    }
  });

  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
});

add_test(function test_unrecognized_message() {
  let mockMessage = {
    command: 'fxaccounts:unrecognized',
    data: {}
  };

  let channel = new FxAccountsWebChannel({
    channel_id: WEBCHANNEL_ID,
    content_uri: URL_STRING
  });

  // no error is expected.
  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
  run_next_test();
});


add_test(function test_helpers_should_allow_relink_same_email() {
  let helpers = new FxAccountsWebChannelHelpers();

  helpers.setPreviousAccountNameHashPref('testuser@testuser.com');
  do_check_true(helpers.shouldAllowRelink('testuser@testuser.com'));

  run_next_test();
});

add_test(function test_helpers_should_allow_relink_different_email() {
  let helpers = new FxAccountsWebChannelHelpers();

  helpers.setPreviousAccountNameHashPref('testuser@testuser.com');

  helpers._promptForRelink = (acctName) => {
    return acctName === 'allowed_to_relink@testuser.com';
  };

  do_check_true(helpers.shouldAllowRelink('allowed_to_relink@testuser.com'));
  do_check_false(helpers.shouldAllowRelink('not_allowed_to_relink@testuser.com'));

  run_next_test();
});

add_task(function* test_helpers_login_without_customize_sync() {
  let helpers = new FxAccountsWebChannelHelpers({
    fxAccounts: {
      setSignedInUser: function(accountData) {
        return new Promise(resolve => {
          // ensure fxAccounts is informed of the new user being signed in.
          do_check_eq(accountData.email, 'testuser@testuser.com');

          // verifiedCanLinkAccount should be stripped in the data.
          do_check_false('verifiedCanLinkAccount' in accountData);

          // the customizeSync pref should not update
          do_check_false(helpers.getShowCustomizeSyncPref());

          // previously signed in user preference is updated.
          do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com'));

          resolve();
        });
      }
    }
  });

  // the show customize sync pref should stay the same
  helpers.setShowCustomizeSyncPref(false);

  // ensure the previous account pref is overwritten.
  helpers.setPreviousAccountNameHashPref('lastuser@testuser.com');

  yield helpers.login({
    email: 'testuser@testuser.com',
    verifiedCanLinkAccount: true,
    customizeSync: false
  });
});

add_task(function* test_helpers_login_with_customize_sync() {
  let helpers = new FxAccountsWebChannelHelpers({
    fxAccounts: {
      setSignedInUser: function(accountData) {
        return new Promise(resolve => {
          // ensure fxAccounts is informed of the new user being signed in.
          do_check_eq(accountData.email, 'testuser@testuser.com');

          // customizeSync should be stripped in the data.
          do_check_false('customizeSync' in accountData);

          // the customizeSync pref should not update
          do_check_true(helpers.getShowCustomizeSyncPref());

          resolve();
        });
      }
    }
  });

  // the customize sync pref should be overwritten
  helpers.setShowCustomizeSyncPref(false);

  yield helpers.login({
    email: 'testuser@testuser.com',
    verifiedCanLinkAccount: true,
    customizeSync: true
  });
});

add_task(function* test_helpers_login_with_customize_sync_and_declined_engines() {
  let helpers = new FxAccountsWebChannelHelpers({
    fxAccounts: {
      setSignedInUser: function(accountData) {
        return new Promise(resolve => {
          // ensure fxAccounts is informed of the new user being signed in.
          do_check_eq(accountData.email, 'testuser@testuser.com');

          // customizeSync should be stripped in the data.
          do_check_false('customizeSync' in accountData);
          do_check_false('declinedSyncEngines' in accountData);
          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false);
          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false);
          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);

          // the customizeSync pref should be disabled
          do_check_false(helpers.getShowCustomizeSyncPref());

          resolve();
        });
      }
    }
  });

  // the customize sync pref should be overwritten
  helpers.setShowCustomizeSyncPref(true);

  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), true);
  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), true);
  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
  yield helpers.login({
    email: 'testuser@testuser.com',
    verifiedCanLinkAccount: true,
    customizeSync: true,
    declinedSyncEngines: ['addons', 'prefs']
  });
});

add_test(function test_helpers_open_sync_preferences() {
  let helpers = new FxAccountsWebChannelHelpers({
    fxAccounts: {
    }
  });

  let mockBrowser = {
    loadURI(uri) {
      do_check_eq(uri, "about:preferences?entrypoint=fxa%3Averification_complete#sync");
      run_next_test();
    }
  };

  helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete");
});

add_task(function* test_helpers_change_password() {
  let wasCalled = {
    updateUserAccountData: false,
    updateDeviceRegistration: false
  };
  let helpers = new FxAccountsWebChannelHelpers({
    fxAccounts: {
      updateUserAccountData(credentials) {
        return new Promise(resolve => {
          do_check_true(credentials.hasOwnProperty("email"));
          do_check_true(credentials.hasOwnProperty("uid"));
          do_check_true(credentials.hasOwnProperty("kA"));
          do_check_true(credentials.hasOwnProperty("deviceId"));
          do_check_null(credentials.deviceId);
          // "foo" isn't a field known by storage, so should be dropped.
          do_check_false(credentials.hasOwnProperty("foo"));
          wasCalled.updateUserAccountData = true;

          resolve();
        });
      },

      updateDeviceRegistration() {
        do_check_eq(arguments.length, 0);
        wasCalled.updateDeviceRegistration = true;
        return Promise.resolve()
      }
    }
  });
  yield helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
  do_check_true(wasCalled.updateUserAccountData);
  do_check_true(wasCalled.updateDeviceRegistration);
});

add_task(function* test_helpers_change_password_with_error() {
  let wasCalled = {
    updateUserAccountData: false,
    updateDeviceRegistration: false
  };
  let helpers = new FxAccountsWebChannelHelpers({
    fxAccounts: {
      updateUserAccountData() {
        wasCalled.updateUserAccountData = true;
        return Promise.reject();
      },

      updateDeviceRegistration() {
        wasCalled.updateDeviceRegistration = true;
        return Promise.resolve()
      }
    }
  });
  try {
    yield helpers.changePassword({});
    do_check_false('changePassword should have rejected');
  } catch (_) {
    do_check_true(wasCalled.updateUserAccountData);
    do_check_false(wasCalled.updateDeviceRegistration);
  }
});

function run_test() {
  run_next_test();
}

function makeObserver(aObserveTopic, aObserveFunc) {
  let callback = function (aSubject, aTopic, aData) {
    log.debug("observed " + aTopic + " " + aData);
    if (aTopic == aObserveTopic) {
      removeMe();
      aObserveFunc(aSubject, aTopic, aData);
    }
  };

  function removeMe() {
    log.debug("removing observer for " + aObserveTopic);
    Services.obs.removeObserver(callback, aObserveTopic);
  }

  Services.obs.addObserver(callback, aObserveTopic, false);
  return removeMe;
}

function validationHelper(params, expected) {
  try {
    new FxAccountsWebChannel(params);
  } catch (e) {
    if (typeof expected === 'string') {
      return do_check_eq(e.toString(), expected);
    } else {
      return do_check_true(e.toString().match(expected));
    }
  }
  throw new Error("Validation helper error");
}
