'use strict';

import * as testHelpers from './testHelpers';
import {AssertionError} from 'assert';
import {DeepPartial} from '../../types/util';
import type sinon from 'sinon';
import util from 'util';
import 'should';

/**
 * Asserts that a stub or apy called specified number of times with args matched specified ones
 * @param {Function} spy Sinon stub or spy
 * @param {Number} calls Expected number of calls
 * @param {Array} args Expected call arguments
 * @throws {AssertionError} If assertion failed
 */
export function callCountWithMatch<Spy extends sinon.SinonSpy>(
  spy: Spy, calls: number, ...args: DeepPartial<Parameters<Spy>>
) {
  if (!(spy as any).isSinonProxy) {
    throw new AssertionError({message: 'Given function is not a sinon spy'});
  }
  let matchedCalls = testHelpers.getCallsWithMatch(spy, ...args);
  if (matchedCalls.length !== calls) {
    throw Object.assign(
      new AssertionError({message: `Spy expected to be called ${calls} times with specified arguments but was called ` +
        `${matchedCalls.length} times with them of ${spy.callCount} total calls`}),
      {callArgs: util.inspect(spy.args, {depth: 3})}
    );
  }
}

/**
 * Asserts that a stub or apy called specified number of times with args that deep equal to specified ones
 * @param {Function} spy Sinon stub or spy
 * @param {Number} calls Expected number of calls
 * @param {Array} args Expected call arguments
 * @throws {AssertionError} If assertion failed
 */
export function callCountWithExactly<Spy extends sinon.SinonSpy>(spy: Spy, calls: number, ...args: Parameters<Spy>) {
  if (!(spy as any).isSinonProxy) {
    throw new AssertionError({message: 'Given function is not a sinon spy'});
  }
  let matchedCalls = testHelpers.getCallsWithExactly(spy, ...args as DeepPartial<Parameters<Spy>>);
  matchedCalls.length.should.equal(calls, `Spy expected to be called ${calls} times with specified arguments ` +
    `but was called ${matchedCalls.length} times with them of ${spy.callCount} total calls`);
}

/**
 * Asserts that array matches to expected one with and the lengths are equal 
 * @param {Array} actualArray Actual array to match
 * @param {Array} expectedArray Expected array to match to
 * @throws {AssertionError} If assertion failed
 */
export function arrayMatchWithEqualLength(actualArray, expectedArray) {
  actualArray.should.match(expectedArray);
  actualArray.length.should.equal(expectedArray.length);
}

/** Spy call options */
export type SpyCall<TArgs extends readonly any[]> = {
  /** Sinon spy */
  spy: sinon.SinonSpy<TArgs>,
  /** Call index */
  call: number,
  /** If specified, asserts args match */
  matchArgs?: DeepPartial<TArgs>,
  /** Logging label. Defaults to `spy` function name, which defaults to `default` */
  label?: string
};

/**
 * Sinon's callOrder seems to have some bug giving incorrect assertion, so this is a manual implementation
 * @param spyCalls spy calls
 * @throws assertion error
 */
export function callOrder(spyCalls: SpyCall<any>[]) {
  type CallId = {id: number, label: string};
  let callIds: CallId[] = spyCalls.map((call, index) => {
    const label = call.label || call.spy.name || 'default';
    let sinonCall = call.spy.getCall(call.call);
    if (!sinonCall) {
      throw new AssertionError({message: `Call ${index} (${label}) does not exist`});
    }
    return {id: (sinonCall as any).callId, label};
  });
  let previousCallId: CallId;
  for (let index = 0; index < spyCalls.length; ++index) {
    if (previousCallId !== undefined && callIds[index].id < previousCallId.id) {
      throw new AssertionError({
        message: `Wrong call order detected in call ids ${callIds.map(id => `\n${id.id} (${id.label})`)}`,
        expected: `Call id larger than the previous call ${previousCallId}`,
        actual: callIds[index]
      });
    }
    if (spyCalls[index].matchArgs) {
      try {
        spyCalls[index].spy.getCall(spyCalls[index].call).args.should.match(spyCalls[index].matchArgs);
      } catch (err) {
        throw Object.assign(
          new AssertionError({message: `Call ${index} (${callIds[index].label}) args do not match`}),
          {index, label: callIds[index].label, cause: err}
        );
      }
    }
    previousCallId = callIds[index];
  }
}
