/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.net.XhrIoTest');
goog.setTestOnly('goog.net.XhrIoTest');

const EntryPointMonitor = goog.require('goog.debug.EntryPointMonitor');
const ErrorHandler = goog.require('goog.debug.ErrorHandler');
const EventType = goog.require('goog.net.EventType');
const MockClock = goog.require('goog.testing.MockClock');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const ReadyState = goog.require('goog.net.XmlHttp.ReadyState');
const TestingNetXhrIo = goog.require('goog.testing.net.XhrIo');
const Uri = goog.require('goog.Uri');
const WrapperXmlHttpFactory = goog.require('goog.net.WrapperXmlHttpFactory');
const XhrIo = goog.require('goog.net.XhrIo');
const XmlHttp = goog.require('goog.net.XmlHttp');
const entryPointRegistry = goog.require('goog.debug.entryPointRegistry');
const events = goog.require('goog.events');
const functions = goog.require('goog.functions');
const object = goog.require('goog.object');
const product = goog.require('goog.userAgent.product');
const recordFunction = goog.require('goog.testing.recordFunction');
const string = goog.require('goog.string');
const testSuite = goog.require('goog.testing.testSuite');

function MockXmlHttp() {
  /**
   * The request headers for this XmlHttpRequest.
   * @type {!Object<string>}
   * @suppress {globalThis} suppression added to enable type checking
   */
  this.requestHeaders = {};

  /**
   * The response headers for this XmlHttpRequest.
   * @type {!Object<string>}
   * @suppress {globalThis} suppression added to enable type checking
   */
  this.responseHeaders = {};

  /**
   * @type {string}
   * @suppress {globalThis} suppression added to enable type checking
   */
  this.responseHeadersString = null;

  /**
   * The upload object associated with this XmlHttpRequest.
   * @type {!Object}
   * @suppress {globalThis} suppression added to enable type checking
   */
  this.upload = {};
}

MockXmlHttp.prototype.readyState = XmlHttp.ReadyState.UNINITIALIZED;

MockXmlHttp.prototype.status = 200;

MockXmlHttp.syncSend = false;

MockXmlHttp.prototype.send = function(opt_data) {
  this.readyState = XmlHttp.ReadyState.UNINITIALIZED;

  if (MockXmlHttp.syncSend) {
    this.complete();
  }
};

MockXmlHttp.prototype.complete = function() {
  this.readyState = XmlHttp.ReadyState.LOADING;
  this.onreadystatechange();

  this.readyState = XmlHttp.ReadyState.LOADED;
  this.onreadystatechange();

  this.readyState = XmlHttp.ReadyState.INTERACTIVE;
  this.onreadystatechange();

  this.readyState = XmlHttp.ReadyState.COMPLETE;
  this.onreadystatechange();
};


MockXmlHttp.prototype.open = function(verb, uri, async) {};

MockXmlHttp.prototype.abort = function() {};

MockXmlHttp.prototype.setRequestHeader = function(key, value) {
  this.requestHeaders[key] = value;
};

/**
 * @param {string} key
 * @return {?string}
 */
MockXmlHttp.prototype.getResponseHeader = function(key) {
  return key in this.responseHeaders ? this.responseHeaders[key] : null;
};

/** @return {?string} */
MockXmlHttp.prototype.getAllResponseHeaders = function() {
  return this.responseHeadersString;
};

let lastMockXmlHttp;
XmlHttp.setGlobalFactory(new WrapperXmlHttpFactory(
    function() {
      /** @suppress {checkTypes} suppression added to enable type checking */
      lastMockXmlHttp = new MockXmlHttp();
      return lastMockXmlHttp;
    },
    function() {
      return {};
    }));


const propertyReplacer = new PropertyReplacer();
let clock;
/** @suppress {visibility} suppression added to enable type checking */
const originalEntryPoint = XhrIo.prototype.onReadyStateChangeEntryPoint_;


testSuite({
  setUp() {
    lastMockXmlHttp = null;
    clock = new MockClock(true);
  },

  tearDown() {
    MockXmlHttp.syncSend = false;
    propertyReplacer.reset();
    clock.dispose();
    /** @suppress {visibility} suppression added to enable type checking */
    XhrIo.prototype.onReadyStateChangeEntryPoint_ = originalEntryPoint;
  },


  testSyncSend() {
    if (product.SAFARI) {
      // TODO(user): Disabled so we can get the rest of the Closure test
      // suite running in a continuous build. Will investigate later.
      return;
    }

    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertTrue('Should be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send('url');
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },

  testSyncSendFailure() {
    if (product.SAFARI) {
      // TODO(user): Disabled so we can get the rest of the Closure test
      // suite running in a continuous build. Will investigate later.
      return;
    }

    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertFalse('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send('url');
    lastMockXmlHttp.status = 404;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendRelativeZeroStatus() {
    if (product.SAFARI) {
      // TODO(user): Disabled so we can get the rest of the Closure test
      // suite running in a continuous build. Will investigate later.
      return;
    }

    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertEquals(
          'Should be the same as ', e.target.isSuccess(),
          window.location.href.toLowerCase().indexOf('file:') == 0);
      count++;
    });

    let inSend = true;
    x.send('relative');
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendRelativeUriZeroStatus() {
    if (product.SAFARI) {
      // TODO(user): Disabled so we can get the rest of the Closure test
      // suite running in a continuous build. Will investigate later.
      return;
    }

    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertEquals(
          'Should be the same as ', e.target.isSuccess(),
          window.location.href.toLowerCase().indexOf('file:') == 0);
      count++;
    });

    let inSend = true;
    x.send(Uri.parse('relative'));
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendHttpZeroStatusFailure() {
    if (product.SAFARI) {
      // TODO(user): Disabled so we can get the rest of the Closure test
      // suite running in a continuous build. Will investigate later.
      return;
    }

    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertFalse('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send('http://foo');
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendHttpUpperZeroStatusFailure() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertFalse('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send('HTTP://foo');
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendHttpUpperUriZeroStatusFailure() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertFalse('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send(Uri.parse('HTTP://foo'));
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendHttpUriZeroStatusFailure() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertFalse('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send(Uri.parse('http://foo'));
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendHttpUriZeroStatusFailure_upperCaseHTTP() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertFalse('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send(Uri.parse('HTTP://foo'));
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendHttpsZeroStatusFailure() {
    if (product.SAFARI) {
      // TODO(user): Disabled so we can get the rest of the Closure test
      // suite running in a continuous build. Will investigate later.
      return;
    }

    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertFalse('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send('https://foo');
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendFileUpperZeroStatusSuccess() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertTrue('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send('FILE:///foo');
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendFileUriZeroStatusSuccess() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertTrue('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send(Uri.parse('file:///foo'));
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendDummyUriZeroStatusSuccess() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertTrue('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send(Uri.parse('dummy:///foo'));
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendFileUpperUriZeroStatusSuccess() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertFalse('Should not fire complete from inside send', inSend);
      assertTrue('Should not be successful', e.target.isSuccess());
      count++;
    });

    let inSend = true;
    x.send(Uri.parse('FILE:///foo'));
    lastMockXmlHttp.status = 0;
    inSend = false;

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testSendFromListener() {
    MockXmlHttp.syncSend = true;
    let count = 0;

    const x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      count++;

      e = assertThrows(function() {
        x.send('url2');
      });
      assertEquals(
          '[goog.net.XhrIo] Object is active with another request=url' +
              '; newUri=url2',
          e.message);
    });

    x.send('url');

    clock.tick(1);  // callOnce(f, 0, ...)

    assertEquals('Complete should have been called once', 1, count);
  },


  testStatesDuringEvents() {
    if (product.SAFARI) {
      // TODO(user): Disabled so we can get the rest of the Closure test
      // suite running in a continuous build. Will investigate later.
      return;
    }

    MockXmlHttp.syncSend = true;

    const x = new XhrIo;
    let readyState = ReadyState.UNINITIALIZED;
    events.listen(x, EventType.READY_STATE_CHANGE, function(e) {
      readyState++;
      assertObjectEquals(e.target, x);
      assertEquals(x.getReadyState(), readyState);
      assertTrue(x.isActive());
    });
    events.listen(x, EventType.COMPLETE, function(e) {
      assertObjectEquals(e.target, x);
      assertTrue(x.isActive());
    });
    events.listen(x, EventType.SUCCESS, function(e) {
      assertObjectEquals(e.target, x);
      assertTrue(x.isActive());
    });
    events.listen(x, EventType.READY, function(e) {
      assertObjectEquals(e.target, x);
      assertFalse(x.isActive());
    });

    x.send('url');

    clock.tick(1);  // callOnce(f, 0, ...)
  },


  testProtectEntryPointCalledOnAsyncSend() {
    MockXmlHttp.syncSend = false;

    let errorHandlerCallbackCalled = false;
    const errorHandler = new ErrorHandler(function() {
      errorHandlerCallbackCalled = true;
    });

    XhrIo.protectEntryPoints(errorHandler);

    const x = new XhrIo;
    events.listen(x, EventType.READY_STATE_CHANGE, function(e) {
      throw new Error();
    });

    x.send('url');
    assertThrows(function() {
      lastMockXmlHttp.complete();
    });

    assertTrue(
        'Error handler callback should be called on async send.',
        errorHandlerCallbackCalled);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testXHRIsDiposedEvenIfAListenerThrowsAnExceptionOnComplete() {
    MockXmlHttp.syncSend = false;

    const x = new XhrIo;

    events.listen(x, EventType.COMPLETE, function(e) {
      throw new Error();
    }, false, x);

    x.send('url');
    assertThrows(function() {
      lastMockXmlHttp.complete();
    });

    // The XHR should have been disposed, even though the listener threw an
    // exception.
    assertNull(x.xhr_);
  },

  testDisposeInternalDoesNotAbortXhrRequestObjectWhenActiveIsFalse() {
    MockXmlHttp.syncSend = false;

    const originalAbort = XmlHttp.prototype.abort;
    let abortCalled = false;
    const x = new XhrIo;

    XmlHttp.prototype.abort = function() {
      abortCalled = true;
    };

    events.listen(x, EventType.COMPLETE, function(e) {
      /** @suppress {visibility} suppression added to enable type checking */
      this.active_ = false;
      this.dispose();
    }, false, x);

    x.send('url');
    lastMockXmlHttp.complete();

    XmlHttp.prototype.abort = originalAbort;
    assertFalse(abortCalled);
  },

  testCallingAbortFromWithinAbortCallbackDoesntLoop() {
    const x = new XhrIo;
    events.listen(x, EventType.ABORT, function(e) {
      x.abort();  // Shouldn't get a stack overflow
    });
    x.send('url');
    x.abort();
  },

  testPostSetsContentTypeHeader() {
    const x = new XhrIo;

    x.send('url', 'POST', 'content');
    const headers = lastMockXmlHttp.requestHeaders;
    assertEquals(1, object.getCount(headers));
    assertEquals(headers[XhrIo.CONTENT_TYPE_HEADER], XhrIo.FORM_CONTENT_TYPE);
  },

  testNonPostSetsContentTypeHeader() {
    const x = new XhrIo;

    x.send('url', 'PUT', 'content');
    const headers = lastMockXmlHttp.requestHeaders;
    assertEquals(1, object.getCount(headers));
    assertEquals(headers[XhrIo.CONTENT_TYPE_HEADER], XhrIo.FORM_CONTENT_TYPE);
  },

  testContentTypeIsTreatedCaseInsensitively() {
    const x = new XhrIo;

    x.send('url', 'POST', 'content', {'content-type': 'testing'});

    assertObjectEquals(
        'Headers should not be modified since they already contain a ' +
            'content type definition',
        {'content-type': 'testing'}, lastMockXmlHttp.requestHeaders);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testPostFormDataDoesNotSetContentTypeHeader() {
    function FakeFormData() {}

    propertyReplacer.set(globalThis, 'FormData', FakeFormData);

    const x = new XhrIo;
    x.send('url', 'POST', new FakeFormData());
    const headers = lastMockXmlHttp.requestHeaders;
    assertTrue(object.isEmpty(headers));
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testNonPostFormDataDoesNotSetContentTypeHeader() {
    function FakeFormData() {}

    propertyReplacer.set(globalThis, 'FormData', FakeFormData);

    const x = new XhrIo;
    x.send('url', 'PUT', new FakeFormData());
    const headers = lastMockXmlHttp.requestHeaders;
    assertTrue(object.isEmpty(headers));
  },

  testFactoryInjection() {
    /** @suppress {checkTypes} suppression added to enable type checking */
    const xhr = new MockXmlHttp();
    let optionsFactoryCalled = 0;
    let xhrFactoryCalled = 0;
    const wrapperFactory = new WrapperXmlHttpFactory(
        function() {
          xhrFactoryCalled++;
          return xhr;
        },
        function() {
          optionsFactoryCalled++;
          return {};
        });
    const xhrIo = new XhrIo(wrapperFactory);

    xhrIo.send('url');

    assertEquals('XHR factory should have been called', 1, xhrFactoryCalled);
    assertEquals(
        'Options factory should have been called', 1, optionsFactoryCalled);
  },

  testGoogTestingNetXhrIoIsInSync() {
    const xhrIo = new XhrIo();
    const testingXhrIo = new TestingNetXhrIo();

    const propertyComparator = function(value, key, obj) {
      if (string.endsWith(key, '_')) {
        // Ignore private properties/methods
        return true;
      } else if (typeof value == 'function' && typeof this[key] != 'function') {
        // Only type check is sufficient for functions
        fail(
            'Mismatched property:' + key + ': XhrIo has:<' + value +
            '>; while goog.testing.net.XhrIo has:<' + this[key] + '>');
        return true;
      } else {
        // Ignore all other type of properties.
        return true;
      }
    };

    object.every(xhrIo, propertyComparator, testingXhrIo);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testEntryPointRegistry() {
    /** @suppress {checkTypes} suppression added to enable type checking */
    const monitor = new EntryPointMonitor();
    const replacement = function() {};
    monitor.wrap = recordFunction(functions.constant(replacement));

    entryPointRegistry.monitorAll(monitor);
    assertTrue(monitor.wrap.getCallCount() >= 1);
    assertEquals(replacement, XhrIo.prototype.onReadyStateChangeEntryPoint_);
  },

  testSetWithCredentials() {
    // Test on XHR objects that don't have the withCredentials property (older
    // browsers).
    let x = new XhrIo;
    x.setWithCredentials(true);
    x.send('url');
    assertFalse(
        'withCredentials should not be set on an XHR object if the property ' +
            'does not exist.',
        object.containsKey(lastMockXmlHttp, 'withCredentials'));

    // Test on XHR objects that have the withCredentials property.
    MockXmlHttp.prototype.withCredentials = false;
    x = new XhrIo;
    x.setWithCredentials(true);
    x.send('url');
    assertTrue(
        'withCredentials should be set on an XHR object if the property exists',
        object.containsKey(lastMockXmlHttp, 'withCredentials'));

    assertTrue(
        'withCredentials value not set on XHR object',
        lastMockXmlHttp.withCredentials);

    // Reset the prototype so it does not effect other tests.
    delete MockXmlHttp.prototype.withCredentials;
  },

  /**
     @suppress {visibility,missingProperties} suppression added to enable type
     checking
   */
  testSetProgressEventsEnabled() {
    // The default MockXhr object contained by the XhrIo object has no
    // reference to the necessary onprogress field. This is equivalent
    // to a browser which does not support progress events.
    const progressNotSupported = new XhrIo;
    progressNotSupported.setProgressEventsEnabled(true);
    assertTrue(progressNotSupported.getProgressEventsEnabled());
    progressNotSupported.send('url');
    assertUndefined(
        'Progress is not supported for downloads on this request.',
        progressNotSupported.xhr_.onprogress);
    assertUndefined(
        'Progress is not supported for uploads on this request.',
        progressNotSupported.xhr_.upload.onprogress);

    // The following tests will include the necessary onprogress fields
    // indicating progress events are supported.
    MockXmlHttp.prototype.onprogress = null;

    const progressDisabled = new XhrIo;
    progressDisabled.setProgressEventsEnabled(false);
    assertFalse(progressDisabled.getProgressEventsEnabled());
    progressDisabled.send('url');
    assertNull(
        'No progress handler should be set for downloads.',
        progressDisabled.xhr_.onprogress);
    assertUndefined(
        'No progress handler should be set for uploads.',
        progressDisabled.xhr_.upload.onprogress);

    const progressEnabled = new XhrIo;
    progressEnabled.setProgressEventsEnabled(true);
    assertTrue(progressEnabled.getProgressEventsEnabled());
    progressEnabled.send('url');
    assertTrue(
        'Progress handler should be set for downloads.',
        typeof progressEnabled.xhr_.onprogress === 'function');
    assertTrue(
        'Progress handler should be set for uploads.',
        typeof progressEnabled.xhr_.upload.onprogress === 'function');

    // Clean-up.
    delete MockXmlHttp.prototype.onprogress;
  },


  testGetResponse() {
    const x = new XhrIo;

    // No XHR yet
    assertEquals(null, x.getResponse());

    // XHR with no .response and no response type, gets text.
    /**
     * @suppress {visibility,checkTypes} suppression added to enable type
     * checking
     */
    x.xhr_ = {};
    /** @suppress {visibility} suppression added to enable type checking */
    x.xhr_.responseText = 'text';
    assertEquals('text', x.getResponse());

    // Response type of text gets text as well.
    x.setResponseType(XhrIo.ResponseType.TEXT);
    /** @suppress {visibility} suppression added to enable type checking */
    x.xhr_.responseText = '';
    assertEquals('', x.getResponse());

    // Response type of array buffer gets the array buffer.
    /**
     * @suppress {visibility, missingProperties} suppression added to enable
     * type checking
     */
    x.xhr_.mozResponseArrayBuffer = 'ab';
    x.setResponseType(XhrIo.ResponseType.ARRAY_BUFFER);
    assertEquals('ab', x.getResponse());

    // With a response field, it is returned no matter what value it has.
    /** @suppress {visibility} suppression added to enable type checking */
    x.xhr_.response = undefined;
    assertEquals(undefined, x.getResponse());

    /** @suppress {visibility} suppression added to enable type checking */
    x.xhr_.response = null;
    assertEquals(null, x.getResponse());

    /** @suppress {visibility} suppression added to enable type checking */
    x.xhr_.response = '';
    assertEquals('', x.getResponse());

    /** @suppress {visibility} suppression added to enable type checking */
    x.xhr_.response = 'resp';
    assertEquals('resp', x.getResponse());
  },

  testGetResponseHeader() {
    const x = new XhrIo();
    x.send('http://foo');

    /**
     * @suppress {visibility,strictMissingProperties} suppression added to
     * enable type checking
     */
    x.xhr_.responseHeaders['foo'] = null;
    /**
     * @suppress {visibility,strictMissingProperties} suppression added to
     * enable type checking
     */
    x.xhr_.responseHeaders['bar'] = 'xyz';
    /**
     * @suppress {visibility,strictMissingProperties} suppression added to
     * enable type checking
     */
    x.xhr_.responseHeaders['baz'] = '';

    // All headers should be undefined prior to the request completing.
    assertUndefined(x.getResponseHeader('foo'));
    assertUndefined(x.getResponseHeader('bar'));
    assertUndefined(x.getResponseHeader('baz'));

    /** @suppress {visibility} suppression added to enable type checking */
    x.xhr_.readyState = ReadyState.COMPLETE;

    assertUndefined(x.getResponseHeader('foo'));
    assertEquals('xyz', x.getResponseHeader('bar'));
    assertEquals('', x.getResponseHeader('baz'));
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetResponseHeaders() {
    MockXmlHttp.syncSend = true;
    const x = new XhrIo();

    // No XHR yet
    assertEquals(0, object.getCount(x.getResponseHeaders()));

    x.send();

    // Simulate an XHR with 2 headers.
    lastMockXmlHttp.responseHeadersString = 'test1: foo\r\ntest2: bar';

    const headers = x.getResponseHeaders();
    assertEquals(2, object.getCount(headers));
    assertEquals('foo', headers['test1']);
    assertEquals('bar', headers['test2']);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetResponseHeadersWithColonInValue() {
    MockXmlHttp.syncSend = true;
    const x = new XhrIo();

    x.send();

    // Simulate an XHR with a colon in the http header value.
    lastMockXmlHttp.responseHeadersString = 'test1: f:o : o';

    const headers = x.getResponseHeaders();
    assertEquals(1, object.getCount(headers));
    assertEquals('f:o : o', headers['test1']);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetResponseHeadersMultipleValuesForOneKey() {
    MockXmlHttp.syncSend = true;
    const x = new XhrIo();

    // No XHR yet
    assertEquals(0, object.getCount(x.getResponseHeaders()));

    x.send();

    // Simulate an XHR with 2 headers.
    lastMockXmlHttp.responseHeadersString = 'test1: foo\r\ntest1: bar';

    const headers = x.getResponseHeaders();
    assertEquals(1, object.getCount(headers));
    assertEquals('foo, bar', headers['test1']);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetResponseHeadersWhitespaceValue() {
    MockXmlHttp.syncSend = true;
    const x = new XhrIo();

    // No XHR yet
    assertEquals(0, object.getCount(x.getResponseHeaders()));

    x.send();

    // Simulate an XHR with whitespace as its value..
    lastMockXmlHttp.responseHeadersString = 'test2:   ';

    const headers = x.getResponseHeaders();
    assertEquals(1, object.getCount(headers));
    assertEquals('', headers['test2']);
  },

  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetResponseHeadersEmptyHeader() {
    MockXmlHttp.syncSend = true;
    const x = new XhrIo();

    // No XHR yet
    assertEquals(0, object.getCount(x.getResponseHeaders()));

    x.send();

    // Simulate an XHR with 2 headers, the last of which is empty.
    lastMockXmlHttp.responseHeadersString = 'test2: bar\r\n';

    const headers = x.getResponseHeaders();
    assertEquals(1, object.getCount(headers));
    assertEquals('bar', headers['test2']);
  },


  /** @suppress {checkTypes} suppression added to enable type checking */
  testGetResponseHeadersNullHeader() {
    MockXmlHttp.syncSend = true;

    const x = new XhrIo();

    // No XHR yet
    assertEquals(0, object.getCount(x.getResponseHeaders()));

    x.send();

    const headers = x.getResponseHeaders();
    assertEquals(0, object.getCount(headers));
  },
  testSetTrustTokenHeaderTrustTokenNotSupported() {
    const trustToken = {
      type: 'send-redemption-record',
      issuers: ['https://www.a.com', 'https://www.b.com'],
      refreshPolicy: 'none',
      signRequestData: 'include',
      includeTimestampHeader: true,
      additionalSignedHeaders: ['sec-time', 'Sec-Redemption-Record'],
      additionalSigningData: 'ENCODED_URL',
    };

    MockXmlHttp.syncSend = true;

    let x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertTrue('Should be successful', e.target.isSuccess());
      count++;
    });

    let count = 0;
    // Test on XHR objects that don't have the setTrustToken function (browser
    // doesn't support or disabled trust token).
    x.setTrustToken(trustToken);
    x.send('url');
    clock.tick(1);  // callOnce(f, 0, ...)
    assertEquals('Complete should have been called once', 1, count);
  },

  testSetTrustTokenHeaderTrustTokenSupported() {
    const trustToken = {
      type: 'send-redemption-record',
      issuers: ['https://www.a.com', 'https://www.b.com'],
      refreshPolicy: 'none',
      signRequestData: 'include',
      includeTimestampHeader: true,
      additionalSignedHeaders: ['sec-time', 'Sec-Redemption-Record'],
      additionalSigningData: 'ENCODED_URL',
    };

    MockXmlHttp.syncSend = true;

    let x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertTrue('Should be successful', e.target.isSuccess());
      count++;
    });

    let count = 0;

    // Test on XHR objects that have the setTrustToken function
    MockXmlHttp.prototype.setTrustToken = () => {};
    x = new XhrIo;
    events.listen(x, EventType.COMPLETE, function(e) {
      assertTrue('Should be successful', e.target.isSuccess());
      count++;
    });
    x.setTrustToken(trustToken);
    x.send('url');
    clock.tick(1);  // callOnce(f, 0, ...)
    assertEquals('Complete should have been called once', 1, count);
    // Reset the prototype so it does not effect other tests.
    delete MockXmlHttp.prototype.setTrustToken;
  },
});
