"use strict";

var slice = require("@sinonjs/commons").prototypes.array.slice;

var assertArgNum = require("./assert-arg-num");
var interpolatePosArg = require("./interpolate-pos-arg");
var interpolateProperties = require("./interpolate-properties");

function createAssertion(referee, type, name, func, minArgs, messageValues) {
    var assertion = function() {
        var fullName = type + "." + name;
        var failed = false;

        assertArgNum(referee.fail, fullName, arguments, minArgs);

        var args = slice(arguments, 0);
        var namedValues = {};

        var ctx = {
            fail: function(msg) {
                if (typeof messageValues === "function") {
                    namedValues = messageValues.apply(this, args);
                }

                failed = true;
                delete this.fail;
                var message = referee[type][name][msg] || msg;
                message = interpolatePosArg(message, args);
                message = interpolateProperties(referee, message, this);
                message = interpolateProperties(referee, message, namedValues);
                var operator = type + "." + name;
                var errorProperties = {
                    operator: operator
                };
                if (type === "assert") {
                    if (
                        namedValues.hasOwnProperty("actual") &&
                        namedValues.hasOwnProperty("expected")
                    ) {
                        errorProperties.actual = namedValues.actual;
                        errorProperties.expected = namedValues.expected;
                    }
                }
                referee.fail("[" + operator + "] " + message, errorProperties);
                return false;
            }
        };

        var result = func.apply(ctx, arguments);

        if (typeof Promise === "function" && result instanceof Promise) {
            // Here we need to return the promise in order to tell test
            // runners that this is an asychronous assertion.
            return result.then(function() {
                referee.pass(["pass", fullName].concat(args));
            });
        }

        if (!result && !failed) {
            // when a function returns false and hasn't already failed with a custom message,
            // fail with default message
            ctx.fail("message");
        }

        if (!failed) {
            referee.pass(["pass", fullName].concat(args));
        }

        return undefined;
    };

    return assertion;
}

// Internal helper. Not the most elegant of functions, but it takes
// care of all the nitty-gritty of assertion functions: counting,
// verifying parameter count, interpolating messages with actual
// values and so on.
function defineAssertion(referee, type, name, func, minArgs, messageValues) {
    referee[type][name] = function() {
        var assertion = createAssertion(
            referee,
            type,
            name,
            func,
            minArgs,
            messageValues
        );
        return assertion.apply(null, arguments);
    };
}

module.exports = defineAssertion;
