// Copyright 2009 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.

goog.provide('goog.debug.ErrorReporterTest');
goog.setTestOnly('goog.debug.ErrorReporterTest');

goog.require('goog.debug.Error');
goog.require('goog.debug.ErrorReporter');
goog.require('goog.debug.errorcontext');
goog.require('goog.events');
goog.require('goog.functions');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.TestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
goog.require('goog.userAgent.product');

MockXhrIo = function() {};

MockXhrIo.prototype.onReadyStateChangeEntryPoint_ = function() {};

MockXhrIo.protectEntryPoints = function() {};

MockXhrIo.lastUrl = null;

MockXhrIo.send = function(
    url, opt_callback, opt_method, opt_content, opt_headers, opt_timeInterval) {
  MockXhrIo.lastUrl = url;
  MockXhrIo.lastContent = opt_content;
  MockXhrIo.lastHeaders = opt_headers;
};

var errorReporter;
var originalSetTimeout = window.setTimeout;
var stubs = new goog.testing.PropertyReplacer();
var url = 'http://www.your.tst/more/bogus.js';
var encodedUrl = 'http%3A%2F%2Fwww.your.tst%2Fmore%2Fbogus.js';

function setUp() {
  // TODO(b/25875505): Fix unreported assertions (go/failonunreportedasserts).
  goog.testing.TestCase.getActiveTestCase().failOnUnreportedAsserts = false;

  stubs.set(goog.net, 'XhrIo', MockXhrIo);
  goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT = true;
}

function tearDown() {
  goog.dispose(errorReporter);
  stubs.reset();
  MockXhrIo.lastUrl = null;
}

function throwAnErrorWith(script, line, message, opt_stack) {
  var error = {message: message, fileName: script, lineNumber: line};
  if (opt_stack) {
    error['stack'] = opt_stack;
  }

  throw error;
}

function testsendErrorReport() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.sendErrorReport('message', 'filename.js', 123, 'trace');

  assertEquals(
      '/log?script=filename.js&error=message&line=123', MockXhrIo.lastUrl);
  assertEquals('trace=trace', MockXhrIo.lastContent);
}

function testsendErrorReportWithCustomSender() {
  var uri = null;
  var method = null;
  var content = null;
  var headers = null;
  function mockXhrSender(uriIn, methodIn, contentIn, headersIn) {
    uri = uriIn;
    method = methodIn;
    content = contentIn;
    headers = headersIn;
  }

  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setXhrSender(mockXhrSender);
  errorReporter.sendErrorReport('message', 'filename.js', 123, 'trace');

  assertEquals('/log?script=filename.js&error=message&line=123', uri);
  assertEquals('POST', method);
  assertEquals('trace=trace', content);
  assertUndefined(headers);
}

function testsendErrorReport_noTrace() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.sendErrorReport('message', 'filename.js', 123);

  assertEquals(
      '/log?script=filename.js&error=message&line=123', MockXhrIo.lastUrl);
  assertEquals('', MockXhrIo.lastContent);
}

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

  stubs.set(goog.userAgent, 'IE', false);
  stubs.set(goog.global, 'setTimeout', function(fcn, time) { fcn.call(); });

  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');

  var errorFunction = goog.partial(throwAnErrorWith, url, 5, 'Hello :)');

  try {
    goog.global.setTimeout(errorFunction, 0);
  } catch (e) {
    // Expected. The error is rethrown after sending.
  }

  assertEquals(
      '/errorreporter?script=' + encodedUrl + '&error=Hello%20%3A)&line=5',
      MockXhrIo.lastUrl);
  assertEquals('trace=Not%20available', MockXhrIo.lastContent);
}

function test_internetExplorerSendErrorReport() {
  stubs.set(goog.userAgent, 'IE', true);
  stubs.set(goog.userAgent, 'isVersionOrHigher', goog.functions.FALSE);

  // Remove test runner's onerror handler so the test doesn't fail.
  stubs.set(goog.global, 'onerror', null);

  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  goog.global.onerror('Goodbye :(', url, 22);
  assertEquals(
      '/errorreporter?script=' + encodedUrl + '&error=Goodbye%20%3A(&line=22',
      MockXhrIo.lastUrl);
  assertEquals('trace=Not%20available', MockXhrIo.lastContent);
}

function test_setLoggingHeaders() {
  stubs.set(goog.userAgent, 'IE', true);
  stubs.set(goog.userAgent, 'isVersionOrHigher', goog.functions.FALSE);
  // Remove test runner's onerror handler so the test doesn't fail.
  stubs.set(goog.global, 'onerror', null);

  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  errorReporter.setLoggingHeaders('header!');
  goog.global.onerror('Goodbye :(', 'http://www.your.tst/more/bogus.js', 22);
  assertEquals('header!', MockXhrIo.lastHeaders);
}

function test_nonInternetExplorerSendErrorReportWithTrace() {
  stubs.set(goog.userAgent, 'IE', false);
  stubs.set(goog.global, 'setTimeout', function(fcn, time) { fcn.call(); });

  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');

  var trace = 'Error(\"Something Wrong\")@:0\n' +
      '$MF$E$Nx$([object Object])@http://a.b.c:83/a/f.js:901\n' +
      '([object Object])@http://a.b.c:813/a/f.js:37';


  var errorFunction = goog.partial(throwAnErrorWith, url, 5, 'Hello :)', trace);

  try {
    goog.global.setTimeout(errorFunction, 0);
  } catch (e) {
    // Expected. The error is rethrown after sending.
  }

  assertEquals(
      '/errorreporter?script=' + encodedUrl + '&error=Hello%20%3A)&line=5',
      MockXhrIo.lastUrl);
  assertEquals(
      'trace=' +
          'Error(%22Something%20Wrong%22)%40%3A0%0A' +
          '%24MF%24E%24Nx%24(%5Bobject%20Object%5D)%40' +
          'http%3A%2F%2Fa.b.c%3A83%2Fa%2Ff.js%3A901%0A' +
          '(%5Bobject%20Object%5D)%40http%3A%2F%2Fa.b.c%3A813%2Fa%2Ff.js%3A37',
      MockXhrIo.lastContent);
}

function testProtectAdditionalEntryPoint_nonIE() {
  stubs.set(goog.userAgent, 'IE', false);

  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  var fn = function() {};
  var protectedFn = errorReporter.protectAdditionalEntryPoint(fn);
  assertNotNull(protectedFn);
  assertNotEquals(fn, protectedFn);
}

function testProtectAdditionalEntryPoint_IE() {
  stubs.set(goog.userAgent, 'IE', true);
  stubs.set(goog.userAgent, 'isVersionOrHigher', goog.functions.FALSE);

  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  var fn = function() {};
  var protectedFn = errorReporter.protectAdditionalEntryPoint(fn);
  assertNull(protectedFn);
}

function testHandleException_dispatchesEvent() {
  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  var loggedErrors = 0;
  goog.events.listen(
      errorReporter, goog.debug.ErrorReporter.ExceptionEvent.TYPE,
      function(event) {
        assertNotNullNorUndefined(event.error);
        loggedErrors++;
      });
  errorReporter.handleException(new Error());
  errorReporter.handleException(new Error());
  assertEquals(
      'Expected 2 errors. ' +
          '(Ensure an exception was not swallowed.)',
      2, loggedErrors);
}

function testHandleException_includesContext() {
  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  var loggedErrors = 0;
  var testError = new Error('test error');
  var testContext = {'contextParam': 'contextValue'};
  goog.events.listen(
      errorReporter, goog.debug.ErrorReporter.ExceptionEvent.TYPE,
      function(event) {
        assertNotNullNorUndefined(event.error);
        assertObjectEquals({contextParam: 'contextValue'}, event.context);
        loggedErrors++;
      });
  errorReporter.handleException(testError, testContext);
  assertEquals(
      'Expected 1 error. ' +
          '(Ensure an exception was not swallowed.)',
      1, loggedErrors);
}

function testContextProvider() {
  errorReporter = goog.debug.ErrorReporter.install(
      '/errorreporter',
      function(error, context) { context.providedContext = 'value'; });
  var loggedErrors = 0;
  var testError = new Error('test error');
  goog.events.listen(
      errorReporter, goog.debug.ErrorReporter.ExceptionEvent.TYPE,
      function(event) {
        assertNotNullNorUndefined(event.error);
        assertObjectEquals({providedContext: 'value'}, event.context);
        loggedErrors++;
      });
  errorReporter.handleException(testError);
  assertEquals(
      'Expected 1 error. ' +
          '(Ensure an exception was not swallowed.)',
      1, loggedErrors);
}

function testContextProvider_withOtherContext() {
  errorReporter = goog.debug.ErrorReporter.install(
      '/errorreporter',
      function(error, context) { context.providedContext = 'value'; });
  var loggedErrors = 0;
  var testError = new Error('test error');
  goog.events.listen(
      errorReporter, goog.debug.ErrorReporter.ExceptionEvent.TYPE,
      function(event) {
        assertNotNullNorUndefined(event.error);
        assertObjectEquals(
            {providedContext: 'value', otherContext: 'value'}, event.context);
        loggedErrors++;
      });
  errorReporter.handleException(testError, {'otherContext': 'value'});
  assertEquals(
      'Expected 1 error. ' +
          '(Ensure an exception was not swallowed.)',
      1, loggedErrors);
}

function testErrorWithContext() {
  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  var loggedErrors = 0;
  var testError = new Error('test error');
  goog.debug.errorcontext.addErrorContext(testError, 'key1', 'value1');
  goog.debug.errorcontext.addErrorContext(testError, 'animalType', 'dog');
  goog.events.listen(
      errorReporter, goog.debug.ErrorReporter.ExceptionEvent.TYPE,
      function(event) {
        assertNotNullNorUndefined(event.error);
        assertObjectEquals({key1: 'value1', animalType: 'dog'}, event.context);
        loggedErrors++;
      });
  errorReporter.handleException(testError);
  assertEquals(
      'Expected 1 error. ' +
          '(Ensure an exception was not swallowed.)',
      1, loggedErrors);
}

function testErrorWithDifferentContextSources() {
  errorReporter = goog.debug.ErrorReporter.install(
      '/errorreporter', function(error, context) {
        context.providedContext = 'provided ctx';
      });
  var loggedErrors = 0;
  var testError = new Error('test error');
  goog.debug.errorcontext.addErrorContext(
      testError, 'addErrorContext', 'some value');
  goog.events.listen(
      errorReporter, goog.debug.ErrorReporter.ExceptionEvent.TYPE,
      function(event) {
        assertNotNullNorUndefined(event.error);
        assertObjectEquals(
            {
              addErrorContext: 'some value',
              providedContext: 'provided ctx',
              handleExceptionContext: 'another value'
            },
            event.context);
        loggedErrors++;
      });
  errorReporter.handleException(
      testError, {handleExceptionContext: 'another value'});
  assertEquals(
      'Expected 1 error. ' +
          '(Ensure an exception was not swallowed.)',
      1, loggedErrors);
}

function testHandleException_ignoresExceptionsDuringEventDispatch() {
  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  goog.events.listen(
      errorReporter, goog.debug.ErrorReporter.ExceptionEvent.TYPE,
      function(event) { fail('This exception should be swallowed.'); });
  errorReporter.handleException(new Error());
}

function testHandleException_doNotReportErrorToServer() {
  var error = new goog.debug.Error();
  error.reportErrorToServer = false;
  errorReporter.handleException(error);
  assertNull(MockXhrIo.lastUrl);
}

function testDisposal() {
  errorReporter = goog.debug.ErrorReporter.install('/errorreporter');
  if (!goog.userAgent.IE) {
    assertNotEquals(originalSetTimeout, window.setTimeout);
  }
  goog.dispose(errorReporter);
  assertEquals(originalSetTimeout, window.setTimeout);
}

function testSetContextPrefix() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setContextPrefix('baz.');
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals('trace=trace&baz.foo=bar', MockXhrIo.lastContent);
}

function testTruncationLimit() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setTruncationLimit(6);
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals('trace=', MockXhrIo.lastContent);
}

function testZeroTruncationLimit() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setTruncationLimit(0);
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals('', MockXhrIo.lastContent);
}

function testTruncationLimitLargerThanBody() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setTruncationLimit(9999);
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals('trace=trace&context.foo=bar', MockXhrIo.lastContent);
}

function testSetNegativeTruncationLimit() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  assertThrows(function() { errorReporter.setTruncationLimit(-10); });
}

function testSetTruncationLimitNull() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setTruncationLimit(null);
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals('trace=trace&context.foo=bar', MockXhrIo.lastContent);
}

function testAttemptAutoProtectWithAllowAutoProtectOff() {
  goog.debug.ErrorReporter.ALLOW_AUTO_PROTECT = false;
  assertThrows(function() {
    errorReporter =
        new goog.debug.ErrorReporter('/log', function(e, context) {}, false);
  });
}

function testSetAdditionalArgumentsArgsEmptyObject() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setAdditionalArguments({});
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals(
      '/log?script=filename.js&error=message&line=123', MockXhrIo.lastUrl);
}

function testSetAdditionalArgumentsSingleArgument() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setAdditionalArguments({'extra': 'arg'});
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals(
      '/log?script=filename.js&error=message&line=123&extra=arg',
      MockXhrIo.lastUrl);
}

function testSetAdditionalArgumentsMultipleArguments() {
  errorReporter = new goog.debug.ErrorReporter('/log');
  errorReporter.setAdditionalArguments({'extra': 'arg', 'cat': 'dog'});
  errorReporter.sendErrorReport(
      'message', 'filename.js', 123, 'trace', {'foo': 'bar'});
  assertEquals(
      '/log?script=filename.js&error=message&line=123&extra=arg&cat=dog',
      MockXhrIo.lastUrl);
}
