"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const lodash_1 = require("./wrap/lodash");
const proxy_safe_clone_deep_with_1 = require("./wrap/proxy-safe-clone-deep-with");
const calls_1 = require("./store/calls");
const store_1 = require("./store");
const arguments_1 = require("./stringify/arguments");
const stubbings_1 = require("./store/stubbings");
const symbols_1 = require("./symbols");
function explain(testDouble) {
    if (lodash_1.default.isFunction(testDouble)) {
        return explainFunction(testDouble);
    }
    else if (lodash_1.default.isObject(testDouble)) {
        return explainObject(testDouble);
    }
    else {
        return explainNonTestDouble(testDouble);
    }
}
exports.default = explain;
function explainObject(obj) {
    const { explanations, children } = explainChildren(obj);
    return {
        name: null,
        callCount: 0,
        calls: [],
        description: describeObject(explanations),
        children,
        isTestDouble: explanations.length > 0
    };
}
function explainChildren(thing) {
    const explanations = [];
    const children = proxy_safe_clone_deep_with_1.default(thing, (val, key, obj, stack) => {
        if (lodash_1.default.isFunction(val) && stack) {
            return lodash_1.default.tap(explainFunction(val), (explanation) => {
                if (explanation.isTestDouble)
                    explanations.push(explanation);
            });
        }
    });
    return { explanations, children };
}
function describeObject(explanations) {
    const count = explanations.length;
    if (count === 0)
        return 'This object contains no test doubles';
    return `This object contains ${count} test double function${count > 1 ? 's' : ''}: [${lodash_1.default.map(explanations, e => `"${e.name}"`).join(', ')}]`;
}
function explainFunction(testDouble) {
    if (store_1.default.for(testDouble, false) == null) {
        return explainNonTestDouble(testDouble);
    }
    const calls = calls_1.default.for(testDouble);
    const stubs = stubbings_1.default.for(testDouble);
    const { children } = explainChildren(testDouble);
    return {
        name: store_1.default.for(testDouble).name,
        callCount: calls.length,
        calls,
        description: testdoubleDescription(testDouble, stubs, calls) +
            stubbingDescription(stubs) +
            callDescription(calls),
        children,
        isTestDouble: true
    };
}
function explainNonTestDouble(thing) {
    return ({
        name: undefined,
        callCount: 0,
        calls: [],
        description: `This is not a test double${lodash_1.default.isFunction(thing) ? ' function' : ''}.`,
        isTestDouble: false
    });
}
function testdoubleDescription(testDouble, stubs, calls) {
    return `This test double ${stringifyName(testDouble)}has ${stubs.length} stubbings and ${calls.length} invocations.`;
}
function stubbingDescription(stubs) {
    return stubs.length > 0
        ? lodash_1.default.reduce(stubs, (desc, stub) => desc + `\n  - when called with \`(${arguments_1.default(stub.args)})\`, then ${planFor(stub)} ${argsFor(stub)}.`, '\n\nStubbings:')
        : '';
}
function planFor(stub) {
    switch (stub.config.plan) {
        case 'thenCallback': return 'callback';
        case 'thenResolve': return 'resolve';
        case 'thenReject': return 'reject';
        default: return 'return';
    }
}
function argsFor(stub) {
    switch (stub.config.plan) {
        case 'thenCallback': return `\`(${arguments_1.default(stub.stubbedValues, ', ')})\``;
        default: return arguments_1.default(stub.stubbedValues, ', then ', '`');
    }
}
function callDescription(calls) {
    return calls.length > 0
        ? lodash_1.default.reduce(calls, (desc, call) => {
            let argDescription;
            if (call.cloneArgs !== symbols_1.default.uncloneable) {
                argDescription = `\`(${arguments_1.default(call.cloneArgs)})\`.`;
            }
            else {
                argDescription = `\`(${arguments_1.default(call.args)})\` [Cloning argument values failed; displaying current references]`;
            }
            return desc + `\n  - called with ${argDescription}`;
        }, '\n\nInvocations:')
        : '';
}
function stringifyName(testDouble) {
    const name = store_1.default.for(testDouble).name;
    return name ? `\`${name}\` ` : '';
}
