import { RecursivePartial } from '../type-helpers';
import { mergeDeep } from './merge-deep';

const mergeSource = Object.freeze({
  level1: true,
  sibling: 10,
  inner: {
    level2: true,
    sibling: 100,
  },
});
type MergeSource = {
  level1: boolean;
  sibling: number;
  extra?: boolean;
  inner: { level2: boolean; sibling: number; extra?: boolean };
};

describe('Merge deep merges properties', () => {
  const cases: [{ source: MergeSource; changes: RecursivePartial<MergeSource>[] }, MergeSource][] = [
    [{ source: mergeSource, changes: [] }, mergeSource],
    [{ source: mergeSource, changes: [{}] }, mergeSource],
    [
      { source: mergeSource, changes: [{ sibling: 20 }] },
      {
        level1: true,
        sibling: 20,
        inner: {
          level2: true,
          sibling: 100,
        },
      },
    ],
    [
      { source: mergeSource, changes: [{ inner: { sibling: 200 } }] },
      {
        level1: true,
        sibling: 10,
        inner: {
          level2: true,
          sibling: 200,
        },
      },
    ],
    [
      { source: mergeSource, changes: [{ inner: { sibling: 200 }, sibling: 20 }] },
      {
        level1: true,
        sibling: 20,
        inner: {
          level2: true,
          sibling: 200,
        },
      },
    ],
    [
      { source: mergeSource, changes: [{ inner: { sibling: 300 } }, { sibling: 30 }] },
      {
        level1: true,
        sibling: 30,
        inner: {
          level2: true,
          sibling: 300,
        },
      },
    ],
    [
      { source: mergeSource, changes: [{ extra: true }] },
      {
        ...mergeSource,
        extra: true,
      },
    ],
    [
      { source: mergeSource, changes: [{ inner: { extra: true } }] },
      {
        ...mergeSource,
        inner: { ...mergeSource.inner, extra: true },
      },
    ],
  ];

  test.each(cases)('merge deep', ({ source, changes }, expectedResult) => {
    const target = structuredClone(source);
    const result = mergeDeep(target, ...(changes as Partial<MergeSource>[]));
    expect(result).toEqual(expectedResult);
    expect(target).toEqual(expectedResult);
  });

  // Tests against prototype pollution
  // eslint-disable-next-line @typescript-eslint/ban-types
  const casesPrototype: [{ source: MergeSource; changes: unknown[] }, MergeSource][] = [
    [
      {
        source: mergeSource,
        changes: [
          {
            constructor: (): void => {
              console.error('');
            },
          },
        ],
      },
      mergeSource,
    ],
    [{ source: mergeSource, changes: [{ __proto__: { test: true } }] }, mergeSource],
    [{ source: mergeSource, changes: [{ prototype: { test: true } }] }, mergeSource],
    [
      { source: mergeSource, changes: [{ sibling: 20, __proto__: { test: true } }] },
      {
        level1: true,
        sibling: 20,
        inner: {
          level2: true,
          sibling: 100,
        },
      },
    ],
    [
      {
        source: mergeSource,
        changes: [
          {
            sibling: 20,
            inner: {
              constructor: (): void => {
                console.error('');
              },
            },
          },
        ],
      },
      {
        level1: true,
        sibling: 20,
        inner: {
          level2: true,
          sibling: 100,
        },
      },
    ],
  ];

  test.each(casesPrototype)('merge deep does not pollute prototype', ({ source, changes }, expectedResult) => {
    const target = structuredClone(source);
    const result = mergeDeep(target, ...(changes as Partial<MergeSource>[]));
    expect(result).toEqual(expectedResult);
    expect(target).toEqual(expectedResult);
  });
});
