import { isBuiltin, createRequire } from 'node:module';
import { pathToFileURL, fileURLToPath } from 'node:url';
import vm from 'node:vm';
import { ssrModuleExportsKey, ssrImportMetaKey, ssrImportKey, ssrDynamicImportKey, ssrExportAllKey } from 'vite/module-runner';
import { T as Traces } from './chunks/traces.CCmnQaNT.js';

const performanceNow = performance.now.bind(performance);
class ModuleDebug {
	executionStack = [];
	startCalculateModuleExecutionInfo(filename, options) {
		const startTime = performanceNow();
		this.executionStack.push({
			filename,
			startTime,
			subImportTime: 0
		});
		return () => {
			const duration = performanceNow() - startTime;
			const currentExecution = this.executionStack.pop();
			if (currentExecution == null) throw new Error("Execution stack is empty, this should never happen");
			const selfTime = duration - currentExecution.subImportTime;
			if (this.executionStack.length > 0) this.executionStack.at(-1).subImportTime += duration;
			return {
				startOffset: options.startOffset,
				external: options.external,
				importer: options.importer,
				duration,
				selfTime
			};
		};
	}
}

const isWindows = process.platform === "win32";
class VitestModuleEvaluator {
	stubs = {};
	env = createImportMetaEnvProxy();
	vm;
	compiledFunctionArgumentsNames;
	compiledFunctionArgumentsValues = [];
	primitives;
	debug = new ModuleDebug();
	_otel;
	_evaluatedModules;
	constructor(vmOptions, options = {}) {
		this.options = options;
		this._otel = options.traces || new Traces({ enabled: false });
		this.vm = vmOptions;
		this.stubs = getDefaultRequestStubs(vmOptions?.context);
		this._evaluatedModules = options.evaluatedModules;
		if (options.compiledFunctionArgumentsNames) this.compiledFunctionArgumentsNames = options.compiledFunctionArgumentsNames;
		if (options.compiledFunctionArgumentsValues) this.compiledFunctionArgumentsValues = options.compiledFunctionArgumentsValues;
		if (vmOptions) this.primitives = vm.runInContext("({ Object, Proxy, Reflect })", vmOptions.context);
		else this.primitives = {
			Object,
			Proxy,
			Reflect
		};
	}
	convertIdToImportUrl(id) {
		// TODO: vitest returns paths for external modules, but Vite returns file://
		// REMOVE WHEN VITE 6 SUPPORT IS OVER
		// unfortunetly, there is a bug in Vite where ID is resolved incorrectly, so we can't return files until the fix is merged
		// https://github.com/vitejs/vite/pull/20449
		if (!isWindows || isBuiltin(id) || /^(?:node:|data:|http:|https:|file:)/.test(id)) return id;
		const [filepath, query] = id.split("?");
		if (query) return `${pathToFileURL(filepath).toString()}?${query}`;
		return pathToFileURL(filepath).toString();
	}
	async runExternalModule(id) {
		if (id in this.stubs) return this.stubs[id];
		const file = this.convertIdToImportUrl(id);
		const importer = (this._evaluatedModules?.getModuleById(id)?.importers)?.values().next().value;
		const filename = id.startsWith("file://") ? fileURLToPath(id) : id;
		const finishModuleExecutionInfo = this.debug.startCalculateModuleExecutionInfo(filename, {
			startOffset: 0,
			external: true,
			importer
		});
		const namespace = await this._otel.$("vitest.module.external", { attributes: { "code.file.path": file } }, () => this.vm ? this.vm.externalModulesExecutor.import(file) : import(file)).finally(() => {
			this.options.moduleExecutionInfo?.set(filename, finishModuleExecutionInfo());
		});
		if (!this.shouldInterop(file, namespace)) return namespace;
		const { mod, defaultExport } = interopModule(namespace);
		const { Proxy, Reflect } = this.primitives;
		return new Proxy(mod, {
			get(mod, prop) {
				if (prop === "default") return defaultExport;
				return mod[prop] ?? defaultExport?.[prop];
			},
			has(mod, prop) {
				if (prop === "default") return defaultExport !== void 0;
				return prop in mod || defaultExport && prop in defaultExport;
			},
			getOwnPropertyDescriptor(mod, prop) {
				const descriptor = Reflect.getOwnPropertyDescriptor(mod, prop);
				if (descriptor) return descriptor;
				if (prop === "default" && defaultExport !== void 0) return {
					value: defaultExport,
					enumerable: true,
					configurable: true
				};
			}
		});
	}
	async runInlinedModule(context, code, module) {
		return this._otel.$("vitest.module.inline", (span) => this._runInlinedModule(context, code, module, span));
	}
	async _runInlinedModule(context, code, module, span) {
		context.__vite_ssr_import_meta__.env = this.env;
		const { Reflect, Proxy, Object } = this.primitives;
		const exportsObject = context[ssrModuleExportsKey];
		const SYMBOL_NOT_DEFINED = Symbol("not defined");
		let moduleExports = SYMBOL_NOT_DEFINED;
		// this proxy is triggered only on exports.{name} and module.exports access
		// inside the module itself. imported module is always "exports"
		const cjsExports = new Proxy(exportsObject, {
			get: (target, p, receiver) => {
				if (Reflect.has(target, p)) return Reflect.get(target, p, receiver);
				return Reflect.get(Object.prototype, p, receiver);
			},
			getPrototypeOf: () => Object.prototype,
			set: (_, p, value) => {
				span.addEvent(`cjs export proxy is triggered for ${String(p)}`);
				// treat "module.exports =" the same as "exports.default =" to not have nested "default.default",
				// so "exports.default" becomes the actual module
				if (p === "default" && this.shouldInterop(module.file, { default: value }) && cjsExports !== value) {
					span.addEvent("`exports.default` is assigned, copying values");
					exportAll(cjsExports, value);
					exportsObject.default = value;
					return true;
				}
				if (!Reflect.has(exportsObject, "default")) exportsObject.default = {};
				// returns undefined, when accessing named exports, if default is not an object
				// but is still present inside hasOwnKeys, this is Node behaviour for CJS
				if (moduleExports !== SYMBOL_NOT_DEFINED && isPrimitive(moduleExports)) {
					span.addEvent(`\`exports.${String(p)}\` is assigned, but module.exports is a primitive. assigning "undefined" values instead to comply with ESM`);
					defineExport(exportsObject, p, () => void 0);
					return true;
				}
				if (!isPrimitive(exportsObject.default)) exportsObject.default[p] = value;
				if (p !== "default") defineExport(exportsObject, p, () => value);
				return true;
			}
		});
		const moduleProxy = {
			set exports(value) {
				span.addEvent("`module.exports` is assigned directly, copying all properties to `exports`");
				exportAll(cjsExports, value);
				exportsObject.default = value;
				moduleExports = value;
			},
			get exports() {
				return cjsExports;
			}
		};
		const meta = context[ssrImportMetaKey];
		if (this.options.getCurrentTestFilepath?.() === module.file) {
			const globalNamespace = this.vm?.context || globalThis;
			Object.defineProperty(meta, "vitest", { get: () => globalNamespace.__vitest_index__ });
		}
		const filename = meta.filename;
		const dirname = meta.dirname;
		span.setAttributes({ "code.file.path": filename });
		const require = this.createRequire(meta.url);
		const argumentsList = [
			ssrModuleExportsKey,
			ssrImportMetaKey,
			ssrImportKey,
			ssrDynamicImportKey,
			ssrExportAllKey,
			"__vite_ssr_exportName__",
			"__filename",
			"__dirname",
			"module",
			"exports",
			"require"
		];
		if (this.compiledFunctionArgumentsNames) argumentsList.push(...this.compiledFunctionArgumentsNames);
		span.setAttribute("vitest.module.arguments", argumentsList);
		// add 'use strict' since ESM enables it by default
		const codeDefinition = `'use strict';async (${argumentsList.join(",")})=>{{`;
		const wrappedCode = `${codeDefinition}${code}\n}}`;
		const options = {
			filename: module.id,
			lineOffset: 0,
			columnOffset: -codeDefinition.length
		};
		// this will always be 1 element because it's cached after load
		const importer = module.importers.values().next().value;
		const finishModuleExecutionInfo = this.debug.startCalculateModuleExecutionInfo(options.filename, {
			startOffset: codeDefinition.length,
			importer
		});
		try {
			await (this.vm ? vm.runInContext(wrappedCode, this.vm.context, options) : vm.runInThisContext(wrappedCode, options))(
				context[ssrModuleExportsKey],
				context[ssrImportMetaKey],
				context[ssrImportKey],
				context[ssrDynamicImportKey],
				context[ssrExportAllKey],
				// vite 7 support, remove when vite 7+ is supported
				context.__vite_ssr_exportName__ || ((name, getter) => Object.defineProperty(exportsObject, name, {
					enumerable: true,
					configurable: true,
					get: getter
				})),
				filename,
				dirname,
				moduleProxy,
				cjsExports,
				require,
				...this.compiledFunctionArgumentsValues
			);
		} finally {
			// moduleExecutionInfo needs to use Node filename instead of the normalized one
			// because we rely on this behaviour in coverage-v8, for example
			this.options.moduleExecutionInfo?.set(options.filename, finishModuleExecutionInfo());
		}
	}
	createRequire(filename) {
		return this.vm ? this.vm.externalModulesExecutor.createRequire(filename) : createRequire(filename);
	}
	shouldInterop(path, mod) {
		if (this.options.interopDefault === false) return false;
		// never interop ESM modules
		// TODO: should also skip for `.js` with `type="module"`
		return !path.endsWith(".mjs") && "default" in mod;
	}
}
function createImportMetaEnvProxy() {
	// packages/vitest/src/node/plugins/index.ts:146
	const booleanKeys = [
		"DEV",
		"PROD",
		"SSR"
	];
	return new Proxy(process.env, {
		get(_, key) {
			if (typeof key !== "string") return;
			if (booleanKeys.includes(key)) return !!process.env[key];
			return process.env[key];
		},
		set(_, key, value) {
			if (typeof key !== "string") return true;
			if (booleanKeys.includes(key)) process.env[key] = value ? "1" : "";
			else process.env[key] = value;
			return true;
		}
	});
}
function updateStyle(id, css) {
	if (typeof document === "undefined") return;
	const element = document.querySelector(`[data-vite-dev-id="${id}"]`);
	if (element) {
		element.textContent = css;
		return;
	}
	const head = document.querySelector("head");
	const style = document.createElement("style");
	style.setAttribute("type", "text/css");
	style.setAttribute("data-vite-dev-id", id);
	style.textContent = css;
	head?.appendChild(style);
}
function removeStyle(id) {
	if (typeof document === "undefined") return;
	const sheet = document.querySelector(`[data-vite-dev-id="${id}"]`);
	if (sheet) document.head.removeChild(sheet);
}
const defaultClientStub = {
	injectQuery: (id) => id,
	createHotContext: () => {
		return {
			accept: () => {},
			prune: () => {},
			dispose: () => {},
			decline: () => {},
			invalidate: () => {},
			on: () => {},
			send: () => {}
		};
	},
	updateStyle: () => {},
	removeStyle: () => {}
};
function getDefaultRequestStubs(context) {
	if (!context) {
		const clientStub = {
			...defaultClientStub,
			updateStyle,
			removeStyle
		};
		return { "/@vite/client": clientStub };
	}
	const clientStub = vm.runInContext(`(defaultClient) => ({ ...defaultClient, updateStyle: ${updateStyle.toString()}, removeStyle: ${removeStyle.toString()} })`, context)(defaultClientStub);
	return { "/@vite/client": clientStub };
}
function exportAll(exports$1, sourceModule) {
	// #1120 when a module exports itself it causes
	// call stack error
	if (exports$1 === sourceModule) return;
	if (isPrimitive(sourceModule) || Array.isArray(sourceModule) || sourceModule instanceof Promise) return;
	for (const key in sourceModule) if (key !== "default" && !(key in exports$1)) try {
		defineExport(exports$1, key, () => sourceModule[key]);
	} catch {}
}
// keep consistency with Vite on how exports are defined
function defineExport(exports$1, key, value) {
	Object.defineProperty(exports$1, key, {
		enumerable: true,
		configurable: true,
		get: value
	});
}
function isPrimitive(v) {
	return !(typeof v === "object" || typeof v === "function") || v == null;
}
function interopModule(mod) {
	if (isPrimitive(mod)) return {
		mod: { default: mod },
		defaultExport: mod
	};
	let defaultExport = "default" in mod ? mod.default : mod;
	if (!isPrimitive(defaultExport) && "__esModule" in defaultExport) {
		mod = defaultExport;
		if ("default" in defaultExport) defaultExport = defaultExport.default;
	}
	return {
		mod,
		defaultExport
	};
}
const VALID_ID_PREFIX = `/@id/`;
const NULL_BYTE_PLACEHOLDER = `__x00__`;
function wrapId(id) {
	return id.startsWith(VALID_ID_PREFIX) ? id : VALID_ID_PREFIX + id.replace("\0", NULL_BYTE_PLACEHOLDER);
}
function unwrapId(id) {
	return id.startsWith(VALID_ID_PREFIX) ? id.slice(VALID_ID_PREFIX.length).replace(NULL_BYTE_PLACEHOLDER, "\0") : id;
}

export { VitestModuleEvaluator, createImportMetaEnvProxy, getDefaultRequestStubs, isPrimitive, unwrapId, wrapId };
