import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { nestie } from '../src';

function run(input, output, msg) {
	const value = JSON.stringify(input);
	assert.equal(nestie(input), output, msg);
	assert.is(JSON.stringify(input), value, 'does not mutate');
}

test('exports', () => {
	assert.type(nestie, 'function');
});

test('wrong inputs', () => {
	run(1, undefined);
	run('', undefined);
	run(0, undefined);

	run(null, undefined);
	run(undefined, undefined);
	run(NaN, undefined);
});

test('custom glue', () => {
	const input = {
		'foo.bar': 123,
		'bar.baz': 456,
		'baz_bat': 789,
	};

	const input_string = JSON.stringify(input);

	assert.equal(
		nestie(input, '_'),
		{
			'foo.bar': 123,
			'bar.baz': 456,
			'baz': { bat: 789 },
		}
	);

	assert.is(
		input_string,
		JSON.stringify(input),
		'does not mutate original'
	);

	assert.equal(
		nestie(input, '~'),
		{
			'foo.bar': 123,
			'bar.baz': 456,
			'baz_bat': 789,
		}
	);

	assert.is(
		input_string,
		JSON.stringify(input),
		'does not mutate original'
	);
});

test('keep nullish', () => {
	run({
		'foo.bar': null,
		'bar.baz': undefined,
		'baz.bat': NaN,
		'foo.baz': 0,
	}, {
		foo: { bar: null, baz: 0 },
		bar: { baz: undefined },
		baz: { bat: NaN },
	});

	run({
		'a.0.x': null,
		'a.1.x': undefined,
		'a.3.x': false,
		'a.1.bat': NaN,
		'a.1.baz': 0,
	}, {
		a: [
			{ x: null },
			{ x: undefined, bat: NaN, baz: 0 },
			,
			{ x: false },
		]
	});
});

test('readme/demo', () => {
	run({
		'a': 'hi',
		'b.b.0': 'foo',
		'b.b.1': '',
		'b.b.3': 'bar',
		'b.d': 'hello',
		'b.e.a': 'yo',
		'b.e.b': null,
		'b.e.c': 'sup',
		'b.e.d': 0,
		'b.e.f.0.foo': 123,
		'b.e.f.0.bar': 123,
		'b.e.f.1.foo': 465,
		'b.e.f.1.bar': 456,
		'c': 'world'
	}, {
		a: 'hi',
		b: {
			b: ['foo', '', , 'bar'],
			d: 'hello',
			e: {
				a: 'yo',
				b: null,
				c: 'sup',
				d: 0,
				f: [
					{ foo: 123, bar: 123 },
					{ foo: 465, bar: 456 },
				]
			}
		},
		c: 'world'
	});
});

test('object :: simple', () => {
	run({
		'aaa': 1,
		'bbb': 2,
		'ccc.foo': 'bar',
		'ccc.baz': 'bat',
		'ddd': 4,
	}, {
		aaa: 1,
		bbb: 2,
		ccc: {
			foo: 'bar',
			baz: 'bat'
		},
		ddd: 4
	});
});

test('object :: nested', () => {
	run({
		'aaa': 1,
		'bbb.aaa': 2,
		'bbb.bbb': 3,
		'bbb.ccc.foo': 'bar',
		'bbb.ccc.baz': 'bat',
		'bbb.ddd': 4,
		'ccc': 3
	}, {
		aaa: 1,
		bbb: {
			aaa: 2,
			bbb: 3,
			ccc: {
				foo: 'bar',
				baz: 'bat'
			},
			ddd: 4
		},
		ccc: 3
	});
});

test('object :: kitchen', () => {
	run({
		'a': 1,

		'b.0.0.a': 1,
		'b.0.0.b.0': 2,
		'b.0.0.b.2': 9,
		'b.0.0.c.a.0': 1,
		'b.0.0.c.b.foo.0': 2,
		'b.0.0.c.b.foo.1': 2,
		'b.0.0.d': 4,

		'b.1.0.a': 2,
		'b.1.0.b.0': 4,
		'b.1.0.b.1': null,
		'b.1.0.b.2': 9,
		'b.1.0.c.a.0': 2,
		'b.1.0.c.b.foo.0': 4,
		'b.1.0.c.b.foo.1': 4,
		'b.1.0.d': 5,

		'b.2.0.a': 3,
		'b.2.0.b.0': 6,
		'b.2.0.b.2': 9,
		'b.2.0.c.a.0': 4,
		'b.2.0.c.b.foo.0': 8,
		'b.2.0.c.b.foo.1': 8,
		'b.2.0.d': 6,

		'c': 3,

		'd.foo': undefined,
		'd.bar.0.a': 1,
		'd.bar.0.b': 2,
		'd.bar.0.c.0.a': 1,
		'd.bar.0.c.0.b.c': 3,
		'd.bar.0.c.1.a': 2,
		'd.bar.0.c.1.b.c': 4,
		'd.bar.0.d': 4,

		'd.baz.0.a': 2,
		'd.baz.0.b': 3,
		'd.baz.0.c.0.a': 2,
		'd.baz.0.c.0.b.c': 4,
		'd.baz.0.c.1.a': 3,
		'd.baz.0.c.1.b.c': 5,
		'd.baz.0.d': 5,
	}, {
		a: 1,
		b: [
			[{ a:1, b:[2,,9], c:{ a:[1], b: { foo: [2, 2] } }, d:4 }],
			[{ a:2, b:[4,null,9], c:{ a:[2], b: { foo: [4, 4] } }, d:5 }],
			[{ a:3, b:[6,,9], c:{ a:[4], b: { foo: [8, 8] } }, d:6 }],
		],
		c: 3,
		d: {
			foo: undefined,
			bar: [{ a:1, b:2, c:[{ a:1, b:{ c:3 } }, { a:2, b:{ c:4 } }], d:4 }],
			baz: [{ a:2, b:3, c:[{ a:2, b:{ c:4 } }, { a:3, b:{ c:5 } }], d:5 }],
		}
	});
});

test('array :: simple', () => {
	run({
		'0': 0,
		'2': null,
		'3': undefined,
		'4': 1,
		'5': 2,
		'6': '',
		'7': 3
	}, [
		0, , null, undefined, 1, 2, '', 3
	]);
});

test('array :: nested', () => {
	run({
		'0.0': 1,
		'0.1': 2,
		'0.2': null,
		'0.3': 3,
		'0.4': 4,
		'1.0': 'foo',
		'1.1': 'bar',
		'1.2.0': 'hello',
		'1.2.2': 'world',
		'1.3': 'baz',
		'2.0': 6,
		'2.1': 7,
		'2.2': 8,
		'2.3': undefined,
		'2.4': 9,
	}, [
		[1, 2, null, 3, 4],
		['foo', 'bar', ['hello', , 'world'], 'baz'],
		[6, 7, 8, undefined, 9]
	]);
});

test('array :: object', () => {
	let baz = ['hello', null, 'world'];
	let bbb = { foo: 123, bar: 456, baz };

	run({
		'0.aaa': 1,
		'0.bbb.foo': 123,
		'0.bbb.bar': 456,
		'0.bbb.baz.0': 'hello',
		'0.bbb.baz.1': null,
		'0.bbb.baz.2': 'world',
		'0.ccc.0': 4,
		'0.ccc.1': 5,

		'1.aaa': 2,
		'1.bbb.foo': 123,
		'1.bbb.bar': 456,
		'1.bbb.baz.0': 'hello',
		'1.bbb.baz.1': null,
		'1.bbb.baz.2': 'world',
		'1.ccc': [],

		'2.aaa': 3,
		'2.bbb.foo': 123,
		'2.bbb.bar': 456,
		'2.bbb.baz.0': 'hello',
		'2.bbb.baz.1': null,
		'2.bbb.baz.2': 'world',
		'2.ccc.0': 9999,
	}, [
		{ aaa: 1, bbb, ccc: [4, 5] },
		{ aaa: 2, bbb, ccc: [] },
		{ aaa: 3, bbb, ccc: [9999] },
	]);
});

test('array :: kitchen', () => {
	run({
		'0': 'hello',

		'1.a': 1,
		'1.b.0.0.a': 1,
		'1.b.0.0.b.0': 2,
		'1.b.0.0.b.1': null,
		'1.b.0.0.b.2': 9,
		'1.b.0.0.c.a.0': 1,
		'1.b.0.0.c.b.foo.0': 2,
		'1.b.0.0.c.b.foo.1': 2,
		'1.b.0.0.d': 4,
		'1.b.1.0.a': 2,
		'1.b.1.0.b.0': 4,
		'1.b.1.0.b.1': undefined,
		'1.b.1.0.b.2': 9,
		'1.b.1.0.c.a.0': 2,
		'1.b.1.0.c.b.foo.0': 4,
		'1.b.1.0.c.b.foo.1': 4,
		'1.b.1.0.d': 5,
		'1.b.2.0.a': 3,
		'1.b.2.0.b.0': 6,
		'1.b.2.0.b.1': null,
		'1.b.2.0.b.2': 9,
		'1.b.2.0.c.a.0': 4,
		'1.b.2.0.c.b.foo.0': 8,
		'1.b.2.0.c.b.foo.1': 8,
		'1.b.2.0.d': 6,
		'1.c': 3,
		'1.d.foo': undefined,
		'1.d.bar.0.a': 1,
		'1.d.bar.0.b': 2,
		'1.d.bar.0.c.0.a': 1,
		'1.d.bar.0.c.0.b.c': 3,
		'1.d.bar.0.c.1.a': 2,
		'1.d.bar.0.c.1.b.c': 4,
		'1.d.bar.0.d': 4,
		'1.d.baz.0.a': 2,
		'1.d.baz.0.b': 3,
		'1.d.baz.0.c.0.a': 2,
		'1.d.baz.0.c.0.b.c': 4,
		'1.d.baz.0.c.1.a': 3,
		'1.d.baz.0.c.1.b.c': 5,
		'1.d.baz.0.d': 5,

		'2': 'world',

		'3.a': 1,
		'3.b.0.0.a': 1,
		'3.b.0.0.b.0': 2,
		'3.b.0.0.b.1': null,
		'3.b.0.0.b.2': 9,
		'3.b.0.0.c.a.0': 1,
		'3.b.0.0.c.b.foo.0': 2,
		'3.b.0.0.c.b.foo.1': 2,
		'3.b.0.0.d': 4,
		'3.b.1.0.a': 2,
		'3.b.1.0.b.0': 4,
		'3.b.1.0.b.1': undefined,
		'3.b.1.0.b.2': 9,
		'3.b.1.0.c.a.0': 2,
		'3.b.1.0.c.b.foo.0': 4,
		'3.b.1.0.c.b.foo.1': 4,
		'3.b.1.0.d': 5,
		'3.b.2.0.a': 3,
		'3.b.2.0.b.0': 6,
		'3.b.2.0.b.1': null,
		'3.b.2.0.b.2': 9,
		'3.b.2.0.c.a.0': 4,
		'3.b.2.0.c.b.foo.0': 8,
		'3.b.2.0.c.b.foo.1': 8,
		'3.b.2.0.d': 6,
		'3.c': 3,
		'3.d.foo': undefined,
		'3.d.bar.0.a': 1,
		'3.d.bar.0.b': 2,
		'3.d.bar.0.c.0.a': 1,
		'3.d.bar.0.c.0.b.c': 3,
		'3.d.bar.0.c.1.a': 2,
		'3.d.bar.0.c.1.b.c': 4,
		'3.d.bar.0.d': 4,
		'3.d.baz.0.a': 2,
		'3.d.baz.0.b': 3,
		'3.d.baz.0.c.0.a': 2,
		'3.d.baz.0.c.0.b.c': 4,
		'3.d.baz.0.c.1.a': 3,
		'3.d.baz.0.c.1.b.c': 5,
		'3.d.baz.0.d': 5,
	}, [
		'hello',
		{
			a: 1,
			b: [
				[{ a:1, b:[2,null,9], c:{ a:[1], b: { foo: [2, 2] } }, d:4 }],
				[{ a:2, b:[4,undefined,9], c:{ a:[2], b: { foo: [4, 4] } }, d:5 }],
				[{ a:3, b:[6,null,9], c:{ a:[4], b: { foo: [8, 8] } }, d:6 }],
			],
			c: 3,
			d: {
				foo: undefined,
				bar: [{ a:1, b:2, c:[{ a:1, b:{ c:3 } }, { a:2, b:{ c:4 } }], d:4 }],
				baz: [{ a:2, b:3, c:[{ a:2, b:{ c:4 } }, { a:3, b:{ c:5 } }], d:5 }],
			}
		},
		'world',
		{
			a: 1,
			b: [
				[{ a:1, b:[2,null,9], c:{ a:[1], b: { foo: [2, 2] } }, d:4 }],
				[{ a:2, b:[4,undefined,9], c:{ a:[2], b: { foo: [4, 4] } }, d:5 }],
				[{ a:3, b:[6,null,9], c:{ a:[4], b: { foo: [8, 8] } }, d:6 }],
			],
			c: 3,
			d: {
				foo: undefined,
				bar: [{ a:1, b:2, c:[{ a:1, b:{ c:3 } }, { a:2, b:{ c:4 } }], d:4 }],
				baz: [{ a:2, b:3, c:[{ a:2, b:{ c:4 } }, { a:3, b:{ c:5 } }], d:5 }],
			}
		},
	]);
});

test('proto pollution :: __proto__ :: toplevel', () => {
	let output = nestie({
		'__proto__.foobar': 123
	});

	let tmp = {};
	assert.equal(output, {});
	assert.is(tmp.foobar, undefined);
});

test('proto pollution :: __proto__ :: midlevel', () => {
	let output = nestie({
		'aaa.__proto__.foobar': 123
	});

	let tmp = {};
	assert.equal(output, { aaa: {} });
	assert.is(tmp.foobar, undefined);
});

test('proto pollution :: __proto__ :: sibling', () => {
	let output = nestie({
		'aaa.bbb': 'abc',
		'__proto__.foobar': 123,
		'aaa.xxx': 'xxx',
		'foo.bar': 456,
	});

	assert.equal(output, {
		aaa: {
			bbb: 'abc',
			xxx: 'xxx',
		},
		foo: {
			bar: 456
		}
	});

	let tmp = {};
	assert.is(tmp.foobar, undefined);
});

test('proto pollution :: prototype', () => {
	let output = nestie({
		'a.prototype.hello': 'world',
	});

	assert.equal(output, {
		a: {
			// converted, then aborted
		}
	});

	assert.is.not({}.hello, 'world');
	assert.is({}.hello, undefined);
});

test('proto pollution :: constructor :: direct', () => {
	function Custom() {
		//
	}

	let output = nestie({
		'a.constructor': Custom,
		'foo.bar': 123,
	});

	assert.equal(output, {
		a: {
			// stopped
		},
		foo: {
			bar: 123,
		}
	});

	// Check existing object
	assert.is.not(output.a.constructor, Custom);
	assert.not.instance(output.a, Custom);
	assert.instance(output.a.constructor, Object, '~> 123 -> {}');
	assert.is(output.a.hasOwnProperty('constructor'), false);

	let tmp = {}; // Check new object
	assert.is.not(tmp.constructor, Custom);
	assert.not.instance(tmp, Custom);

	assert.instance(tmp.constructor, Object, '~> 123 -> {}');
	assert.is(tmp.hasOwnProperty('constructor'), false);
});

test('proto pollution :: constructor :: nested', () => {
	let output = nestie({
		'constructor.prototype.hello': 'world',
		'foo': 123,
	});

	assert.equal(output, {
		foo: 123
	});

	assert.is(output.hasOwnProperty('constructor'), false);
	assert.is(output.hasOwnProperty('hello'), false);

	let tmp = {};
	assert.is(tmp.hasOwnProperty('constructor'), false);
	assert.is(tmp.hasOwnProperty('hello'), false);
});

test.run();