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

// Emulate Promise.jsm semantics.
Promise.defer = function() { return new Deferred(); }
function Deferred()  {
  this.promise = new Promise(function(resolve, reject) {
    this.resolve = resolve;
    this.reject = reject;
  }.bind(this));
  Object.freeze(this);
}

const STOCK_HOSTAPD_NAME = 'goldfish-hostapd';
const HOSTAPD_CONFIG_PATH = '/data/misc/wifi/remote-hostapd/';

const SETTINGS_RIL_DATA_ENABLED = 'ril.data.enabled';
const SETTINGS_TETHERING_WIFI_ENABLED = 'tethering.wifi.enabled';
const SETTINGS_TETHERING_WIFI_IP = 'tethering.wifi.ip';
const SETTINGS_TETHERING_WIFI_SECURITY = 'tethering.wifi.security.type';

const HOSTAPD_COMMON_CONFIG = {
  driver: 'test',
  ctrl_interface: '/data/misc/wifi/remote-hostapd',
  test_socket: 'DIR:/data/misc/wifi/sockets',
  hw_mode: 'b',
  channel: '2',
};

const HOSTAPD_CONFIG_LIST = [
  { ssid: 'ap0' },

  { ssid: 'ap1',
    wpa: 1,
    wpa_pairwise: 'TKIP CCMP',
    wpa_passphrase: '12345678'
  },

  { ssid: 'ap2',
    wpa: 2,
    rsn_pairwise: 'CCMP',
    wpa_passphrase: '12345678',
  },
];

var gTestSuite = (function() {
  let suite = {};

  // Private member variables of the returned object |suite|.
  let wifiManager;
  let wifiOrigEnabled;
  let pendingEmulatorShellCount = 0;
  let sdkVersion;

  /**
   * A wrapper function of "is".
   *
   * Calls the marionette function "is" as well as throws an exception
   * if the givens values are not equal.
   *
   * @param value1
   *        Any type of value to compare.
   *
   * @param value2
   *        Any type of value to compare.
   *
   * @param message
   *        Debug message for this check.
   *
   */
  function isOrThrow(value1, value2, message) {
    is(value1, value2, message);
    if (value1 !== value2) {
      throw message;
    }
  }

  /**
   * Send emulator shell command with safe guard.
   *
   * We should only call |finish()| after all emulator command transactions
   * end, so here comes with the pending counter.  Resolve when the emulator
   * gives positive response, and reject otherwise.
   *
   * Fulfill params:
   *   result -- an array of emulator response lines.
   * Reject params:
   *   result -- an array of emulator response lines.
   *
   * @param aCommand
   *        A string command to be passed to emulator through its telnet console.
   *
   * @return A deferred promise.
   */
  function runEmulatorShellSafe(aCommand) {
    let deferred = Promise.defer();

    ++pendingEmulatorShellCount;
    runEmulatorShell(aCommand, function(aResult) {
      --pendingEmulatorShellCount;

      ok(true, "Emulator shell response: " + JSON.stringify(aResult));
      if (Array.isArray(aResult)) {
        deferred.resolve(aResult);
      } else {
        deferred.reject(aResult);
      }
    });

    return deferred.promise;
  }

  /**
   * Wait for one named MozWifiManager event.
   *
   * Resolve if that named event occurs.  Never reject.
   *
   * Fulfill params: the DOMEvent passed.
   *
   * @param aEventName
   *        A string event name.
   *
   * @return A deferred promise.
   */
  function waitForWifiManagerEventOnce(aEventName) {
    let deferred = Promise.defer();

    wifiManager.addEventListener(aEventName, function onevent(aEvent) {
      wifiManager.removeEventListener(aEventName, onevent);

      ok(true, "WifiManager event '" + aEventName + "' got.");
      deferred.resolve(aEvent);
    });

    return deferred.promise;
  }

  /**
   * Wait for one named MozMobileConnection event.
   *
   * Resolve if that named event occurs.  Never reject.
   *
   * Fulfill params: the DOMEvent passed.
   *
   * @param aEventName
   *        A string event name.
   *
   * @return A deferred promise.
   */
  function waitForMobileConnectionEventOnce(aEventName, aServiceId) {
    aServiceId = aServiceId || 0;

    let deferred = Promise.defer();
    let mobileconnection = navigator.mozMobileConnections[aServiceId];

    mobileconnection.addEventListener(aEventName, function onevent(aEvent) {
      mobileconnection.removeEventListener(aEventName, onevent);

      ok(true, "Mobile connection event '" + aEventName + "' got.");
      deferred.resolve(aEvent);
    });

    return deferred.promise;
  }

  /**
   * Get the detail of currently running processes containing the given name.
   *
   * Use shell command 'ps' to get the desired process's detail. Never reject.
   *
   * Fulfill params:
   *   result -- an array of { pname, pid }
   *
   * @param aProcessName
   *        The process to get the detail.
   *
   * @return A deferred promise.
   */
  function getProcessDetail(aProcessName) {
    return runEmulatorShellSafe(['ps'])
      .then(processes => {
        // Sample 'ps' output:
        //
        // USER     PID   PPID  VSIZE  RSS     WCHAN    PC         NAME
        // root      1     0     284    204   c009e6c4 0000deb4 S /init
        // root      2     0     0      0     c0052c64 00000000 S kthreadd
        // root      3     2     0      0     c0044978 00000000 S ksoftirqd/0
        //
        let detail = [];

        processes.shift(); // Skip the first line.
        for (let i = 0; i < processes.length; i++) {
          let tokens = processes[i].split(/\s+/);
          let pname = tokens[tokens.length - 1];
          let pid = tokens[1];
          if (-1 !== pname.indexOf(aProcessName)) {
            detail.push({ pname: pname, pid: pid });
          }
        }

        return detail;
      });
  }

  /**
   * Add required permissions for wifi testing. Never reject.
   *
   * The permissions required for wifi testing are 'wifi-manage' and 'settings-write'.
   * Never reject.
   *
   * Fulfill params: (none)
   *
   * @return A deferred promise.
   */
  function addRequiredPermissions() {
    let deferred = Promise.defer();

    let permissions = [{ 'type': 'wifi-manage', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-write', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-read', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-api-write', 'allow': 1, 'context': window.document },
                       { 'type': 'settings-api-read', 'allow': 1, 'context': window.document },
                       { 'type': 'mobileconnection', 'allow': 1, 'context': window.document }];

    SpecialPowers.pushPermissions(permissions, function() {
      deferred.resolve();
    });

    return deferred.promise;
  }

  /**
   * Wrap DOMRequest onsuccess/onerror events to Promise resolve/reject.
   *
   * Fulfill params: A DOMEvent.
   * Reject params: A DOMEvent.
   *
   * @param aRequest
   *        A DOMRequest instance.
   *
   * @return A deferred promise.
   */
  function wrapDomRequestAsPromise(aRequest) {
    let deferred = Promise.defer();

    ok(aRequest instanceof DOMRequest,
       "aRequest is instanceof " + aRequest.constructor);

    aRequest.addEventListener("success", function(aEvent) {
      deferred.resolve(aEvent);
    });
    aRequest.addEventListener("error", function(aEvent) {
      deferred.reject(aEvent);
    });

    return deferred.promise;
  }

  /**
   * Ensure wifi is enabled/disabled.
   *
   * Issue a wifi enable/disable request if wifi is not in the desired state;
   * return a resolved promise otherwise. Note that you cannot rely on this
   * function to test the correctness of enabling/disabling wifi.
   * (use requestWifiEnabled instead)
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return a resolved promise or deferred promise.
   */
  function ensureWifiEnabled(aEnabled, useAPI) {
    if (wifiManager.enabled === aEnabled) {
      log('Already ' + (aEnabled ? 'enabled' : 'disabled'));
      return Promise.resolve();
    }
    return requestWifiEnabled(aEnabled, useAPI);
  }

  /**
   * Issue a request to enable/disable wifi.
   *
   * This function will attempt to enable/disable wifi, by calling API or by
   * writing settings 'wifi.enabled' regardless of the wifi state, based on the
   * value of |userAPI| parameter.
   * Default is using settings.
   *
   * Note there's a limitation of co-existance of both method, per bug 930355,
   * that once enable/disable wifi by API, the settings method won't work until
   * reboot. So the test of wifi enable API should be executed last.
   * TODO: Remove settings method after enable/disable wifi by settings is
   *       removed after bug 1050147.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function requestWifiEnabled(aEnabled, useAPI) {
    return Promise.all([
      waitForWifiManagerEventOnce(aEnabled ? 'enabled' : 'disabled'),
      useAPI ?
        wrapDomRequestAsPromise(wifiManager.setWifiEnabled(aEnabled)) :
        setSettings({ 'wifi.enabled': aEnabled }),
    ]);
  }

  /**
   * Wait for RIL data being connected.
   *
   * This function will check |MozMobileConnection.data.connected| on
   * every 'datachange' event. Resolve when |MozMobileConnection.data.connected|
   * becomes the expected state. Never reject.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aConnected
   *        Boolean that indicates the desired data state.
   *
   * @param aServiceId [optional]
   *        A numeric DSDS service id. Default: 0.
   *
   * @return A deferred promise.
   */
  function waitForRilDataConnected(aConnected, aServiceId) {
    aServiceId = aServiceId || 0;
    return waitForMobileConnectionEventOnce('datachange', aServiceId)
      .then(function () {
        let mobileconnection = navigator.mozMobileConnections[aServiceId];
        if (mobileconnection.data.connected !== aConnected) {
          return waitForRilDataConnected(aConnected, aServiceId);
        }
      });
  }

  /**
   * Request to enable/disable wifi tethering.
   *
   * Enable/disable wifi tethering by changing the settings value 'tethering.wifi.enabled'.
   * Resolve when the routing is verified to set up successfully in 20 seconds. The polling
   * period is 1 second.
   *
   * Fulfill params: (none)
   * Reject params: The error message.
   *
   * @param aEnabled
   *        Boolean that indicates to enable or disable wifi tethering.
   *
   * @return A deferred promise.
   */
  function requestTetheringEnabled(aEnabled) {
    let RETRY_INTERVAL_MS = 1000;
    let retryCnt = 20;

    return setSettings1(SETTINGS_TETHERING_WIFI_ENABLED, aEnabled)
      .then(function waitForRoutingVerified() {
        return verifyTetheringRouting(aEnabled)
          .then(null, function onreject(aReason) {

            log('verifyTetheringRouting rejected due to ' + aReason +
                ' (' + retryCnt + ')');

            if (!retryCnt--) {
              throw aReason;
            }

            return waitForTimeout(RETRY_INTERVAL_MS).then(waitForRoutingVerified);
          });
      });
  }

  /**
   * Forget the given network.
   *
   * Resolve when we successfully forget the given network; reject when any error
   * occurs.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aNetwork
   *        An object of MozWifiNetwork.
   *
   * @return A deferred promise.
   */
  function forgetNetwork(aNetwork) {
    let request = wifiManager.forget(aNetwork);
    return wrapDomRequestAsPromise(request)
      .then(event => event.target.result);
  }

  /**
   * Forget all known networks.
   *
   * Resolve when we successfully forget all the known network;
   * reject when any error occurs.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function forgetAllKnownNetworks() {

    function createForgetNetworkChain(aNetworks) {
      let chain = Promise.resolve();

      aNetworks.forEach(function (aNetwork) {
        chain = chain.then(() => forgetNetwork(aNetwork));
      });

      return chain;
    }

    return getKnownNetworks()
      .then(networks => createForgetNetworkChain(networks));
  }

  /**
   * Get all known networks.
   *
   * Resolve when we get all the known networks; reject when any error
   * occurs.
   *
   * Fulfill params: An array of MozWifiNetwork
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function getKnownNetworks() {
    let request = wifiManager.getKnownNetworks();
    return wrapDomRequestAsPromise(request)
      .then(event => event.target.result);
  }

  /**
   * Set the given network to static ip mode.
   *
   * Resolve when we set static ip mode successfully; reject when any error
   * occurs.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function setStaticIpMode(aNetwork, aConfig) {
    let request = wifiManager.setStaticIpMode(aNetwork, aConfig);
    return wrapDomRequestAsPromise(request)
      .then(event => event.target.result);
  }

  /**
   * Issue a request to scan all wifi available networks.
   *
   * Resolve when we get the scan result; reject when any error
   * occurs.
   *
   * Fulfill params: An array of MozWifiNetwork
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function requestWifiScan() {
    let request = wifiManager.getNetworks();
    return wrapDomRequestAsPromise(request)
      .then(event => event.target.result);
  }

  /**
   * Import a certificate with nickname and password.
   *
   * Resolve when we import certificate successfully; reject when any error
   * occurs.
   *
   * Fulfill params: An object of certificate information.
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function importCert(certBlob, password, nickname) {
    let request = wifiManager.importCert(certBlob, password, nickname);
    return wrapDomRequestAsPromise(request)
      .then(event => event.target.result);
  }

  /**
   * Delete certificate of nickname.
   *
   * Resolve when we delete certificate successfully; reject when any error
   * occurs.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function deleteCert(nickname) {
    let request = wifiManager.deleteCert(nickname);
    return wrapDomRequestAsPromise(request)
      .then(event => event.target.result);
  }

  /**
   * Get list of imported certificates.
   *
   * Resolve when we get certificate list successfully; reject when any error
   * occurs.
   *
   * Fulfill params: Nickname of imported certificate arranged by usage.
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function getImportedCerts() {
    let request = wifiManager.getImportedCerts();
    return wrapDomRequestAsPromise(request)
      .then(event => event.target.result);
  }

  /**
   * Request wifi scan and verify the scan result as well.
   *
   * Issue a wifi scan request and check if the result is expected.
   * Since the old APs may be cached and the newly added APs may be
   * still not scan-able, a couple of attempts are acceptable.
   * Resolve if we eventually get the expected scan result; reject otherwise.
   *
   * Fulfill params: The scan result, which is an array of MozWifiNetwork
   * Reject params: (none)
   *
   * @param aRetryCnt
   *        The maxmimum number of attempts until we get the expected scan result.
   * @param aExpectedNetworks
   *        An array of object, each of which contains at least the |ssid| property.
   *
   * @return A deferred promise.
   */
  function testWifiScanWithRetry(aRetryCnt, aExpectedNetworks) {

    // Check if every single ssid of each |aScanResult| exists in |aExpectedNetworks|
    // as well as the length of |aScanResult| equals to |aExpectedNetworks|.
    function isScanResultExpected(aScanResult) {
      if (aScanResult.length !== aExpectedNetworks.length) {
        return false;
      }

      for (let i = 0; i < aScanResult.length; i++) {
        if (-1 === getFirstIndexBySsid(aScanResult[i].ssid, aExpectedNetworks)) {
          return false;
        }
      }
      return true;
    }

    return requestWifiScan()
      .then(function (networks) {
        if (isScanResultExpected(networks, aExpectedNetworks)) {
          return networks;
        }
        if (aRetryCnt > 0) {
          return testWifiScanWithRetry(aRetryCnt - 1, aExpectedNetworks);
        }
        throw 'Unexpected scan result!';
      });
  }

  /**
   * Test wifi association.
   *
   * Associate with the given network object which is obtained by
   * MozWifiManager.getNetworks() (i.e. MozWifiNetwork).
   * Resolve when the 'connected' status change event is received.
   * Note that we might see other events like 'connecting'
   * before 'connected'. So we need to call |waitForWifiManagerEventOnce|
   * again whenever non 'connected' event is seen. Never reject.
   *
   * Fulfill params: (none)
   *
   * @param aNetwork
   *        An object of MozWifiNetwork.
   *
   * @return A deferred promise.
   */
  function testAssociate(aNetwork) {
    setPasswordIfNeeded(aNetwork);

    let promises = [];

    // Register the event listerner to wait for 'connected' event first
    // to avoid racing issue.
    promises.push(waitForConnected(aNetwork));

    // Then we do the association.
    let request = wifiManager.associate(aNetwork);
    promises.push(wrapDomRequestAsPromise(request));

    return Promise.all(promises);
  }

  function waitForConnected(aExpectedNetwork) {
    return waitForWifiManagerEventOnce('statuschange')
      .then(function onstatuschange(event) {
        log("event.status: " + event.status);
        log("event.network.ssid: " + (event.network ? event.network.ssid : ''));

        if ("connected" === event.status &&
            event.network.ssid === aExpectedNetwork.ssid) {
          return; // Got expected 'connected' event from aNetwork.ssid.
        }

        log('Not expected "connected" statuschange event. Wait again!');
        return waitForConnected(aExpectedNetwork);
      });
  }

  /**
   * Set the password for associating the given network if needed.
   *
   * Set the password by looking up HOSTAPD_CONFIG_LIST. This function
   * will also set |keyManagement| properly.
   *
   * @param aNetwork
   *        The MozWifiNetwork object.
   */
  function setPasswordIfNeeded(aNetwork) {
    let i = getFirstIndexBySsid(aNetwork.ssid, HOSTAPD_CONFIG_LIST);
    if (-1 === i) {
      log('unknown ssid: ' + aNetwork.ssid);
      return; // Unknown network. Assume insecure.
    }

    if (!aNetwork.security.length) {
      return; // No need to set password.
    }

    let security = aNetwork.security[0];
    if (/PSK$/.test(security)) {
      aNetwork.psk = HOSTAPD_CONFIG_LIST[i].wpa_passphrase;
      aNetwork.keyManagement = 'WPA-PSK';
    } else if (/WEP$/.test(security)) {
      aNetwork.wep = HOSTAPD_CONFIG_LIST[i].wpa_passphrase;
      aNetwork.keyManagement = 'WEP';
    }
  }

  /**
   * Set mozSettings values.
   *
   * Resolve if that mozSettings value is set successfully, reject otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aSettings
   *        An object of format |{key1: value1, key2: value2, ...}|.
   * @param aAllowError [optional]
   *        A boolean value.  If set to true, an error response won't be treated
   *        as test failure.  Default: false.
   *
   * @return A deferred promise.
   */
  function setSettings(aSettings) {
    let lock = window.navigator.mozSettings.createLock();
    let request = lock.set(aSettings);
    let deferred = Promise.defer();
    lock.onsettingstransactionsuccess = function () {
        ok(true, "setSettings(" + JSON.stringify(aSettings) + ")");
      deferred.resolve();
    };
    lock.onsettingstransactionfailure = function (aEvent) {
      ok(false, "setSettings(" + JSON.stringify(aSettings) + ")");
      deferred.reject();
      throw aEvent.target.error;
    };
    return deferred.promise;
  }

  /**
   * Set mozSettings value with only one key.
   *
   * Resolve if that mozSettings value is set successfully, reject otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aKey
   *        A string key.
   * @param aValue
   *        An object value.
   * @param aAllowError [optional]
   *        A boolean value.  If set to true, an error response won't be treated
   *        as test failure.  Default: false.
   *
   * @return A deferred promise.
   */
  function setSettings1(aKey, aValue, aAllowError) {
    let settings = {};
    settings[aKey] = aValue;
    return setSettings(settings, aAllowError);
  }

  /**
   * Get mozSettings value specified by @aKey.
   *
   * Resolve if that mozSettings value is retrieved successfully, reject
   * otherwise.
   *
   * Fulfill params:
   *   The corresponding mozSettings value of the key.
   * Reject params: (none)
   *
   * @param aKey
   *        A string.
   * @param aAllowError [optional]
   *        A boolean value.  If set to true, an error response won't be treated
   *        as test failure.  Default: false.
   *
   * @return A deferred promise.
   */
  function getSettings(aKey, aAllowError) {
    let request =
      navigator.mozSettings.createLock().get(aKey);
    return wrapDomRequestAsPromise(request)
      .then(function resolve(aEvent) {
        ok(true, "getSettings(" + aKey + ") - success");
        return aEvent.target.result[aKey];
      }, function reject(aEvent) {
        ok(aAllowError, "getSettings(" + aKey + ") - error");
      });
  }


  /**
   * Start hostapd processes with given configuration list.
   *
   * For starting one hostapd, we need to generate a specific config file
   * then launch a hostapd process with the confg file path passed. The
   * config file is generated by two sources: one is the common
   * part (HOSTAPD_COMMON_CONFIG) and the other is from the given |aConfigList|.
   * Resolve when all the hostpads are requested to start. It is not guaranteed
   * that all the hostapds will be up and running successfully. Never reject.
   *
   * Fulfill params: (none)
   *
   * @param aConfigList
   *        An array of config objects, each property in which will be
   *        output to the confg file with the format: [key]=[value] in one line.
   *        See http://hostap.epitest.fi/cgit/hostap/plain/hostapd/hostapd.conf
   *        for more information.
   *
   * @return A deferred promise.
   */
  function startHostapds(aConfigList) {

    function createConfigFromCommon(aIndex) {
      // Create an copy of HOSTAPD_COMMON_CONFIG.
      let config = JSON.parse(JSON.stringify(HOSTAPD_COMMON_CONFIG));

      // Add user config.
      for (let key in aConfigList[aIndex]) {
        config[key] = aConfigList[aIndex][key];
      }

      // 'interface' is a required field but no need of being configurable
      // for a test case. So we initialize this field on our own.
      config.interface = 'AP-' + aIndex;

      return config;
    }

    function startOneHostapd(aIndex) {
      let configFileName = HOSTAPD_CONFIG_PATH + 'ap' + aIndex + '.conf';
      return writeHostapdConfFile(configFileName, createConfigFromCommon(aIndex))
        .then(() => runEmulatorShellSafe(['hostapd', '-B', configFileName]))
        .then(function (reply) {
          // It may fail at the first time due to the previous ungracefully terminated one.
          if (reply.length === 0) {
            // The hostapd starts successfully
            return;
          }

          if (reply[0].indexOf('bind(PF_UNIX): Address already in use') !== -1) {
            return startOneHostapd(aIndex);
          }
        });
    }

    return Promise.all(aConfigList.map(function(aConfig, aIndex) {
      return startOneHostapd(aIndex);
    }));
  }

  /**
   * Kill all the running hostapd processes.
   *
   * Use shell command 'kill -9' to kill all hostapds. Never reject.
   *
   * Fulfill params: (none)
   *
   * @return A deferred promise.
   */
  function killAllHostapd() {
    return getProcessDetail('hostapd')
      .then(function (runningHostapds) {
        let promises = runningHostapds.map(runningHostapd => {
          return runEmulatorShellSafe(['kill', '-9', runningHostapd.pid]);
        });
        return Promise.all(promises);
      });
  }

  /**
   * Write out the config file to the given path.
   *
   * For each key/value pair in |aConfig|,
   *
   * [key]=[value]
   *
   * will be output to one new line. Never reject.
   *
   * Fulfill params: (none)
   *
   * @param aFilePath
   *        The file path that we desire the config file to be located.
   *
   * @param aConfig
   *        The config object.
   *
   * @return A deferred promise.
   */
  function writeHostapdConfFile(aFilePath, aConfig) {
    let content = '';
    for (let key in aConfig) {
      if (aConfig.hasOwnProperty(key)) {
        content += (key + '=' + aConfig[key] + '\n');
      }
    }
    return writeFile(aFilePath, content);
  }

  /**
   * Write file to the given path filled with given content.
   *
   * For now it is implemented by shell command 'echo'. Also, if the
   * content contains whitespace, we need to quote the content to
   * avoid error. Never reject.
   *
   * Fulfill params: (none)
   *
   * @param aFilePath
   *        The file path that we desire the file to be located.
   *
   * @param aContent
   *        The content as string which should be written to the file.
   *
   * @return A deferred promise.
   */
  function writeFile(aFilePath, aContent) {
    const CONTENT_MAX_LENGTH = 900;
    var commands = [];
    for (var i = 0; i < aContent.length; i += CONTENT_MAX_LENGTH) {
      var content = aContent.substr(i, CONTENT_MAX_LENGTH);
      if (-1 === content.indexOf(' ')) {
        content = '"' + content + '"';
      }
      commands.push(['echo', '-n', content, i === 0 ? '>' : '>>', aFilePath]);
    }

    let chain = Promise.resolve();
    commands.forEach(function (command) {
      chain = chain.then(() => runEmulatorShellSafe(command));
    });
    return chain;
  }

  /**
   * Check if a init service is running or not.
   *
   * Check the android property 'init.svc.[aServiceName]' to determine if
   * a init service is running. Reject if the propery is neither 'running'
   * nor 'stopped'.
   *
   * Fulfill params:
   *   result -- |true| if the init service is running; |false| otherwise.
   * Reject params: (none)
   *
   * @param aServiceName
   *        The init service name.
   *
   * @return A deferred promise.
   */
  function isInitServiceRunning(aServiceName) {
    return runEmulatorShellSafe(['getprop', 'init.svc.' + aServiceName])
      .then(function (result) {
        if ('running' !== result[0] && 'stopped' !== result[0]) {
          throw 'Init service running state should be "running" or "stopped".';
        }
        return 'running' === result[0];
      });
  }

  /**
   * Wait for timeout.
   *
   * Resolve when the given duration elapsed. Never reject.
   *
   * Fulfill params: (none)
   *
   * @param aTimeoutMs
   *        The duration after which the timeout event should occurs.
   *
   * @return A deferred promise.
   */
  function waitForTimeout(aTimeoutMs) {
    let deferred = Promise.defer();

    setTimeout(function() {
      deferred.resolve();
    }, aTimeoutMs);

    return deferred.promise;
  }

  /**
   * Start or stop an init service.
   *
   * Use shell command 'start'/'stop' to start/stop an init service.
   * The running state will also be checked after we start/stop the service.
   * Resolve if the service is successfully started/stopped; Reject otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aServiceName
   *        The name of the service we want to start/stop.
   *
   * @param aStart
   *        |true| for starting the init service. |false| for stopping.
   *
   * @return A deferred promise.
   */
  function startStopInitService(aServiceName, aStart) {
    let retryCnt = 5;

    return runEmulatorShellSafe([aStart ? 'start' : 'stop', aServiceName])
      .then(() => isInitServiceRunning(aServiceName))
      .then(function onIsServiceRunningResolved(aIsRunning) {
        if (aStart === aIsRunning) {
          return;
        }

        if (retryCnt-- > 0) {
          log('Failed to ' + (aStart ? 'start ' : 'stop ') + aServiceName +
              '. Retry: ' + retryCnt);

          return waitForTimeout(500)
            .then(() => isInitServiceRunning(aServiceName))
            .then(onIsServiceRunningResolved);
        }

        throw 'Failed to ' + (aStart ? 'start' : 'stop') + ' ' + aServiceName;
      });
  }

  /**
   * Start the stock hostapd.
   *
   * Since the stock hostapd is an init service, use |startStopInitService| to
   * start it. Note that we might fail to start the stock hostapd at the first time
   * for unknown reason so give it the second chance to start again.
   * Resolve when we are eventually successful to start the stock hostapd; Reject
   * otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function startStockHostapd() {
    return startStopInitService(STOCK_HOSTAPD_NAME, true)
      .then(null, function onreject() {
        log('Failed to restart goldfish-hostapd at the first time. Try again!');
        return startStopInitService((STOCK_HOSTAPD_NAME), true);
      });
  }

  /**
   * Stop the stock hostapd.
   *
   * Since the stock hostapd is an init service, use |startStopInitService| to
   * stop it.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function stopStockHostapd() {
    return startStopInitService(STOCK_HOSTAPD_NAME, false);
  }

  /**
   * Get the index of the first matching entry by |ssid|.
   *
   * Find the index of the first entry of |aArray| which property |ssid|
   * is same as |aSsid|.
   *
   * @param aSsid
   *        The ssid that we want to match.
   * @param aArray
   *        An array of objects, each of which should have the property |ssid|.
   *
   * @return The 0-based index of first matching entry if found; -1 otherwise.
   */
  function getFirstIndexBySsid(aSsid, aArray) {
    for (let i = 0; i < aArray.length; i++) {
      if (aArray[i].ssid === aSsid) {
        return i;
      }
    }
    return -1;
  }

  /**
   * Count the number of running process and verify if the count is expected.
   *
   * Return a promise that resolves when the process has expected number
   * of running instances and rejects otherwise.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aOrigWifiEnabled
   *        Boolean which indicates wifi was originally enabled.
   *
   * @return A deferred promise.
   */
  function verifyNumOfProcesses(aProcessName, aExpectedNum) {
    return getProcessDetail(aProcessName)
      .then(function (detail) {
        if (detail.length === aExpectedNum) {
          return;
        }
        throw 'Unexpected number of running processes:' + aProcessName +
              ', expected: ' + aExpectedNum + ', actual: ' + detail.length;
      });
  }

  /**
   * Execute 'netcfg' shell and parse the result.
   *
   * Resolve when the executing is successful and reject otherwise.
   *
   * Fulfill params: Command result object, each key of which is the interface
   *                 name and value is { ip(string), prefix(string) }.
   * Reject params: String that indicates the reason of rejection.
   *
   * @return A deferred promise.
   */
  function exeAndParseNetcfg() {
    return runEmulatorShellSafe(['netcfg'])
      .then(function (aLines) {
        // Sample output:
        //
        // lo       UP     127.0.0.1/8   0x00000049 00:00:00:00:00:00
        // eth0     UP     10.0.2.15/24  0x00001043 52:54:00:12:34:56
        // rmnet1   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:58
        // rmnet2   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:59
        // rmnet3   DOWN   0.0.0.0/0   0x00001002 52:54:00:12:34:5a
        // wlan0    UP     192.168.1.1/24  0x00001043 52:54:00:12:34:5b
        // sit0     DOWN   0.0.0.0/0   0x00000080 00:00:00:00:00:00
        // rmnet0   UP     10.0.2.100/24  0x00001043 52:54:00:12:34:57
        //
        let netcfgResult = {};
        aLines.forEach(function (aLine) {
          let tokens = aLine.split(/\s+/);
          if (tokens.length < 5) {
            return;
          }
          let ifname = tokens[0];
          let [ip, prefix] = tokens[2].split('/');
          netcfgResult[ifname] = { ip: ip, prefix: prefix };
        });
        log("netcfg result:" + JSON.stringify(netcfgResult));

        return netcfgResult;
      });
  }

  /**
   * Execute 'ip route' and parse the result.
   *
   * Resolve when the executing is successful and reject otherwise.
   *
   * Fulfill params: Command result object, each key of which is the interface
   *                 name and value is { src(string), gateway(string),
   *                 default(boolean) }.
   * Reject params: String that indicates the reason of rejection.
   *
   * @return A deferred promise.
   */
  function exeAndParseIpRoute() {
    return runEmulatorShellSafe(['ip', 'route'])
      .then(function (aLines) {
        // Sample output:
        //
        // 10.0.2.4 via 10.0.2.2 dev rmnet0
        // 10.0.2.3 via 10.0.2.2 dev rmnet0
        // 192.168.1.0/24 dev wlan0  proto kernel  scope link  src 192.168.1.1
        // 10.0.2.0/24 dev eth0  proto kernel  scope link  src 10.0.2.15
        // 10.0.2.0/24 dev rmnet0  proto kernel  scope link  src 10.0.2.100
        // default via 10.0.2.2 dev rmnet0
        // default via 10.0.2.2 dev eth0  metric 2
        //

        let ipRouteResult = {};

        // Parse source ip for each interface.
        aLines.forEach(function (aLine) {
          let tokens = aLine.trim().split(/\s+/);
          let srcIndex = tokens.indexOf('src');
          if (srcIndex < 0 || srcIndex + 1 >= tokens.length) {
            return;
          }
          let ifname = tokens[2];
          let src = tokens[srcIndex + 1];
          ipRouteResult[ifname] = { src: src, default: false, gateway: null };
        });

        // Parse default interfaces.
        aLines.forEach(function (aLine) {
          let tokens = aLine.split(/\s+/);
          if (tokens.length < 2) {
            return;
          }
          if ('default' === tokens[0]) {
            let ifnameIndex = tokens.indexOf('dev');
            if (ifnameIndex < 0 || ifnameIndex + 1 >= tokens.length) {
              return;
            }
            let ifname = tokens[ifnameIndex + 1];
            if (!ipRouteResult[ifname]) {
              return;
            }
            ipRouteResult[ifname].default = true;
            let gwIndex = tokens.indexOf('via');
            if (gwIndex < 0 || gwIndex + 1 >= tokens.length) {
              return;
            }
            ipRouteResult[ifname].gateway = tokens[gwIndex + 1];
            return;
          }
        });
        log("ip route result:" + JSON.stringify(ipRouteResult));

        return ipRouteResult;
      });
  }

  /**
   * Verify everything about routing when the wifi tethering is either on or off.
   *
   * We use two unix commands to verify the routing: 'netcfg' and 'ip route'.
   * For now the following two things will be checked:
   *   1) The default route interface should be 'rmnet0'.
   *   2) wlan0 is up and its ip is set to a private subnet.
   *
   * We also verify iptables output as netd's NatController will execute
   *   $ iptables -t nat -A POSTROUTING -o rmnet0 -j MASQUERADE
   *
   * Resolve when the verification is successful and reject otherwise.
   *
   * Fulfill params: (none)
   * Reject params: String that indicates the reason of rejection.
   *
   * @return A deferred promise.
   */
  function verifyTetheringRouting(aEnabled) {
    let netcfgResult;
    let ipRouteResult;

    // Find MASQUERADE in POSTROUTING section. 'MASQUERADE' should be found
    // when tethering is enabled. 'MASQUERADE' shouldn't be found when tethering
    // is disabled.
    function verifyIptables() {
      let MASQUERADE_checkSection = 'POSTROUTING';
      if (sdkVersion > 15) {
        // Check 'natctrl_nat_POSTROUTING' section after ICS.
        MASQUERADE_checkSection = 'natctrl_nat_POSTROUTING';
      }

      return runEmulatorShellSafe(['iptables', '-t', 'nat', '-L', MASQUERADE_checkSection])
        .then(function(aLines) {
          // $ iptables -t nat -L POSTROUTING
          //
          // Sample output (tethering on):
          //
          // Chain POSTROUTING (policy ACCEPT)
          // target     prot opt source               destination
          // MASQUERADE  all  --  anywhere             anywhere
          //
          let found = (function find_MASQUERADE() {
            // Skip first two lines.
            for (let i = 2; i < aLines.length; i++) {
              if (-1 !== aLines[i].indexOf('MASQUERADE')) {
                return true;
              }
            }
            return false;
          })();

          if ((aEnabled && !found) || (!aEnabled && found)) {
            throw 'MASQUERADE' + (found ? '' : ' not') + ' found while tethering is ' +
                  (aEnabled ? 'enabled' : 'disabled');
          }
        });
    }

    function verifyDefaultRouteAndIp(aExpectedWifiTetheringIp) {
      if (aEnabled) {
        isOrThrow(ipRouteResult['rmnet0'].src, netcfgResult['rmnet0'].ip, 'rmnet0.ip');
        isOrThrow(ipRouteResult['rmnet0'].default, true, 'rmnet0.default');

        isOrThrow(ipRouteResult['wlan0'].src, netcfgResult['wlan0'].ip, 'wlan0.ip');
        isOrThrow(ipRouteResult['wlan0'].src, aExpectedWifiTetheringIp, 'expected ip');
        isOrThrow(ipRouteResult['wlan0'].default, false, 'wlan0.default');
      }
    }

    return verifyIptables()
      .then(exeAndParseNetcfg)
      .then((aResult) => { netcfgResult = aResult; })
      .then(exeAndParseIpRoute)
      .then((aResult) => { ipRouteResult = aResult; })
      .then(() => getSettings(SETTINGS_TETHERING_WIFI_IP))
      .then(ip => verifyDefaultRouteAndIp(ip));
  }

  /**
   * Clean up all the allocated resources and running services for the test.
   *
   * After the test no matter success or failure, we should
   * 1) Restore to the wifi original state (enabled or disabled)
   * 2) Wait until all pending emulator shell commands are done.
   *
   * |finsih| will be called in the end.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @return A deferred promise.
   */
  function cleanUp() {
    waitFor(function() {
      return ensureWifiEnabled(true)
        .then(forgetAllKnownNetworks)
        .then(() => ensureWifiEnabled(wifiOrigEnabled))
        .then(finish);
    }, function() {
      return pendingEmulatorShellCount === 0;
    });
  }

  /**
   * Init the test environment.
   *
   * Mainly add the required permissions and initialize the wifiManager
   * and the orignal state of wifi. Reject if failing to create
   * window.navigator.mozWifiManager; resolve if all is well.
   *
   * |finsih| will be called in the end.
   *
   * Fulfill params: (none)
   * Reject params: The reject reason.
   *
   * @return A deferred promise.
   */
  function initTestEnvironment() {
    return addRequiredPermissions()
      .then(function() {
        wifiManager = window.navigator.mozWifiManager;
        if (!wifiManager) {
          throw 'window.navigator.mozWifiManager is NULL';
        }
        wifiOrigEnabled = wifiManager.enabled;
      })
      .then(() => runEmulatorShellSafe(['getprop', 'ro.build.version.sdk']))
      .then(aLines => {
        sdkVersion = parseInt(aLines[0]);
      });
  }

  //---------------------------------------------------
  // Public test suite functions
  //---------------------------------------------------
  suite.getWifiManager = (() => wifiManager);
  suite.ensureWifiEnabled = ensureWifiEnabled;
  suite.requestWifiEnabled = requestWifiEnabled;
  suite.startHostapds = startHostapds;
  suite.getProcessDetail = getProcessDetail;
  suite.killAllHostapd = killAllHostapd;
  suite.wrapDomRequestAsPromise = wrapDomRequestAsPromise;
  suite.waitForWifiManagerEventOnce = waitForWifiManagerEventOnce;
  suite.verifyNumOfProcesses = verifyNumOfProcesses;
  suite.testWifiScanWithRetry = testWifiScanWithRetry;
  suite.getFirstIndexBySsid = getFirstIndexBySsid;
  suite.testAssociate = testAssociate;
  suite.getKnownNetworks = getKnownNetworks;
  suite.setStaticIpMode = setStaticIpMode;
  suite.requestWifiScan = requestWifiScan;
  suite.waitForConnected = waitForConnected;
  suite.forgetNetwork = forgetNetwork;
  suite.waitForTimeout = waitForTimeout;
  suite.waitForRilDataConnected = waitForRilDataConnected;
  suite.requestTetheringEnabled = requestTetheringEnabled;
  suite.importCert = importCert;
  suite.getImportedCerts = getImportedCerts;
  suite.deleteCert = deleteCert;
  suite.writeFile = writeFile;
  suite.exeAndParseNetcfg = exeAndParseNetcfg;
  suite.exeAndParseIpRoute = exeAndParseIpRoute;

  /**
   * Common test routine.
   *
   * Start a test with the given test case chain. The test environment will be
   * settled down before the test. After the test, all the affected things will
   * be restored.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aTestCaseChain
   *        The test case entry point, which can be a function or a promise.
   *
   * @return A deferred promise.
   */
  suite.doTest = function(aTestCaseChain) {
    return initTestEnvironment()
      .then(aTestCaseChain)
      .then(function onresolve() {
        cleanUp();
      }, function onreject(aReason) {
        ok(false, 'Promise rejects during test' + (aReason ? '(' + aReason + ')' : ''));
        cleanUp();
      });
  };

  /**
   * Common test routine without the presence of stock hostapd.
   *
   * Same as doTest except stopping the stock hostapd before test
   * and restarting it after test.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aTestCaseChain
   *        The test case entry point, which can be a function or a promise.
   *
   * @return A deferred promise.
   */
  suite.doTestWithoutStockAp = function(aTestCaseChain) {
    return suite.doTest(function() {
      return stopStockHostapd()
        .then(aTestCaseChain)
        .then(startStockHostapd);
    });
  };

  /**
   * The common test routine for wifi tethering.
   *
   * Similar as doTest except that it will set 'ril.data.enabled' to true
   * before testing and restore it afterward. It will also verify 'ril.data.enabled'
   * and 'tethering.wifi.enabled' to be false in the beginning. Note that this routine
   * will NOT change the state of 'tethering.wifi.enabled' so the user should enable
   * than disable on his/her own. This routine will only check if tethering is turned
   * off after testing.
   *
   * Fulfill params: (none)
   * Reject params: (none)
   *
   * @param aTestCaseChain
   *        The test case entry point, which can be a function or a promise.
   *
   * @return A deferred promise.
   */
  suite.doTestTethering = function(aTestCaseChain) {

    function verifyInitialState() {
      return getSettings(SETTINGS_RIL_DATA_ENABLED)
        .then(enabled => isOrThrow(enabled, false, SETTINGS_RIL_DATA_ENABLED))
        .then(() => getSettings(SETTINGS_TETHERING_WIFI_ENABLED))
        .then(enabled => isOrThrow(enabled, false, SETTINGS_TETHERING_WIFI_ENABLED));
    }

    function initTetheringTestEnvironment() {
      // Enable ril data.
      return Promise.all([waitForRilDataConnected(true),
                          setSettings1(SETTINGS_RIL_DATA_ENABLED, true)])
        .then(setSettings1(SETTINGS_TETHERING_WIFI_SECURITY, 'open'));
    }

    function restoreToInitialState() {
      return setSettings1(SETTINGS_RIL_DATA_ENABLED, false)
        .then(() => getSettings(SETTINGS_TETHERING_WIFI_ENABLED))
        .then(enabled => is(enabled, false, 'Tethering should be turned off.'));
    }

    return suite.doTest(function() {
      return verifyInitialState()
        .then(initTetheringTestEnvironment)
        // Since stock hostapd is not reliable after ICS, we just
        // turn off potential stock hostapd during testing to avoid 
        // interference.
        .then(stopStockHostapd)
        .then(aTestCaseChain)
        .then(startStockHostapd)
        .then(restoreToInitialState, function onreject(aReason) {
          return restoreToInitialState()
            .then(() => { throw aReason; }); // Re-throw the orignal reject reason.
        });
    });
  };

  /**
   * Run test with imported certificate.
   *
   * Certificate will be imported and confirmed before running test, and be
   * deleted after running test.
   *
   * Fulfill params: (none)
   *
   * @param certBlob
   *        Certificate content as Blob.
   * @param password
   *        Password for importing certificate, only used for importing PKCS#12.
   * @param nickanem
   *        Nickname for imported certificate.
   * @param usage
   *        Expected usage of imported certificate.
   * @param aTestCaseChain
   *        The test case entry point, which can be a function or a promise.
   *
   * @return A deferred promise.
   */
  suite.doTestWithCertificate = function(certBlob, password, nickname, usage, aTestCaseChain) {
    return suite.doTestWithoutStockAp(function() {
      return ensureWifiEnabled(true)
      // Import test certificate.
      .then(() => importCert(certBlob, password, nickname))
      .then(function(info) {
        // Check import result.
        is(info.nickname, nickname, "Imported nickname");
        for (let i = 0; i < usage.length; i++) {
          isnot(info.usage.indexOf(usage[i]), -1, "Usage " + usage[i]);
        }
      })
      // Get imported certificate list.
      .then(getImportedCerts)
      // Check if certificate exists in imported certificate list.
      .then(function(list) {
        for (let i = 0; i < usage.length; i++) {
          isnot(list[usage[i]].indexOf(nickname), -1,
                "Certificate \"" + nickname + "\" of usage " + usage[i] + " is imported");
        }
      })
      // Run test case.
      .then(aTestCaseChain)
      // Delete imported certificates.
      .then(() => deleteCert(nickname))
      // Check if certificate doesn't exist in imported certificate list.
      .then(getImportedCerts)
      .then(function(list) {
        for (let i = 0; i < usage.length; i++) {
          is(list[usage[i]].indexOf(nickname), -1, "Certificate is deleted");
        }
      })
    });
  };

  return suite;
})();
