'use strict';

function _exit() {
  const data = _interopRequireDefault(require('exit'));

  _exit = function _exit() {
    return data;
  };

  return data;
}

function _throat() {
  const data = _interopRequireDefault(require('throat'));

  _throat = function _throat() {
    return data;
  };

  return data;
}

function _jestWorker() {
  const data = _interopRequireDefault(require('jest-worker'));

  _jestWorker = function _jestWorker() {
    return data;
  };

  return data;
}

var _runTest = _interopRequireDefault(require('./runTest'));

var _testWorker = require('./testWorker');

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

function _objectSpread(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i] != null ? arguments[i] : {};
    var ownKeys = Object.keys(source);
    if (typeof Object.getOwnPropertySymbols === 'function') {
      ownKeys = ownKeys.concat(
        Object.getOwnPropertySymbols(source).filter(function(sym) {
          return Object.getOwnPropertyDescriptor(source, sym).enumerable;
        })
      );
    }
    ownKeys.forEach(function(key) {
      _defineProperty(target, key, source[key]);
    });
  }
  return target;
}

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function() {
    var self = this,
      args = arguments;
    return new Promise(function(resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
      }
      _next(undefined);
    });
  };
}

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;
}

const TEST_WORKER_PATH = require.resolve('./testWorker');

/* eslint-disable-next-line no-redeclare */
class TestRunner {
  constructor(globalConfig, context) {
    _defineProperty(this, '_globalConfig', void 0);

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

    this._globalConfig = globalConfig;
    this._context = context || {};
  }

  runTests(tests, watcher, onStart, onResult, onFailure, options) {
    var _this = this;

    return _asyncToGenerator(function*() {
      return yield options.serial
        ? _this._createInBandTestRun(
            tests,
            watcher,
            onStart,
            onResult,
            onFailure
          )
        : _this._createParallelTestRun(
            tests,
            watcher,
            onStart,
            onResult,
            onFailure
          );
    })();
  }

  _createInBandTestRun(tests, watcher, onStart, onResult, onFailure) {
    var _this2 = this;

    return _asyncToGenerator(function*() {
      process.env.JEST_WORKER_ID = '1';
      const mutex = (0, _throat().default)(1);
      return tests.reduce(
        (promise, test) =>
          mutex(() =>
            promise
              .then(
                /*#__PURE__*/
                _asyncToGenerator(function*() {
                  if (watcher.isInterrupted()) {
                    throw new CancelRun();
                  }

                  yield onStart(test);
                  return (0,
                  _runTest.default)(test.path, _this2._globalConfig, test.context.config, test.context.resolver, _this2._context);
                })
              )
              .then(result => onResult(test, result))
              .catch(err => onFailure(test, err))
          ),
        Promise.resolve()
      );
    })();
  }

  _createParallelTestRun(tests, watcher, onStart, onResult, onFailure) {
    var _this3 = this;

    return _asyncToGenerator(function*() {
      const resolvers = new Map();
      var _iteratorNormalCompletion = true;
      var _didIteratorError = false;
      var _iteratorError = undefined;

      try {
        for (
          var _iterator = tests[Symbol.iterator](), _step;
          !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
          _iteratorNormalCompletion = true
        ) {
          const test = _step.value;

          if (!resolvers.has(test.context.config.name)) {
            resolvers.set(test.context.config.name, {
              config: test.context.config,
              serializableModuleMap: test.context.moduleMap.toJSON()
            });
          }
        }
      } catch (err) {
        _didIteratorError = true;
        _iteratorError = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion && _iterator.return != null) {
            _iterator.return();
          }
        } finally {
          if (_didIteratorError) {
            throw _iteratorError;
          }
        }
      }

      const worker = new (_jestWorker()).default(TEST_WORKER_PATH, {
        exposedMethods: ['worker'],
        forkOptions: {
          stdio: 'pipe'
        },
        maxRetries: 3,
        numWorkers: _this3._globalConfig.maxWorkers,
        setupArgs: [
          {
            serializableResolvers: Array.from(resolvers.values())
          }
        ]
      });
      if (worker.getStdout()) worker.getStdout().pipe(process.stdout);
      if (worker.getStderr()) worker.getStderr().pipe(process.stderr);
      const mutex = (0, _throat().default)(_this3._globalConfig.maxWorkers); // Send test suites to workers continuously instead of all at once to track
      // the start time of individual tests.

      const runTestInWorker = test =>
        mutex(
          /*#__PURE__*/
          _asyncToGenerator(function*() {
            if (watcher.isInterrupted()) {
              return Promise.reject();
            }

            yield onStart(test);
            return worker.worker({
              config: test.context.config,
              context: _objectSpread({}, _this3._context, {
                changedFiles:
                  _this3._context.changedFiles &&
                  Array.from(_this3._context.changedFiles)
              }),
              globalConfig: _this3._globalConfig,
              path: test.path
            });
          })
        );

      const onError =
        /*#__PURE__*/
        (function() {
          var _ref3 = _asyncToGenerator(function*(err, test) {
            yield onFailure(test, err);

            if (err.type === 'ProcessTerminatedError') {
              console.error(
                'A worker process has quit unexpectedly! ' +
                  'Most likely this is an initialization error.'
              );
              (0, _exit().default)(1);
            }
          });

          return function onError(_x, _x2) {
            return _ref3.apply(this, arguments);
          };
        })();

      const onInterrupt = new Promise((_, reject) => {
        watcher.on('change', state => {
          if (state.interrupted) {
            reject(new CancelRun());
          }
        });
      });
      const runAllTests = Promise.all(
        tests.map(test =>
          runTestInWorker(test)
            .then(testResult => onResult(test, testResult))
            .catch(error => onError(error, test))
        )
      );

      const cleanup = () => worker.end();

      return Promise.race([runAllTests, onInterrupt]).then(cleanup, cleanup);
    })();
  }
}

class CancelRun extends Error {
  constructor(message) {
    super(message);
    this.name = 'CancelRun';
  }
}

module.exports = TestRunner;