import { describe, test, expect } from '@jest/globals';
import * as Options from '#js/util/Options.js';

const SYMB = Symbol('symbol');
const OPTIONS = Options.OPTIONS;
const optionError = OPTIONS.optionError;

describe('Options utility', () => {
  test('keys()', () => {
    expect(Options.keys({ a: 1, [Symbol.iterator]: 2 })).toEqual([
      'a',
      Symbol.iterator,
    ]);
    expect(Options.keys(null)).toEqual([]);
    expect(Options.keys({})).toEqual([]);
  });

  test('copy()', () => {
    //
    // Copy() copies deeply
    //
    const orig: any = {
      a: 1,
      b: { x: 'x' },
      c: { y: 'y' },
      d: [1, { z: 'z' }, { w: 'w' }],
      e: [1],
      [SYMB]: 1,
    };
    let copy = Options.copy(orig);
    expect(copy).toEqual(orig);

    //
    //  Changing original doesn't change copy
    //
    orig.a = 2;
    orig.b.x = 'xx';
    orig.c = { yy: 'yy' };
    orig.d[0] = 2;
    orig.d[1].z = 'zz';
    orig.d[2] = { ww: 'ww' };
    orig.e = [2];
    orig.f = 2;
    orig[SYMB] = 2;
    expect(copy).toEqual({
      a: 1,
      b: { x: 'x' },
      c: { y: 'y' },
      d: [1, { z: 'z' }, { w: 'w' }],
      e: [1],
      [SYMB]: 1,
    });

    //
    //  Copy() copies symbols
    //
    expect((copy as any)[SYMB]).toBe(1);

    //
    //  Copy() copies getter and setter
    //
    copy = Options.copy({
      _a: 1,
      get a() {
        return this._a;
      },
      set a(x) {
        this._a = x;
      },
    });
    expect(copy.a).toBe(1);
    copy.a = 2;
    expect(copy._a).toBe(2);

    //
    //  Copy() handle empty objects
    //
    expect(Options.copy({})).toEqual({});
  });

  test('insert()', () => {
    let warnings = [] as string[];
    OPTIONS.optionError = (_msg: string, key: string) => {
      warnings.push(key);
    };

    const options = { a: 1, b: { x: 'x', y: [0] }, c: [1, 2, 3] };
    let copy = Options.insert({}, options, false);
    expect(copy).toEqual(options);
    expect(warnings).toEqual([]);

    //
    //  Error on unknown keys
    //
    copy = Options.insert({}, options);
    expect(copy).toEqual({});
    expect(warnings).toEqual(['a', 'b', 'c']); // invalid keys
    warnings = [];
    copy = Options.insert({ a: 2, b: {}, c: [4] }, options, true);
    expect(copy).toEqual({ a: 1, b: {}, c: [1, 2, 3] });
    expect(warnings).toEqual(['x', 'y']); // invalid keys
    warnings = [];
    copy = Options.insert({}, { [SYMB]: 1 });
    expect(copy).toEqual({});
    expect(warnings).toEqual(['Symbol(symbol)']);
    warnings = [];

    //
    //  insert() adds to objects and replaces other values
    //
    copy = Options.insert({ a: 2, b: {}, c: [4] }, options, false);
    expect(copy).toEqual(options);

    //
    //  insert() merges objects
    //
    copy = Options.insert({ a: {}, b: 2 }, { a: { x: 'x' } }, false);
    expect(copy).toEqual({ a: { x: 'x' }, b: 2 });

    //
    //  insert() merges into functions
    //
    copy = Options.insert({ a: function () {} }, { a: { x: 'x' } }, false);
    expect(copy.a.x).toBe('x');

    //
    //  insert() changes object type
    //
    copy = Options.insert({ a: { x: 'x' } }, { a: 1 }, false);
    expect(copy).toEqual({ a: 1 });

    //
    //  insert() replaces arrays
    //
    copy = Options.insert({ a: [1, 2] }, { a: [3, 4] }, false);
    expect(copy).toEqual({ a: [3, 4] });

    //
    //  insert() appends to arrays with APPEND key
    //
    copy = Options.insert(
      { a: [1, 2] },
      { a: { [Options.APPEND]: [3, 4] } },
      false
    );
    expect(copy).toEqual({ a: [1, 2, 3, 4] });

    //
    //  insert() removes from arrays with REMOVE key
    //
    copy = Options.insert(
      { a: [1, 2, 3, 4] },
      { a: { [Options.REMOVE]: [1, 3] } },
      false
    );
    expect(copy).toEqual({ a: [2, 4] });

    //
    //  insert() adds and removes from arrays
    //
    copy = Options.insert(
      { a: [1, 2, 3, 4] },
      { a: { [Options.REMOVE]: [1, 3], [Options.APPEND]: [3, 5] } },
      false
    );
    expect(copy).toEqual({ a: [2, 4, 3, 5] });

    //
    //  insert() handles empty objects
    //
    copy = Options.insert({ a: 1 }, {});
    expect(copy).toEqual({ a: 1 });
    expect(warnings).toEqual([]);

    OPTIONS.optionError = optionError;
  });

  test('expandable()', () => {
    const warnings = [] as string[];
    OPTIONS.optionError = (_msg: string, key: string) => {
      warnings.push(key);
    };

    //
    //  No error for unknonw keys in expandables
    //
    const options = { a: Options.expandable({ x: 1 }) };
    let copy = Options.userOptions(options, { a: { y: 2 } });
    expect(copy).toEqual({ a: { x: 1, y: 2 } });
    expect(warnings).toEqual([]);

    OPTIONS.optionError = optionError;

    //
    //  Expandable copies as expandable
    //
    copy = Options.copy(Options.expandable({ x: 1 }));
    expect(copy instanceof Options.Expandable).toBe(true);
    expect(copy).toEqual({ x: 1 });
  });

  test('defaultOptions()', () => {
    let warnings = 0;
    OPTIONS.optionError = () => {
      warnings++;
    };

    const copy = Options.defaultOptions({}, { a: 1 }, { b: 2 });
    expect(copy).toEqual({ a: 1, b: 2 });
    expect(warnings).toBe(0);

    OPTIONS.optionError = optionError;
  });

  test('userOptions()', () => {
    let warnings = [] as string[];
    OPTIONS.optionError = (_msg: string, key: string) => {
      warnings.push(key);
    };

    //
    // userOptions() warns about invalid options
    //
    let copy = Options.userOptions({}, { a: 1 }, { b: 2 });
    expect(copy).toEqual({});
    expect(warnings).toEqual(['a', 'b']);
    warnings = [];

    //
    // userOptions() warns about invalid secondary options
    //
    copy = Options.userOptions({ a: 'x' }, { a: 1 }, { b: 2 });
    expect(copy).toEqual({ a: 1 });
    expect(warnings).toEqual(['b']);
    warnings = [];

    //
    // userOptions() merges multiple options
    //
    copy = Options.userOptions({ a: 'x', b: 'y' }, { a: 1 }, { b: 2 });
    expect(copy).toEqual({ a: 1, b: 2 });
    expect(warnings).toEqual([]);

    OPTIONS.optionError = optionError;
  });

  test('selectOptions()', () => {
    //
    //  selectOptions() finds existing keys and skips others
    //
    const options = { a: 1, b: 2, c: 3, d: 4 };
    let copy = Options.selectOptions(options, 'a', 'c', 'x', 'y');
    expect(copy).toEqual({ a: 1, c: 3 });

    //
    //  selectOptions() handles empty list
    //
    copy = Options.selectOptions({}, 'x', 'y');
    expect(copy).toEqual({});

    //
    //  selectOptions() handles empty list of keys
    //
    copy = Options.selectOptions(options);
    expect(copy).toEqual({});
  });

  test('selectOptionsFromKeys()', () => {
    //
    //  selectOptionsFromKeys() finds existing keys and skips others
    //
    const options = { a: 1, b: 2, c: 3, d: 4 };
    let copy = Options.selectOptionsFromKeys(options, {
      a: true,
      c: true,
      x: true,
      y: true,
    });
    expect(copy).toEqual({ a: 1, c: 3 });
    //
    //  selectOptionsFromKeys() handles empty source list
    //
    copy = Options.selectOptionsFromKeys({}, options);
    expect(copy).toEqual({});
    //
    //  selectOptionsFromKeys() handles empty key list;
    //
    copy = Options.selectOptionsFromKeys(options, {});
    expect(copy).toEqual({});
  });

  test('separateOptions()', () => {
    //
    //  separateOptions() works with one set
    //
    const options = { a: 1, b: 2, c: 3, d: 4, e: 5 };
    let result = Options.separateOptions(options, { a: true, c: true });
    expect(result).toEqual([
      { b: 2, d: 4, e: 5 },
      { a: 1, c: 3 },
    ]);

    //
    //  separateOptions() works with two sets
    //
    result = Options.separateOptions(
      options,
      { a: true, c: true },
      { d: true }
    );
    expect(result).toEqual([{ b: 2, e: 5 }, { a: 1, c: 3 }, { d: 4 }]);

    //
    //  separateOptions() works with no sets
    //
    result = Options.separateOptions(options);
    expect(result).toEqual([options]);

    //
    //  separateOptions() works with unknown keys
    //
    result = Options.separateOptions(options, { a: true, c: true, x: true });
    expect(result).toEqual([
      { b: 2, d: 4, e: 5 },
      { a: 1, c: 3 },
    ]);

    //
    //  separateOptions() works with empty options
    //
    result = Options.separateOptions({}, { a: true });
    expect(result).toEqual([{}, {}]);

    //
    //  separateOptions() works with no options
    //
    result = Options.separateOptions(null, { a: true });
    expect(result).toEqual([{}, {}]);
  });

  test('OPTIONS settings', () => {
    let copy: Options.OptionList;

    //
    //  Fatal throws an error
    //
    OPTIONS.invalidOption = 'fatal';
    try {
      copy = Options.userOptions({}, { a: 1 });
    } catch (err) {
      expect(err.message).toBe('Invalid option "a" (no default value).');
    }
    expect(copy).toEqual(undefined);

    //
    //  Warn does not throw an error
    //
    const warn = console.warn;
    console.warn = () => {};
    OPTIONS.invalidOption = 'warn';
    try {
      copy = Options.userOptions({}, { a: 1 });
    } catch {
      // Should not throw an error
    }
    expect(copy).toEqual({});
    console.warn = warn;
  });

  test('makeArray()', () => {
    expect(Options.makeArray([1, 2])).toEqual([1, 2]);
    expect(Options.makeArray(1)).toEqual([1]);
  });

  test('lookup()', () => {
    const options = { a: 1, b: 'x' };
    expect(Options.lookup('a', options)).toBe(1);
    expect(Options.lookup('c', options, 'default')).toBe('default');
    expect(Options.lookup('c', options)).toBe(null);
  });
});
