import test, { Implementation } from "ava";
import { dirname } from "path";
import * as tsModule from "typescript";

type TestFunction = (title: string, implementation: Implementation) => void;

type TsModuleKind = "current" | "3.5" | "3.6" | "3.7" | "3.8";

const TS_MODULES_ALL: TsModuleKind[] = ["current", "3.5", "3.6", "3.7", "3.8"];

const TS_MODULES_DEFAULT: TsModuleKind[] = ["3.5", "3.6", "3.7", "3.8"];

/**
 * Returns the name of the module to require for a specific ts module kind
 * @param kind
 */
function getTsModuleNameWithKind(kind: TsModuleKind | undefined): string {
	// Return the corresponding ts module
	switch (kind) {
		case "3.5":
		case "3.6":
		case "3.7":
		case "3.8":
			return `typescript-${kind}`;
		case "current":
		case undefined:
		case null:
			// Fall back to "default"
			return "typescript";
		default:
			throw new Error(`Unknown ts module "${kind}"`);
	}
}

/**
 * Returns a ts module based on a ts module kind
 * @param kind
 */
function getTsModuleWithKind(kind: TsModuleKind | undefined): typeof tsModule {
	return require(getTsModuleNameWithKind(kind));
}

function setCurrentTsModuleKind(kind: TsModuleKind | undefined) {
	if (kind == null) {
		delete process.env.TS_MODULE;
	} else {
		process.env.TS_MODULE = kind;
	}
}

/**
 * Returns the current ts module kind based on environment vars
 */
function getCurrentTsModuleKind(): TsModuleKind | undefined {
	const kind = process.env.TS_MODULE as TsModuleKind | undefined;

	// Validate the value
	if (kind != null && !TS_MODULES_ALL.includes(kind)) {
		throw new Error(`Unknown ts module "${kind}"`);
	}

	return kind;
}

/**
 * Returns the current ts module based based on environment vars
 */
export function getCurrentTsModule(): typeof tsModule {
	return getTsModuleWithKind(getCurrentTsModuleKind());
}

/**
 * Returns the directory of the current ts module
 */
export function getCurrentTsModuleDirectory(): string {
	const moduleName = getTsModuleNameWithKind(getCurrentTsModuleKind());
	return dirname(require.resolve(moduleName));
}

/**
 * Sets up an ava test with specified ts module kind
 * @param testFunction
 * @param tsModuleKind
 * @param title
 * @param cb
 */
function setupTest(testFunction: TestFunction, tsModuleKind: TsModuleKind | undefined, title: string, cb: Implementation) {
	// Generate title based on the ts module
	const version = getTsModuleWithKind(tsModuleKind).version;
	const titleWithModule = `[ts${version}] ${title}`;

	// Setup up the ava test
	testFunction(titleWithModule, (...args: Parameters<Implementation>) => {
		// Set the ts module as environment variable before running the test
		setCurrentTsModuleKind(tsModuleKind);

		const res = cb(...args);

		// Reset the selected TS_MODULE
		setCurrentTsModuleKind(undefined);

		return res;
	});
}

/**
 * Sets up an ava test that runs multiple times with different ts modules
 * @param testFunction
 * @param title
 * @param cb
 */
function setupTests(testFunction: (title: string, implementation: Implementation) => void, title: string, cb: Implementation) {
	// Find the user specified TS_MODULE at setup time
	const moduleKinds: TsModuleKind[] = (() => {
		const currentTsModuleKind = getCurrentTsModuleKind();

		// Default to running all ts modules if TS_MODULE is not set
		return currentTsModuleKind != null ? [currentTsModuleKind] : TS_MODULES_DEFAULT;
	})();

	// Set up tests for each ts module
	for (const tsModuleKind of moduleKinds) {
		setupTest(testFunction, tsModuleKind, title, cb);
	}
}

/**
 * Wraps an ava test and runs it multiple times with different ts modules
 * @param testFunction
 */
export function wrapAvaTest(testFunction: TestFunction): TestFunction {
	return (title, implementation) => {
		return setupTests(testFunction, title, implementation);
	};
}

/**
 * Wrap the ava test module in these helper functions
 */
export const tsTest = Object.assign(wrapAvaTest(test), {
	only: wrapAvaTest(test.only),
	skip: wrapAvaTest(test.skip)
});
