import { Moment } from 'moment';
import moment from 'moment-timezone';
import sinon, { SinonSpy, SinonSpyCall } from 'sinon';

/**
 * passes if the initial parameters on spyCall are args
 * @param spy Spy function
 * @param args Initial parameters
 */
function calledOnceWithInitialArgs(spy: SinonSpy, ...args: any[]) {
  sinon.assert.calledOnce(spy);
  expect(spy.firstCall.args.slice(0, args.length)).toStrictEqual(args);
}

/**
 * Passes if the argPosition parameter on the spyCall call is instanceof constructor
 * @param spyCall Spy function
 * @param argPosition Position of the argument
 * @param constructor Expected type
 */
function argIsInstanceOf(
  spyCall: SinonSpyCall,
  argPosition: number,
  constructor: FunctionConstructor,
) {
  expect(spyCall.args[argPosition]).toBeInstanceOf(constructor);
}

/**
 * Passes if the argPosition parameter on the spyCall call is value
 * @param spyCall Spy function
 * @param argPosition Position of the argument
 * @param value Expected value
 */
function argEqualsTo(spyCall: SinonSpyCall, argPosition: number, value: any) {
  expect(spyCall.args[argPosition]).toStrictEqual(value);
}

/**
 * Passes if spy was called two times with the provided arguments and no others.
 * The spy function can be a wrapped state.
 * @param spy Spy function
 * @param params Parameters for th wrapped state call
 * @param expected Expected value for the call
 */
function calledOnceWrappedState(spy: SinonSpy, params: any[], expected: any) {
  sinon.assert.calledOnce(spy);
  expect(spy.firstCall.args[0](...params)).toStrictEqual(expected);
}

/**
 * Passes if spy was called two times with the provided arguments and no others.
 * The spy function can be a wrapped state.
 * @param spy Spy function
 * @param args1 Parameters for the first wrapped state call
 * @param expect1 Expected value for the first call
 * @param args2 Parameters for the second wrapped state call
 * @param expect2 Expected value for the second call
 */
function calledTwiceWrappedState(
  spy: SinonSpy,
  args1: any[],
  expect1: any,
  args2: any[],
  expect2: any,
) {
  sinon.assert.calledTwice(spy);

  if (typeof spy.firstCall.args[0] === 'function') {
    expect(spy.firstCall.args[0](...args1)).toStrictEqual(expect1);
  } else {
    expect(spy.firstCall.args[0]).toStrictEqual(expect1);
  }
  if (typeof spy.secondCall.args[0] === 'function') {
    expect(spy.secondCall.args[0](...args2)).toStrictEqual(expect2);
  } else {
    expect(spy.secondCall.args[0]).toStrictEqual(expect2);
  }
}

/**
 * Passes if spy was called N times with the provided arguments and no others.
 * @param spy Spy function
 * @param args Matrix where each row is a call and contains an array with the parameters ordered
 */
function calledNTimesWithExactly(spy: SinonSpy, callCount: number, ...args: any[]) {
  sinon.assert.callCount(spy, callCount);

  for (let i = 0; i < callCount; ++i) {
    const msg = `Call ${i} expect [${args[i]}] arguments, but [${spy.args[i]}] was received`;
    expect({ msg, result: spy.args[i] }).toStrictEqual({ msg, result: args[i] });
  }
}

/**
 * Passes if spy was called two times with the provided arguments and no others.
 * @param spy Spy function
 * @param args1 First call arguments
 * @param args2 Second call arguments
 */
function calledTwiceWithExactly(spy: SinonSpy, args1: any[], args2: any[]) {
  calledNTimesWithExactly(spy, 2, args1, args2);
}

/**
 * Passes if spy was called three times with the provided arguments and no others.
 * @param spy Spy function
 * @param args1 First call arguments
 * @param args2 Second call arguments
 * @param args3 Third call arguments
 */
function calledThriceWithExactly(spy: SinonSpy, args1: any[], args2: any[], args3: any[]) {
  calledNTimesWithExactly(spy, 3, args1, args2, args3);
}

/**
 * Passes if date has the values defined in the parameters
 * @param date Date to check
 * @param year Expected year
 * @param month Expected month
 * @param day Expected day
 * @param hour Expected hour
 * @param minutes Expected minutes
 * @param seconds Expected seconds
 * @param keepLocalTime Optional, if true, hours value will not change after date UTC offset
 */
function dateEquals(
  date: Moment,
  year: number,
  month: number,
  day: number,
  hour = 0,
  minutes = 0,
  seconds = 0,
  keepLocalTime = false,
) {
  date.utcOffset(0, keepLocalTime);
  const expectedDate = moment([year, month - 1, day, hour, minutes, seconds]).utcOffset(0, true);

  !date.isSame(expectedDate) &&
    fail(`Received '${date.toString()}' but '${expectedDate.toString()}' was expected`);
}

export default {
  ...sinon.assert,
  calledOnceWithInitialArgs,
  argEqualsTo,
  calledOnceWrappedState,
  calledTwiceWithExactly,
  calledTwiceWrappedState,
  calledThriceWithExactly,
  calledNTimesWithExactly,
  argIsInstanceOf,
  dateEquals,
};
