// Copyright 2008 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.ErrorHandlerTest');
goog.setTestOnly('goog.debug.ErrorHandlerTest');

goog.require('goog.debug.ErrorHandler');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.jsunit');

var oldGetObjectByName;

// provide our own window that implements our instrumented and
// immediate-call versions of setTimeout and setInterval
var fakeWin = {};

var errorHandler;
var mockControl;

function badTimer() {
  arguments.callee.called = true;
  throw 'die die die';
}

function setUp() {
  mockControl = new goog.testing.MockControl();
  // On IE, globalEval happens async. So make it synchronous.
  goog.globalEval = function(str) { eval(str); };

  oldGetObjectByName = goog.getObjectByName;
  goog.getObjectByName = function(name) {
    if (name == 'window') {
      return fakeWin;
    } else {
      return oldGetObjectByName(name);
    }
  };

  fakeWin.setTimeout = function(fn, time) {
    fakeWin.setTimeout.called = true;
    fakeWin.setTimeout.that = this;
    if (goog.isString(fn)) {
      eval(fn);
    } else {
      fn.apply(this, Array.prototype.slice.call(arguments, 2));
    }
  };

  fakeWin.setInterval = function(fn, time) {
    fakeWin.setInterval.called = true;
    fakeWin.setInterval.that = this;
    if (goog.isString(fn)) {
      eval(fn);
    } else {
      fn.apply(this, Array.prototype.slice.call(arguments, 2));
    }
  };

  fakeWin.requestAnimationFrame = function(fn) {
    fakeWin.requestAnimationFrame.called = true;
    fakeWin.requestAnimationFrame.that = this;
    fn();
  };

  // just record the exception in the error handler when it happens
  errorHandler = new goog.debug.ErrorHandler(function(ex) { this.ex = ex; });
}

function tearDown() {
  mockControl.$tearDown();
  goog.dispose(errorHandler);
  errorHandler = null;

  goog.getObjectByName = oldGetObjectByName;

  delete badTimer['__protected__'];
}

function testWrapSetTimeout() {
  errorHandler.protectWindowSetTimeout();

  var caught;

  try {
    fakeWin.setTimeout(badTimer, 3);
  } catch (ex) {
    caught = ex;
  }
  assertSetTimeoutError(caught);
}

function testWrapSetTimeoutWithoutException() {
  errorHandler.protectWindowSetTimeout();

  fakeWin.setTimeout(function(x, y) {
    assertEquals('test', x);
    assertEquals(7, y);
  }, 3, 'test', 7);
}

function testWrapSetTimeoutWithString() {
  errorHandler.protectWindowSetTimeout();

  var caught;

  try {
    fakeWin.setTimeout('badTimer()', 3);
  } catch (ex) {
    caught = ex;
  }
  assertSetTimeoutError(caught);
}

function testWrapSetInterval() {
  errorHandler.protectWindowSetInterval();

  var caught;

  try {
    fakeWin.setInterval(badTimer, 3);
  } catch (ex) {
    caught = ex;
  }
  assertSetIntervalError(caught);
}

function testWrapSetIntervalWithoutException() {
  errorHandler.protectWindowSetInterval();

  fakeWin.setInterval(function(x, y) {
    assertEquals('test', x);
    assertEquals(7, y);
  }, 3, 'test', 7);
}

function testWrapSetIntervalWithString() {
  errorHandler.protectWindowSetInterval();

  var caught;

  try {
    fakeWin.setInterval('badTimer()', 3);
  } catch (ex) {
    caught = ex;
  }
  assertSetIntervalError(caught);
}

function testWrapRequestAnimationFrame() {
  errorHandler.protectWindowRequestAnimationFrame();

  var caught;
  try {
    fakeWin.requestAnimationFrame(badTimer);
  } catch (ex) {
    caught = ex;
  }
  assertRequestAnimationFrameError(caught);
}

function testDisposal() {
  fakeWin = goog.getObjectByName('window');
  var originalSetTimeout = fakeWin.setTimeout;
  var originalSetInterval = fakeWin.setInterval;

  errorHandler.protectWindowSetTimeout();
  errorHandler.protectWindowSetInterval();

  assertNotEquals(originalSetTimeout, fakeWin.setTimeout);
  assertNotEquals(originalSetInterval, fakeWin.setInterval);

  errorHandler.dispose();

  assertEquals(originalSetTimeout, fakeWin.setTimeout);
  assertEquals(originalSetInterval, fakeWin.setInterval);
}

function testUnwrap() {
  var fn = function() {};
  var wrappedFn = errorHandler.wrap(fn);
  assertNotEquals(wrappedFn, fn);

  assertEquals(fn, errorHandler.unwrap(fn));
  assertEquals(fn, errorHandler.unwrap(wrappedFn));
}

function testStackPreserved() {
  var e;
  var hasStacks;
  function specialFunctionName() {
    var e = Error();
    hasStacks = !!e.stack;
    throw e;
  }
  var wrappedFn = errorHandler.wrap(specialFunctionName);
  try {
    wrappedFn();
  } catch (exception) {
    e = exception;
  }
  assertTrue(!!e);
  if (hasStacks) {
    assertContains('specialFunctionName', e.stack);
  }
}

function testGetProtectedFunction() {
  var fn = function() { throw new Error('Foo'); };
  var protectedFn = errorHandler.getProtectedFunction(fn);
  var e = assertThrows(protectedFn);
  assertTrue(e instanceof goog.debug.ErrorHandler.ProtectedFunctionError);
  assertEquals('Foo', e.cause.message);
}

function testGetProtectedFunctionNullError() {
  var fn = function() { throw null; };
  var protectedFn = errorHandler.getProtectedFunction(fn);
  var e = assertThrows(protectedFn);
  assertTrue(e instanceof goog.debug.ErrorHandler.ProtectedFunctionError);
  assertNull(e.cause);
}

function testGetProtectedFunction_withoutWrappedErrors() {
  var shouldCallErrorLog = !!Error.captureStackTrace;
  if (shouldCallErrorLog) {
    mockControl.createMethodMock(goog.global.console, 'error');
  }
  errorHandler.setWrapErrors(false);
  var fn = function() {
    var e = new Error('Foo');
    e.stack = 'STACK';
    throw e;
  };
  var protectedFn = errorHandler.getProtectedFunction(fn);
  if (shouldCallErrorLog) {
    goog.global.console.error('Foo', 'STACK');
  }
  mockControl.$replayAll();
  var e = assertThrows(protectedFn);
  mockControl.$verifyAll();
  assertTrue(e instanceof Error);
  assertEquals('Foo', e.message);
  assertEquals(e.stack, 'STACK');
}

function testGetProtectedFunction_withoutWrappedErrorsWithMessagePrefix() {
  errorHandler.setWrapErrors(false);
  errorHandler.setPrefixErrorMessages(true);
  var fn = function() { throw new Error('Foo'); };
  var protectedFn = errorHandler.getProtectedFunction(fn);
  var e = assertThrows(protectedFn);
  assertTrue(e instanceof Error);
  assertEquals(
      goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX + 'Foo',
      e.message);

  var stringError = function() { throw 'String'; };
  protectedFn = errorHandler.getProtectedFunction(stringError);
  e = assertThrows(protectedFn);
  assertEquals('string', typeof e);
  assertEquals(
      goog.debug.ErrorHandler.ProtectedFunctionError.MESSAGE_PREFIX + 'String',
      e);
}

function testProtectedFunction_infiniteLoop() {
  var numErrors = 0;
  var errorHandler = new goog.debug.ErrorHandler(function(ex) { numErrors++; });
  errorHandler.protectWindowSetTimeout();

  fakeWin.setTimeout(function() { fakeWin.setTimeout(badTimer, 3); }, 3);
  assertEquals(
      'Error handler should only have been executed once.', 1, numErrors);
}

function assertSetTimeoutError(caught) {
  assertMethodCalledHelper('setTimeout', caught);
}

function assertSetIntervalError(caught) {
  assertMethodCalledHelper('setInterval', caught);
}

function assertRequestAnimationFrameError(caught) {
  assertMethodCalledHelper('requestAnimationFrame', caught);
}

function assertMethodCalledHelper(method, caught) {
  assertTrue('exception not thrown', !!caught);
  assertEquals(
      'exception not caught by error handler', caught.cause, errorHandler.ex);
  assertTrue('fake ' + method + ' not called', !!fakeWin[method].called);
  assertTrue(
      '"this" not passed to original ' + method,
      fakeWin[method].that === fakeWin);
}
