"use strict";

var CC = Components.Constructor;

const ServerSocket = CC(
  "@mozilla.org/network/server-socket;1",
  "nsIServerSocket",
  "init"
);

/**
 * TestServer: A single instance of this is created as |serv|.  When created,
 * it starts listening on the loopback address on port |serv.port|.  Tests will
 * connect to it after setting |serv.acceptCallback|, which is invoked after it
 * accepts a connection.
 *
 * Within |serv.acceptCallback|, various properties of |serv| can be used to
 * run checks. After the callback, the connection is closed, but the server
 * remains listening until |serv.stop|
 *
 * Note: TestServer can only handle a single connection at a time.  Tests
 * should use run_next_test at the end of |serv.acceptCallback| to start the
 * following test which creates a connection.
 */
function TestServer() {
  this.reset();

  // start server.
  // any port (-1), loopback only (true), default backlog (-1)
  this.listener = ServerSocket(-1, true, -1);
  this.port = this.listener.port;
  info("server: listening on " + this.port);
  this.listener.asyncListen(this);
}

TestServer.prototype = {
  onSocketAccepted(socket, trans) {
    info("server: got client connection");

    // one connection at a time.
    if (this.input !== null) {
      try {
        socket.close();
      } catch (ignore) {}
      do_throw("Test written to handle one connection at a time.");
    }

    try {
      this.input = trans.openInputStream(0, 0, 0);
      this.output = trans.openOutputStream(0, 0, 0);
      this.selfAddr = trans.getScriptableSelfAddr();
      this.peerAddr = trans.getScriptablePeerAddr();

      this.acceptCallback();
    } catch (e) {
      /* In a native callback such as onSocketAccepted, exceptions might not
       * get output correctly or logged to test output. Send them through
       * do_throw, which fails the test immediately. */
      do_report_unexpected_exception(e, "in TestServer.onSocketAccepted");
    }

    this.reset();
  },

  onStopListening(socket) {},

  /**
   * Called to close a connection and clean up properties.
   */
  reset() {
    if (this.input) {
      try {
        this.input.close();
      } catch (ignore) {}
    }
    if (this.output) {
      try {
        this.output.close();
      } catch (ignore) {}
    }

    this.input = null;
    this.output = null;
    this.acceptCallback = null;
    this.selfAddr = null;
    this.peerAddr = null;
  },

  /**
   * Cleanup for TestServer and this test case.
   */
  stop() {
    this.reset();
    try {
      this.listener.close();
    } catch (ignore) {}
  },
};

/**
 * Helper function.
 * Compares two nsINetAddr objects and ensures they are logically equivalent.
 */
function checkAddrEqual(lhs, rhs) {
  Assert.equal(lhs.family, rhs.family);

  if (lhs.family === Ci.nsINetAddr.FAMILY_INET) {
    Assert.equal(lhs.address, rhs.address);
    Assert.equal(lhs.port, rhs.port);
  }

  /* TODO: fully support ipv6 and local */
}

/**
 * An instance of SocketTransportService, used to create connections.
 */
var sts;

/**
 * Single instance of TestServer
 */
var serv;

/**
 * A place for individual tests to place Objects of importance for access
 * throughout asynchronous testing.  Particularly important for any output or
 * input streams opened, as cleanup of those objects (by the garbage collector)
 * causes the stream to close and may have other side effects.
 */
var testDataStore = null;

/**
 * IPv4 test.
 */
function testIpv4() {
  testDataStore = {
    transport: null,
    ouput: null,
  };

  serv.acceptCallback = function () {
    // disable the timeoutCallback
    serv.timeoutCallback = function () {};

    var selfAddr = testDataStore.transport.getScriptableSelfAddr();
    var peerAddr = testDataStore.transport.getScriptablePeerAddr();

    // check peerAddr against expected values
    Assert.equal(peerAddr.family, Ci.nsINetAddr.FAMILY_INET);
    Assert.equal(peerAddr.port, testDataStore.transport.port);
    Assert.equal(peerAddr.port, serv.port);
    Assert.equal(peerAddr.address, "127.0.0.1");

    // check selfAddr against expected values
    Assert.equal(selfAddr.family, Ci.nsINetAddr.FAMILY_INET);
    Assert.equal(selfAddr.address, "127.0.0.1");

    // check that selfAddr = server.peerAddr and vice versa.
    checkAddrEqual(selfAddr, serv.peerAddr);
    checkAddrEqual(peerAddr, serv.selfAddr);

    testDataStore = null;
    executeSoon(run_next_test);
  };

  // Useful timeout for debugging test hangs
  /*serv.timeoutCallback = function(tname) {
    if (tname === 'testIpv4')
      do_throw('testIpv4 never completed a connection to TestServ');
  };
  do_timeout(connectTimeout, function(){ serv.timeoutCallback('testIpv4'); });*/

  testDataStore.transport = sts.createTransport(
    [],
    "127.0.0.1",
    serv.port,
    null,
    null
  );
  /*
   * Need to hold |output| so that the output stream doesn't close itself and
   * the associated connection.
   */
  testDataStore.output = testDataStore.transport.openOutputStream(
    Ci.nsITransport.OPEN_BLOCKING,
    0,
    0
  );

  /* NEXT:
   * openOutputStream -> onSocketAccepted -> acceptedCallback -> run_next_test
   * OR (if the above timeout is uncommented)
   * <connectTimeout lapses> -> timeoutCallback -> do_throw
   */
}

/**
 * Running the tests.
 */
function run_test() {
  sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
    Ci.nsISocketTransportService
  );
  serv = new TestServer();

  registerCleanupFunction(function () {
    serv.stop();
  });

  add_test(testIpv4);
  /* TODO: testIpv6 */
  /* TODO: testLocal */

  run_next_test();
}