/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */
goog.module('goog.async.nextTickTest');
goog.setTestOnly();

const ErrorHandler = goog.require('goog.debug.ErrorHandler');
const GoogPromise = goog.require('goog.Promise');
const MockClock = goog.require('goog.testing.MockClock');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const TagName = goog.require('goog.dom.TagName');
const browser = goog.require('goog.labs.userAgent.browser');
const dom = goog.require('goog.dom');
const entryPointRegistry = goog.require('goog.debug.entryPointRegistry');
const nextTick = goog.require('goog.async.nextTick');
const testSuite = goog.require('goog.testing.testSuite');

let clock;
const propertyReplacer = new PropertyReplacer();

testSuite({
  setUp() {
    clock = null;
  },

  /** @suppress {visibility} */
  tearDown() {
    if (clock) {
      clock.uninstall();
    }
    // Unset the cached nextTickImpl behavior so it's re-evaluated for each
    // test.
    nextTick.nextTickImpl = /** @type {?} */ (undefined);
    propertyReplacer.reset();
  },

  testNextTick() {
    return new GoogPromise((resolve, reject) => {
      let c = 0;
      const max = 100;
      let async = true;
      const counterStep = (i) => {
        async = false;
        assertEquals('Order correct', i, c);
        c++;
        if (c === max) {
          resolve();
        }
      };
      for (let i = 0; i < max; i++) {
        nextTick(goog.partial(counterStep, i));
      }
      assertTrue(async);
    });
  },

  testNextTickSetImmediate() {
    return new GoogPromise((resolve, reject) => {
      let c = 0;
      const max = 100;
      let async = true;
      const counterStep = (i) => {
        async = false;
        assertEquals('Order correct', i, c);
        c++;
        if (c === max) {
          resolve();
        }
      };
      for (let i = 0; i < max; i++) {
        nextTick(
            goog.partial(counterStep, i), undefined,
            /* opt_useSetImmediate */ true);
      }
      assertTrue(async);
    });
  },

  testNextTickContext() {
    return new GoogPromise((resolve, reject) => {
      const context = {};
      let c = 0;
      const max = 10;
      let async = true;
      const counterStep = function(i) {
        async = false;
        assertEquals('Order correct', i, c);
        assertEquals(context, this);
        c++;
        if (c === max) {
          resolve();
        }
      };
      for (let i = 0; i < max; i++) {
        nextTick(goog.partial(counterStep, i), context);
      }
      assertTrue(async);
    });
  },

  testNextTickMockClock() {
    clock = new MockClock(true);
    let result = '';
    nextTick(() => {
      result += 'a';
    });
    nextTick(() => {
      result += 'b';
    });
    nextTick(() => {
      result += 'c';
    });
    assertEquals('', result);
    clock.tick(0);
    assertEquals('abc', result);
  },

  testNextTickDoesntSwallowError() {
    return new GoogPromise((resolve, reject) => {
      const sentinel = 'sentinel';

      propertyReplacer.replace(window, 'onerror', (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);
        }
        resolve();
        return false;
      });

      nextTick(() => {
        throw sentinel;
      });
    });
  },

  testNextTickProtectEntryPoint() {
    return new GoogPromise((resolve, reject) => {
      let errorHandlerCallbackCalled = false;
      const errorHandler = new ErrorHandler(() => {
        errorHandlerCallbackCalled = true;
      });

      // MS Edge will always use globalThis.setImmediate, so ensure we get
      // to setImmediate_ here. See useSetImmediate_ implementation for details
      // on Edge special casing.
      propertyReplacer.set(nextTick, 'useSetImmediate_', () => false);

      // This is only testing wrapping the callback with the protected entry
      // point, so it's okay to replace this function with a fake.
      propertyReplacer.set(nextTick, 'nextTickImpl', (cb) => {
        try {
          cb();
          fail('The callback should have thrown an error.');
        } catch (e) {
          assertTrue(errorHandlerCallbackCalled);
          assertTrue(e instanceof ErrorHandler.ProtectedFunctionError);
        } finally {
          // Restore setImmediate so it doesn't interfere with Promise behavior.
          propertyReplacer.reset();
        }
        resolve();
      });

      entryPointRegistry.monitorAll(errorHandler);
      nextTick(() => {
        throw new Error('This should be caught by the protected 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.
    let timeout;
    function busy() {
      timeout = setTimeout(() => {
        busy();
      }, 0);
    }
    busy();

    return new GoogPromise((resolve, reject) => {
      nextTick(() => {
        if (timeout) {
          clearTimeout(timeout);
        }
        resolve();
      });
    });
  },

  /**
   * 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).
   *
   * TODO(nickreid): Delete this test? It's testing for a an adversarial input
   * case and depend on deep implementation details.
   */
  testPostMessagePolyfillDoesNotPumpCallbackQueueIfMessageIsIncorrect() {
    // EDGE/IE does not use the postMessage polyfill.
    if (browser.isIE() || browser.isEdge()) {
      return;
    }

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

    let atNextTick = new GoogPromise(nextTick);

    const frame = dom.getElementsByTagName(TagName.IFRAME)[0];
    frame.contentWindow.postMessage(
        'bogus message',
        window.location.protocol + '//' + window.location.host);

    // The test passes if no error is ever reported by the <iframe>, as would
    // happen if the queue was pumped without an callbacks to run.
    frame.contentWindow.onerror = window.onerror;

    return atNextTick.thenAlways(() => {
      dom.removeNode(frame);
    });
  },

  testBehaviorOnPagesWithOverriddenWindowConstructor() {
    propertyReplacer.set(globalThis, 'Window', {});
    this.testNextTick();
    this.testNextTickSetImmediate();
    this.testNextTickMockClock();
  },
});
