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

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

const ETHERNET_MANAGER_CONTRACT_ID = "@mozilla.org/ethernetManager;1";

const INTERFACE_UP = "UP";
const INTERFACE_DOWN = "DOWN";

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

  // Private member variables of the returned object |suite|.
  let ethernetManager = SpecialPowers.Cc[ETHERNET_MANAGER_CONTRACT_ID]
                                     .getService(SpecialPowers.Ci.nsIEthernetManager);
  let pendingEmulatorShellCount = 0;

  /**
   * 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: an array of emulator response lines.
   * Reject params: an array of emulator response lines.
   *
   * @param command
   *        A string command to be passed to emulator through its telnet console.
   *
   * @return A deferred promise.
   */
  function runEmulatorShellSafe(command) {
    let deferred = Promise.defer();

    ++pendingEmulatorShellCount;
    runEmulatorShell(command, 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;
  }

  /**
   * Get the system network conifg by the given interface name.
   *
   * Use shell command 'netcfg' to get the list of network cofig.
   *
   * Fulfill params: An object of { name, flag, ip }
   *
   * @parm ifname
   *       Interface name.
   *
   * @return A deferred promise.
   */
  function getNetworkConfig(ifname) {
    return runEmulatorShellSafe(['netcfg'])
      .then(result => {
        // Sample 'netcfg' 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
        // eth1     DOWN                                   0.0.0.0/0   0x00001002 52:54:00:12:34:57
        // rmnet1   DOWN                                   0.0.0.0/0   0x00001002 52:54:00:12:34:59

        let config;

        for (let i = 0; i < result.length; i++) {
          let tokens = result[i].split(/\s+/);
          let name = tokens[0];
          let flag = tokens[1];
          let ip = tokens[2].split(/\/+/)[0];
          if (name == ifname) {
            config = { name: name, flag: flag, ip: ip };
            break;
          }
        }

        return config;
      });
  }

  /**
   * Get the ip assigned by dhcp server of a given interface name.
   *
   * Get the ip from android property 'dhcp.[ifname].ipaddress'.
   *
   * Fulfill params: A string of ip address.
   *
   * @parm ifname
   *       Interface name.
   *
   * @return A deferred promise.
   */
  function getDhcpIpAddr(ifname) {
    return runEmulatorShellSafe(['getprop', 'dhcp.' + ifname + '.ipaddress'])
      .then(function(ipAddr) {
        return ipAddr[0];
      });
  }

  /**
   * Get the gateway assigned by dhcp server of a given interface name.
   *
   * Get the ip from android property 'dhcp.[ifname].gateway'.
   *
   * Fulfill params: A string of gateway.
   *
   * @parm ifname
   *       Interface name.
   *
   * @return A deferred promise.
   */
  function getDhcpGateway(ifname) {
    return runEmulatorShellSafe(['getprop', 'dhcp.' + ifname + '.gateway'])
      .then(function(gateway) {
        return gateway[0];
      });
  }

  /**
   * Get the default route.
   *
   * Use shell command 'ip route' to get the default of device.
   *
   * Fulfill params: An array of { name, gateway }
   *
   * @return A deferred promise.
   */
  function getDefaultRoute() {
    return runEmulatorShellSafe(['ip', 'route'])
      .then(result => {
        // Sample 'ip route' output:
        //
        // 10.0.2.0/24 dev eth0  proto kernel  scope link  src 10.0.2.15
        // default via 10.0.2.2 dev eth0  metric 2

        let routeInfo = [];

        for (let i = 0; i < result.length; i++) {
          if (!result[i].match('default')) {
            continue;
          }

          let tokens = result[i].split(/\s+/);
          let name = tokens[4];
          let gateway = tokens[2];
          routeInfo.push({ name: name, gateway: gateway });
        }

        return routeInfo;
      });
  }

  /**
   * Check a specific interface is enabled or not.
   *
   * @parm ifname
   *       Interface name.
   * @parm enabled
   *       A boolean value used to check interface is disable or not.
   *
   * @return A deferred promise.
   */
  function checkInterfaceIsEnabled(ifname, enabled) {
    return getNetworkConfig(ifname)
      .then(function(config) {
        if (enabled) {
          is(config.flag, INTERFACE_UP, "Interface is enabled as expected.");
        } else {
          is(config.flag, INTERFACE_DOWN, "Interface is disabled as expected.");
        }
      });
  }

  /**
   * Check the ip of a specific interface is equal to given ip or not.
   *
   * @parm ifname
   *       Interface name.
   * @parm ip
   *       Given ip address.
   *
   * @return A deferred promise.
   */
  function checkInterfaceIpAddr(ifname, ip) {
    return getNetworkConfig(ifname)
      .then(function(config) {
        is(config.ip, ip, "IP is right as expected.");
      });
  }

  /**
   * Check the default gateway of a specific interface is equal to given gateway
   * or not.
   *
   * @parm ifname
   *       Interface name.
   * @parm gateway
   *       Given gateway.
   *
   * @return A deferred promise.
   */
  function checkDefaultRoute(ifname, gateway) {
    return getDefaultRoute()
      .then(function(routeInfo) {
        for (let i = 0; i < routeInfo.length; i++) {
          if (routeInfo[i].name == ifname) {
            is(routeInfo[i].gateway, gateway,
               "Default gateway is right as expected.");
            return true;
          }
        }

        if (!gateway) {
          ok(true, "Default route is cleared.");
          return true;
        }

        // TODO: should we ok(false, ......) here?
        return false;
      });
  }

  /**
   * Check the length of interface list in EthernetManager is equal to given
   * length or not.
   *
   * @parm length
   *       Given length.
   */
  function checkInterfaceListLength(length) {
    let list = ethernetManager.interfaceList;
    is(length, list.length, "List length is equal as expected.");
  }

  /**
   * Check the given interface exists on device or not.
   *
   * @parm ifname
   *       Interface name.
   *
   * @return A deferred promise.
   */
  function checkInterfaceExist(ifname) {
    return scanInterfaces()
      .then(list => {
        let index = list.indexOf(ifname);
        if (index < 0) {
          throw "Interface " + ifname + " not found.";
        }

        ok(true, ifname + " exists.");
      });
  }

  /**
   * Scan for available ethernet interfaces.
   *
   * Fulfill params: A list of available interfaces found in device.
   *
   * @return A deferred promise.
   */
  function scanInterfaces() {
    let deferred = Promise.defer();

    ethernetManager.scan(function onScan(list) {
      deferred.resolve(list);
    });

    return deferred.promise;
  }

  /**
   * Add an interface into interface list.
   *
   * Fulfill params: A boolean value indicates success or not.
   *
   * @param ifname
   *        Interface name.
   *
   * @return A deferred promise.
   */
  function addInterface(ifname) {
    let deferred = Promise.defer();

    ethernetManager.addInterface(ifname, function onAdd(success, message) {
      ok(success, "Add interface " + ifname + " succeeded.");
      is(message, "ok", "Message is as expected.");

      deferred.resolve(success);
    });

    return deferred.promise;
  }

  /**
   * Remove an interface form the interface list.
   *
   * Fulfill params: A boolean value indicates success or not.
   *
   * @param ifname
   *        Interface name.
   *
   * @return A deferred promise.
   */
  function removeInterface(ifname) {
    let deferred = Promise.defer();

    ethernetManager.removeInterface(ifname, function onRemove(success, message) {
      ok(success, "Remove interface " + ifname + " succeeded.");
      is(message, "ok", "Message is as expected.");

      deferred.resolve(success);
    });

    return deferred.promise;
  }

  /**
   * Enable networking of an interface in the interface list.
   *
   * Fulfill params: A boolean value indicates success or not.
   *
   * @param ifname
   *        Interface name.
   *
   * @return A deferred promise.
   */
  function enableInterface(ifname) {
    let deferred = Promise.defer();

    ethernetManager.enable(ifname, function onEnable(success, message) {
      ok(success, "Enable interface " + ifname + " succeeded.");
      is(message, "ok", "Message is as expected.");

      deferred.resolve(success);
    });

    return deferred.promise;
  }

  /**
   * Disable networking of an interface in the interface list.
   *
   * Fulfill params: A boolean value indicates success or not.
   *
   * @param ifname
   *        Interface name.
   *
   * @return A deferred promise.
   */
  function disableInterface(ifname) {
    let deferred = Promise.defer();

    ethernetManager.disable(ifname, function onDisable(success, message) {
      ok(success, "Disable interface " + ifname + " succeeded.");
      is(message, "ok", "Message is as expected.");

      deferred.resolve(success);
    });

    return deferred.promise;
  }

  /**
   * Make an interface connect to network.
   *
   * Fulfill params: A boolean value indicates success or not.
   *
   * @param ifname
   *        Interface name.
   *
   * @return A deferred promise.
   */
  function makeInterfaceConnect(ifname) {
    let deferred = Promise.defer();

    ethernetManager.connect(ifname, function onConnect(success, message) {
      ok(success, "Interface " + ifname + " is connected successfully.");
      is(message, "ok", "Message is as expected.");

      deferred.resolve(success);
    });

    return deferred.promise;
  }

  /**
   * Make an interface disconnect to network.
   *
   * Fulfill params: A boolean value indicates success or not.
   *
   * @param ifname
   *        Interface name.
   *
   * @return A deferred promise.
   */
  function makeInterfaceDisconnect(ifname) {
    let deferred = Promise.defer();

    ethernetManager.disconnect(ifname, function onDisconnect(success, message) {
      ok(success, "Interface " + ifname + " is disconnected successfully.");
      is(message, "ok", "Message is as expected.");

      deferred.resolve(success);
    });

    return deferred.promise;
  }

  /**
   * Update the config the an interface in the interface list.
   *
   * @param ifname
   *        Interface name.
   * @param config
   *        .ip: ip address.
   *        .prefixLength: mask length.
   *        .gateway: gateway.
   *        .dnses: dnses.
   *        .httpProxyHost: http proxy host.
   *        .httpProxyPort: http porxy port.
   *        .usingDhcp: an boolean value indicates using dhcp or not.
   *
   * @return A deferred promise.
   */
  function updateInterfaceConfig(ifname, config) {
    let deferred = Promise.defer();

    ethernetManager.updateInterfaceConfig(ifname, config,
                                          function onUpdated(success, message) {
      ok(success, "Interface " + ifname + " config is updated successfully " +
                  "with " + JSON.stringify(config));
      is(message, "ok", "Message is as expected.");

      deferred.resolve(success);
    });

    return deferred.promise;
  }

  /**
   * Wait for timeout.
   *
   * @param timeout
   *        Time in ms.
   *
   * @return A deferred promise.
   */
  function waitForTimeout(timeout) {
    let deferred = Promise.defer();

    setTimeout(function() {
      ok(true, "waitForTimeout " + timeout);
      deferred.resolve();
    }, timeout);

    return deferred.promise;
  }

  /**
   * Wait for default route of a specific interface being set and
   * check.
   *
   * @param ifname
   *        Interface name.
   * @param gateway
   *        Target gateway.
   *
   * @return A deferred promise.
   */
  function waitForDefaultRouteSet(ifname, gateway) {
    return gTestSuite.waitForTimeout(500)
      .then(() => gTestSuite.checkDefaultRoute(ifname, gateway))
      .then(success => {
        if (success) {
          ok(true, "Default route is set as expected." + gateway);
          return;
        }

        ok(true, "Default route is not set yet, check again. " + success);
        return waitForDefaultRouteSet(ifname, gateway);
      });
  }

  //---------------------------------------------------
  // Public test suite functions
  //---------------------------------------------------
  suite.scanInterfaces = scanInterfaces;
  suite.addInterface = addInterface;
  suite.removeInterface = removeInterface;
  suite.enableInterface = enableInterface;
  suite.disableInterface = disableInterface;
  suite.makeInterfaceConnect = makeInterfaceConnect;
  suite.makeInterfaceDisconnect = makeInterfaceDisconnect;
  suite.updateInterfaceConfig = updateInterfaceConfig;
  suite.getDhcpIpAddr = getDhcpIpAddr;
  suite.getDhcpGateway = getDhcpGateway;
  suite.checkInterfaceExist = checkInterfaceExist;
  suite.checkInterfaceIsEnabled = checkInterfaceIsEnabled;
  suite.checkInterfaceIpAddr = checkInterfaceIpAddr;
  suite.checkDefaultRoute = checkDefaultRoute;
  suite.checkInterfaceListLength = checkInterfaceListLength;
  suite.waitForTimeout = waitForTimeout;
  suite.waitForDefaultRouteSet = waitForDefaultRouteSet;

  /**
   * End up the test run.
   *
   * Wait until all pending emulator shell commands are done and then |finish|
   * will be called in the end.
   */
  function cleanUp() {
    waitFor(finish, function() {
      return pendingEmulatorShellCount === 0;
    });
  }

  /**
   * 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.
   *
   * @param aTestCaseChain
   *        The test case entry point, which can be a function or a promise.
   *
   * @return A deferred promise.
   */
  suite.doTest = function(aTestCaseChain) {
    return Promise.resolve()
      .then(aTestCaseChain)
      .then(function onresolve() {
        cleanUp();
      }, function onreject(aReason) {
        ok(false, 'Promise rejects during test' + (aReason ? '(' + aReason + ')' : ''));
        cleanUp();
      });
  };

  return suite;
})();
