// 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.
goog.provide('goog.async.nextTickTest');
goog.setTestOnly('goog.async.nextTickTest');

goog.require('goog.async.nextTick');
goog.require('goog.debug.ErrorHandler');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.dom');
goog.require('goog.labs.userAgent.browser');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');

var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(
    'asyncNextTickTest');

var clock;
var propertyReplacer = new goog.testing.PropertyReplacer();

function setUp() {
  clock = null;
}

function tearDown() {
  if (clock) {
    clock.uninstall();
  }
  goog.async.nextTick.setImmediate_ = undefined;
  propertyReplacer.reset();
}


function testNextTick() {
  var c = 0;
  var max = 100;
  var async = true;
  var counterStep = function(i) {
    async = false;
    assertEquals('Order correct', i, c);
    c++;
    if (c === max) {
      asyncTestCase.continueTesting();
    }
  };
  for (var i = 0; i < max; i++) {
    goog.async.nextTick(goog.partial(counterStep, i));
  }
  assertTrue(async);
  asyncTestCase.waitForAsync('Wait for callback');
}


function testNextTickSetImmediate() {
  var c = 0;
  var max = 100;
  var async = true;
  var counterStep = function(i) {
    async = false;
    assertEquals('Order correct', i, c);
    c++;
    if (c === max) {
      asyncTestCase.continueTesting();
    }
  };
  for (var i = 0; i < max; i++) {
    goog.async.nextTick(goog.partial(counterStep, i), undefined,
        /* opt_useSetImmediate */ true);
  }
  assertTrue(async);
  asyncTestCase.waitForAsync('Wait for callback');
}

function testNextTickContext() {
  var context = {};
  var c = 0;
  var max = 10;
  var async = true;
  var counterStep = function(i) {
    async = false;
    assertEquals('Order correct', i, c);
    assertEquals(context, this);
    c++;
    if (c === max) {
      asyncTestCase.continueTesting();
    }
  };
  for (var i = 0; i < max; i++) {
    goog.async.nextTick(goog.partial(counterStep, i), context);
  }
  assertTrue(async);
  asyncTestCase.waitForAsync('Wait for callback');
}


function testNextTickMockClock() {
  clock = new goog.testing.MockClock(true);
  var result = '';
  goog.async.nextTick(function() {
    result += 'a';
  });
  goog.async.nextTick(function() {
    result += 'b';
  });
  goog.async.nextTick(function() {
    result += 'c';
  });
  assertEquals('', result);
  clock.tick(0);
  assertEquals('abc', result);
}


function testNextTickDoesntSwallowError() {
  var before = window.onerror;
  var sentinel = 'sentinel';
  window.onerror = function(e) {
    e = '' + e;
    // Don't test for contents in IE7, which does not preserve the exception
    // message.
    if (e.indexOf('Exception thrown and not caught') == -1) {
      assertContains(sentinel, e);
    }
    window.onerror = before;
    asyncTestCase.continueTesting();
    return false;
  };
  goog.async.nextTick(function() {
    throw sentinel;
  });
  asyncTestCase.waitForAsync('Wait for error');
}


function testNextTickProtectEntryPoint() {
  var errorHandlerCallbackCalled = false;
  var errorHandler = new goog.debug.ErrorHandler(function() {
    errorHandlerCallbackCalled = true;
  });

  // This is only testing wrapping the callback with the protected entry point,
  // so it's okay to replace this function with a fake.
  goog.async.nextTick.setImmediate_ = function(cb) {
    try {
      cb();
      fail('The callback should have thrown an error.');
    } catch (e) {
      assertTrue(
          e instanceof goog.debug.ErrorHandler.ProtectedFunctionError);
    }
    asyncTestCase.continueTesting();
  };
  var origSetImmediate;
  if (window.setImmediate) {
    origSetImmediate = window.setImmediate;
    window.setImmediate = goog.async.nextTick.setImmediate_;
  }
  goog.debug.entryPointRegistry.monitorAll(errorHandler);

  function thrower() {
    throw Error('This should be caught by the protected function.');
  }
  asyncTestCase.waitForAsync('Wait for callback');
  goog.async.nextTick(thrower);
  window.setImmediate = origSetImmediate;
}


function testNextTick_notStarvedBySetTimeout() {
  // This test will timeout when affected by
  // http://codeforhire.com/2013/09/21/setimmediate-and-messagechannel-broken-on-internet-explorer-10/
  // This test would fail without the fix introduced in cl/72472221
  // It keeps scheduling 0 timeouts and a single nextTick. If the nextTick
  // ever fires, the IE specific problem does not occur.
  var timeout;
  function busy() {
    timeout = setTimeout(function() {
      busy();
    }, 0);
  }
  busy();
  goog.async.nextTick(function() {
    if (timeout) {
      clearTimeout(timeout);
    }
    asyncTestCase.continueTesting();
  });
  asyncTestCase.waitForAsync('Waiting not to starve');
}


/**
 * Test a scenario in which the iframe used by the postMessage polyfill gets a
 * message that does not have match what is expected. In this case, the polyfill
 * should not try to invoke a callback (which would result in an error because
 * there would be no callbacks in the linked list).
 */
function testPostMessagePolyfillDoesNotPumpCallbackQueueIfMessageIsIncorrect() {
  // IE does not use the postMessage polyfill.
  if (goog.labs.userAgent.browser.isIE()) {
    return;
  }

  // Force postMessage pollyfill for setImmediate.
  propertyReplacer.set(window, 'setImmediate', undefined);
  propertyReplacer.set(window, 'MessageChannel', undefined);

  var callbackCalled = false;
  goog.async.nextTick(function() {
    callbackCalled = true;
  });

  var frame = document.getElementsByTagName('iframe')[0];
  frame.contentWindow.postMessage('bogus message',
      window.location.protocol + '//' + window.location.host);

  var error = null;
  frame.contentWindow.onerror = function(e) {
    error = e;
  };

  setTimeout(function() {
    assert('Callback should have been called.', callbackCalled);
    assertNull('An unexpected error was thrown.', error);
    goog.dom.removeNode(frame);
    asyncTestCase.continueTesting();
  }, 0);

  asyncTestCase.waitForAsync('Waiting for callbacks to be invoked.');
}


function testBehaviorOnPagesWithOverriddenWindowConstructor() {
  propertyReplacer.set(goog.global, 'Window', {});
  testNextTick();
  testNextTickSetImmediate();
  testNextTickMockClock();
}
