/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import { describe, it, expect } from 'vitest';
import { customDeepMerge } from './deepMerge.js';
import { MergeStrategy } from '../config/settingsSchema.js';

describe('customDeepMerge', () => {
  it('should merge simple objects', () => {
    const target = { a: 1, b: 2 };
    const source = { b: 3, c: 4 };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: 1, b: 3, c: 4 });
  });

  it('should merge nested objects', () => {
    const target = { a: { x: 1 }, b: 2 };
    const source = { a: { y: 2 }, c: 3 };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: { x: 1, y: 2 }, b: 2, c: 3 });
  });

  it('should replace arrays by default', () => {
    const target = { a: [1, 2] };
    const source = { a: [3, 4] };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: [3, 4] });
  });

  it('should concatenate arrays with CONCAT strategy', () => {
    const target = { a: [1, 2] };
    const source = { a: [3, 4] };
    const getMergeStrategy = (path: string[]) =>
      path.join('.') === 'a' ? MergeStrategy.CONCAT : undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: [1, 2, 3, 4] });
  });

  it('should union arrays with UNION strategy', () => {
    const target = { a: [1, 2, 3] };
    const source = { a: [3, 4, 5] };
    const getMergeStrategy = (path: string[]) =>
      path.join('.') === 'a' ? MergeStrategy.UNION : undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: [1, 2, 3, 4, 5] });
  });

  it('should shallow merge objects with SHALLOW_MERGE strategy', () => {
    const target = { a: { x: 1, y: 1 } };
    const source = { a: { y: 2, z: 2 } };
    const getMergeStrategy = (path: string[]) =>
      path.join('.') === 'a' ? MergeStrategy.SHALLOW_MERGE : undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    // This is still a deep merge, but the properties of the object are merged.
    expect(result).toEqual({ a: { x: 1, y: 2, z: 2 } });
  });

  it('should handle multiple source objects', () => {
    const target = { a: 1 };
    const source1 = { b: 2 };
    const source2 = { c: 3 };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target, source1, source2);
    expect(result).toEqual({ a: 1, b: 2, c: 3 });
  });

  it('should return an empty object if no sources are provided', () => {
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy);
    expect(result).toEqual({});
  });

  it('should return a deep copy of the first source if only one is provided', () => {
    const target = { a: { b: 1 } };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target);
    expect(result).toEqual(target);
    expect(result).not.toBe(target);
  });

  it('should not mutate the original source objects', () => {
    const target = { a: { x: 1 }, b: [1, 2] };
    const source = { a: { y: 2 }, b: [3, 4] };
    const originalTarget = JSON.parse(JSON.stringify(target));
    const originalSource = JSON.parse(JSON.stringify(source));
    const getMergeStrategy = () => undefined;

    customDeepMerge(getMergeStrategy, target, source);

    expect(target).toEqual(originalTarget);
    expect(source).toEqual(originalSource);
  });

  it('should not mutate sources when merging multiple levels deep', () => {
    const s1 = { data: { common: { val: 'from s1' }, s1_only: true } };
    const s2 = { data: { common: { val: 'from s2' }, s2_only: true } };
    const s1_original = JSON.parse(JSON.stringify(s1));
    const s2_original = JSON.parse(JSON.stringify(s2));

    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, s1, s2);

    expect(s1).toEqual(s1_original);
    expect(s2).toEqual(s2_original);
    expect(result).toEqual({
      data: {
        common: { val: 'from s2' },
        s1_only: true,
        s2_only: true,
      },
    });
  });

  it('should handle complex nested strategies', () => {
    const target = {
      level1: {
        arr1: [1, 2],
        arr2: [1, 2],
        obj1: { a: 1 },
      },
    };
    const source = {
      level1: {
        arr1: [3, 4],
        arr2: [2, 3],
        obj1: { b: 2 },
      },
    };
    const getMergeStrategy = (path: string[]) => {
      const p = path.join('.');
      if (p === 'level1.arr1') return MergeStrategy.CONCAT;
      if (p === 'level1.arr2') return MergeStrategy.UNION;
      if (p === 'level1.obj1') return MergeStrategy.SHALLOW_MERGE;
      return undefined;
    };

    const result = customDeepMerge(getMergeStrategy, target, source);

    expect(result).toEqual({
      level1: {
        arr1: [1, 2, 3, 4],
        arr2: [1, 2, 3],
        obj1: { a: 1, b: 2 },
      },
    });
  });

  it('should not pollute the prototype', () => {
    const maliciousSource = JSON.parse('{"__proto__": {"polluted1": "true"}}');
    const getMergeStrategy = () => undefined;
    let result = customDeepMerge(getMergeStrategy, {}, maliciousSource);

    expect(result).toEqual({});
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect(({} as any).polluted1).toBeUndefined();

    const maliciousSource2 = JSON.parse(
      '{"constructor": {"prototype": {"polluted2": "true"}}}',
    );
    result = customDeepMerge(getMergeStrategy, {}, maliciousSource2);
    expect(result).toEqual({});
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect(({} as any).polluted2).toBeUndefined();

    const maliciousSource3 = JSON.parse('{"prototype": {"polluted3": "true"}}');
    result = customDeepMerge(getMergeStrategy, {}, maliciousSource3);
    expect(result).toEqual({});
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect(({} as any).polluted3).toBeUndefined();
  });

  it('should use additionalProperties merge strategy for dynamic properties', () => {
    // Simulates how hooks work: hooks.disabled uses UNION, but hooks.BeforeTool (dynamic) uses CONCAT
    const target = {
      hooks: {
        BeforeTool: [{ command: 'user-hook-1' }, { command: 'user-hook-2' }],
        disabled: ['hook-a'],
      },
    };
    const source = {
      hooks: {
        BeforeTool: [{ command: 'workspace-hook-1' }],
        disabled: ['hook-b'],
      },
    };

    // Mock the getMergeStrategyForPath behavior for hooks
    const getMergeStrategy = (path: string[]) => {
      const p = path.join('.');
      // hooks.disabled uses UNION strategy (explicitly defined in schema)
      if (p === 'hooks.disabled') return MergeStrategy.UNION;
      // hooks.BeforeTool uses CONCAT strategy (via additionalProperties)
      if (p === 'hooks.BeforeTool') return MergeStrategy.CONCAT;
      return undefined;
    };

    const result = customDeepMerge(getMergeStrategy, target, source);

    // BeforeTool should concatenate
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((result as any)['hooks']['BeforeTool']).toEqual([
      { command: 'user-hook-1' },
      { command: 'user-hook-2' },
      { command: 'workspace-hook-1' },
    ]);
    // disabled should union (deduplicate)
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    expect((result as any)['hooks']['disabled']).toEqual(['hook-a', 'hook-b']);
  });

  it('should overwrite primitive with object', () => {
    const target = { a: 1 };
    const source = { a: { b: 2 } };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: { b: 2 } });
  });

  it('should overwrite object with primitive', () => {
    const target = { a: { b: 2 } };
    const source = { a: 1 };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: 1 });
  });

  it('should not overwrite with undefined', () => {
    const target = { a: 1 };
    const source = { a: undefined };
    const getMergeStrategy = () => undefined;
    const result = customDeepMerge(getMergeStrategy, target, source);
    expect(result).toEqual({ a: 1 });
  });
});
