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

MARIONETTE_CONTEXT = "chrome";

const SETTINGS_KEY_DATA_ENABLED = "ril.data.enabled";
const SETTINGS_KEY_DATA_APN_SETTINGS  = "ril.data.apnSettings";
const SETTINGS_KEY_WIFI_ENABLED = "wifi.enabled";

const TOPIC_CONNECTION_STATE_CHANGED = "network-connection-state-changed";
const TOPIC_NETWORK_ACTIVE_CHANGED = "network-active-changed";

const NETWORK_STATE_UNKNOWN = Ci.nsINetworkInfo.NETWORK_STATE_UNKNOWN;
const NETWORK_STATE_CONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTING;
const NETWORK_STATE_CONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED;
const NETWORK_STATE_DISCONNECTING = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTING;
const NETWORK_STATE_DISCONNECTED = Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED;

const NETWORK_TYPE_MOBILE = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE;
const NETWORK_TYPE_MOBILE_MMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_MMS;
const NETWORK_TYPE_MOBILE_SUPL = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_SUPL;
const NETWORK_TYPE_MOBILE_IMS = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_IMS;
const NETWORK_TYPE_MOBILE_DUN = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_DUN;
const NETWORK_TYPE_MOBILE_FOTA = Ci.nsINetworkInfo.NETWORK_TYPE_MOBILE_FOTA;

const networkTypes = [
  NETWORK_TYPE_MOBILE,
  NETWORK_TYPE_MOBILE_MMS,
  NETWORK_TYPE_MOBILE_SUPL,
  NETWORK_TYPE_MOBILE_IMS,
  NETWORK_TYPE_MOBILE_DUN,
  NETWORK_TYPE_MOBILE_FOTA
];

var Promise = Cu.import("resource://gre/modules/Promise.jsm").Promise;

var ril = Cc["@mozilla.org/ril;1"].getService(Ci.nsIRadioInterfaceLayer);
ok(ril, "ril.constructor is " + ril.constructor);

var radioInterface = ril.getRadioInterface(0);
ok(radioInterface, "radioInterface.constructor is " + radioInterface.constrctor);

var _pendingEmulatorShellCmdCount = 0;
var _pendingEmulatorCmdCount = 0;

/**
 * Send emulator shell command with safe guard.
 *
 * We should only call |finish()| after all emulator shell command transactions
 * end, so here comes with the pending counter.  Resolve when the emulator
 * shell gives response. Never reject.
 *
 * Fulfill params:
 *   result -- an array of emulator shell response lines.
 *
 * @param aCommands
 *        A string array commands to be passed to emulator through adb shell.
 *
 * @return A deferred promise.
 */
function runEmulatorShellCmdSafe(aCommands) {
  return new Promise(function(aResolve, aReject) {
    ++_pendingEmulatorShellCmdCount;
    runEmulatorShell(aCommands, function(aResult) {
      --_pendingEmulatorShellCmdCount;

      log("Emulator shell response: " + JSON.stringify(aResult));
      aResolve(aResult);
    });
  });
}

/**
 * Send emulator 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 runEmulatorCmdSafe(aCommand) {
  log(aCommand);
  return new Promise(function(aResolve, aReject) {
    ++_pendingEmulatorCmdCount;
    runEmulatorCmd(aCommand, function(aResult) {
      --_pendingEmulatorCmdCount;

      log("Emulator console response: " + JSON.stringify(aResult));
      if (Array.isArray(aResult) &&
          aResult[aResult.length - 1] === "OK") {
        aResolve(aResult);
      } else {
        aReject(aResult);
      }
    });
  });
}

/**
 * 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 = window.navigator.mozSettings.createLock().get(aKey);
  return request.then(function resolve(aValue) {
      log("getSettings(" + aKey + ") - success");
      return aValue[aKey];
    }, function reject(aError) {
      ok(aAllowError, "getSettings(" + aKey + ") - error");
    });
}

/**
 * Set mozSettings values.
 *
 * 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 setSettings(aKey, aValue, aAllowError) {
  let settings = {};
  settings[aKey] = aValue;
  let lock = window.navigator.mozSettings.createLock();
  let request = lock.set(settings);
  let deferred = Promise.defer();
  lock.onsettingstransactionsuccess = function () {
      log("setSettings(" + JSON.stringify(settings) + ") - success");
    deferred.resolve();
  };
  lock.onsettingstransactionfailure = function () {
      ok(aAllowError, "setSettings(" + JSON.stringify(settings) + ") - error");
    // We resolve even though we've thrown an error, since the ok()
    // will do that.
    deferred.resolve();
  };
  return deferred.promise;
}

/**
 * Wait for observer event.
 *
 * Resolve if that topic event occurs.  Never reject.
 *
 * Fulfill params: the subject passed.
 *
 * @param aTopic
 *        A string topic name.
 *
 * @return A deferred promise.
 */
function waitForObserverEvent(aTopic) {
  let obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
  let deferred = Promise.defer();

  obs.addObserver(function observer(subject, topic, data) {
    if (topic === aTopic) {
      obs.removeObserver(observer, aTopic);
      deferred.resolve(subject);
    }
  }, aTopic, false);

  return deferred.promise;
}

/**
 * Wait for one named event.
 *
 * Resolve if that named event occurs.  Never reject.
 *
 * Fulfill params: the DOMEvent passed.
 *
 * @param aEventTarget
 *        An EventTarget object.
 * @param aEventName
 *        A string event name.
 * @param aMatchFun [optional]
 *        A matching function returns true or false to filter the event.
 *
 * @return A deferred promise.
 */
function waitForTargetEvent(aEventTarget, aEventName, aMatchFun) {
  return new Promise(function(aResolve, aReject) {
    aEventTarget.addEventListener(aEventName, function onevent(aEvent) {
      if (!aMatchFun || aMatchFun(aEvent)) {
        aEventTarget.removeEventListener(aEventName, onevent);
        ok(true, "Event '" + aEventName + "' got.");
        aResolve(aEvent);
      }
    });
  });
}

/**
 * Set the default data connection enabling state, wait for
 * "network-connection-state-changed" event and verify state.
 *
 * Fulfill params: instance of nsIRilNetworkInfo of the network connected.
 *
 * @param aEnabled
 *        A boolean state.
 *
 * @return A deferred promise.
 */
function setDataEnabledAndWait(aEnabled) {
  let promises = [];
  promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED)
    .then(function(aSubject) {
      ok(aSubject instanceof Ci.nsIRilNetworkInfo,
         "subject should be an instance of nsIRilNetworkInfo");
      is(aSubject.type, NETWORK_TYPE_MOBILE,
         "subject.type should be " + NETWORK_TYPE_MOBILE);
      is(aSubject.state,
         aEnabled ? Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED
                  : Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED,
         "subject.state should be " + aEnabled ? "CONNECTED" : "DISCONNECTED");

      return aSubject;
    }));
  promises.push(setSettings(SETTINGS_KEY_DATA_ENABLED, aEnabled));

  return Promise.all(promises).then(aValues => aValues[0]);
}

/**
 * Setup a certain type of data connection, wait for
 * "network-connection-state-changed" event and verify state.
 *
 * Fulfill params: instance of nsIRilNetworkInfo of the network connected.
 *
 * @param aNetworkType
 *        The mobile network type to setup.
 *
 * @return A deferred promise.
 */
function setupDataCallAndWait(aNetworkType) {
  log("setupDataCallAndWait: " + aNetworkType);

  let promises = [];
  promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED)
    .then(function(aSubject) {
      ok(aSubject instanceof Ci.nsIRilNetworkInfo,
         "subject should be an instance of nsIRilNetworkInfo");
      is(aSubject.type, aNetworkType,
         "subject.type should be " + aNetworkType);
      is(aSubject.state, Ci.nsINetworkInfo.NETWORK_STATE_CONNECTED,
         "subject.state should be CONNECTED");

      return aSubject;
    }));
  promises.push(radioInterface.setupDataCallByType(aNetworkType));

  return Promise.all(promises).then(aValues => aValues[0]);
}

/**
 * Deactivate a certain type of data connection, wait for
 * "network-connection-state-changed" event and verify state.
 *
 * Fulfill params: (none)
 *
 * @param aNetworkType
 *        The mobile network type to deactivate.
 *
 * @return A deferred promise.
 */
function deactivateDataCallAndWait(aNetworkType) {
  log("deactivateDataCallAndWait: " + aNetworkType);

  let promises = [];
  promises.push(waitForObserverEvent(TOPIC_CONNECTION_STATE_CHANGED)
    .then(function(aSubject) {
      ok(aSubject instanceof Ci.nsIRilNetworkInfo,
         "subject should be an instance of nsIRilNetworkInfo");
      is(aSubject.type, aNetworkType,
         "subject.type should be " + aNetworkType);
      is(aSubject.state, Ci.nsINetworkInfo.NETWORK_STATE_DISCONNECTED,
         "subject.state should be DISCONNECTED");
    }));
  promises.push(radioInterface.deactivateDataCallByType(aNetworkType));

  return Promise.all(promises);
}

/**
 * Wait for pending emulator transactions and call |finish()|.
 */
function cleanUp() {
  // Use ok here so that we have at least one test run.
  ok(true, ":: CLEANING UP ::");

  waitFor(finish, function() {
    return _pendingEmulatorShellCmdCount === 0 &&
           _pendingEmulatorCmdCount === 0;
  });
}

/**
 * Basic test routine helper.
 *
 * This helper does nothing but clean-ups.
 *
 * @param aTestCaseMain
 *        A function that takes no parameter.
 */
function startTestBase(aTestCaseMain) {
  Promise.resolve()
    .then(aTestCaseMain)
    .then(cleanUp, function(aException) {
      ok(false, "promise rejects during test: " + aException);
      cleanUp();
    });
}
