"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = require("../wrap/lodash");
const args_match_1 = require("../args-match");
const is_callback_1 = require("../matchers/is-callback");
const notify_after_satisfaction_1 = require("../matchers/notify-after-satisfaction");
const config_1 = require("../config");
const log_1 = require("../log");
const index_1 = require("./index");
exports.default = {
    add(testDouble, args, stubbedValues, config) {
        return index_1.default.for(testDouble).stubbings.push({
            callCount: 0,
            stubbedValues,
            args: config.cloneArgs ? lodash_1.default.cloneDeep(args) : args,
            config
        });
    },
    invoke(testDouble, actualArgs, actualContext) {
        const stubbing = stubbingFor(testDouble, actualArgs);
        if (stubbing) {
            (0, notify_after_satisfaction_1.default)(stubbing.args, actualArgs);
            return executePlan(stubbing, actualArgs, actualContext);
        }
    },
    for(testDouble) {
        return index_1.default.for(testDouble).stubbings;
    }
};
const stubbingFor = (testDouble, actualArgs) => lodash_1.default.findLast(index_1.default.for(testDouble).stubbings, stubbing => isSatisfied(stubbing, actualArgs));
const executePlan = (stubbing, actualArgs, actualContext) => {
    const value = stubbedValueFor(stubbing);
    stubbing.callCount += 1;
    invokeCallbackFor(stubbing, actualArgs);
    switch (stubbing.config.plan) {
        case 'thenReturn': return value;
        case 'thenDo': return value.apply(actualContext, actualArgs);
        case 'thenThrow': throw value;
        case 'thenResolve': return createPromise(stubbing, value, true);
        case 'thenReject': return createPromise(stubbing, value, false);
    }
};
const invokeCallbackFor = (stubbing, actualArgs) => {
    if (lodash_1.default.some(stubbing.args, is_callback_1.default)) {
        lodash_1.default.each(stubbing.args, (expectedArg, i) => {
            if ((0, is_callback_1.default)(expectedArg)) {
                callCallback(stubbing, actualArgs[i], callbackArgs(stubbing, expectedArg));
            }
        });
    }
};
const callbackArgs = (stubbing, expectedArg) => {
    if (expectedArg.args != null) {
        return expectedArg.args;
    }
    else if (stubbing.config.plan === 'thenCallback') {
        return stubbing.stubbedValues;
    }
    else {
        return [];
    }
};
const callCallback = (stubbing, callback, args) => {
    if (stubbing.config.delay) {
        lodash_1.default.delay(callback, stubbing.config.delay, ...args);
    }
    else if (stubbing.config.defer) {
        lodash_1.default.defer(callback, ...args);
    }
    else {
        callback(...args); // eslint-disable-line
    }
};
const createPromise = (stubbing, value, willResolve) => {
    const Promise = (0, config_1.default)().promiseConstructor;
    ensurePromise(Promise);
    return new Promise((resolve, reject) => {
        callCallback(stubbing, () => willResolve ? resolve(value) : reject(value), [value]);
    });
};
const stubbedValueFor = (stubbing) => stubbing.callCount < stubbing.stubbedValues.length
    ? stubbing.stubbedValues[stubbing.callCount]
    : lodash_1.default.last(stubbing.stubbedValues);
const isSatisfied = (stubbing, actualArgs) => (0, args_match_1.default)(stubbing.args, actualArgs, stubbing.config) &&
    hasTimesRemaining(stubbing);
const hasTimesRemaining = (stubbing) => stubbing.config.times == null
    ? true
    : stubbing.callCount < stubbing.config.times;
const ensurePromise = (Promise) => {
    if (Promise == null) {
        return log_1.default.error('td.when', `\
no promise constructor is set (perhaps this runtime lacks a native Promise
function?), which means this stubbing can't return a promise to your
subject under test, resulting in this error. To resolve the issue, set
a promise constructor with \`td.config\`, like this:

  td.config({
    promiseConstructor: require('bluebird')
  })\
`);
    }
};
