'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});
exports.default = _default;

var _assert = require('assert');

var _chalk = _interopRequireDefault(require('chalk'));

var _jestMessageUtil = require('jest-message-util');

var _jestUtil = require('jest-util');

var _assertionErrorMessage = _interopRequireDefault(
  require('../assertionErrorMessage')
);

var _isError = _interopRequireDefault(require('../isError'));

var _queueRunner = _interopRequireDefault(require('../queueRunner'));

var _treeProcessor = _interopRequireDefault(require('../treeProcessor'));

function _interopRequireDefault(obj) {
  return obj && obj.__esModule ? obj : {default: obj};
}

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

function _default(j$) {
  var _temp;

  return (
    (_temp = class Env {
      constructor(_options) {
        _defineProperty(this, 'specFilter', void 0);

        _defineProperty(this, 'catchExceptions', void 0);

        _defineProperty(this, 'throwOnExpectationFailure', void 0);

        _defineProperty(this, 'catchingExceptions', void 0);

        _defineProperty(this, 'topSuite', void 0);

        _defineProperty(this, 'fail', void 0);

        _defineProperty(this, 'pending', void 0);

        _defineProperty(this, 'afterAll', void 0);

        _defineProperty(this, 'fit', void 0);

        _defineProperty(this, 'throwingExpectationFailures', void 0);

        _defineProperty(this, 'randomizeTests', void 0);

        _defineProperty(this, 'randomTests', void 0);

        _defineProperty(this, 'seed', void 0);

        _defineProperty(this, 'execute', void 0);

        _defineProperty(this, 'fdescribe', void 0);

        _defineProperty(this, 'spyOn', void 0);

        _defineProperty(this, 'beforeEach', void 0);

        _defineProperty(this, 'afterEach', void 0);

        _defineProperty(this, 'clearReporters', void 0);

        _defineProperty(this, 'addReporter', void 0);

        _defineProperty(this, 'it', void 0);

        _defineProperty(this, 'xdescribe', void 0);

        _defineProperty(this, 'xit', void 0);

        _defineProperty(this, 'beforeAll', void 0);

        _defineProperty(this, 'todo', void 0);

        _defineProperty(this, 'provideFallbackReporter', void 0);

        _defineProperty(this, 'allowRespy', void 0);

        _defineProperty(this, 'describe', void 0);

        let totalSpecsDefined = 0;
        let catchExceptions = true;
        const realSetTimeout = global.setTimeout;
        const realClearTimeout = global.clearTimeout;
        const runnableResources = {};
        const currentlyExecutingSuites = [];
        let currentSpec = null;
        let throwOnExpectationFailure = false;
        let random = false;
        let seed = null;
        let nextSpecId = 0;
        let nextSuiteId = 0;

        const getNextSpecId = function () {
          return 'spec' + nextSpecId++;
        };

        const getNextSuiteId = function () {
          return 'suite' + nextSuiteId++;
        };

        const topSuite = new j$.Suite({
          id: getNextSuiteId(),
          description: '',

          getTestPath() {
            return j$.testPath;
          }
        });
        let currentDeclarationSuite = topSuite;

        const currentSuite = function () {
          return currentlyExecutingSuites[currentlyExecutingSuites.length - 1];
        };

        const currentRunnable = function () {
          return currentSpec || currentSuite();
        };

        const reporter = new j$.ReportDispatcher([
          'jasmineStarted',
          'jasmineDone',
          'suiteStarted',
          'suiteDone',
          'specStarted',
          'specDone'
        ]);

        this.specFilter = function () {
          return true;
        };

        const defaultResourcesForRunnable = function (id, _parentRunnableId) {
          const resources = {
            spies: []
          };
          runnableResources[id] = resources;
        };

        const clearResourcesForRunnable = function (id) {
          spyRegistry.clearSpies();
          delete runnableResources[id];
        };

        const beforeAndAfterFns = function (suite) {
          return function () {
            let afters = [];
            let befores = [];

            while (suite) {
              befores = befores.concat(suite.beforeFns);
              afters = afters.concat(suite.afterFns);
              suite = suite.parentSuite;
            }

            return {
              befores: befores.reverse(),
              afters
            };
          };
        };

        const getSpecName = function (spec, suite) {
          const fullName = [spec.description];
          const suiteFullName = suite.getFullName();

          if (suiteFullName !== '') {
            fullName.unshift(suiteFullName);
          }

          return fullName.join(' ');
        };

        this.catchExceptions = function (value) {
          catchExceptions = !!value;
          return catchExceptions;
        };

        this.catchingExceptions = function () {
          return catchExceptions;
        };

        this.throwOnExpectationFailure = function (value) {
          throwOnExpectationFailure = !!value;
        };

        this.throwingExpectationFailures = function () {
          return throwOnExpectationFailure;
        };

        this.randomizeTests = function (value) {
          random = !!value;
        };

        this.randomTests = function () {
          return random;
        };

        this.seed = function (value) {
          if (value) {
            seed = value;
          }

          return seed;
        };

        const queueRunnerFactory = options => {
          options.clearTimeout = realClearTimeout;
          options.fail = this.fail;
          options.setTimeout = realSetTimeout;
          return (0, _queueRunner.default)(options);
        };

        this.topSuite = function () {
          return topSuite;
        };

        const uncaught = err => {
          if (currentSpec) {
            currentSpec.onException(err);
            currentSpec.cancel();
          } else {
            console.error('Unhandled error');
            console.error(err.stack);
          }
        };

        let oldListenersException;
        let oldListenersRejection;

        const executionSetup = function () {
          // Need to ensure we are the only ones handling these exceptions.
          oldListenersException = process
            .listeners('uncaughtException')
            .slice();
          oldListenersRejection = process
            .listeners('unhandledRejection')
            .slice();
          j$.process.removeAllListeners('uncaughtException');
          j$.process.removeAllListeners('unhandledRejection');
          j$.process.on('uncaughtException', uncaught);
          j$.process.on('unhandledRejection', uncaught);
        };

        const executionTeardown = function () {
          j$.process.removeListener('uncaughtException', uncaught);
          j$.process.removeListener('unhandledRejection', uncaught); // restore previous exception handlers

          oldListenersException.forEach(listener => {
            j$.process.on('uncaughtException', listener);
          });
          oldListenersRejection.forEach(listener => {
            j$.process.on('unhandledRejection', listener);
          });
        };

        this.execute = async function (runnablesToRun, suiteTree = topSuite) {
          if (!runnablesToRun) {
            if (focusedRunnables.length) {
              runnablesToRun = focusedRunnables;
            } else {
              runnablesToRun = [suiteTree.id];
            }
          }

          if (currentlyExecutingSuites.length === 0) {
            executionSetup();
          }

          const lastDeclarationSuite = currentDeclarationSuite;
          await (0, _treeProcessor.default)({
            nodeComplete(suite) {
              if (!suite.disabled) {
                clearResourcesForRunnable(suite.id);
              }

              currentlyExecutingSuites.pop();

              if (suite === topSuite) {
                reporter.jasmineDone({
                  failedExpectations: topSuite.result.failedExpectations
                });
              } else {
                reporter.suiteDone(suite.getResult());
              }
            },

            nodeStart(suite) {
              currentlyExecutingSuites.push(suite);
              defaultResourcesForRunnable(
                suite.id,
                suite.parentSuite && suite.parentSuite.id
              );

              if (suite === topSuite) {
                reporter.jasmineStarted({
                  totalSpecsDefined
                });
              } else {
                reporter.suiteStarted(suite.result);
              }
            },

            queueRunnerFactory,
            runnableIds: runnablesToRun,
            tree: suiteTree
          });
          currentDeclarationSuite = lastDeclarationSuite;

          if (currentlyExecutingSuites.length === 0) {
            executionTeardown();
          }
        };

        this.addReporter = function (reporterToAdd) {
          reporter.addReporter(reporterToAdd);
        };

        this.provideFallbackReporter = function (reporterToAdd) {
          reporter.provideFallbackReporter(reporterToAdd);
        };

        this.clearReporters = function () {
          reporter.clearReporters();
        };

        const spyRegistry = new j$.SpyRegistry({
          currentSpies() {
            if (!currentRunnable()) {
              throw new Error(
                'Spies must be created in a before function or a spec'
              );
            }

            return runnableResources[currentRunnable().id].spies;
          }
        });

        this.allowRespy = function (allow) {
          spyRegistry.allowRespy(allow);
        };

        this.spyOn = function (...args) {
          return spyRegistry.spyOn.apply(spyRegistry, args);
        };

        const suiteFactory = function (description) {
          const suite = new j$.Suite({
            id: getNextSuiteId(),
            description,
            parentSuite: currentDeclarationSuite,
            throwOnExpectationFailure,

            getTestPath() {
              return j$.testPath;
            }
          });
          return suite;
        };

        this.describe = function (description, specDefinitions) {
          const suite = suiteFactory(description);

          if (specDefinitions === undefined) {
            throw new Error(
              `Missing second argument. It must be a callback function.`
            );
          }

          if (typeof specDefinitions !== 'function') {
            throw new Error(
              `Invalid second argument, ${specDefinitions}. It must be a callback function.`
            );
          }

          if (specDefinitions.length > 0) {
            throw new Error('describe does not expect any arguments');
          }

          if (currentDeclarationSuite.markedPending) {
            suite.pend();
          }

          if (currentDeclarationSuite.markedTodo) {
            // @ts-expect-error TODO Possible error: Suite does not have todo method
            suite.todo();
          }

          addSpecsToSuite(suite, specDefinitions);
          return suite;
        };

        this.xdescribe = function (description, specDefinitions) {
          const suite = suiteFactory(description);
          suite.pend();
          addSpecsToSuite(suite, specDefinitions);
          return suite;
        };

        const focusedRunnables = [];

        this.fdescribe = function (description, specDefinitions) {
          const suite = suiteFactory(description);
          suite.isFocused = true;
          focusedRunnables.push(suite.id);
          unfocusAncestor();
          addSpecsToSuite(suite, specDefinitions);
          return suite;
        };

        const addSpecsToSuite = (suite, specDefinitions) => {
          const parentSuite = currentDeclarationSuite;
          parentSuite.addChild(suite);
          currentDeclarationSuite = suite;
          let declarationError = undefined;
          let describeReturnValue;

          try {
            describeReturnValue = specDefinitions.call(suite);
          } catch (e) {
            declarationError = e;
          } // TODO throw in Jest 25: declarationError = new Error

          if ((0, _jestUtil.isPromise)(describeReturnValue)) {
            console.log(
              (0, _jestMessageUtil.formatExecError)(
                new Error(
                  _chalk.default.yellow(
                    'Returning a Promise from "describe" is not supported. Tests must be defined synchronously.\n' +
                      'Returning a value from "describe" will fail the test in a future version of Jest.'
                  )
                ),
                {
                  rootDir: '',
                  testMatch: []
                },
                {
                  noStackTrace: false
                }
              )
            );
          } else if (describeReturnValue !== undefined) {
            console.log(
              (0, _jestMessageUtil.formatExecError)(
                new Error(
                  _chalk.default.yellow(
                    'A "describe" callback must not return a value.\n' +
                      'Returning a value from "describe" will fail the test in a future version of Jest.'
                  )
                ),
                {
                  rootDir: '',
                  testMatch: []
                },
                {
                  noStackTrace: false
                }
              )
            );
          }

          if (declarationError) {
            this.it('encountered a declaration exception', () => {
              throw declarationError;
            });
          }

          currentDeclarationSuite = parentSuite;
        };

        function findFocusedAncestor(suite) {
          while (suite) {
            if (suite.isFocused) {
              return suite.id;
            }

            suite = suite.parentSuite;
          }

          return null;
        }

        function unfocusAncestor() {
          const focusedAncestor = findFocusedAncestor(currentDeclarationSuite);

          if (focusedAncestor) {
            for (let i = 0; i < focusedRunnables.length; i++) {
              if (focusedRunnables[i] === focusedAncestor) {
                focusedRunnables.splice(i, 1);
                break;
              }
            }
          }
        }

        const specFactory = (description, fn, suite, timeout) => {
          totalSpecsDefined++;
          const spec = new j$.Spec({
            id: getNextSpecId(),
            beforeAndAfterFns: beforeAndAfterFns(suite),
            resultCallback: specResultCallback,

            getSpecName(spec) {
              return getSpecName(spec, suite);
            },

            getTestPath() {
              return j$.testPath;
            },

            onStart: specStarted,
            description,
            queueRunnerFactory,

            userContext() {
              return suite.clonedSharedUserContext();
            },

            queueableFn: {
              fn,

              timeout() {
                return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
              }
            },
            throwOnExpectationFailure
          });

          if (!this.specFilter(spec)) {
            spec.disable();
          }

          return spec;

          function specResultCallback(result) {
            clearResourcesForRunnable(spec.id);
            currentSpec = null;
            reporter.specDone(result);
          }

          function specStarted(spec) {
            currentSpec = spec;
            defaultResourcesForRunnable(spec.id, suite.id);
            reporter.specStarted(spec.result);
          }
        };

        this.it = function (description, fn, timeout) {
          if (typeof description !== 'string') {
            throw new Error(
              `Invalid first argument, ${description}. It must be a string.`
            );
          }

          if (fn === undefined) {
            throw new Error(
              'Missing second argument. It must be a callback function. Perhaps you want to use `test.todo` for a test placeholder.'
            );
          }

          if (typeof fn !== 'function') {
            throw new Error(
              `Invalid second argument, ${fn}. It must be a callback function.`
            );
          }

          const spec = specFactory(
            description,
            fn,
            currentDeclarationSuite,
            timeout
          );

          if (currentDeclarationSuite.markedPending) {
            spec.pend();
          } // When a test is defined inside another, jasmine will not run it.
          // This check throws an error to warn the user about the edge-case.

          if (currentSpec !== null) {
            throw new Error(
              `Tests cannot be nested. Test "${spec.description}" cannot run because it is nested within "${currentSpec.description}".`
            );
          }

          currentDeclarationSuite.addChild(spec);
          return spec;
        };

        this.xit = function (...args) {
          const spec = this.it.apply(this, args);
          spec.pend('Temporarily disabled with xit');
          return spec;
        };

        this.todo = function () {
          const description = arguments[0];

          if (arguments.length !== 1 || typeof description !== 'string') {
            throw new _jestUtil.ErrorWithStack(
              'Todo must be called with only a description.',
              this.todo
            );
          }

          const spec = specFactory(
            description,
            () => {},
            currentDeclarationSuite
          );
          spec.todo();
          currentDeclarationSuite.addChild(spec);
          return spec;
        };

        this.fit = function (description, fn, timeout) {
          const spec = specFactory(
            description,
            fn,
            currentDeclarationSuite,
            timeout
          );
          currentDeclarationSuite.addChild(spec);
          focusedRunnables.push(spec.id);
          unfocusAncestor();
          return spec;
        };

        this.beforeEach = function (beforeEachFunction, timeout) {
          currentDeclarationSuite.beforeEach({
            fn: beforeEachFunction,

            timeout() {
              return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
            }
          });
        };

        this.beforeAll = function (beforeAllFunction, timeout) {
          currentDeclarationSuite.beforeAll({
            fn: beforeAllFunction,

            timeout() {
              return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
            }
          });
        };

        this.afterEach = function (afterEachFunction, timeout) {
          currentDeclarationSuite.afterEach({
            fn: afterEachFunction,

            timeout() {
              return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
            }
          });
        };

        this.afterAll = function (afterAllFunction, timeout) {
          currentDeclarationSuite.afterAll({
            fn: afterAllFunction,

            timeout() {
              return timeout || j$._DEFAULT_TIMEOUT_INTERVAL;
            }
          });
        };

        this.pending = function (message) {
          let fullMessage = j$.Spec.pendingSpecExceptionMessage;

          if (message) {
            fullMessage += message;
          }

          throw fullMessage;
        };

        this.fail = function (error) {
          let checkIsError;
          let message;

          if (
            error instanceof _assert.AssertionError ||
            (error && error.name === _assert.AssertionError.name)
          ) {
            checkIsError = false; // @ts-expect-error TODO Possible error: j$.Spec does not have expand property

            message = (0, _assertionErrorMessage.default)(error, {
              expand: j$.Spec.expand
            });
          } else {
            const check = (0, _isError.default)(error);
            checkIsError = check.isError;
            message = check.message;
          }

          const errorAsErrorObject = checkIsError ? error : new Error(message);
          const runnable = currentRunnable();

          if (!runnable) {
            errorAsErrorObject.message =
              'Caught error after test environment was torn down\n\n' +
              errorAsErrorObject.message;
            throw errorAsErrorObject;
          }

          runnable.addExpectationResult(false, {
            matcherName: '',
            passed: false,
            expected: '',
            actual: '',
            message,
            error: errorAsErrorObject
          });
        };
      }
    }),
    _temp
  );
}
