/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */
import '../zone-spec/fake-async-test';

Zone.__load_patch('fakeasync', (global: any, Zone: ZoneType, api: _ZonePrivate) => {
  const FakeAsyncTestZoneSpec = Zone && (Zone as any)['FakeAsyncTestZoneSpec'];
  type ProxyZoneSpec = {
    setDelegate(delegateSpec: ZoneSpec): void; getDelegate(): ZoneSpec; resetDelegate(): void;
  };
  const ProxyZoneSpec: {get(): ProxyZoneSpec; assertPresent: () => ProxyZoneSpec} =
      Zone && (Zone as any)['ProxyZoneSpec'];

  let _fakeAsyncTestZoneSpec: any = null;

  /**
   * Clears out the shared fake async zone for a test.
   * To be called in a global `beforeEach`.
   *
   * @experimental
   */
  function resetFakeAsyncZone() {
    if (_fakeAsyncTestZoneSpec) {
      _fakeAsyncTestZoneSpec.unlockDatePatch();
    }
    _fakeAsyncTestZoneSpec = null;
    // in node.js testing we may not have ProxyZoneSpec in which case there is nothing to reset.
    ProxyZoneSpec && ProxyZoneSpec.assertPresent().resetDelegate();
  }

  /**
   * Wraps a function to be executed in the fakeAsync zone:
   * - microtasks are manually executed by calling `flushMicrotasks()`,
   * - timers are synchronous, `tick()` simulates the asynchronous passage of time.
   *
   * If there are any pending timers at the end of the function, an exception will be thrown.
   *
   * Can be used to wrap inject() calls.
   *
   * ## Example
   *
   * {@example core/testing/ts/fake_async.ts region='basic'}
   *
   * @param fn
   * @returns The function wrapped to be executed in the fakeAsync zone
   *
   * @experimental
   */
  function fakeAsync(fn: Function): (...args: any[]) => any {
    // Not using an arrow function to preserve context passed from call site
    const fakeAsyncFn: any = function(this: unknown, ...args: any[]) {
      const proxyZoneSpec = ProxyZoneSpec.assertPresent();
      if (Zone.current.get('FakeAsyncTestZoneSpec')) {
        throw new Error('fakeAsync() calls can not be nested');
      }
      try {
        // in case jasmine.clock init a fakeAsyncTestZoneSpec
        if (!_fakeAsyncTestZoneSpec) {
          if (proxyZoneSpec.getDelegate() instanceof FakeAsyncTestZoneSpec) {
            throw new Error('fakeAsync() calls can not be nested');
          }

          _fakeAsyncTestZoneSpec = new FakeAsyncTestZoneSpec();
        }

        let res: any;
        const lastProxyZoneSpec = proxyZoneSpec.getDelegate();
        proxyZoneSpec.setDelegate(_fakeAsyncTestZoneSpec);
        _fakeAsyncTestZoneSpec.lockDatePatch();
        try {
          res = fn.apply(this, args);
          flushMicrotasks();
        } finally {
          proxyZoneSpec.setDelegate(lastProxyZoneSpec);
        }

        if (_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length > 0) {
          throw new Error(
              `${_fakeAsyncTestZoneSpec.pendingPeriodicTimers.length} ` +
              `periodic timer(s) still in the queue.`);
        }

        if (_fakeAsyncTestZoneSpec.pendingTimers.length > 0) {
          throw new Error(
              `${_fakeAsyncTestZoneSpec.pendingTimers.length} timer(s) still in the queue.`);
        }
        return res;
      } finally {
        resetFakeAsyncZone();
      }
    };
    (fakeAsyncFn as any).isFakeAsync = true;
    return fakeAsyncFn;
  }

  function _getFakeAsyncZoneSpec(): any {
    if (_fakeAsyncTestZoneSpec == null) {
      _fakeAsyncTestZoneSpec = Zone.current.get('FakeAsyncTestZoneSpec');
      if (_fakeAsyncTestZoneSpec == null) {
        throw new Error('The code should be running in the fakeAsync zone to call this function');
      }
    }
    return _fakeAsyncTestZoneSpec;
  }

  /**
   * Simulates the asynchronous passage of time for the timers in the fakeAsync zone.
   *
   * The microtasks queue is drained at the very start of this function and after any timer callback
   * has been executed.
   *
   * ## Example
   *
   * {@example core/testing/ts/fake_async.ts region='basic'}
   *
   * @experimental
   */
  function tick(millis: number = 0, ignoreNestedTimeout = false): void {
    _getFakeAsyncZoneSpec().tick(millis, null, ignoreNestedTimeout);
  }

  /**
   * Simulates the asynchronous passage of time for the timers in the fakeAsync zone by
   * draining the macrotask queue until it is empty. The returned value is the milliseconds
   * of time that would have been elapsed.
   *
   * @param maxTurns
   * @returns The simulated time elapsed, in millis.
   *
   * @experimental
   */
  function flush(maxTurns?: number): number {
    return _getFakeAsyncZoneSpec().flush(maxTurns);
  }

  /**
   * Discard all remaining periodic tasks.
   *
   * @experimental
   */
  function discardPeriodicTasks(): void {
    const zoneSpec = _getFakeAsyncZoneSpec();
    const pendingTimers = zoneSpec.pendingPeriodicTimers;
    zoneSpec.pendingPeriodicTimers.length = 0;
  }

  /**
   * Flush any pending microtasks.
   *
   * @experimental
   */
  function flushMicrotasks(): void {
    _getFakeAsyncZoneSpec().flushMicrotasks();
  }
  (Zone as any)[api.symbol('fakeAsyncTest')] =
      {resetFakeAsyncZone, flushMicrotasks, discardPeriodicTasks, tick, flush, fakeAsync};
});
