'use strict';
const {pathToFileURL} = require('url');
const currentlyUnhandled = require('currently-unhandled')();

require('./ensure-forked'); // eslint-disable-line import/no-unassigned-import

const ipc = require('./ipc');

const supportsESM = async () => {
	try {
		await import('data:text/javascript,'); // eslint-disable-line node/no-unsupported-features/es-syntax
		return true;
	} catch {}

	return false;
};

ipc.send({type: 'ready-for-options'});
ipc.options.then(async options => {
	require('./options').set(options);
	require('../chalk').set(options.chalkOptions);

	if (options.chalkOptions.level > 0) {
		const {stdout, stderr} = process;
		global.console = Object.assign(global.console, new console.Console({stdout, stderr, colorMode: true}));
	}

	const nowAndTimers = require('../now-and-timers');
	const providerManager = require('../provider-manager');
	const Runner = require('../runner');
	const serializeError = require('../serialize-error');
	const dependencyTracking = require('./dependency-tracker');
	const lineNumberSelection = require('./line-numbers');

	async function exit(code) {
		if (!process.exitCode) {
			process.exitCode = code;
		}

		dependencyTracking.flush();
		await ipc.flush();
		process.exit(); // eslint-disable-line unicorn/no-process-exit
	}

	// TODO: Initialize providers here, then pass to lineNumberSelection() so they
	// can be used to parse the test file.
	let checkSelectedByLineNumbers;
	try {
		checkSelectedByLineNumbers = lineNumberSelection({
			file: options.file,
			lineNumbers: options.lineNumbers
		});
	} catch (error) {
		ipc.send({type: 'line-number-selection-error', err: serializeError('Line number selection error', false, error, options.file)});
		checkSelectedByLineNumbers = () => false;
	}

	const runner = new Runner({
		checkSelectedByLineNumbers,
		experiments: options.experiments,
		failFast: options.failFast,
		failWithoutAssertions: options.failWithoutAssertions,
		file: options.file,
		match: options.match,
		projectDir: options.projectDir,
		recordNewSnapshots: options.recordNewSnapshots,
		runOnlyExclusive: options.runOnlyExclusive,
		serial: options.serial,
		snapshotDir: options.snapshotDir,
		updateSnapshots: options.updateSnapshots
	});

	ipc.peerFailed.then(() => { // eslint-disable-line promise/prefer-await-to-then
		runner.interrupt();
	});

	const attributedRejections = new Set();
	process.on('unhandledRejection', (reason, promise) => {
		if (runner.attributeLeakedError(reason)) {
			attributedRejections.add(promise);
		}
	});

	runner.on('dependency', dependencyTracking.track);
	runner.on('stateChange', state => ipc.send(state));

	runner.on('error', error => {
		ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, error, runner.file)});
		exit(1);
	});

	runner.on('finish', () => {
		try {
			const touchedFiles = runner.saveSnapshotState();
			if (touchedFiles) {
				ipc.send({type: 'touched-files', files: touchedFiles});
			}
		} catch (error) {
			ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, error, runner.file)});
			exit(1);
			return;
		}

		nowAndTimers.setImmediate(() => {
			currentlyUnhandled()
				.filter(rejection => !attributedRejections.has(rejection.promise))
				.forEach(rejection => {
					ipc.send({type: 'unhandled-rejection', err: serializeError('Unhandled rejection', true, rejection.reason, runner.file)});
				});

			exit(0);
		});
	});

	process.on('uncaughtException', error => {
		if (runner.attributeLeakedError(error)) {
			return;
		}

		ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error, runner.file)});
		exit(1);
	});

	let accessedRunner = false;
	exports.getRunner = () => {
		accessedRunner = true;
		return runner;
	};

	// Store value to prevent required modules from modifying it.
	const testPath = options.file;

	// Install basic source map support.
	const sourceMapSupport = require('source-map-support');
	sourceMapSupport.install({
		environment: 'node',
		handleUncaughtExceptions: false
	});

	const extensionsToLoadAsModules = Object.entries(options.moduleTypes)
		.filter(([, type]) => type === 'module')
		.map(([extension]) => extension);

	// Install before processing options.require, so if helpers are added to the
	// require configuration the *compiled* helper will be loaded.
	const {projectDir, providerStates = []} = options;
	const providers = providerStates.map(({type, state}) => {
		if (type === 'babel') {
			const provider = providerManager.babel(projectDir).worker({extensionsToLoadAsModules, state});
			runner.powerAssert = provider.powerAssert;
			return provider;
		}

		if (type === 'typescript') {
			return providerManager.typescript(projectDir).worker({extensionsToLoadAsModules, state});
		}

		return null;
	}).filter(provider => provider !== null);

	let requireFn = require;
	let isESMSupported;
	const load = async ref => {
		for (const extension of extensionsToLoadAsModules) {
			if (ref.endsWith(`.${extension}`)) {
				if (typeof isESMSupported !== 'boolean') {
					// Lazily determine support since this prints an experimental warning.
					// eslint-disable-next-line no-await-in-loop
					isESMSupported = await supportsESM();
				}

				if (isESMSupported) {
					return import(pathToFileURL(ref)); // eslint-disable-line node/no-unsupported-features/es-syntax
				}

				ipc.send({type: 'internal-error', err: serializeError('Internal runner error', false, new Error('ECMAScript Modules are not supported in this Node.js version.'))});
				exit(1);
				return;
			}
		}

		for (const provider of providers) {
			if (provider.canLoad(ref)) {
				return provider.load(ref, {requireFn});
			}
		}

		return requireFn(ref);
	};

	try {
		for await (const ref of (options.require || [])) {
			const mod = await load(ref);

			try {
				if (Reflect.has(mod, Symbol.for('esm:package'))) {
					requireFn = mod(module);
				}
			} catch (_) {}
		}

		// Install dependency tracker after the require configuration has been evaluated
		// to make sure we also track dependencies with custom require hooks
		dependencyTracking.install(testPath);

		if (options.debug) {
			require('inspector').open(options.debug.port, options.debug.host, true); // eslint-disable-line node/no-unsupported-features/node-builtins
			if (options.debug.break) {
				debugger; // eslint-disable-line no-debugger
			}
		}

		await load(testPath);

		if (accessedRunner) {
			// Unreference the IPC channel if the test file required AVA. This stops it
			// from keeping the event loop busy, which means the `beforeExit` event can be
			// used to detect when tests stall.
			ipc.unref();
		} else {
			ipc.send({type: 'missing-ava-import'});
			exit(1);
		}
	} catch (error) {
		ipc.send({type: 'uncaught-exception', err: serializeError('Uncaught exception', true, error, runner.file)});
		exit(1);
	}
}).catch(error => {
	// There shouldn't be any errors, but if there are we may not have managed
	// to bootstrap enough code to serialize them. Re-throw and let the process
	// crash.
	setImmediate(() => {
		throw error;
	});
});
