"use strict";

require("./helpers/warmup-webpack");
const path = require("path");
const fs = require("graceful-fs");
const vm = require("vm");
const { pathToFileURL, URL } = require("url");
const rimraf = require("rimraf");
const checkArrayExpectation = require("./checkArrayExpectation");
const createLazyTestEnv = require("./helpers/createLazyTestEnv");
const deprecationTracking = require("./helpers/deprecationTracking");
const captureStdio = require("./helpers/captureStdio");
const asModule = require("./helpers/asModule");
const filterInfraStructureErrors = require("./helpers/infrastructureLogErrors");

const casesPath = path.join(__dirname, "cases");
let categories = fs.readdirSync(casesPath);
categories = categories.map(cat => {
	return {
		name: cat,
		tests: fs
			.readdirSync(path.join(casesPath, cat))
			.filter(folder => folder.indexOf("_") < 0)
	};
});

const createLogger = appendTarget => {
	return {
		log: l => appendTarget.push(l),
		debug: l => appendTarget.push(l),
		trace: l => appendTarget.push(l),
		info: l => appendTarget.push(l),
		warn: console.warn.bind(console),
		error: console.error.bind(console),
		logTime: () => {},
		group: () => {},
		groupCollapsed: () => {},
		groupEnd: () => {},
		profile: () => {},
		profileEnd: () => {},
		clear: () => {},
		status: () => {}
	};
};

const describeCases = config => {
	describe(config.name, () => {
		let stderr;
		beforeEach(() => {
			stderr = captureStdio(process.stderr, true);
		});
		afterEach(() => {
			stderr.restore();
		});
		categories.forEach(category => {
			describe(category.name, function () {
				jest.setTimeout(20000);

				category.tests
					.filter(test => {
						const testDirectory = path.join(casesPath, category.name, test);
						const filterPath = path.join(testDirectory, "test.filter.js");
						if (fs.existsSync(filterPath) && !require(filterPath)(config)) {
							describe.skip(test, () => {
								it("filtered", () => {});
							});
							return false;
						}
						return true;
					})
					.forEach(testName => {
						let infraStructureLog = [];

						describe(testName, () => {
							const testDirectory = path.join(
								casesPath,
								category.name,
								testName
							);
							const outputDirectory = path.join(
								__dirname,
								"js",
								config.name,
								category.name,
								testName
							);
							const cacheDirectory = path.join(
								__dirname,
								"js/.cache",
								config.name,
								category.name,
								testName
							);
							let testConfig = {};
							const testConfigPath = path.join(testDirectory, "test.config.js");
							if (fs.existsSync(testConfigPath)) {
								testConfig = require(testConfigPath);
							}
							const TerserPlugin = require("terser-webpack-plugin");
							const terserForTesting = new TerserPlugin({
								parallel: false
							});
							let options = {
								context: casesPath,
								entry: "./" + category.name + "/" + testName + "/",
								target: config.target || "async-node",
								devtool: config.devtool,
								mode: config.mode || "none",
								optimization: config.mode
									? {
											emitOnErrors: true,
											minimizer: [terserForTesting],
											...config.optimization
									  }
									: {
											removeAvailableModules: true,
											removeEmptyChunks: true,
											mergeDuplicateChunks: true,
											flagIncludedChunks: true,
											sideEffects: true,
											providedExports: true,
											usedExports: true,
											mangleExports: true,
											emitOnErrors: true,
											concatenateModules: false,
											moduleIds: "size",
											chunkIds: "size",
											minimizer: [terserForTesting],
											...config.optimization
									  },
								performance: {
									hints: false
								},
								node: {
									__dirname: "mock",
									__filename: "mock"
								},
								cache: config.cache && {
									cacheDirectory,
									...config.cache
								},
								output: {
									pathinfo: "verbose",
									path: outputDirectory,
									filename: config.module ? "bundle.mjs" : "bundle.js"
								},
								resolve: {
									modules: ["web_modules", "node_modules"],
									mainFields: [
										"webpack",
										"browser",
										"web",
										"browserify",
										["jam", "main"],
										"main"
									],
									aliasFields: ["browser"],
									extensions: [".webpack.js", ".web.js", ".js", ".json"]
								},
								resolveLoader: {
									modules: [
										"web_loaders",
										"web_modules",
										"node_loaders",
										"node_modules"
									],
									mainFields: ["webpackLoader", "webLoader", "loader", "main"],
									extensions: [
										".webpack-loader.js",
										".web-loader.js",
										".loader.js",
										".js"
									]
								},
								module: {
									rules: [
										{
											test: /\.coffee$/,
											loader: "coffee-loader"
										},
										{
											test: /\.pug/,
											loader: "pug-loader"
										},
										{
											test: /\.wat$/i,
											loader: "wast-loader",
											type: "webassembly/async"
										}
									]
								},
								plugins: (config.plugins || []).concat(function () {
									this.hooks.compilation.tap("TestCasesTest", compilation => {
										[
											"optimize",
											"optimizeModules",
											"optimizeChunks",
											"afterOptimizeTree",
											"afterOptimizeAssets"
										].forEach(hook => {
											compilation.hooks[hook].tap("TestCasesTest", () =>
												compilation.checkConstraints()
											);
										});
									});
								}),
								experiments: {
									asyncWebAssembly: true,
									topLevelAwait: true,
									backCompat: false,
									...(config.module ? { outputModule: true } : {})
								},
								infrastructureLogging: config.cache && {
									debug: true,
									console: createLogger(infraStructureLog)
								}
							};
							const cleanups = [];
							afterAll(() => {
								options = undefined;
								testConfig = undefined;
								for (const fn of cleanups) fn();
							});
							beforeAll(done => {
								rimraf(cacheDirectory, done);
							});
							if (config.cache) {
								it(
									`${testName} should pre-compile to fill disk cache (1st)`,
									done => {
										const oldPath = options.output.path;
										options.output.path = path.join(
											options.output.path,
											"cache1"
										);
										infraStructureLog.length = 0;
										const deprecationTracker = deprecationTracking.start();
										const webpack = require("..");
										webpack(options, err => {
											deprecationTracker();
											options.output.path = oldPath;
											if (err) return done(err);
											const infrastructureLogErrors =
												filterInfraStructureErrors(infraStructureLog, {
													run: 1,
													options
												});
											if (
												infrastructureLogErrors.length &&
												checkArrayExpectation(
													testDirectory,
													{ infrastructureLogs: infrastructureLogErrors },
													"infrastructureLog",
													"infrastructure-log",
													"InfrastructureLog",
													done
												)
											) {
												return;
											}
											done();
										});
									},
									testConfig.timeout || 60000
								);
								it(
									`${testName} should pre-compile to fill disk cache (2nd)`,
									done => {
										const oldPath = options.output.path;
										options.output.path = path.join(
											options.output.path,
											"cache2"
										);
										infraStructureLog.length = 0;
										const deprecationTracker = deprecationTracking.start();
										const webpack = require("..");
										webpack(options, err => {
											deprecationTracker();
											options.output.path = oldPath;
											if (err) return done(err);
											const infrastructureLogErrors =
												filterInfraStructureErrors(infraStructureLog, {
													run: 2,
													options
												});
											if (
												infrastructureLogErrors.length &&
												checkArrayExpectation(
													testDirectory,
													{ infrastructureLogs: infrastructureLogErrors },
													"infrastructureLog",
													"infrastructure-log",
													"InfrastructureLog",
													done
												)
											) {
												return;
											}
											done();
										});
									},
									testConfig.cachedTimeout || testConfig.timeout || 10000
								);
							}
							it(
								testName + " should compile",
								done => {
									infraStructureLog.length = 0;
									const webpack = require("..");
									const compiler = webpack(options);
									const run = () => {
										const deprecationTracker = deprecationTracking.start();
										compiler.run((err, stats) => {
											const deprecations = deprecationTracker();
											if (err) return done(err);
											const infrastructureLogErrors =
												filterInfraStructureErrors(infraStructureLog, {
													run: 3,
													options
												});
											if (
												infrastructureLogErrors.length &&
												checkArrayExpectation(
													testDirectory,
													{ infrastructureLogs: infrastructureLogErrors },
													"infrastructureLog",
													"infrastructure-log",
													"InfrastructureLog",
													done
												)
											) {
												return;
											}
											compiler.close(err => {
												if (err) return done(err);
												const statOptions = {
													preset: "verbose",
													colors: false,
													modules: true,
													reasonsSpace: 1000
												};
												fs.mkdirSync(outputDirectory, { recursive: true });
												fs.writeFileSync(
													path.join(outputDirectory, "stats.txt"),
													stats.toString(statOptions),
													"utf-8"
												);
												const jsonStats = stats.toJson({
													errorDetails: true,
													modules: false,
													assets: false,
													chunks: false
												});
												if (
													checkArrayExpectation(
														testDirectory,
														jsonStats,
														"error",
														"Error",
														done
													)
												) {
													return;
												}
												if (
													checkArrayExpectation(
														testDirectory,
														jsonStats,
														"warning",
														"Warning",
														done
													)
												) {
													return;
												}
												const infrastructureLogging = stderr.toString();
												if (infrastructureLogging) {
													done(
														new Error(
															"Errors/Warnings during build:\n" +
																infrastructureLogging
														)
													);
												}

												expect(deprecations).toEqual(config.deprecations || []);

												Promise.resolve().then(done);
											});
										});
									};
									if (config.cache) {
										// pre-compile to fill memory cache
										const deprecationTracker = deprecationTracking.start();
										compiler.run(err => {
											deprecationTracker();
											if (err) return done(err);
											run();
										});
									} else {
										run();
									}
								},
								testConfig.cachedTimeout ||
									testConfig.timeout ||
									(config.cache ? 20000 : 60000)
							);

							it(
								testName + " should load the compiled tests",
								done => {
									const esmContext = vm.createContext({
										it: _it,
										expect,
										process,
										global,
										URL,
										Buffer,
										setTimeout,
										setImmediate,
										nsObj: function (m) {
											Object.defineProperty(m, Symbol.toStringTag, {
												value: "Module"
											});
											return m;
										}
									});
									cleanups.push(() => (esmContext.it = undefined));
									function _require(module, esmMode) {
										if (module.startsWith("./")) {
											const p = path.join(outputDirectory, module);
											const content = fs.readFileSync(p, "utf-8");
											if (p.endsWith(".mjs")) {
												let esm;
												try {
													esm = new vm.SourceTextModule(content, {
														identifier: p,
														context: esmContext,
														initializeImportMeta: (meta, module) => {
															meta.url = pathToFileURL(p).href;
														},
														importModuleDynamically: async (
															specifier,
															module
														) => {
															const result = await _require(
																specifier,
																"evaluated"
															);
															return await asModule(result, module.context);
														}
													});
													cleanups.push(() => (esmContext.it = undefined));
												} catch (e) {
													console.log(e);
													e.message += `\nwhile parsing ${p}`;
													throw e;
												}
												if (esmMode === "unlinked") return esm;
												return (async () => {
													await esm.link(async (specifier, module) => {
														return await asModule(
															await _require(specifier, "unlinked"),
															module.context,
															true
														);
													});
													// node.js 10 needs instantiate
													if (esm.instantiate) esm.instantiate();
													await esm.evaluate();
													if (esmMode === "evaluated") return esm;
													const ns = esm.namespace;
													return ns.default && ns.default instanceof Promise
														? ns.default
														: ns;
												})();
											} else {
												const fn = vm.runInThisContext(
													"(function(require, module, exports, __dirname, __filename, it, expect) {" +
														"global.expect = expect;" +
														'function nsObj(m) { Object.defineProperty(m, Symbol.toStringTag, { value: "Module" }); return m; }' +
														content +
														"\n})",
													p
												);
												const m = {
													exports: {},
													webpackTestSuiteModule: true
												};
												fn.call(
													m.exports,
													_require,
													m,
													m.exports,
													outputDirectory,
													p,
													_it,
													expect
												);
												return m.exports;
											}
										} else return require(module);
									}
									_require.webpackTestSuiteRequire = true;
									Promise.resolve()
										.then(() => _require("./" + options.output.filename))
										.then(() => {
											if (getNumberOfTests() === 0)
												return done(
													new Error("No tests exported by test case")
												);
											done();
										}, done);
								},
								10000
							);

							const { it: _it, getNumberOfTests } = createLazyTestEnv(
								testConfig.timeout || 10000
							);
						});
					});
			});
		});
	});
};

exports.describeCases = describeCases;
