'use strict';

// SHA-1 vectors generated by Node.js
// SHA-256/SHA-512 test vectors from:
// https://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors
// https://stackoverflow.com/questions/15593184/pbkdf2-hmac-sha-512-test-vectors
var tape = require('tape');
var satisfies = require('semver').satisfies;
var Buffer = require('safe-buffer').Buffer;

var node = require('crypto');

var fixtures = require('./fixtures');
var js = require('../browser');
var browserImpl = require('../lib/sync-browser');

var errMsg = function (name) { return new RegExp(name + ' must be a string, a Buffer, a Uint8Array, or a DataView'); };

var pVersionMajor = parseInt(process.version.split('.')[0].slice(1), 10);
/* istanbul ignore next */
if (pVersionMajor !== 4 || process.browser) {
	fixtures.invalid.push({
		key: 'password',
		salt: 'salt',
		iterations: 1,
		dkLen: -1,
		exception: 'Bad key length'
	}, {
		key: 'password',
		salt: 'salt',
		iterations: 1,
		dkLen: 4073741824,
		exception: 'Bad key length'
	});
}
/* istanbul ignore next */
if (pVersionMajor >= 5 || process.browser) {
	fixtures.invalid.push({
		key: 'password',
		salt: 'salt',
		iterations: 1,
		dkLen: NaN,
		exception: 'Bad key length'
	}, {
		key: 'password',
		salt: 'salt',
		iterations: 1,
		dkLen: Infinity,
		exception: 'Bad key length'
	});
}
/* istanbul ignore next */
if (pVersionMajor >= 6 || process.browser) {
	fixtures.valid.push({
		description: 'Unicode salt, no truncation',
		key: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
		salt: 'mnemonicメートルガバヴァぱばぐゞちぢ十人十色',
		iterations: 2048,
		dkLen: 64,
		results: {
			sha1: '7e042a2f41ba17e2235fbc794e22a150816b0f54a1dfe113919fccb7a056066a109385e538f183c92bad896ae8b7d8e0f4cd66df359c77c8c7785cd1001c9a2c',
			sha256: '0b57118f2b6b079d9371c94da3a8315c3ada87a1e819b40c4c4e90b36ff2d3c8fd7555538b5119ac4d3da7844aa4259d92f9dd2188e78ac33c4b08d8e6b5964b',
			sha512: 'ba553eedefe76e67e2602dc20184c564010859faada929a090dd2c57aacb204ceefd15404ab50ef3e8dbeae5195aeae64b0def4d2eead1cdc728a33ced520ffd',
			sha224: 'd76474c525616ce2a527d23df8d6f6fcc4251cc3535cae4e955810a51ead1ec6acbe9c9619187ca5a3c4fd636de5b5fe58d031714731290bbc081dbf0fcb8fc1',
			sha384: '15010450f456769467e834db7fa93dd9d353e8bb733b63b0621090f96599ac3316908eb64ac9366094f0787cd4bfb2fea25be41dc271a19309710db6144f9b34',
			ripemd160: '255321c22a32f41ed925032043e01afe9cacf05470c6506621782c9d768df03c74cb3fe14a4296feba4c2825e736486fb3871e948f9c413ca006cc20b7ff6d37'
		}
	});

// 'binary' behaviour, Node 6 defaulted to 'utf-8'
} else {
	fixtures.valid.push({
		description: 'Unicode salt, suffers from truncation',
		key: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
		salt: 'mnemonicメートルガバヴァぱばぐゞちぢ十人十色',
		iterations: 2048,
		dkLen: 64,
		results: {
			sha1: 'd85d14adcb7bdb5d976160e504f520a98cf71aca4cd5fceadf37759743bd6e1d2ff78bdd4403552aef7658094384b341ede80fffd334182be076f9d988a0a40f',
			sha256: 'b86b5b900c29ed2724359afd793e10ffc1eb0e7d6f624fc9c85b8ac1785d9a2f0575af52a2338e611f2e6cffdee544adfff6f3d4f43be2ba0e2bd7e917b38a14',
			sha512: '3a863fa00f2e97a83fa9b18805e0047a6282cbae0ff48438b33a14475771c52d05137daa12e364cb34d84547ac07568b801c5c7f8dd4baaeee18a67a5c6a3377',
			sha224: '95727793842437774ad9ae27b8154a6f37f208b75a03d3a4d4a2443422bb6bc85efcfa92aa4376926ea89a8f5a63118eecdb58c8ca28ab31007da79437e0a1ef',
			sha384: '1a7e02e8ba0e357269a55642024b85738b95238d6cdc49bc440204995aefeff499e22cba76d4c7e96b7d4a9596a70e744f53fa94f3547e7dc506fcaf16ceb4a2',
			ripemd160: 'bac7849db13e90604620945695288ffee20369107c3a6632d6b1d6b926175ac914319b5a742e6b1a37b82841b6f010ad47ebdb5cd608026eb48513bf68cb54f5'
		}
	});
}

function runTests(name, compat) {
	tape(name + ' defaults to sha1 and handles buffers', function (t) {
		t.plan(3);

		var resultSync = compat.pbkdf2Sync('password', 'salt', 1, 32);
		t.equal(resultSync.toString('hex'), '0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164');

		compat.pbkdf2(Buffer.from('password'), Buffer.from('salt'), 1, 32, function (err, result) {
			t.error(err);
			t.equal(result.toString('hex'), '0c60c80f961f0e71f3a9b524af6012062fe037a6e0f0eb94fe8fc46bdc637164');
		});
	});

	tape(name + ' should throw if no callback is provided', function (t) {
		t.plan(1);

		t['throws'](function () {
			compat.pbkdf2('password', 'salt', 1, 32, 'sha1');
		}, /No callback provided to pbkdf2/);
	});

	tape(name + ' should throw if the password is not a string or an ArrayBuffer', function (t) {
		t.plan(2);

		t['throws'](
			function () { compat.pbkdf2(['a'], 'salt', 1, 32, 'sha1', function () {}); },
			errMsg('Password')
		);

		t['throws'](
			function () { compat.pbkdf2Sync(['a'], 'salt', 1, 32, 'sha1'); },
			errMsg('Password')
		);
	});

	tape(name + ' should throw if the salt is not a string or an ArrayBuffer', function (t) {
		t.plan(2);

		t['throws'](
			function () { compat.pbkdf2('pass', ['salt'], 1, 32, 'sha1'); },
			errMsg('Salt')
		);

		t['throws'](
			function () { compat.pbkdf2Sync('pass', ['salt'], 1, 32, 'sha1'); },
			errMsg('Salt')
		);
	});

	var algos = ['sha1', 'sha224', 'sha256', 'sha384', 'sha512', 'ripemd160'];
	algos.forEach(function (algorithm) {
		var isUnsupported = satisfies(process.version, '^17') && algorithm === 'ripemd160';
		tape(
			name + ' + ' + algorithm,
			{ skip: isUnsupported && 'this node version does not support ' + algorithm },
			function (t) {
				fixtures.valid.forEach(function (f) {
					var key, keyType, salt, saltType;
					if (f.keyUint8Array) {
						key = new Uint8Array(f.keyUint8Array);
						keyType = 'Uint8Array';
					} else if (f.keyInt32Array) {
						key = new Int32Array(f.keyInt32Array);
						keyType = 'Int32Array';
					} else if (f.keyFloat64Array) {
						key = new Float64Array(f.keyFloat64Array);
						keyType = 'Float64Array';
					} else if (f.keyHex) {
						key = Buffer.from(f.keyHex, 'hex');
						keyType = 'hex';
					} else {
						key = f.key;
						keyType = 'string';
					}
					if (f.saltUint8Array) {
						salt = new Uint8Array(f.saltUint8Array);
						saltType = 'Uint8Array';
					} else if (f.saltInt32Array) {
						salt = new Int32Array(f.saltInt32Array);
						saltType = 'Int32Array';
					} else if (f.saltFloat64Array) {
						salt = new Float64Array(f.saltFloat64Array);
						saltType = 'Float64Array';
					} else if (f.saltHex) {
						salt = Buffer.from(f.saltHex, 'hex');
						saltType = 'hex';
					} else {
						salt = f.salt;
						saltType = 'string';
					}
					var expected = f.results[algorithm];
					var description = algorithm + ' encodes "' + key + '" (' + keyType + ') with salt "' + salt + '" (' + saltType + ') with ' + algorithm + ' to ' + expected;

					t.test(name + ' async w/ ' + description, function (st) {
						st.plan(3);

						compat.pbkdf2(key, salt, f.iterations, f.dkLen, algorithm, function (err, result) {
							st.error(err);

							var hash = result.toString('hex');

							st.doesNotMatch(hash, /^0+$/, 'is not the all-zeroes result');
							st.equal(hash, expected);
						});
					});

					t.test(name + 'sync w/ ' + description, function (st) {
						st.plan(2);

						var result = compat.pbkdf2Sync(key, salt, f.iterations, f.dkLen, algorithm);
						var hash = result.toString('hex');

						st.doesNotMatch(hash, /^0+$/, 'is not the all-zeroes result');
						st.equal(hash, expected);
					});
				});

				fixtures.invalid.forEach(function (f) {
					var description = algorithm + ' should throw ' + f.exception;

					t.test(name + ' async w/ ' + description, function (st) {
						st.plan(1);
						/* istanbul ignore next */
						function noop() {}
						st['throws'](function () {
							compat.pbkdf2(f.key, f.salt, f.iterations, f.dkLen, f.algo, noop);
						}, new RegExp(f.exception));
					});

					t.test(name + ' sync w/' + description, function (st) {
						st.plan(1);

						st['throws'](function () {
							compat.pbkdf2Sync(f.key, f.salt, f.iterations, f.dkLen, f.algo);
						}, new RegExp(f.exception));
					});
				});

				t.end();
			}
		);
	});
}

tape('does not return all zeroes for any algorithm', function (t) {
	var algos2 = [
		// 'sha3-512',
		// 'sha3-256',
		// 'SHA3-384',
		// 'blake2b512',
		'Sha256',
		'ShA256',
		'Sha512',
		'sha512-256',
		'SHA512',
		'SHA1',
		's-h-a-1',
		'sha-1',
		'RMD160',
		'RIPEMD-160',
		'ripemd-160'
	];
	algos2.forEach(function (algo) {
		var throwCount = 0;
		var impls = { __proto__: null, node: node.pbkdf2Sync, lib: js.pbkdf2Sync, browser: browserImpl };
		var results = { __proto__: null };
		var throws = { __proto__: null };
		for (var implName in impls) { // eslint-disable-line no-restricted-syntax
			var pbkdf2Sync = impls[implName];
			try {
				var key = pbkdf2Sync('secret', 'salt', 1e4, 64, algo).toString('hex');
				results[implName] = key;
				t.doesNotMatch(key, /^0+$/, implName + ' does not return all zeros for ' + algo);
			} catch (e) {
				throwCount += 1;
				throws[implName] = true;
				t.ok(e, implName + ' throws for ' + algo);
				t.comment(e);
			}
		}

		if (throwCount === 0) {
			t.equal(throwCount, 0, 'all implementations return a value for ' + algo);
			t.deepEqual(throws, { __proto__: null }, 'no implementations throw for ' + algo);
			t.equal(
				results.node,
				results.lib,
				'node and js pbkdf2Sync should return the same value for ' + algo
			);

			t.equal(
				results.node,
				results.browser,
				'node and browser pbkdf2Sync should return the same value for ' + algo
			);
		} else {
			var expected = {
				__proto__: null,
				node: true,
				lib: true,
				browser: true
			};
			if (
				(algo.toLowerCase() === 'ripemd-160' || algo.toLowerCase() === 'rmd160')
				&& satisfies(process.version, '< 18') // node < 18 doesn't support ripemd160
			) {
				if (!throws.browser) {
					delete expected.browser;
				}
				if (!throws.lib) {
					delete expected.lib;
				}
			}

			if (
				algo.toLowerCase() === 'sha-1'
				&& satisfies(process.version, '< 17') // node < 17 doesn't support "sha-1"
				&& !throws.lib
				&& !throws.browser
			) {
				delete expected.lib;
				delete expected.browser;
			}

			if (
				(algo.toLowerCase() === 'sha256' || algo.toLowerCase() === 'sha512')
				&& satisfies(process.version, '< 10') // node < 10 doesn't support "sha256" or "sha512"
				&& !throws.lib
				&& !throws.browser
			) {
				delete expected.lib;
				delete expected.browser;
			}

			t.deepEqual(
				throws,
				expected,
				'all implementations throw for ' + algo,
				{ todo: throwCount === 1 && algo === 'sha512-256' && 'sha.js does not yet support sha512-256' }
			);
		}
	});

	t.end();
});

runTests('JavaScript pbkdf2', js);

var assign = require('object.assign');

/* istanbul ignore next */
if (!process.browser) {
	/* eslint global-require: 0 */
	var browser = assign({}, js);
	browser.pbkdf2Sync = browserImpl;
	runTests('browser pbkdf2', {
		pbkdf2: browser.pbkdf2,
		pbkdf2Sync: require('../lib/sync-browser')
	});
	runTests('node pbkdf2', require('../'));
}
