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: function(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: function(socket) {} ,

  /**
   * Called to close a connection and clean up properties.
   */
  reset: function() {
    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: function() {
    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;

/**
 * Connections have 5 seconds to be made, or a timeout function fails this
 * test.  This prevents the test from hanging and bringing down the entire
 * xpcshell test chain.
 */
var connectTimeout = 5*1000;

/**
 * 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(null, 0, '127.0.0.1', serv.port, 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();
}
