'use strict';

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

  _exit = function () {
    return data;
  };

  return data;
}

function _chalk() {
  const data = _interopRequireDefault(require('chalk'));

  _chalk = function () {
    return data;
  };

  return data;
}

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

  _throat = function () {
    return data;
  };

  return data;
}

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

  _jestWorker = function () {
    return data;
  };

  return data;
}

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

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

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

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

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

  async runTests(tests, watcher, onStart, onResult, onFailure, options) {
    return await (options.serial
      ? this._createInBandTestRun(tests, watcher, onStart, onResult, onFailure)
      : this._createParallelTestRun(
          tests,
          watcher,
          onStart,
          onResult,
          onFailure
        ));
  }

  async _createInBandTestRun(tests, watcher, onStart, onResult, onFailure) {
    process.env.JEST_WORKER_ID = '1';
    const mutex = (0, _throat().default)(1);
    return tests.reduce(
      (promise, test) =>
        mutex(() =>
          promise
            .then(async () => {
              if (watcher.isInterrupted()) {
                throw new CancelRun();
              }

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

  async _createParallelTestRun(tests, watcher, onStart, onResult, onFailure) {
    const resolvers = new Map();

    for (const test of tests) {
      if (!resolvers.has(test.context.config.name)) {
        resolvers.set(test.context.config.name, {
          config: test.context.config,
          serializableModuleMap: test.context.moduleMap.toJSON()
        });
      }
    }

    const worker = new (_jestWorker().default)(TEST_WORKER_PATH, {
      exposedMethods: ['worker'],
      forkOptions: {
        stdio: 'pipe'
      },
      maxRetries: 3,
      numWorkers: this._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)(this._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(async () => {
        if (watcher.isInterrupted()) {
          return Promise.reject();
        }

        await onStart(test);
        return worker.worker({
          config: test.context.config,
          context: {
            ...this._context,
            changedFiles:
              this._context.changedFiles &&
              Array.from(this._context.changedFiles),
            sourcesRelatedToTestsInChangedFiles:
              this._context.sourcesRelatedToTestsInChangedFiles &&
              Array.from(this._context.sourcesRelatedToTestsInChangedFiles)
          },
          globalConfig: this._globalConfig,
          path: test.path
        });
      });

    const onError = async (err, test) => {
      await 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);
      }
    };

    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 = async () => {
      const {forceExited} = await worker.end();

      if (forceExited) {
        console.error(
          _chalk().default.yellow(
            'A worker process has failed to exit gracefully and has been force exited. ' +
              'This is likely caused by tests leaking due to improper teardown. ' +
              'Try running with --runInBand --detectOpenHandles to find leaks.'
          )
        );
      }
    };

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

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

module.exports = TestRunner;
