"use strict";

require("./helpers/warmup-webpack");

/** @typedef {{ name: string, tests: string[] }} Category */
/**
 * @typedef {object} SuiteConfig
 * @property {string} name suite name
 * @property {string | string[]=} target target
 */
/**
 * @typedef {object} HotTestConfig
 * @property {((scope: EXPECTED_ANY, options: import("../").Configuration) => void)=} moduleScope
 */

const path = require("path");
const fs = require("graceful-fs");
/** @type {{ sync: (p: string) => void }} */
const rimraf = require("rimraf");
const checkArrayExpectation = require("./checkArrayExpectation");
const { TestRunner } = require("./harness/runner");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const deprecationTracking = require("./helpers/deprecationTracking");
const supportsObjectHasOwn = require("./helpers/supportsObjectHasOwn");
const supportsOptionalChaining = require("./helpers/supportsOptionalChaining");

const casesPath = path.join(__dirname, "hotCases");
/** @type {Category[]} */
const categories = fs
	.readdirSync(casesPath)
	.filter((dir) => fs.statSync(path.join(casesPath, dir)).isDirectory())
	.map((cat) => ({
		name: cat,
		tests: fs
			.readdirSync(path.join(casesPath, cat))
			.filter((folder) => !folder.includes("_"))
	}));

/**
 * @param {SuiteConfig} config suite config
 */
const describeCases = (config) => {
	// universal targets run the same (ESM) bundle once per environment; the
	// suite forces module output, so probe with `output.module` set
	const isUniversal = TestRunner.isUniversalTarget({
		target: config.target,
		output: { module: true }
	});

	describe(config.name, () => {
		for (const category of categories) {
			// `universal` cases only run in the universal suite, and vice versa.
			if ((category.name === "universal") !== isUniversal) {
				continue;
			}

			describe(category.name, () => {
				for (const testName of category.tests) {
					const testDirectory = path.join(casesPath, category.name, testName);
					const filterPath = path.join(testDirectory, "test.filter.js");
					if (fs.existsSync(filterPath) && !require(filterPath)(config)) {
						// eslint-disable-next-line jest/no-disabled-tests
						describe.skip(testName, () => {
							it("filtered", () => {});
						});

						continue;
					}

					describe(testName, () => {
						/** @type {import("../").Compiler} */
						let compiler;

						afterAll((/** @type {EXPECTED_ANY} */ callback) => {
							compiler.close(callback);
							compiler = /** @type {EXPECTED_ANY} */ (undefined);
						});

						it(`${testName} should compile`, (done) => {
							const webpack = require("..");

							const outputDirectory = path.join(
								__dirname,
								"js",
								`hot-cases-${config.name}`,
								category.name,
								testName
							);
							rimraf.sync(outputDirectory);
							const recordsPath = path.join(outputDirectory, "records.json");
							const fakeUpdateLoaderOptions = {
								updateIndex: 0
							};
							const configPath = path.join(testDirectory, "webpack.config.js");
							/** @type {import("../").Configuration} */
							let options = /** @type {import("../").Configuration} */ ({});
							if (fs.existsSync(configPath)) options = require(configPath);
							if (
								typeof (/** @type {EXPECTED_ANY} */ (options)) === "function"
							) {
								options = /** @type {EXPECTED_ANY} */ (options)({ config });
							}
							if (!options.mode) options.mode = "development";
							if (!options.devtool) options.devtool = false;
							if (!options.context) options.context = testDirectory;
							if (!options.entry) options.entry = "./index.js";
							if (!options.output) options.output = {};
							if (isUniversal) {
								// universal target requires ESM output to run in node and web
								if (!options.experiments) options.experiments = {};
								if (options.experiments.outputModule === undefined) {
									options.experiments.outputModule = true;
								}
								if (options.output.module === undefined) {
									options.output.module = true;
								}
								if (options.output.chunkFormat === undefined) {
									options.output.chunkFormat = "module";
								}
							}
							if (!options.output.environment) options.output.environment = {};
							if (
								options.output.environment.optionalChaining === undefined &&
								!supportsOptionalChaining()
							) {
								// generated runtime runs in this Node.js process; avoid `?.` on Node < 14
								options.output.environment.optionalChaining = false;
							}
							if (
								options.output.environment.hasOwn === undefined &&
								!supportsObjectHasOwn()
							) {
								// generated runtime runs in this Node.js process; avoid `Object.hasOwn` on Node < 16.9
								options.output.environment.hasOwn = false;
							}
							if (!options.output.path) options.output.path = outputDirectory;
							if (!options.output.filename) {
								options.output.filename = `bundle${
									options.experiments && options.experiments.outputModule
										? ".mjs"
										: ".js"
								}`;
							}
							if (!options.output.chunkFilename) {
								options.output.chunkFilename = `[name].chunk.[fullhash]${
									options.experiments && options.experiments.outputModule
										? ".mjs"
										: ".js"
								}`;
							}
							if (options.output.pathinfo === undefined) {
								options.output.pathinfo = true;
							}
							if (options.output.publicPath === undefined) {
								options.output.publicPath = "https://test.cases/path/";
							}
							if (options.output.library === undefined) {
								options.output.library = {
									type:
										options.experiments && options.experiments.outputModule
											? "module"
											: "commonjs2"
								};
							}
							if (!options.optimization) options.optimization = {};
							if (!options.optimization.moduleIds) {
								options.optimization.moduleIds = "named";
							}
							if (!options.module) options.module = {};
							if (!options.module.rules) options.module.rules = [];
							options.module.rules.push({
								loader: path.join(
									__dirname,
									"hotCases",
									"fake-update-loader.js"
								),
								enforce: "pre"
							});
							if (!options.target) options.target = config.target;
							if (!options.plugins) options.plugins = [];
							options.plugins.push(
								new webpack.HotModuleReplacementPlugin(),
								new webpack.LoaderOptionsPlugin(fakeUpdateLoaderOptions)
							);
							if (!options.recordsPath) options.recordsPath = recordsPath;
							/** @type {HotTestConfig} */
							let testConfig = {};
							try {
								// try to load a test file
								testConfig = Object.assign(
									testConfig,
									require(path.join(testDirectory, "test.config.js"))
								);
							} catch (_err) {
								// ignored
							}

							const onCompiled = (
								/** @type {Error | null} */ err,
								/** @type {import("../").Stats} */ stats
							) => {
								const deprecations = deprecationTracker();
								if (err) return done(err);
								const jsonStats = stats.toJson({
									errorDetails: true
								});
								if (
									checkArrayExpectation(
										testDirectory,
										jsonStats,
										"error",
										"Error",
										options,
										done
									)
								) {
									return;
								}
								if (
									checkArrayExpectation(
										testDirectory,
										jsonStats,
										"warning",
										"Warning",
										options,
										done
									)
								) {
									return;
								}
								if (
									checkArrayExpectation(
										testDirectory,
										{ deprecations },
										"deprecation",
										"Deprecation",
										options,
										done
									)
								) {
									return;
								}

								function runCompiler(
									/** @type {(err: EXPECTED_ANY, stats?: EXPECTED_ANY) => void} */ callback
								) {
									fakeUpdateLoaderOptions.updateIndex++;
									const deprecationTracker = deprecationTracking.start();
									compiler.run((err, _stats) => {
										const stats = /** @type {import("../").Stats} */ (_stats);
										const deprecations = deprecationTracker();
										if (err) return callback(err);
										const jsonStats = stats.toJson({
											errorDetails: true
										});
										if (
											checkArrayExpectation(
												testDirectory,
												jsonStats,
												"error",
												`errors${fakeUpdateLoaderOptions.updateIndex}`,
												"Error",
												options,
												callback
											)
										) {
											return;
										}
										if (
											checkArrayExpectation(
												testDirectory,
												jsonStats,
												"warning",
												`warnings${fakeUpdateLoaderOptions.updateIndex}`,
												"Warning",
												options,
												callback
											)
										) {
											return;
										}
										if (
											checkArrayExpectation(
												testDirectory,
												{ deprecations },
												"deprecation",
												`deprecations${fakeUpdateLoaderOptions.updateIndex}`,
												"Deprecation",
												options,
												callback
											)
										) {
											return;
										}
										callback(null, jsonStats);
									});
								}

								const _stats = stats.toJson({ all: false, entrypoints: true });
								const { results } = TestRunner.runBundles({
									optionsArr: [options],
									outputDirectory,
									testConfig: {
										...testConfig,
										evaluateScriptOnAttached: true
									},
									category,
									testName,
									setupRunner: ({ runner }) => {
										if (testConfig.moduleScope) {
											testConfig.moduleScope(runner._moduleScope, options);
										}
										runner.mergeModuleScope({
											it: _it,
											beforeEach: _beforeEach,
											afterEach: _afterEach,
											STATE: jsonStats,
											NEXT: runCompiler,
											NEXT_DEFERRED: (/** @type {EXPECTED_ANY} */ cb) => {
												// https://github.com/webpack/webpack/actions/runs/22039709807/job/63678606467?pr=20412
												// When lazyCompilation is enabled, delay the first compilation re-run by 1000ms during HMR
												// to ensure that HTTP requests from dynamic imports (e.g., const promiseA = import("./moduleA"))
												// have already reached lazyCompilationBackend. This prevents NEXT from triggering
												// a recompilation while moduleA is still not marked as Activated and still returns
												// LazyCompilationProxyModule, which would cause a "No update available" error.
												setTimeout(() => {
													runCompiler(cb);
												}, 1000);
											},
											// Re-run the same compilation version (without advancing the
											// fake-update index): lets a test wait out the lazy-compilation
											// activation race instead of relying on a single fixed delay.
											NEXT_RETRY: (/** @type {EXPECTED_ANY} */ cb) => {
												fakeUpdateLoaderOptions.updateIndex--;
												runCompiler(cb);
											}
										});
									},
									getBundlePaths: (_i, _options, runner) => {
										const bundles = /** @type {EXPECTED_ANY[]} */ (
											/** @type {EXPECTED_ANY} */ (_stats.entrypoints).main
												.assets
										).map((/** @type {EXPECTED_ANY} */ i) => i.name);
										// universal expands to one runner per target; pick by its target
										const isWeb = isUniversal
											? runner.hasWebTarget()
											: config.target === "web";
										if (isWeb) {
											return bundles;
										}
										// node runs the JS entry only; skip CSS/other assets that may sort last
										const jsBundles = bundles.filter(
											(/** @type {string} */ n) => /\.[cm]?js$/.test(n)
										);
										const nodeBundles =
											jsBundles.length > 0 ? jsBundles : bundles;
										return [nodeBundles[nodeBundles.length - 1]];
									}
								});
								Promise.all(results).then(
									() => {
										if (getNumberOfTests() < 1) {
											return done(new Error("No tests exported by test case"));
										}

										done();
									},
									(err) => {
										console.log(err);
										done(err);
									}
								);
							};
							const deprecationTracker = deprecationTracking.start();
							compiler = webpack(options);
							compiler.run(/** @type {EXPECTED_ANY} */ (onCompiled));
						}, 20000);

						const {
							it: _it,
							beforeEach: _beforeEach,
							afterEach: _afterEach,
							getNumberOfTests
						} = createLazyTestEnv(20000);
					});
				}
			});
		}
	});
};

// eslint-disable-next-line jest/no-export
module.exports.describeCases = describeCases;
