"use strict";

const assert = require("assert");

const path = require("path");
const url = require("url");
const resolve = require("../");
const { describe, it } = require("./_runner");

const fixtures = path.join(__dirname, "fixtures");

const asyncContextResolve = resolve.create({
	extensions: [".js", ".json", ".node"],
	resolveToContext: true,
});

const syncContextResolve = resolve.create.sync({
	extensions: [".js", ".json", ".node"],
	resolveToContext: true,
});

const promiseContextResolve = resolve.create.promise({
	extensions: [".js", ".json", ".node"],
	resolveToContext: true,
});

const issue238Resolve = resolve.create({
	extensions: [".js", ".jsx", ".ts", ".tsx"],
	modules: ["src/a", "src/b", "src/common", "node_modules"],
});

const preferRelativeResolve = resolve.create({
	preferRelative: true,
});

/**
 * @param {string} name name
 * @param {string} context context
 * @param {string} moduleName module name
 * @param {string} result result
 * @returns {void}
 */
function testResolve(name, context, moduleName, result) {
	describe(name, () => {
		it("should resolve sync correctly", () => {
			const filename = resolve.sync(context, moduleName);
			assert.notStrictEqual(filename, undefined);
			assert.deepStrictEqual(filename, result);
		});

		it("should resolve async correctly", (t, done) => {
			resolve(context, moduleName, (err, filename) => {
				if (err) return done(err);
				assert.notStrictEqual(filename, undefined);
				assert.deepStrictEqual(filename, result);
				done();
			});
		});

		it("should resolve promise correctly", async () => {
			const filename = await resolve.promise(context, moduleName);
			assert.notStrictEqual(filename, undefined);
			assert.deepStrictEqual(filename, result);
		});
	});
}

/**
 * @param {string} name name
 * @param {string} context context
 * @param {string} moduleName module name
 * @param {string} result result
 * @returns {void}
 */
function testResolveContext(name, context, moduleName, result) {
	describe(name, () => {
		it("should resolve async correctly", (t, done) => {
			asyncContextResolve(context, moduleName, (err, filename) => {
				if (err) done(err);
				assert.notStrictEqual(filename, undefined);
				assert.deepStrictEqual(filename, result);
				done();
			});
		});

		it("should resolve sync correctly", () => {
			const filename = syncContextResolve(context, moduleName);
			assert.notStrictEqual(filename, undefined);
			assert.deepStrictEqual(filename, result);
		});

		it("should resolve promise correctly", async () => {
			const filename = await promiseContextResolve(context, moduleName);
			assert.notStrictEqual(filename, undefined);
			assert.deepStrictEqual(filename, result);
		});
	});
}

describe("resolve", () => {
	testResolve(
		"absolute path",
		fixtures,
		path.join(fixtures, "main1.js"),
		path.join(fixtures, "main1.js"),
	);

	testResolve(
		"file with .js",
		fixtures,
		"./main1.js",
		path.join(fixtures, "main1.js"),
	);
	testResolve(
		"file without extension",
		fixtures,
		"./main1",
		path.join(fixtures, "main1.js"),
	);
	testResolve(
		"another file with .js",
		fixtures,
		"./a.js",
		path.join(fixtures, "a.js"),
	);
	testResolve(
		"another file without extension",
		fixtures,
		"./a",
		path.join(fixtures, "a.js"),
	);
	testResolve(
		"file in module with .js",
		fixtures,
		"m1/a.js",
		path.join(fixtures, "node_modules", "m1", "a.js"),
	);
	testResolve(
		"file in module without extension",
		fixtures,
		"m1/a",
		path.join(fixtures, "node_modules", "m1", "a.js"),
	);
	testResolve(
		"another file in module without extension",
		fixtures,
		"complexm/step1",
		path.join(fixtures, "node_modules", "complexm", "step1.js"),
	);
	testResolve(
		"from submodule to file in sibling module",
		path.join(fixtures, "node_modules", "complexm"),
		"m2/b.js",
		path.join(fixtures, "node_modules", "m2", "b.js"),
	);
	testResolve(
		"from submodule to file in sibling of parent module",
		path.join(fixtures, "node_modules", "complexm", "web_modules", "m1"),
		"m2/b.js",
		path.join(fixtures, "node_modules", "m2", "b.js"),
	);
	testResolve(
		"from nested directory to overwritten file in module",
		path.join(fixtures, "multiple_modules"),
		"m1/a.js",
		path.join(fixtures, "multiple_modules", "node_modules", "m1", "a.js"),
	);
	testResolve(
		"from nested directory to not overwritten file in module",
		path.join(fixtures, "multiple_modules"),
		"m1/b.js",
		path.join(fixtures, "node_modules", "m1", "b.js"),
	);

	testResolve(
		"file with query",
		fixtures,
		"./main1.js?query",
		`${path.join(fixtures, "main1.js")}?query`,
	);
	testResolve(
		"file with fragment",
		fixtures,
		"./main1.js#fragment",
		`${path.join(fixtures, "main1.js")}#fragment`,
	);
	testResolve(
		"file with fragment and query",
		fixtures,
		"./main1.js#fragment?query",
		`${path.join(fixtures, "main1.js")}#fragment?query`,
	);
	testResolve(
		"file with query and fragment",
		fixtures,
		"./main1.js?#fragment",
		`${path.join(fixtures, "main1.js")}?#fragment`,
	);

	testResolve(
		"file in module with query",
		fixtures,
		"m1/a?query",
		`${path.join(fixtures, "node_modules", "m1", "a.js")}?query`,
	);
	testResolve(
		"file in module with fragment",
		fixtures,
		"m1/a#fragment",
		`${path.join(fixtures, "node_modules", "m1", "a.js")}#fragment`,
	);
	testResolve(
		"file in module with fragment and query",
		fixtures,
		"m1/a#fragment?query",
		`${path.join(fixtures, "node_modules", "m1", "a.js")}#fragment?query`,
	);
	testResolve(
		"file in module with query and fragment",
		fixtures,
		"m1/a?#fragment",
		`${path.join(fixtures, "node_modules", "m1", "a.js")}?#fragment`,
	);

	testResolveContext("context for fixtures", fixtures, "./", fixtures);
	testResolveContext(
		"context for fixtures/lib",
		fixtures,
		"./lib",
		path.join(fixtures, "lib"),
	);
	testResolveContext(
		"context for fixtures with ..",
		fixtures,
		"./lib/../../fixtures/./lib/..",
		fixtures,
	);

	testResolveContext(
		"context for fixtures with query",
		fixtures,
		"./?query",
		`${fixtures}?query`,
	);

	testResolve(
		"differ between directory and file, resolve file",
		fixtures,
		"./dirOrFile",
		path.join(fixtures, "dirOrFile.js"),
	);
	testResolve(
		"differ between directory and file, resolve directory",
		fixtures,
		"./dirOrFile/",
		path.join(fixtures, "dirOrFile", "index.js"),
	);

	testResolve(
		"find node_modules outside of node_modules",
		path.join(fixtures, "browser-module", "node_modules"),
		"m1/a",
		path.join(fixtures, "node_modules", "m1", "a.js"),
	);

	testResolve(
		"don't crash on main field pointing to self",
		fixtures,
		"./main-field-self",
		path.join(fixtures, "main-field-self", "index.js"),
	);

	testResolve(
		"don't crash on main field pointing to self",
		fixtures,
		"./main-field-self2",
		path.join(fixtures, "main-field-self2", "index.js"),
	);

	testResolve(
		"handle fragment edge case (no fragment)",
		fixtures,
		"./no#fragment/#/#",
		path.join(fixtures, "no\0#fragment/\0#", "\0#.js"),
	);

	testResolve(
		"handle fragment edge case (fragment)",
		fixtures,
		"./no#fragment/#/",
		`${path.join(fixtures, "no.js")}#fragment/#/`,
	);

	testResolve(
		"handle fragment escaping",
		fixtures,
		"./no\0#fragment/\0#/\0##fragment",
		`${path.join(fixtures, "no\0#fragment/\0#", "\0#.js")}#fragment`,
	);

	testResolve(
		"handle file URL",
		fixtures,
		url.pathToFileURL(path.resolve(fixtures, "./main1.js")).toString(),
		path.join(fixtures, "main1.js"),
	);

	testResolve(
		"handle file URL with query",
		fixtures,
		`${url
			.pathToFileURL(path.resolve(fixtures, "./main1.js"))
			.toString()}?query`,
		path.join(fixtures, "main1.js"),
	);

	testResolve(
		"handle file URL with fragment",
		fixtures,
		`${url
			.pathToFileURL(path.resolve(fixtures, "./main1.js"))
			.toString()}#fragment`,
		path.join(fixtures, "main1.js"),
	);

	testResolve(
		"handle file URL with query and fragment",
		fixtures,
		`${url
			.pathToFileURL(path.resolve(fixtures, "./main1.js"))
			.toString()}?query#fragment`,
		path.join(fixtures, "main1.js"),
	);

	it("should correctly resolve", (t, done) => {
		const issue238 = path.resolve(fixtures, "issue-238");

		issue238Resolve(
			path.resolve(issue238, "./src/common"),
			"config/myObjectFile",
			(err, filename) => {
				if (err) done(err);
				assert.notStrictEqual(filename, undefined);
				assert.deepStrictEqual(
					filename,
					path.resolve(issue238, "./src/common/config/myObjectFile.js"),
				);
				done();
			},
		);
	});

	it("should correctly resolve with preferRelative", (t, done) => {
		preferRelativeResolve(fixtures, "main1.js", (err, filename) => {
			if (err) done(err);
			assert.notStrictEqual(filename, undefined);
			assert.deepStrictEqual(filename, path.join(fixtures, "main1.js"));
			done();
		});
	});

	it("should correctly resolve with preferRelative #2", (t, done) => {
		preferRelativeResolve(fixtures, "m1/a.js", (err, filename) => {
			if (err) done(err);
			assert.notStrictEqual(filename, undefined);
			assert.deepStrictEqual(
				filename,
				path.join(fixtures, "node_modules", "m1", "a.js"),
			);
			done();
		});
	});

	it("should not crash when passing undefined as path", (t, done) => {
		// @ts-expect-error testing invalid arguments
		resolve(fixtures, undefined, (err) => {
			assert.ok(err instanceof Error);
			done();
		});
	});

	it("should not crash when passing undefined as context", (t, done) => {
		// @ts-expect-error testing invalid arguments
		resolve({}, undefined, "./test/resolve.js", (err) => {
			assert.ok(err instanceof Error);
			done();
		});
	});

	it("should not crash when passing undefined everywhere", (t, done) => {
		// @ts-expect-error testing invalid arguments
		resolve(undefined, undefined, undefined, undefined, (err) => {
			assert.ok(err instanceof Error);
			done();
		});
	});
});
