import { augmentArray, augmentObject } from '../src/augment-object';
import type { IDataObject } from '../src/interfaces';
import { deepCopy } from '../src/utils';

describe('AugmentObject', () => {
	describe('augmentArray', () => {
		test('should work with arrays', () => {
			const originalObject = [1, 2, 3, 4, null];
			const copyOriginal = deepCopy(originalObject);

			const augmentedObject = augmentArray(originalObject);

			expect(augmentedObject.constructor.name).toEqual('Array');

			expect(augmentedObject.push(5)).toEqual(6);
			expect(augmentedObject).toEqual([1, 2, 3, 4, null, 5]);
			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject.pop()).toEqual(5);
			expect(augmentedObject).toEqual([1, 2, 3, 4, null]);
			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject.shift()).toEqual(1);
			expect(augmentedObject).toEqual([2, 3, 4, null]);
			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject.unshift(1)).toEqual(5);
			expect(augmentedObject).toEqual([1, 2, 3, 4, null]);
			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject.splice(1, 1)).toEqual([2]);
			expect(augmentedObject).toEqual([1, 3, 4, null]);
			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject.slice(1)).toEqual([3, 4, null]);
			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject.reverse()).toEqual([null, 4, 3, 1]);
			expect(originalObject).toEqual(copyOriginal);
		});

		test('should work with arrays on any level', () => {
			const originalObject = {
				a: {
					b: {
						c: [
							{
								a3: {
									b3: {
										c3: '03' as string | null,
									},
								},
								aa3: '01',
							},
							{
								a3: {
									b3: {
										c3: '13',
									},
								},
								aa3: '11',
							},
						],
					},
				},
				aa: [
					{
						a3: {
							b3: '2',
						},
						aa3: '1',
					},
				],
			};
			const copyOriginal = deepCopy(originalObject);

			const augmentedObject = augmentObject(originalObject);

			// On first level
			augmentedObject.aa[0].a3.b3 = '22';
			expect(augmentedObject.aa[0].a3.b3).toEqual('22');
			expect(originalObject.aa[0].a3.b3).toEqual('2');

			// Make sure that also array operations as push and length work as expected
			// On lower levels
			augmentedObject.a.b.c[0].a3.b3.c3 = '033';
			expect(augmentedObject.a.b.c[0].a3.b3.c3).toEqual('033');
			expect(originalObject.a.b.c[0].a3.b3.c3).toEqual('03');

			augmentedObject.a.b.c[1].a3.b3.c3 = '133';
			expect(augmentedObject.a.b.c[1].a3.b3.c3).toEqual('133');
			expect(originalObject.a.b.c[1].a3.b3.c3).toEqual('13');

			augmentedObject.a.b.c.push({
				a3: {
					b3: {
						c3: '23',
					},
				},
				aa3: '21',
			});
			augmentedObject.a.b.c[2].a3.b3.c3 = '233';
			expect(augmentedObject.a.b.c[2].a3.b3.c3).toEqual('233');

			augmentedObject.a.b.c[2].a3.b3.c3 = '2333';
			expect(augmentedObject.a.b.c[2].a3.b3.c3).toEqual('2333');

			augmentedObject.a.b.c[2].a3.b3.c3 = null;
			expect(augmentedObject.a.b.c[2].a3.b3.c3).toEqual(null);

			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject.a.b.c.length).toEqual(3);

			expect(augmentedObject.aa).toEqual([
				{
					a3: {
						b3: '22',
					},
					aa3: '1',
				},
			]);

			expect(augmentedObject.a.b.c).toEqual([
				{
					a3: {
						b3: {
							c3: '033',
						},
					},
					aa3: '01',
				},
				{
					a3: {
						b3: {
							c3: '133',
						},
					},
					aa3: '11',
				},
				{
					a3: {
						b3: {
							c3: null,
						},
					},
					aa3: '21',
				},
			]);

			expect(augmentedObject).toEqual({
				a: {
					b: {
						c: [
							{
								a3: {
									b3: {
										c3: '033',
									},
								},
								aa3: '01',
							},
							{
								a3: {
									b3: {
										c3: '133',
									},
								},
								aa3: '11',
							},
							{
								a3: {
									b3: {
										c3: null,
									},
								},
								aa3: '21',
							},
						],
					},
				},
				aa: [
					{
						a3: {
							b3: '22',
						},
						aa3: '1',
					},
				],
			});

			expect(originalObject).toEqual(copyOriginal);
		});
	});

	describe('augmentObject', () => {
		test('should work with simple values on first level', () => {
			const date = new Date(1680089084200);
			const regexp = new RegExp('^test$', 'ig');
			const originalObject: IDataObject = {
				1: 11,
				2: '22',
				a: 111,
				b: '222',
				d: date,
				r: regexp,
			};
			const copyOriginal = deepCopy(originalObject);

			const augmentedObject = augmentObject(originalObject);

			expect(augmentedObject.constructor.name).toEqual('Object');

			augmentedObject[1] = 911;
			expect(originalObject[1]).toEqual(11);
			expect(augmentedObject[1]).toEqual(911);

			augmentedObject[2] = '922';
			expect(originalObject[2]).toEqual('22');
			expect(augmentedObject[2]).toEqual('922');

			augmentedObject.a = 9111;
			expect(originalObject.a).toEqual(111);
			expect(augmentedObject.a).toEqual(9111);

			augmentedObject.b = '9222';
			expect(originalObject.b).toEqual('222');
			expect(augmentedObject.b).toEqual('9222');

			augmentedObject.c = 3;

			expect({ ...originalObject, d: date.toJSON(), r: {} }).toEqual(copyOriginal);

			expect(augmentedObject).toEqual({
				1: 911,
				2: '922',
				a: 9111,
				b: '9222',
				c: 3,
				d: date.toJSON(),
				r: regexp.toString(),
			});
		});

		test('should work with simple values on sub-level', () => {
			const originalObject = {
				a: {
					b: {
						cc: '3',
					},
					bb: '2',
				},
				aa: '1',
			};
			const copyOriginal = deepCopy(originalObject);

			const augmentedObject = augmentObject(originalObject);

			augmentedObject.a.bb = '92';
			expect(originalObject.a.bb).toEqual('2');
			expect(augmentedObject.a.bb).toEqual('92');

			augmentedObject.a.b.cc = '93';
			expect(originalObject.a.b.cc).toEqual('3');
			expect(augmentedObject.a.b.cc).toEqual('93');

			// @ts-ignore
			augmentedObject.a.b.ccc = {
				d: '4',
			};

			// @ts-ignore
			expect(augmentedObject.a.b.ccc).toEqual({ d: '4' });

			// @ts-ignore
			augmentedObject.a.b.ccc.d = '94';
			// @ts-ignore
			expect(augmentedObject.a.b.ccc.d).toEqual('94');

			expect(originalObject).toEqual(copyOriginal);

			expect(augmentedObject).toEqual({
				a: {
					b: {
						cc: '93',
						ccc: {
							d: '94',
						},
					},
					bb: '92',
				},
				aa: '1',
			});
		});

		test('should work with complex values on first level', () => {
			const originalObject: any = {
				a: {
					b: {
						cc: '3',
						c2: null,
					},
					bb: '2',
				},
				aa: '1',
			};
			const copyOriginal = deepCopy(originalObject);

			const augmentedObject = augmentObject(originalObject);

			augmentedObject.a = { new: 'NEW' };
			expect(originalObject.a).toEqual({
				b: {
					c2: null,
					cc: '3',
				},
				bb: '2',
			});
			expect(augmentedObject.a).toEqual({ new: 'NEW' });

			augmentedObject.aa = '11';
			expect(originalObject.aa).toEqual('1');
			expect(augmentedObject.aa).toEqual('11');

			augmentedObject.aaa = {
				bbb: {
					ccc: '333',
				},
			};

			expect(originalObject).toEqual(copyOriginal);
			expect(augmentedObject).toEqual({
				a: {
					new: 'NEW',
				},
				aa: '11',
				aaa: {
					bbb: {
						ccc: '333',
					},
				},
			});
		});

		test('should work with delete and reset', () => {
			const originalObject = {
				a: {
					b: {
						c: {
							d: '4' as string | undefined,
						} as { d?: string; dd?: string } | undefined,
						cc: '3' as string | undefined,
					},
					bb: '2' as string | undefined,
				},
				aa: '1' as string | undefined,
			};
			const copyOriginal = deepCopy(originalObject);

			const augmentedObject = augmentObject(originalObject);

			// Remove multiple values
			delete augmentedObject.a.b.c!.d;
			expect(augmentedObject.a.b.c!.d).toEqual(undefined);
			expect(originalObject.a.b.c!.d).toEqual('4');

			expect(augmentedObject).toEqual({
				a: {
					b: {
						c: {},
						cc: '3',
					},
					bb: '2',
				},
				aa: '1',
			});
			expect(originalObject).toEqual(copyOriginal);

			delete augmentedObject.a.b.c;
			expect(augmentedObject.a.b.c).toEqual(undefined);
			expect(originalObject.a.b.c).toEqual({ d: '4' });

			expect(augmentedObject).toEqual({
				a: {
					b: {
						cc: '3',
					},
					bb: '2',
				},
				aa: '1',
			});
			expect(originalObject).toEqual(copyOriginal);

			// Set deleted values again
			augmentedObject.a.b.c = { dd: '444' };
			expect(augmentedObject.a.b.c).toEqual({ dd: '444' });
			expect(originalObject).toEqual(copyOriginal);

			augmentedObject.a.b.c.d = '44';
			expect(augmentedObject).toEqual({
				a: {
					b: {
						c: {
							d: '44',
							dd: '444',
						},
						cc: '3',
					},
					bb: '2',
				},
				aa: '1',
			});
			expect(originalObject).toEqual(copyOriginal);
		});

		// Is almost identical to above test
		test('should work with setting to undefined and reset', () => {
			const originalObject = {
				a: {
					b: {
						c: {
							d: '4' as string | undefined,
						} as { d?: string; dd?: string } | undefined,
						cc: '3' as string | undefined,
					},
					bb: '2' as string | undefined,
				},
				aa: '1' as string | undefined,
			};
			const copyOriginal = deepCopy(originalObject);

			const augmentedObject = augmentObject(originalObject);

			// Remove multiple values
			augmentedObject.a.b.c!.d = undefined;
			expect(augmentedObject.a.b.c!.d).toEqual(undefined);
			expect(originalObject.a.b.c!.d).toEqual('4');

			expect(augmentedObject).toEqual({
				a: {
					b: {
						c: {},
						cc: '3',
					},
					bb: '2',
				},
				aa: '1',
			});
			expect(originalObject).toEqual(copyOriginal);

			augmentedObject.a.b.c = undefined;
			expect(augmentedObject.a.b.c).toEqual(undefined);
			expect(originalObject.a.b.c).toEqual({ d: '4' });

			expect(augmentedObject).toEqual({
				a: {
					b: {
						cc: '3',
					},
					bb: '2',
				},
				aa: '1',
			});
			expect(originalObject).toEqual(copyOriginal);

			// Set deleted values again
			augmentedObject.a.b.c = { dd: '444' };
			expect(augmentedObject.a.b.c).toEqual({ dd: '444' });
			expect(originalObject).toEqual(copyOriginal);

			augmentedObject.a.b.c.d = '44';
			expect(augmentedObject).toEqual({
				a: {
					b: {
						c: {
							d: '44',
							dd: '444',
						},
						cc: '3',
					},
					bb: '2',
				},
				aa: '1',
			});
			expect(originalObject).toEqual(copyOriginal);
		});

		test('should ignore non-enumerable keys', () => {
			const originalObject = { a: 1, b: 2 };
			Object.defineProperty(originalObject, '__hiddenProp', { enumerable: false });

			const augmentedObject = augmentObject(originalObject);
			expect(Object.keys(augmentedObject)).toEqual(['a', 'b']);
		});

		test('should return property descriptors', () => {
			const originalObject: any = {
				x: {
					y: {},
					z: {},
				},
			};
			const augmentedObject = augmentObject(originalObject);

			expect(Object.getOwnPropertyDescriptor(augmentedObject.x, 'y')).toEqual({
				configurable: true,
				enumerable: true,
				value: {},
				writable: true,
			});

			delete augmentedObject.x.y;
			expect(augmentedObject.x.hasOwnProperty('y')).toEqual(false);

			augmentedObject.x.y = 42;
			expect(augmentedObject.x.hasOwnProperty('y')).toEqual(true);
			expect(Object.getOwnPropertyDescriptor(augmentedObject.x, 'y')).toEqual({
				configurable: true,
				enumerable: true,
				value: 42,
				writable: true,
			});
		});

		test('should return valid values on `has` calls', () => {
			const originalObject: any = {
				x: {
					y: {},
				},
			};
			const augmentedObject = augmentObject(originalObject);
			expect('y' in augmentedObject.x).toBe(true);
			expect('z' in augmentedObject.x).toBe(false);

			augmentedObject.x.z = 5;
			expect('z' in augmentedObject.x).toBe(true);
			expect('y' in augmentedObject.x).toBe(true);
		});

		test('should ignore non-enumerable keys', () => {
			const originalObject: { toString?: string } = { toString: '123' };
			const augmentedObject = augmentObject(originalObject);
			expect('toString' in augmentedObject).toBe(true);
			expect(Object.keys(augmentedObject)).toEqual(['toString']);
			expect(Object.getOwnPropertyDescriptor(augmentedObject, 'toString')?.value).toEqual(
				originalObject.toString,
			);
			expect(augmentedObject.toString).toEqual(originalObject.toString);

			augmentedObject.toString = '456';
			expect(augmentedObject.toString).toBe('456');

			delete augmentedObject.toString;
			expect(augmentedObject.toString).toBeUndefined();
		});

		test('should handle constructor property correctly', () => {
			const originalObject: any = {
				a: {
					b: {
						c: {
							d: '4',
						},
					},
				},
			};
			const augmentedObject = augmentObject(originalObject);

			expect(augmentedObject.constructor.name).toEqual('Object');
			expect(augmentedObject.a.constructor.name).toEqual('Object');
			expect(augmentedObject.a.b.constructor.name).toEqual('Object');
			expect(augmentedObject.a.b.c.constructor.name).toEqual('Object');

			augmentedObject.constructor = {};
			expect(augmentedObject.constructor.name).toEqual('Object');

			delete augmentedObject.constructor;
			expect(augmentedObject.constructor.name).toEqual('Object');
		});
	});
});
