"use strict";

const { createHash, randomBytes } = require("crypto");

const wasmHashes = {
	xxhash64: () => {
		const createHash = require("../lib/util/hash/xxhash64");
		const createReferenceHash =
			// @ts-expect-error
			require("hash-wasm/dist/xxhash64.umd.min").createXXHash64;

		return {
			createHash,
			createReferenceHash: async () => (await createReferenceHash()).init(),
			regExp: /^[0-9a-f]{16}$/
		};
	},
	"xxhash64-createHash": () => {
		const createXxHash = require("../lib/util/hash/xxhash64");
		const createHash = require("../lib/util/createHash");

		return {
			createHash: () => createHash("xxhash64"),
			createReferenceHash: createXxHash,
			regExp: /^[0-9a-f]{16}$/
		};
	},
	md4: () => {
		const createMd4Hash = require("../lib/util/hash/md4");

		return {
			createHash: createMd4Hash,
			createReferenceHash:
				Number.parseInt(process.version.slice(1), 10) < 17
					? async () => createHash("md4")
					: createMd4Hash,
			regExp: /^[0-9a-f]{32}$/
		};
	},
	"md4-createHash": () => {
		const createMd4Hash = require("../lib/util/hash/md4");
		const createHash = require("../lib/util/createHash");

		return {
			createHash: () => createHash("md4"),
			createReferenceHash: createMd4Hash,
			regExp: /^[0-9a-f]{32}$/
		};
	}
};

for (const name of Object.keys(wasmHashes)) {
	const { createHash, createReferenceHash, regExp } = wasmHashes[name]();

	describe(name, () => {
		const sizes = [
			1,
			2,
			3,
			4,
			5,
			7,
			8,
			9,
			16,
			31,
			32,
			33,
			64 - 10,
			64 - 9,
			64 - 8,
			63,
			64,
			65,
			100,
			1000,
			65536 - 1,
			65536,
			65536 + 1,
			65536 + 31,
			65536 * 5,
			65536 * 7 - 1,
			65536 * 9 + 31
		];

		/**
		 * @param {string} name name
		 * @param {number[]} sizes sizes
		 */
		const test = (name, sizes) => {
			it(`${name} should generate a hash from binary data`, async () => {
				const hash = createHash();
				const hashString = createHash();
				const reference = await createReferenceHash();
				for (const size of sizes) {
					const bytes = randomBytes(size);
					const string = bytes.toString("base64");
					hash.update(bytes);
					hashString.update(string, "base64");
					reference.update(bytes);
				}
				const result = hash.digest("hex");
				expect(result).toMatch(regExp);
				const resultFromString = hashString.digest("hex");
				expect(resultFromString).toMatch(regExp);
				const expected = reference.digest("hex");
				expect(result).toBe(expected);
				expect(resultFromString).toBe(expected);
			});
		};

		test("empty hash", []);

		for (const size of sizes) {
			test(`single update ${size} bytes`, [size]);
		}

		for (const size1 of sizes) {
			for (const size2 of sizes) {
				test(`two updates ${size1} + ${size2} bytes`, [size1, size2]);
			}
		}

		test("many updates 1", sizes);

		test("many updates 2", [...sizes].reverse());

		test("many updates 3", [...sizes, ...[...sizes].reverse()]);

		test("many updates 4", [...[...sizes].reverse(), ...sizes]);

		/**
		 * @param {string} name name
		 * @param {string | number[]} codePoints code points
		 */
		const unicodeTest = (name, codePoints) => {
			it(`${name} should hash unicode chars correctly`, async () => {
				const hash = createHash();
				const reference = await createReferenceHash();
				const str =
					typeof codePoints === "string"
						? codePoints
						: String.fromCodePoint(...codePoints);
				hash.update(str);
				reference.update(str);
				const result = hash.digest("hex");
				expect(result).toMatch(regExp);
				const expected = reference.digest("hex");
				expect(result).toBe(expected);
			});
		};

		/**
		 * @param {string} name name
		 * @param {number} start start
		 * @param {number} end end
		 */
		const unicodeRangeTest = (name, start, end) => {
			const codePoints = [];
			for (let i = start; i <= end; i++) {
				codePoints.push(i);
			}
			unicodeTest(name, codePoints);
		};

		// cspell:word Thaana
		unicodeRangeTest("Latin-1 Supplement", 0xa0, 0xff);
		unicodeRangeTest("Latin Extended", 0x100, 0x24f);
		unicodeRangeTest("Thaana", 0x780, 0x7bf);
		unicodeRangeTest("Devanagari", 0x900, 0x97f);
		unicodeRangeTest("Emoticons", 0x1f600, 0x1f64f);

		unicodeTest("with zero char", "abc\0💩");
		unicodeTest("weird code point after long code point", [1497, 243248]);

		for (let i = 0; i < 1000; i++) {
			const chars = Array.from({ length: 20 }, () =>
				Math.floor(Math.random() * 0x10ffff)
			);
			unicodeTest(`fuzzy ${JSON.stringify(chars)}`, chars);
		}
	});
}
