import { describe, test, expect } from '@jest/globals';
import { FunctionList, AnyFunction } from '#js/util/FunctionList.js';

//
//  Set up a function list with 6 functions that captures output
//
function LIST(): [FunctionList, string[]] {
  const output = [] as string[];
  const list = new FunctionList();
  for (const i of [5, 0, 2, 1, 6, 3, 4]) {
    list.add(FN(i, output), i);
  }
  return [list, output];
}

//
//  A function that buffers the value of its parameters plus i
//    unless the parameter is false or fail, which do special actions,
//    or if the second is 'p', when we return a promise with a delay.
//
function FN(i: number, output: string[] = []) {
  return function (x?: string, y?: string) {
    if (x === 'false' && i === 3) return false;
    if (x === 'fail' && i === 3) throw new Error('fail');
    output.push(x === undefined ? String(i) : y == null ? x + i : x + y + i);
    if (x === 'delay' && i === 3) {
      return new Promise<void>((ok, fail) => {
        setTimeout(() => {
          y === 'fail' ? fail('Failed!') : ok();
        }, 10);
      });
    }
    return true;
  };
}

describe('FunctionList functionality', () => {
  test('Empty list is empty array', () => {
    expect(Array.from(new FunctionList())).toEqual([]);
  });

  test('Adding one item', () => {
    const list = new FunctionList();
    const fn = FN(0);
    const item = list.add(fn);
    expect(Array.from(list)).toEqual([{ item: item, priority: 5 }]);
    expect(item).toBe(fn);
  });

  test('Adding a list of items', () => {
    const fns = [(_: any) => {}, [(_: any) => {}, 1]] as [
      AnyFunction,
      [AnyFunction, number],
    ];
    const list = new FunctionList(fns);
    expect(Array.from(list)).toEqual([
      { item: fns[1][0], priority: 1 },
      { item: fns[0], priority: 5 },
    ]);
  });

  test('Removing one item', () => {
    const list = new FunctionList();
    const fn = list.add(FN(0));
    const item = list.remove(fn);
    expect(Array.from(list)).toEqual([]);
    expect(item).toBe(list);
  });

  test('Executing list', () => {
    const output = [] as string[];
    const list = new FunctionList();
    list.add(FN(0, output));
    expect(list.execute()).toBe(true);
    expect(output).toEqual(['0']);
  });

  test('Sorting of list', () => {
    const [list, output] = LIST();
    expect(list.execute()).toBe(true);
    expect(output).toEqual(['0', '1', '2', '3', '4', '5', '6']);
  });

  test('Passing one parameter', () => {
    const [list, output] = LIST();
    expect(list.execute('x')).toBe(true);
    expect(output).toEqual(['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6']);
  });

  test('Passing multiple parameters', () => {
    const [list, output] = LIST();
    expect(list.execute('x', 'y')).toBe(true);
    expect(output).toEqual(['xy0', 'xy1', 'xy2', 'xy3', 'xy4', 'xy5', 'xy6']);
  });

  test('Function returning false', () => {
    const [list, output] = LIST();
    expect(list.execute('false')).toBe(false);
    expect(output).toEqual(['false0', 'false1', 'false2']);
  });

  test('asyncExecute() returns promise', () => {
    const list = new FunctionList();
    expect(list.asyncExecute() instanceof Promise).toBe(true);
  });

  test('asyncExecute() runs', () => {
    const [list, output] = LIST();
    list.asyncExecute().then((result: boolean) => {
      expect(result).toBe(true);
      expect(output).toEqual(['0', '1', '2', '3', '4', '5', '6']);
    });
  });

  test('Passing parameter to asyncExecute()', () => {
    const [list, output] = LIST();
    list.asyncExecute('x').then((result: boolean) => {
      expect(result).toBe(true);
      expect(output).toEqual(['x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6']);
    });
  });

  test('Passing multiple parameters to asyncExecute()', () => {
    const [list, output] = LIST();
    list.asyncExecute('x', 'y').then((result: boolean) => {
      expect(result).toBe(true);
      expect(output).toEqual(['xy0', 'xy1', 'xy2', 'xy3', 'xy4', 'xy5', 'xy6']);
    });
  });

  test('Function returning false in asyncExecute()', () => {
    const [list, output] = LIST();
    list.asyncExecute('false').then((result: boolean) => {
      expect(result).toBe(false);
      expect(output).toEqual(['false0', 'false1', 'false2']);
    });
  });

  test('Function error in asyncExecute()', () => {
    const [list, output] = LIST();
    expect.assertions(2);
    list.asyncExecute('fail').catch((err: Error) => {
      expect(output).toEqual(['fail0', 'fail1', 'fail2']);
      expect(err.message).toBe('fail');
    });
  });

  test('Delay in asyncExecute()', () => {
    const [list, output] = LIST();
    list.asyncExecute('delay', '').then((result: boolean) => {
      expect(result).toBe(true);
      expect(output).toEqual([
        'delay0',
        'delay1',
        'delay2',
        'delay3',
        'delay4',
        'delay5',
        'delay6',
      ]);
    });
  });

  test('Failed promise in asyncExecute()', () => {
    const [list, output] = LIST();
    list.asyncExecute('delay', 'fail').catch((result: string) => {
      expect(output).toEqual([
        'delayfail0',
        'delayfail1',
        'delayfail2',
        'delayfail3',
      ]);
      expect(result).toBe('Failed!');
    });
  });
});
