"use strict";

module.exports = (globalTimeout = 2000, nameSuffix = "") => {
	const state = global.JEST_STATE_SYMBOL;
	let currentDescribeBlock;
	let currentlyRunningTest;
	let runTests = -1;
	const disposables = [];

	// this function allows to release memory in fn context
	// manually, usually after the suite has been run.
	const createDisposableFn = (fn, isTest) => {
		if (!fn) return null;
		const rfn =
			fn.length >= 1
				? (done) => {
						fn((...args) => {
							if (isTest) runTests++;
							done(...args);
						});
					}
				: () => {
						const r = fn();
						if (isTest) runTests++;
						return r;
					};
		disposables.push(() => {
			fn = null;
		});
		return rfn;
	};

	describe(
		nameSuffix ? `exported tests ${nameSuffix}` : "exported tests",
		() => {
			// this must have a child to be handled correctly
			it("should run the exported tests", () => {
				runTests++;
			});
			afterAll((done) => {
				for (const dispose of disposables) {
					dispose();
				}
				done();
			});
			currentDescribeBlock = state.currentDescribeBlock;
			currentlyRunningTest = state.currentlyRunningTest;
		}
	);
	let numberOfTests = 0;
	const inSuite = (fn) => {
		const {
			currentDescribeBlock: oldCurrentDescribeBlock,
			currentlyRunningTest: oldCurrentlyRunningTest,
			hasStarted: oldHasStarted
		} = state;
		state.currentDescribeBlock = currentDescribeBlock;
		state.currentlyRunningTest = currentlyRunningTest;
		state.hasStarted = false;
		try {
			fn();
		} catch (err) {
			/* eslint-disable no-unused-expressions */
			// avoid leaking memory
			/** @type {EXPECTED_ANY} */
			(err).stack;
			/* eslint-enable no-unused-expressions */
			throw err;
		}
		state.currentDescribeBlock = oldCurrentDescribeBlock;
		state.currentlyRunningTest = oldCurrentlyRunningTest;
		state.hasStarted = oldHasStarted;
	};
	const fixAsyncError = (block) => {
		// By default jest leaks memory as it stores asyncError
		// for each "it" call to track the origin test suite
		// We want to evaluate this early here to avoid leaking memory
		block.asyncError = {
			stack: block.asyncError.stack
		};
	};
	return {
		/**
		 * @param {number} time time
		 */
		setDefaultTimeout(time) {
			globalTimeout = time;
		},
		getNumberOfTests() {
			return numberOfTests;
		},
		it(...args) {
			numberOfTests++;
			if (runTests >= numberOfTests) throw new Error("it called too late");
			args[1] = createDisposableFn(args[1], true);
			args[2] = args[2] || globalTimeout;
			inSuite(() => {
				// @ts-expect-error expected
				it(...args);
				fixAsyncError(
					currentDescribeBlock.tests[currentDescribeBlock.tests.length - 1]
				);
			});
		},
		beforeEach(...args) {
			if (runTests >= numberOfTests) {
				throw new Error("beforeEach called too late");
			}
			args[0] = createDisposableFn(args[0]);
			inSuite(() => {
				// @ts-expect-error expected
				beforeEach(...args);
				fixAsyncError(
					currentDescribeBlock.hooks[currentDescribeBlock.hooks.length - 1]
				);
			});
		},
		afterEach(...args) {
			if (runTests >= numberOfTests) {
				throw new Error("afterEach called too late");
			}
			args[0] = createDisposableFn(args[0]);
			inSuite(() => {
				// @ts-expect-error expected
				afterEach(...args);
				fixAsyncError(
					currentDescribeBlock.hooks[currentDescribeBlock.hooks.length - 1]
				);
			});
		}
	};
};
