// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview Unit tests for goog.labs.net.webChannel.WebChannelBase.
 * @suppress {accessControls} Private methods are accessed for test purposes.
 *
 */


goog.provide('goog.labs.net.webChannel.webChannelBaseTest');

goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.functions');
goog.require('goog.json');
goog.require('goog.labs.net.webChannel.ChannelRequest');
goog.require('goog.labs.net.webChannel.ForwardChannelRequestPool');
goog.require('goog.labs.net.webChannel.WebChannelBase');
goog.require('goog.labs.net.webChannel.WebChannelBaseTransport');
goog.require('goog.labs.net.webChannel.WebChannelDebug');
goog.require('goog.labs.net.webChannel.Wire');
goog.require('goog.labs.net.webChannel.netUtils');
goog.require('goog.labs.net.webChannel.requestStats');
goog.require('goog.labs.net.webChannel.requestStats.Stat');
goog.require('goog.structs.Map');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.asserts');
goog.require('goog.testing.jsunit');

goog.setTestOnly('goog.labs.net.webChannel.webChannelBaseTest');


/**
 * Delay between a network failure and the next network request.
 */
var RETRY_TIME = 1000;


/**
 * A really long time - used to make sure no more timeouts will fire.
 */
var ALL_DAY_MS = 1000 * 60 * 60 * 24;

var stubs = new goog.testing.PropertyReplacer();

var channel;
var deliveredMaps;
var handler;
var mockClock;
var gotError;
var numStatEvents;
var lastStatEvent;
var numTimingEvents;
var lastPostSize;
var lastPostRtt;
var lastPostRetryCount;

// Set to true to see the channel debug output in the browser window.
var debug = false;
// Debug message to print out when debug is true.
var debugMessage = '';

function debugToWindow(message) {
  if (debug) {
    debugMessage += message + '<br>';
    goog.dom.getElement('debug').innerHTML = debugMessage;
  }
}


/**
 * Stubs goog.labs.net.webChannel.netUtils to always time out. It maintains the
 * contract given by goog.labs.net.webChannel.netUtils.testNetwork, but always
 * times out (calling callback(false)).
 *
 * stubNetUtils should be called in tests that require it before
 * a call to testNetwork happens. It is reset at tearDown.
 */
function stubNetUtils() {
  stubs.set(
      goog.labs.net.webChannel.netUtils, 'testLoadImage',
      function(url, timeout, callback) {
        goog.Timer.callOnce(goog.partial(callback, false), timeout);
      });
}


/**
 * Stubs
 * goog.labs.net.webChannel.ForwardChannelRequestPool.isSpdyOrHttp2Enabled_ to
 * manage the max pool size for the forward channel.
 *
 * @param {boolean} spdyEnabled Whether SPDY is enabled for the test.
 */
function stubSpdyCheck(spdyEnabled) {
  stubs.set(
      goog.labs.net.webChannel.ForwardChannelRequestPool,
      'isSpdyOrHttp2Enabled_', function() {
        return spdyEnabled;
      });
}



/**
 * Mock ChannelRequest.
 * @constructor
 * @struct
 * @final
 */
var MockChannelRequest = function(
    channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
  this.channel_ = channel;
  this.channelDebug_ = channelDebug;
  this.sessionId_ = opt_sessionId;
  this.requestId_ = opt_requestId;
  this.successful_ = true;
  this.lastError_ = null;
  this.lastStatusCode_ = 200;

  // For debugging, keep track of whether this is a back or forward channel.
  this.isBack = !!(opt_requestId == 'rpc');
  this.isForward = !this.isBack;

  this.pendingMessages_ = [];
};

MockChannelRequest.prototype.postData_ = null;

MockChannelRequest.prototype.requestStartTime_ = null;

/**
 * @param {Object} extraHeaders The HTTP headers.
 */
MockChannelRequest.prototype.setExtraHeaders = function(extraHeaders) {};

/**
 * @param {number} timeout   The timeout in MS for when we fail the request.
 */
MockChannelRequest.prototype.setTimeout = function(timeout) {};

/**
 * @param {number} throttle The throttle in ms.  A value of zero indicates
 *   no throttle.
 */
MockChannelRequest.prototype.setReadyStateChangeThrottle = function(throttle) {
};

/**
 * @param {goog.Uri} uri  The uri of the request.
 * @param {?string} postData  The data for the post body.
 * @param {boolean} decodeChunks  Whether to the result is expected to be
 *     encoded for chunking and thus requires decoding.
 */
MockChannelRequest.prototype.xmlHttpPost = function(
    uri, postData, decodeChunks) {
  this.channelDebug_.debug(
      '---> POST: ' + uri + ', ' + postData + ', ' + decodeChunks);
  this.postData_ = postData;
  this.requestStartTime_ = goog.now();
};

/**
 * @param {goog.Uri} uri  The uri of the request.
 * @param {boolean} decodeChunks  Whether to the result is expected to be
 *   encoded for chunking and thus requires decoding.
 * @param {?string} hostPrefix  The host prefix, if we might be using a
 *   secondary domain.  Note that it should also be in the URL, adding this
 *   won't cause it to be added to the URL.
 */
MockChannelRequest.prototype.xmlHttpGet = function(
    uri, decodeChunks, hostPrefix) {
  this.channelDebug_.debug(
      '<--- GET: ' + uri + ', ' + decodeChunks + ', ' + hostPrefix);
  this.requestStartTime_ = goog.now();
};

/**
 * @param {goog.Uri} uri The uri to send a request to.
 */
MockChannelRequest.prototype.sendCloseRequest = function(uri) {
  this.requestStartTime_ = goog.now();
};

/**
 * Cancel.
 */
MockChannelRequest.prototype.cancel = function() {
  this.successful_ = false;
};

/**
 * @returns {boolean}
 */
MockChannelRequest.prototype.getSuccess = function() {
  return this.successful_;
};

/**
 * @return {?ChannelRequest.Error}  The last error.
 */
MockChannelRequest.prototype.getLastError = function() {
  return this.lastError_;
};

/**
 * @return {number} The status code of the last request.
 */
MockChannelRequest.prototype.getLastStatusCode = function() {
  return this.lastStatusCode_;
};

/**
 * @return {string|undefined} The session ID.
 */
MockChannelRequest.prototype.getSessionId = function() {
  return this.sessionId_;
};

/**
 * @return {string|number|undefined} The request ID.
 */
MockChannelRequest.prototype.getRequestId = function() {
  return this.requestId_;
};

/**
 * @return {?string} The POST data provided by the request initiator.
 */
MockChannelRequest.prototype.getPostData = function() {
  return this.postData_;
};

/**
 * @return {?number} The time the request started, as returned by goog.now().
 */
MockChannelRequest.prototype.getRequestStartTime = function() {
  return this.requestStartTime_;
};

/**
 * @return {?goog.net.XhrIo} Any XhrIo request created for this object.
 */
MockChannelRequest.prototype.getXhr = function() {
  return null;
};

/**
 * @param {!Array<goog.labs.net.webChannel.Wire.QueuedMap>} messages
 *   The pending messages for this request.
 */
MockChannelRequest.prototype.setPendingMessages = function(messages) {
  this.pendingMessages_ = messages;
};

/**
 * @return {!Array<goog.labs.net.webChannel.Wire.QueuedMap>} The pending
 *   messages for this request.
 */
MockChannelRequest.prototype.getPendingMessages = function() {
  return this.pendingMessages_;
};

/**
 * @return {boolean} true if X_HTTP_INITIAL_RESPONSE has been handled.
 */
MockChannelRequest.prototype.isInitialResponseDecoded = function() {
  return false;
};

/**
 * Decodes X_HTTP_INITIAL_RESPONSE if present.
 */
MockChannelRequest.prototype.setDecodeInitialResponse = function() {};

function shouldRunTests() {
  return goog.labs.net.webChannel.ChannelRequest.supportsXhrStreaming();
}


/**
 * @suppress {invalidCasts} The cast from MockChannelRequest to
 * ChannelRequest is invalid and will not compile.
 */
function setUpPage() {
  // Use our MockChannelRequests instead of the real ones.
  goog.labs.net.webChannel.ChannelRequest.createChannelRequest = function(
      channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
    return /** @type {!goog.labs.net.webChannel.ChannelRequest} */ (
        new MockChannelRequest(
            channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId));
  };

  // Mock out the stat notification code.
  goog.labs.net.webChannel.requestStats.notifyStatEvent = function(stat) {
    numStatEvents++;
    lastStatEvent = stat;
  };

  goog.labs.net.webChannel.requestStats.notifyTimingEvent = function(
      size, rtt, retries) {
    numTimingEvents++;
    lastPostSize = size;
    lastPostRtt = rtt;
    lastPostRetryCount = retries;
  };
}

function setUp() {
  numTimingEvents = 0;
  lastPostSize = null;
  lastPostRtt = null;
  lastPostRetryCount = null;

  mockClock = new goog.testing.MockClock(true);
  channel = new goog.labs.net.webChannel.WebChannelBase('1');
  // restore channel-test for tests that rely on the channel-test state
  channel.backgroundChannelTest_ = false;

  gotError = false;

  handler = new goog.labs.net.webChannel.WebChannelBase.Handler();
  handler.channelOpened = function() {};
  handler.channelError = function(channel, error) { gotError = true; };
  handler.channelSuccess = function(channel, request) {
    deliveredMaps = goog.array.clone(request.getPendingMessages());
  };

  /**
   * @suppress {checkTypes} The callback function type declaration is skipped.
   */
  handler.channelClosed = function(
      channel, opt_pendingMaps, opt_undeliveredMaps) {
    // Mock out the handler, and let it set a formatted user readable string
    // of the undelivered maps which we can use when verifying our assertions.
    if (opt_pendingMaps) {
      handler.pendingMapsString = formatArrayOfMaps(opt_pendingMaps);
    }
    if (opt_undeliveredMaps) {
      handler.undeliveredMapsString = formatArrayOfMaps(opt_undeliveredMaps);
    }
  };
  handler.channelHandleMultipleArrays = function() {};
  handler.channelHandleArray = function() {};

  channel.setHandler(handler);

  // Provide a predictable retry time for testing.
  channel.getRetryTime_ = function(retryCount) { return RETRY_TIME; };

  var channelDebug = new goog.labs.net.webChannel.WebChannelDebug();
  channelDebug.debug = function(message) { debugToWindow(message); };
  channel.setChannelDebug(channelDebug);

  numStatEvents = 0;
  lastStatEvent = null;
}


function tearDown() {
  mockClock.dispose();
  stubs.reset();
  debugToWindow('<hr>');
}


function getSingleForwardRequest() {
  var pool = channel.forwardChannelRequestPool_;
  if (!pool.hasPendingRequest()) {
    return null;
  }
  return pool.request_ || pool.requestPool_.getValues()[0];
}


/**
 * Helper function to return a formatted string representing an array of maps.
 */
function formatArrayOfMaps(arrayOfMaps) {
  var result = [];
  for (var i = 0; i < arrayOfMaps.length; i++) {
    var map = arrayOfMaps[i];
    var keys = map.map.getKeys();
    for (var j = 0; j < keys.length; j++) {
      var tmp = keys[j] + ':' + map.map.get(keys[j]) +
          (map.context ? ':' + map.context : '');
      result.push(tmp);
    }
  }
  return result.join(', ');
}


function testFormatArrayOfMaps() {
  // This function is used in a non-trivial test, so let's verify that it works.
  var map1 = new goog.structs.Map();
  map1.set('k1', 'v1');
  map1.set('k2', 'v2');
  var map2 = new goog.structs.Map();
  map2.set('k3', 'v3');
  var map3 = new goog.structs.Map();
  map3.set('k4', 'v4');
  map3.set('k5', 'v5');
  map3.set('k6', 'v6');

  // One map.
  var a = [];
  a.push(new goog.labs.net.webChannel.Wire.QueuedMap(0, map1));
  assertEquals('k1:v1, k2:v2', formatArrayOfMaps(a));

  // Many maps.
  var b = [];
  b.push(new goog.labs.net.webChannel.Wire.QueuedMap(0, map1));
  b.push(new goog.labs.net.webChannel.Wire.QueuedMap(0, map2));
  b.push(new goog.labs.net.webChannel.Wire.QueuedMap(0, map3));
  assertEquals(
      'k1:v1, k2:v2, k3:v3, k4:v4, k5:v5, k6:v6', formatArrayOfMaps(b));

  // One map with a context.
  var c = [];
  c.push(
      new goog.labs.net.webChannel.Wire.QueuedMap(0, map1, new String('c1')));
  assertEquals('k1:v1:c1, k2:v2:c1', formatArrayOfMaps(c));
}


/**
 * @param {number=} opt_serverVersion
 * @param {string=} opt_hostPrefix
 * @param {string=} opt_uriPrefix
 * @param {boolean=} opt_spdyEnabled
 */
function connectForwardChannel(
    opt_serverVersion, opt_hostPrefix, opt_uriPrefix, opt_spdyEnabled) {
  stubSpdyCheck(!!opt_spdyEnabled);
  var uriPrefix = opt_uriPrefix || '';
  channel.connect(uriPrefix + '/test', uriPrefix + '/bind', null);
  mockClock.tick(0);
  completeTestConnection();
  completeForwardChannel(opt_serverVersion, opt_hostPrefix);
}


/**
 * @param {number=} opt_serverVersion
 * @param {string=} opt_hostPrefix
 * @param {string=} opt_uriPrefix
 * @param {boolean=} opt_spdyEnabled
 */
function connect(
    opt_serverVersion, opt_hostPrefix, opt_uriPrefix, opt_spdyEnabled) {
  connectForwardChannel(
      opt_serverVersion, opt_hostPrefix, opt_uriPrefix, opt_spdyEnabled);
  completeBackChannel();
}


function disconnect() {
  channel.disconnect();
  mockClock.tick(0);
}


function completeTestConnection() {
  completeForwardTestConnection();
  completeBackTestConnection();
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.OPENING,
      channel.getState());
}


function completeForwardTestConnection() {
  channel.connectionTest_.onRequestData(
      channel.connectionTest_.request_, '["b"]');
  channel.connectionTest_.onRequestComplete(channel.connectionTest_.request_);
  mockClock.tick(0);
}


function completeBackTestConnection() {
  channel.connectionTest_.onRequestData(
      channel.connectionTest_.request_, '11111');
  mockClock.tick(0);
}


/**
 * @param {number=} opt_serverVersion
 * @param {string=} opt_hostPrefix
 */
function completeForwardChannel(opt_serverVersion, opt_hostPrefix) {
  var responseData = '[[0,["c","1234567890ABCDEF",' +
      (opt_hostPrefix ? '"' + opt_hostPrefix + '"' : 'null') +
      (opt_serverVersion ? ',' + opt_serverVersion : '') + ']]]';
  channel.onRequestData(getSingleForwardRequest(), responseData);
  channel.onRequestComplete(getSingleForwardRequest());
  mockClock.tick(0);
}


function completeBackChannel() {
  channel.onRequestData(channel.backChannelRequest_, '[[1,["foo"]]]');
  channel.onRequestComplete(channel.backChannelRequest_);
  mockClock.tick(0);
}


function responseDone() {
  channel.onRequestData(getSingleForwardRequest(), '[1,0,0]');  // mock data
  channel.onRequestComplete(getSingleForwardRequest());
  mockClock.tick(0);
}


/**
 *
 * @param {number=} opt_lastArrayIdSentFromServer
 * @param {number=} opt_outstandingDataSize
 */
function responseNoBackchannel(
    opt_lastArrayIdSentFromServer, opt_outstandingDataSize) {
  var responseData = goog.json.serialize(
      [0, opt_lastArrayIdSentFromServer, opt_outstandingDataSize]);
  channel.onRequestData(getSingleForwardRequest(), responseData);
  channel.onRequestComplete(getSingleForwardRequest());
  mockClock.tick(0);
}

function response(lastArrayIdSentFromServer, outstandingDataSize) {
  var responseData =
      goog.json.serialize([1, lastArrayIdSentFromServer, outstandingDataSize]);
  channel.onRequestData(getSingleForwardRequest(), responseData);
  channel.onRequestComplete(getSingleForwardRequest());
  mockClock.tick(0);
}


function receive(data) {
  channel.onRequestData(channel.backChannelRequest_, '[[1,' + data + ']]');
  channel.onRequestComplete(channel.backChannelRequest_);
  mockClock.tick(0);
}


function responseTimeout() {
  getSingleForwardRequest().lastError_ =
      goog.labs.net.webChannel.ChannelRequest.Error.TIMEOUT;
  getSingleForwardRequest().successful_ = false;
  channel.onRequestComplete(getSingleForwardRequest());
  mockClock.tick(0);
}


/**
 * @param {number=} opt_statusCode
 */
function responseRequestFailed(opt_statusCode) {
  getSingleForwardRequest().lastError_ =
      goog.labs.net.webChannel.ChannelRequest.Error.STATUS;
  getSingleForwardRequest().lastStatusCode_ = opt_statusCode || 503;
  getSingleForwardRequest().successful_ = false;
  channel.onRequestComplete(getSingleForwardRequest());
  mockClock.tick(0);
}


function responseUnknownSessionId() {
  getSingleForwardRequest().lastError_ =
      goog.labs.net.webChannel.ChannelRequest.Error.UNKNOWN_SESSION_ID;
  getSingleForwardRequest().successful_ = false;
  channel.onRequestComplete(getSingleForwardRequest());
  mockClock.tick(0);
}


/**
 * @param {string} key
 * @param {string} value
 * @param {string=} opt_context
 */
function sendMap(key, value, opt_context) {
  var map = new goog.structs.Map();
  map.set(key, value);
  channel.sendMap(map, opt_context);
  mockClock.tick(0);
}


function hasForwardChannel() {
  return !!getSingleForwardRequest();
}


function hasBackChannel() {
  return !!channel.backChannelRequest_;
}


function hasDeadBackChannelTimer() {
  return goog.isDefAndNotNull(channel.deadBackChannelTimerId_);
}


function assertHasForwardChannel() {
  assertTrue('Forward channel missing.', hasForwardChannel());
}


function assertHasBackChannel() {
  assertTrue('Back channel missing.', hasBackChannel());
}


function testConnect() {
  connect();
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.OPENED, channel.getState());
  // If the server specifies no version, the client assumes the latest version
  assertEquals(
      goog.labs.net.webChannel.Wire.LATEST_CHANNEL_VERSION,
      channel.channelVersion_);
  assertFalse(channel.isBuffered());
}

function testConnect_backChannelEstablished() {
  connect();
  assertHasBackChannel();
}

function testConnect_withServerHostPrefix() {
  connect(undefined, 'serverHostPrefix');
  assertEquals('serverHostPrefix', channel.hostPrefix_);
}

function testConnect_withClientHostPrefix() {
  handler.correctHostPrefix = function(hostPrefix) {
    return 'clientHostPrefix';
  };
  connect();
  assertEquals('clientHostPrefix', channel.hostPrefix_);
}

function testConnect_overrideServerHostPrefix() {
  handler.correctHostPrefix = function(hostPrefix) {
    return 'clientHostPrefix';
  };
  connect(undefined, 'serverHostPrefix');
  assertEquals('clientHostPrefix', channel.hostPrefix_);
}

function testConnect_withServerVersion() {
  connect(8);
  assertEquals(8, channel.channelVersion_);
}

function testConnect_notOkToMakeRequestForTest() {
  handler.okToMakeRequest = goog.functions.constant(
      goog.labs.net.webChannel.WebChannelBase.Error.NETWORK);
  channel.connect('/test', '/bind', null);
  mockClock.tick(0);
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());
}

function testConnect_notOkToMakeRequestForBind() {
  channel.connect('/test', '/bind', null);
  mockClock.tick(0);
  completeTestConnection();
  handler.okToMakeRequest = goog.functions.constant(
      goog.labs.net.webChannel.WebChannelBase.Error.NETWORK);
  completeForwardChannel();
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());
}


function testSendMap() {
  connect();
  sendMapOnce();
}


function testSendMapWithSpdyEnabled() {
  connect(undefined, undefined, undefined, true);
  sendMapOnce();
}


function sendMapOnce() {
  assertEquals(1, numTimingEvents);
  sendMap('foo', 'bar');
  responseDone();
  assertEquals(2, numTimingEvents);
  assertEquals('foo:bar', formatArrayOfMaps(deliveredMaps));
}


function testSendMap_twice() {
  connect();
  sendMapTwice();
}


function testSendMap_twiceWithSpdyEnabled() {
  connect(undefined, undefined, undefined, true);
  sendMapTwice();
}


function sendMapTwice() {
  sendMap('foo1', 'bar1');
  responseDone();
  assertEquals('foo1:bar1', formatArrayOfMaps(deliveredMaps));
  sendMap('foo2', 'bar2');
  responseDone();
  assertEquals('foo2:bar2', formatArrayOfMaps(deliveredMaps));
}


function testSendMap_andReceive() {
  connect();
  sendMap('foo', 'bar');
  responseDone();
  receive('["the server reply"]');
}


function testReceive() {
  connect();
  receive('["message from server"]');
  assertHasBackChannel();
}


function testReceive_twice() {
  connect();
  receive('["message one from server"]');
  receive('["message two from server"]');
  assertHasBackChannel();
}


function testReceive_andSendMap() {
  connect();
  receive('["the server reply"]');
  sendMap('foo', 'bar');
  responseDone();
  assertHasBackChannel();
}


function testBackChannelRemainsEstablished_afterSingleSendMap() {
  connect();

  sendMap('foo', 'bar');
  responseDone();
  receive('["ack"]');

  assertHasBackChannel();
}


function testBackChannelRemainsEstablished_afterDoubleSendMap() {
  connect();

  sendMap('foo1', 'bar1');
  sendMap('foo2', 'bar2');
  responseDone();
  receive('["ack"]');

  // This assertion would fail prior to CL 13302660.
  assertHasBackChannel();
}


function testTimingEvent() {
  connect();
  assertEquals(1, numTimingEvents);
  sendMap('', '');
  assertEquals(1, numTimingEvents);
  mockClock.tick(20);
  var expSize = getSingleForwardRequest().getPostData().length;
  responseDone();

  assertEquals(2, numTimingEvents);
  assertEquals(expSize, lastPostSize);
  assertEquals(20, lastPostRtt);
  assertEquals(0, lastPostRetryCount);

  sendMap('abcdefg', '123456');
  expSize = getSingleForwardRequest().getPostData().length;
  responseTimeout();
  assertEquals(2, numTimingEvents);
  mockClock.tick(RETRY_TIME + 1);
  responseDone();
  assertEquals(3, numTimingEvents);
  assertEquals(expSize, lastPostSize);
  assertEquals(1, lastPostRetryCount);
  assertEquals(1, lastPostRtt);
}


/**
 * Make sure that dropping the forward channel retry limit below the retry count
 * reports an error, and prevents another request from firing.
 */
function testSetFailFastWhileWaitingForRetry() {
  stubNetUtils();

  connect();
  setFailFastWhileWaitingForRetry();
}


function testSetFailFastWhileWaitingForRetryWithSpdyEnabled() {
  stubNetUtils();

  connect(undefined, undefined, undefined, true);
  setFailFastWhileWaitingForRetry();
}


function setFailFastWhileWaitingForRetry() {
  assertEquals(1, numTimingEvents);

  sendMap('foo', 'bar');
  assertNull(channel.forwardChannelTimerId_);
  assertNotNull(getSingleForwardRequest());
  assertEquals(0, channel.forwardChannelRetryCount_);

  // Watchdog timeout.
  responseTimeout();
  assertNotNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(1, channel.forwardChannelRetryCount_);

  // Almost finish the between-retry timeout.
  mockClock.tick(RETRY_TIME - 1);
  assertNotNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(1, channel.forwardChannelRetryCount_);

  // Setting max retries to 0 should cancel the timer and raise an error.
  channel.setFailFast(true);
  assertNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(1, channel.forwardChannelRetryCount_);

  // We get the error immediately before starting to ping google.com.
  assertTrue(gotError);
  assertEquals(0, deliveredMaps.length);

  // Simulate that timing out. We should not get another error.
  gotError = false;
  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT);
  assertFalse('Extra error after network ping timed out.', gotError);

  // Make sure no more retry timers are firing.
  mockClock.tick(ALL_DAY_MS);
  assertNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(1, channel.forwardChannelRetryCount_);
  assertEquals(1, numTimingEvents);
}


/**
 * Make sure that dropping the forward channel retry limit below the retry count
 * reports an error, and prevents another request from firing.
 */
function testSetFailFastWhileRetryXhrIsInFlight() {
  stubNetUtils();

  connect();
  setFailFastWhileRetryXhrIsInFlight();
}


function testSetFailFastWhileRetryXhrIsInFlightWithSpdyEnabled() {
  stubNetUtils();

  connect(undefined, undefined, undefined, true);
  setFailFastWhileRetryXhrIsInFlight();
}


function setFailFastWhileRetryXhrIsInFlight() {
  assertEquals(1, numTimingEvents);

  sendMap('foo', 'bar');
  assertNull(channel.forwardChannelTimerId_);
  assertNotNull(getSingleForwardRequest());
  assertEquals(0, channel.forwardChannelRetryCount_);

  // Watchdog timeout.
  responseTimeout();
  assertNotNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(1, channel.forwardChannelRetryCount_);

  // Wait for the between-retry timeout.
  mockClock.tick(RETRY_TIME);
  assertNull(channel.forwardChannelTimerId_);
  assertNotNull(getSingleForwardRequest());
  assertEquals(1, channel.forwardChannelRetryCount_);

  // Simulate a second watchdog timeout.
  responseTimeout();
  assertNotNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(2, channel.forwardChannelRetryCount_);

  // Wait for another between-retry timeout.
  mockClock.tick(RETRY_TIME);
  // Now the third req is in flight.
  assertNull(channel.forwardChannelTimerId_);
  assertNotNull(getSingleForwardRequest());
  assertEquals(2, channel.forwardChannelRetryCount_);

  // Set fail fast, killing the request
  channel.setFailFast(true);
  assertNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(2, channel.forwardChannelRetryCount_);

  // We get the error immediately before starting to ping google.com.
  assertTrue(gotError);

  // Simulate that timing out. We should not get another error.
  gotError = false;
  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT);
  assertFalse('Extra error after network ping timed out.', gotError);

  // Make sure no more retry timers are firing.
  mockClock.tick(ALL_DAY_MS);
  assertNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(2, channel.forwardChannelRetryCount_);
  assertEquals(1, numTimingEvents);
}


/**
 * Makes sure that setting fail fast while not retrying doesn't cause a failure.
 */
function testSetFailFastAtRetryCount() {
  stubNetUtils();

  connect();
  assertEquals(1, numTimingEvents);

  sendMap('foo', 'bar');
  assertNull(channel.forwardChannelTimerId_);
  assertNotNull(getSingleForwardRequest());
  assertEquals(0, channel.forwardChannelRetryCount_);

  // Set fail fast.
  channel.setFailFast(true);
  // Request should still be alive.
  assertNull(channel.forwardChannelTimerId_);
  assertNotNull(getSingleForwardRequest());
  assertEquals(0, channel.forwardChannelRetryCount_);

  // Watchdog timeout. Now we should get an error.
  responseTimeout();
  assertNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(0, channel.forwardChannelRetryCount_);

  // We get the error immediately before starting to ping google.com.
  assertTrue(gotError);
  // We get the error immediately before starting to ping google.com.
  // Simulate that timing out. We should not get another error in addition
  // to the initial failure.
  gotError = false;
  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT);
  assertFalse('Extra error after network ping timed out.', gotError);

  // Make sure no more retry timers are firing.
  mockClock.tick(ALL_DAY_MS);
  assertNull(channel.forwardChannelTimerId_);
  assertNull(getSingleForwardRequest());
  assertEquals(0, channel.forwardChannelRetryCount_);
  assertEquals(1, numTimingEvents);
}


function testRequestFailedClosesChannel() {
  stubNetUtils();

  connect();
  requestFailedClosesChannel();
}


function testRequestFailedClosesChannelWithSpdyEnabled() {
  stubNetUtils();

  connect(undefined, undefined, undefined, true);
  requestFailedClosesChannel();
}


function requestFailedClosesChannel() {
  assertEquals(1, numTimingEvents);

  sendMap('foo', 'bar');
  responseRequestFailed();

  assertEquals(
      'Should be closed immediately after request failed.',
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());

  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT);

  assertEquals(
      'Should remain closed after the ping timeout.',
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());
  assertEquals(1, numTimingEvents);
}


function testStatEventReportedOnlyOnce() {
  stubNetUtils();

  connect();
  sendMap('foo', 'bar');
  numStatEvents = 0;
  lastStatEvent = null;
  responseUnknownSessionId();

  assertEquals(1, numStatEvents);
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.ERROR_OTHER, lastStatEvent);

  numStatEvents = 0;
  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT);
  assertEquals('No new stat events should be reported.', 0, numStatEvents);
}


function testStatEventReportedOnlyOnce_onNetworkUp() {
  stubNetUtils();

  connect();
  sendMap('foo', 'bar');
  numStatEvents = 0;
  lastStatEvent = null;
  responseRequestFailed();

  assertEquals(
      'No stat event should be reported before we know the reason.', 0,
      numStatEvents);

  // Let the ping time out.
  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT);

  // Assert we report the correct stat event.
  assertEquals(1, numStatEvents);
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.ERROR_NETWORK, lastStatEvent);
}


function testStatEventReportedOnlyOnce_onNetworkDown() {
  stubNetUtils();

  connect();
  sendMap('foo', 'bar');
  numStatEvents = 0;
  lastStatEvent = null;
  responseRequestFailed();

  assertEquals(
      'No stat event should be reported before we know the reason.', 0,
      numStatEvents);

  // Wait half the ping timeout period, and then fake the network being up.
  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT / 2);
  channel.testNetworkCallback_(true);

  // Assert we report the correct stat event.
  assertEquals(1, numStatEvents);
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.ERROR_OTHER, lastStatEvent);
}


function testOutgoingMapsAwaitsResponse() {
  connect();
  outgoingMapsAwaitsResponse();
}


function testOutgoingMapsAwaitsResponseWithSpdyEnabled() {
  connect(undefined, undefined, undefined, true);
  outgoingMapsAwaitsResponse();
}


function outgoingMapsAwaitsResponse() {
  assertEquals(0, channel.outgoingMaps_.length);

  sendMap('foo1', 'bar');
  assertEquals(0, channel.outgoingMaps_.length);
  sendMap('foo2', 'bar');
  assertEquals(1, channel.outgoingMaps_.length);
  sendMap('foo3', 'bar');
  assertEquals(2, channel.outgoingMaps_.length);
  sendMap('foo4', 'bar');
  assertEquals(3, channel.outgoingMaps_.length);

  responseDone();
  // Now the forward channel request is completed and a new started, so all maps
  // are dequeued from the array of outgoing maps into this new forward request.
  assertEquals(0, channel.outgoingMaps_.length);
}


function testUndeliveredMaps_doesNotNotifyWhenSuccessful() {
  /**
   * @suppress {checkTypes} The callback function type declaration is skipped.
   */
  handler.channelClosed = function(
      channel, opt_pendingMaps, opt_undeliveredMaps) {
    if (opt_pendingMaps || opt_undeliveredMaps) {
      fail('No pending or undelivered maps should be reported.');
    }
  };

  connect();
  sendMap('foo1', 'bar1');
  responseDone();
  sendMap('foo2', 'bar2');
  responseDone();
  disconnect();
}


function testUndeliveredMaps_doesNotNotifyIfNothingWasSent() {
  /**
   * @suppress {checkTypes} The callback function type declaration is skipped.
   */
  handler.channelClosed = function(
      channel, opt_pendingMaps, opt_undeliveredMaps) {
    if (opt_pendingMaps || opt_undeliveredMaps) {
      fail('No pending or undelivered maps should be reported.');
    }
  };

  connect();
  mockClock.tick(ALL_DAY_MS);
  disconnect();
}


function testUndeliveredMaps_clearsPendingMapsAfterNotifying() {
  connect();
  sendMap('foo1', 'bar1');
  sendMap('foo2', 'bar2');
  sendMap('foo3', 'bar3');

  assertEquals(
      1, channel.forwardChannelRequestPool_.getPendingMessages().length);
  assertEquals(2, channel.outgoingMaps_.length);

  disconnect();

  assertEquals(
      0, channel.forwardChannelRequestPool_.getPendingMessages().length);
  assertEquals(0, channel.outgoingMaps_.length);
}


function testUndeliveredMaps_notifiesWithContext() {
  connect();

  // First send two messages that succeed.
  sendMap('foo1', 'bar1', 'context1');
  responseDone();
  sendMap('foo2', 'bar2', 'context2');
  responseDone();

  // Pretend the server hangs and no longer responds.
  sendMap('foo3', 'bar3', 'context3');
  sendMap('foo4', 'bar4', 'context4');
  sendMap('foo5', 'bar5', 'context5');

  // Give up.
  disconnect();

  // Assert that we are informed of any undelivered messages; both about
  // #3 that was sent but which we don't know if the server received, and
  // #4 and #5 which remain in the outgoing maps and have not yet been sent.
  assertEquals('foo3:bar3:context3', handler.pendingMapsString);
  assertEquals(
      'foo4:bar4:context4, foo5:bar5:context5', handler.undeliveredMapsString);
}


function testUndeliveredMaps_serviceUnavailable() {
  // Send a few maps, and let one fail.
  connect();
  sendMap('foo1', 'bar1');
  responseDone();
  sendMap('foo2', 'bar2');
  responseRequestFailed();

  // After a failure, the channel should be closed.
  disconnect();

  assertEquals('foo2:bar2', handler.pendingMapsString);
  assertEquals('', handler.undeliveredMapsString);
}


function testUndeliveredMaps_onPingTimeout() {
  stubNetUtils();

  connect();

  // Send a message.
  sendMap('foo1', 'bar1');

  // Fake REQUEST_FAILED, triggering a ping to check the network.
  responseRequestFailed();

  // Let the ping time out, unsuccessfully.
  mockClock.tick(goog.labs.net.webChannel.netUtils.NETWORK_TIMEOUT);

  // Assert channel is closed.
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());

  // Assert that the handler is notified about the undelivered messages.
  assertEquals('foo1:bar1', handler.pendingMapsString);
  assertEquals('', handler.undeliveredMapsString);
}


function testResponseNoBackchannelPostNotBeforeBackchannel() {
  connect(8);
  sendMap('foo1', 'bar1');

  mockClock.tick(10);
  assertFalse(
      channel.backChannelRequest_.getRequestStartTime() <
      getSingleForwardRequest().getRequestStartTime());
  responseNoBackchannel();
  assertNotEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_MISSING,
      lastStatEvent);
}


function testResponseNoBackchannel() {
  connect(8);
  sendMap('foo1', 'bar1');
  response(-1, 0);
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE + 1);
  sendMap('foo2', 'bar2');
  assertTrue(
      channel.backChannelRequest_.getRequestStartTime() +
          goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE <
      getSingleForwardRequest().getRequestStartTime());
  responseNoBackchannel();
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_MISSING,
      lastStatEvent);
}


function testResponseNoBackchannelWithNoBackchannel() {
  connect(8);
  sendMap('foo1', 'bar1');
  assertNull(channel.backChannelTimerId_);
  channel.backChannelRequest_.cancel();
  channel.backChannelRequest_ = null;
  responseNoBackchannel();
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_MISSING,
      lastStatEvent);
}


function testResponseNoBackchannelWithStartTimer() {
  connect(8);
  sendMap('foo1', 'bar1');

  channel.backChannelRequest_.cancel();
  channel.backChannelRequest_ = null;
  channel.backChannelTimerId_ = 123;
  responseNoBackchannel();
  assertNotEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_MISSING,
      lastStatEvent);
}


function testResponseWithNoArraySent() {
  connect(8);
  sendMap('foo1', 'bar1');

  // Send a response as if the server hasn't sent down an array.
  response(-1, 0);

  // POST response with an array ID lower than our last received is OK.
  assertEquals(1, channel.lastArrayId_);
  assertEquals(-1, channel.lastPostResponseArrayId_);
}


function testResponseWithArraysMissing() {
  connect(8);
  sendMap('foo1', 'bar1');
  assertEquals(-1, channel.lastPostResponseArrayId_);

  // Send a response as if the server has sent down seven arrays.
  response(7, 111);

  assertEquals(1, channel.lastArrayId_);
  assertEquals(7, channel.lastPostResponseArrayId_);
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE * 2);
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_DEAD,
      lastStatEvent);
}


function testMultipleResponsesWithArraysMissing() {
  connect(8);
  sendMap('foo1', 'bar1');
  assertEquals(-1, channel.lastPostResponseArrayId_);

  // Send a response as if the server has sent down seven arrays.
  response(7, 111);

  assertEquals(1, channel.lastArrayId_);
  assertEquals(7, channel.lastPostResponseArrayId_);
  sendMap('foo2', 'bar2');
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE);
  response(8, 119);
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE);
  // The original timer should still fire.
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_DEAD,
      lastStatEvent);
}


function testOnlyRetryOnceBasedOnResponse() {
  connect(8);
  sendMap('foo1', 'bar1');
  assertEquals(-1, channel.lastPostResponseArrayId_);

  // Send a response as if the server has sent down seven arrays.
  response(7, 111);

  assertEquals(1, channel.lastArrayId_);
  assertEquals(7, channel.lastPostResponseArrayId_);
  assertTrue(hasDeadBackChannelTimer());
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE * 2);
  assertEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_DEAD,
      lastStatEvent);
  assertEquals(1, channel.backChannelRetryCount_);
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE);
  sendMap('foo2', 'bar2');
  assertFalse(hasDeadBackChannelTimer());
  response(8, 119);
  assertFalse(hasDeadBackChannelTimer());
}


function testResponseWithArraysMissingAndLiveChannel() {
  connect(8);
  sendMap('foo1', 'bar1');
  assertEquals(-1, channel.lastPostResponseArrayId_);

  // Send a response as if the server has sent down seven arrays.
  response(7, 111);

  assertEquals(1, channel.lastArrayId_);
  assertEquals(7, channel.lastPostResponseArrayId_);
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE);
  assertTrue(hasDeadBackChannelTimer());
  receive('["ack"]');
  assertFalse(hasDeadBackChannelTimer());
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE);
  assertNotEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_DEAD,
      lastStatEvent);
}


function testResponseWithBigOutstandingData() {
  connect(8);
  sendMap('foo1', 'bar1');
  assertEquals(-1, channel.lastPostResponseArrayId_);

  // Send a response as if the server has sent down seven arrays and 50kbytes.
  response(7, 50000);

  assertEquals(1, channel.lastArrayId_);
  assertEquals(7, channel.lastPostResponseArrayId_);
  assertFalse(hasDeadBackChannelTimer());
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE * 2);
  assertNotEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_DEAD,
      lastStatEvent);
}


function testResponseInBufferedMode() {
  connect(8);
  channel.useChunked_ = false;
  sendMap('foo1', 'bar1');
  assertEquals(-1, channel.lastPostResponseArrayId_);
  response(7, 111);

  assertEquals(1, channel.lastArrayId_);
  assertEquals(7, channel.lastPostResponseArrayId_);
  assertFalse(hasDeadBackChannelTimer());
  mockClock.tick(goog.labs.net.webChannel.WebChannelBase.RTT_ESTIMATE * 2);
  assertNotEquals(
      goog.labs.net.webChannel.requestStats.Stat.BACKCHANNEL_DEAD,
      lastStatEvent);
}


function testResponseWithGarbage() {
  connect(8);
  sendMap('foo1', 'bar1');
  channel.onRequestData(getSingleForwardRequest(), 'garbage');
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());
}


function testResponseWithGarbageInArray() {
  connect(8);
  sendMap('foo1', 'bar1');
  channel.onRequestData(getSingleForwardRequest(), '["garbage"]');
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());
}


function testResponseWithEvilData() {
  connect(8);
  sendMap('foo1', 'bar1');
  channel.onRequestData(
      getSingleForwardRequest(), 'foo=<script>evil()\<\/script>&' +
          'bar=<script>moreEvil()\<\/script>');
  assertEquals(
      goog.labs.net.webChannel.WebChannelBase.State.CLOSED, channel.getState());
}


function testPathAbsolute() {
  connect(8, undefined, '/talkgadget');
  assertEquals(channel.backChannelUri_.getDomain(), window.location.hostname);
  assertEquals(
      channel.forwardChannelUri_.getDomain(), window.location.hostname);
}


function testPathRelative() {
  connect(8, undefined, 'talkgadget');
  assertEquals(channel.backChannelUri_.getDomain(), window.location.hostname);
  assertEquals(
      channel.forwardChannelUri_.getDomain(), window.location.hostname);
}


function testPathWithHost() {
  connect(8, undefined, 'https://example.com');
  assertEquals(channel.backChannelUri_.getScheme(), 'https');
  assertEquals(channel.backChannelUri_.getDomain(), 'example.com');
  assertEquals(channel.forwardChannelUri_.getScheme(), 'https');
  assertEquals(channel.forwardChannelUri_.getDomain(), 'example.com');
}

function testCreateXhrIo() {
  var xhr = channel.createXhrIo(null);
  assertFalse(xhr.getWithCredentials());

  assertThrows(
      'Error connection to different host without CORS',
      goog.bind(channel.createXhrIo, channel, 'some_host'));

  channel.setSupportsCrossDomainXhrs(true);

  xhr = channel.createXhrIo(null);
  assertTrue(xhr.getWithCredentials());

  xhr = channel.createXhrIo('some_host');
  assertTrue(xhr.getWithCredentials());
}

function testSpdyLimitOption() {
  var webChannelTransport =
      new goog.labs.net.webChannel.WebChannelBaseTransport();
  stubSpdyCheck(true);
  var webChannelDefault = webChannelTransport.createWebChannel('/foo');
  assertEquals(
      10, webChannelDefault.getRuntimeProperties().getConcurrentRequestLimit());
  assertTrue(webChannelDefault.getRuntimeProperties().isSpdyEnabled());

  var options = {'concurrentRequestLimit': 100};

  stubSpdyCheck(false);
  var webChannelDisabled =
      webChannelTransport.createWebChannel('/foo', options);
  assertEquals(
      1, webChannelDisabled.getRuntimeProperties().getConcurrentRequestLimit());
  assertFalse(webChannelDisabled.getRuntimeProperties().isSpdyEnabled());

  stubSpdyCheck(true);
  var webChannelEnabled = webChannelTransport.createWebChannel('/foo', options);
  assertEquals(
      100,
      webChannelEnabled.getRuntimeProperties().getConcurrentRequestLimit());
  assertTrue(webChannelEnabled.getRuntimeProperties().isSpdyEnabled());
}
