/**
 * @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
 */

'use strict';

Zone.__load_patch('jest', (context: any, Zone: ZoneType) => {
  if (typeof jest === 'undefined' || jest['__zone_patch__']) {
    return;
  }

  jest['__zone_patch__'] = true;


  if (typeof Zone === 'undefined') {
    throw new Error('Missing Zone.js');
  }

  const ProxyZoneSpec = (Zone as any)['ProxyZoneSpec'];
  const SyncTestZoneSpec = (Zone as any)['SyncTestZoneSpec'];

  if (!ProxyZoneSpec) {
    throw new Error('Missing ProxyZoneSpec');
  }

  const rootZone = Zone.current;
  const syncZone = rootZone.fork(new SyncTestZoneSpec('jest.describe'));
  const proxyZone = rootZone.fork(new ProxyZoneSpec());

  function wrapDescribeFactoryInZone(originalJestFn: Function) {
    return function(this: unknown, ...tableArgs: any[]) {
      const originalDescribeFn = originalJestFn.apply(this, tableArgs);
      return function(this: unknown, ...args: any[]) {
        args[1] = wrapDescribeInZone(args[1]);
        return originalDescribeFn.apply(this, args);
      };
    };
  }

  function wrapTestFactoryInZone(originalJestFn: Function) {
    return function(this: unknown, ...tableArgs: any[]) {
      return function(this: unknown, ...args: any[]) {
        args[1] = wrapTestInZone(args[1]);
        return originalJestFn.apply(this, tableArgs).apply(this, args);
      };
    };
  }

  /**
   * Gets a function wrapping the body of a jest `describe` block to execute in a
   * synchronous-only zone.
   */
  function wrapDescribeInZone(describeBody: Function): Function {
    return function(this: unknown, ...args: any[]) {
      return syncZone.run(describeBody, this, args);
    };
  }

  /**
   * Gets a function wrapping the body of a jest `it/beforeEach/afterEach` block to
   * execute in a ProxyZone zone.
   * This will run in the `proxyZone`.
   */
  function wrapTestInZone(testBody: Function): Function {
    if (typeof testBody !== 'function') {
      return testBody;
    }
    const wrappedFunc = function() {
      return proxyZone.run(testBody, null, arguments as any);
    };
    // Update the length of wrappedFunc to be the same as the length of the testBody
    // So jest core can handle whether the test function has `done()` or not correctly
    Object.defineProperty(
        wrappedFunc, 'length', {configurable: true, writable: true, enumerable: false});
    wrappedFunc.length = testBody.length;
    return wrappedFunc;
  }

  ['describe', 'xdescribe', 'fdescribe'].forEach(methodName => {
    let originalJestFn: Function = context[methodName];
    if (context[Zone.__symbol__(methodName)]) {
      return;
    }
    context[Zone.__symbol__(methodName)] = originalJestFn;
    context[methodName] = function(this: unknown, ...args: any[]) {
      args[1] = wrapDescribeInZone(args[1]);
      return originalJestFn.apply(this, args);
    };
    context[methodName].each = wrapDescribeFactoryInZone((originalJestFn as any).each);
  });
  context.describe.only = context.fdescribe;
  context.describe.skip = context.xdescribe;

  ['it', 'xit', 'fit', 'test', 'xtest'].forEach(methodName => {
    let originalJestFn: Function = context[methodName];
    if (context[Zone.__symbol__(methodName)]) {
      return;
    }
    context[Zone.__symbol__(methodName)] = originalJestFn;
    context[methodName] = function(this: unknown, ...args: any[]) {
      args[1] = wrapTestInZone(args[1]);
      return originalJestFn.apply(this, args);
    };
    context[methodName].each = wrapTestFactoryInZone((originalJestFn as any).each);
    context[methodName].todo = (originalJestFn as any).todo;
  });

  context.it.only = context.fit;
  context.it.skip = context.xit;
  context.test.only = context.fit;
  context.test.skip = context.xit;

  ['beforeEach', 'afterEach', 'beforeAll', 'afterAll'].forEach(methodName => {
    let originalJestFn: Function = context[methodName];
    if (context[Zone.__symbol__(methodName)]) {
      return;
    }
    context[Zone.__symbol__(methodName)] = originalJestFn;
    context[methodName] = function(this: unknown, ...args: any[]) {
      args[0] = wrapTestInZone(args[0]);
      return originalJestFn.apply(this, args);
    };
  });
});
