import { bind } from 'lodash';
import { ObjectWith } from '../interfaces';
import { curry } from './curry';

/* eslint-disable max-params */

describe('curry()', () => {
  //
  // stolen from https://github.com/lodash/lodash
  //

  it('should curry based on the number of arguments given', () => {
    function fn(_a: number, _b: number, _c: number, _d: number): any {
      return Array.prototype.slice.call(arguments);
    }

    const curried = curry(fn);

    expect(curried(1)(2)(3)(4)).toEqual([1, 2, 3, 4]);
    expect(curried(1, 2)(3, 4)).toEqual([1, 2, 3, 4]);
    expect(curried(1, 2, 3, 4)).toEqual([1, 2, 3, 4]);
  });

  it('should allow specifying `arity`', () => {
    function fn(_a: number, _b: number, _c: number, _d: number): any {
      return Array.prototype.slice.call(arguments);
    }

    const curried = curry(fn, 3);

    expect(curried(1)(2, 3)).toEqual([1, 2, 3]);
    expect(curried(1, 2)(3)).toEqual([1, 2, 3]);
    expect(curried(1, 2, 3)).toEqual([1, 2, 3]);
  });

  it('should provide additional arguments after reaching arity', () => {
    function fn(_a: number, _b: number, _c: number, _d: number): any {
      return Array.prototype.slice.call(arguments);
    }

    const curried: any = curry(fn, 3);

    expect(curried(1)(2, 3, 4)).toEqual([1, 2, 3, 4]);
    expect(curried(1, 2)(3, 4, 5)).toEqual([1, 2, 3, 4, 5]);
    expect(curried(1, 2, 3, 4, 5, 6)).toEqual([1, 2, 3, 4, 5, 6]);
  });

  it('should create a function with a `length` of `0`', () => {
    function fn(_a: number, _b: number, _c: number, _d: number): any {
      return Array.prototype.slice.call(arguments);
    }

    const curried = curry(fn);

    expect(curried.length).toEqual(0);
    expect(curried(1).length).toEqual(0);
    expect(curried(1, 2).length).toEqual(0);
  });

  it('should use `this` binding of function', () => {
    const object = { a: 1, b: 2, c: 3 };
    const expected = [1, 2, 3];
    type Fn = (a: string, b: string, c: string) => number[];

    function fn(
      this: ObjectWith<number> | undefined,
      a: string,
      b: string,
      c: string,
    ): number[] {
      const value = this ?? {};
      return [value[a], value[b], value[c]];
    }

    expect(curry(bind(fn, object) as Fn, 3)('a')('b')('c')).toEqual(expected);
    expect(curry(bind(fn, object) as Fn, 3)('a', 'b')('c')).toEqual(expected);
    expect(curry(bind(fn, object) as Fn, 3)('a', 'b', 'c')).toEqual(expected);

    expect(bind(curry(fn), object)('a')('b')('c')).toEqual(Array(3));
    expect(bind(curry(fn), object)('a', 'b')('c')).toEqual(Array(3));
    expect(bind(curry(fn), object)('a', 'b', 'c')).toEqual(expected);
  });

  it('should work for names that shadow `Object.prototype`', () => {
    const curried = curry((a: number, b: number, c: number): number[] => [
      a,
      b,
      c,
    ]);

    expect(curried(1)(2)(3)).toEqual([1, 2, 3]);
  });
});