"use strict";

var arrayProto = require("@sinonjs/commons").prototypes.array;
var calledInOrder = require("@sinonjs/commons").calledInOrder;
var createMatcher = require("@sinonjs/samsam").createMatcher;
var orderByFirstCall = require("@sinonjs/commons").orderByFirstCall;
var timesInWords = require("./util/core/times-in-words");
var format = require("./util/core/format");
var stringSlice = require("@sinonjs/commons").prototypes.string.slice;
var globalObject = require("@sinonjs/commons").global;

var arraySlice = arrayProto.slice;
var concat = arrayProto.concat;
var forEach = arrayProto.forEach;
var join = arrayProto.join;
var splice = arrayProto.splice;

function createAssertObject() {
    var assert;

    function verifyIsStub() {
        var args = arraySlice(arguments);

        forEach(args, function(method) {
            if (!method) {
                assert.fail("fake is not a spy");
            }

            if (method.proxy && method.proxy.isSinonProxy) {
                verifyIsStub(method.proxy);
            } else {
                if (typeof method !== "function") {
                    assert.fail(method + " is not a function");
                }

                if (typeof method.getCall !== "function") {
                    assert.fail(method + " is not stubbed");
                }
            }
        });
    }

    function verifyIsValidAssertion(assertionMethod, assertionArgs) {
        switch (assertionMethod) {
            case "notCalled":
            case "called":
            case "calledOnce":
            case "calledTwice":
            case "calledThrice":
                if (assertionArgs.length !== 0) {
                    assert.fail(
                        assertionMethod +
                            " takes 1 argument but was called with " +
                            (assertionArgs.length + 1) +
                            " arguments"
                    );
                }
                break;
            default:
                break;
        }
    }

    function failAssertion(object, msg) {
        var obj = object || globalObject;
        var failMethod = obj.fail || assert.fail;
        failMethod.call(obj, msg);
    }

    function mirrorPropAsAssertion(name, method, message) {
        var msg = message;
        var meth = method;
        if (arguments.length === 2) {
            msg = method;
            meth = name;
        }

        assert[name] = function(fake) {
            verifyIsStub(fake);

            var args = arraySlice(arguments, 1);
            var failed = false;

            verifyIsValidAssertion(name, args);

            if (typeof meth === "function") {
                failed = !meth(fake);
            } else {
                failed = typeof fake[meth] === "function" ? !fake[meth].apply(fake, args) : !fake[meth];
            }

            if (failed) {
                failAssertion(this, (fake.printf || fake.proxy.printf).apply(fake, concat([msg], args)));
            } else {
                assert.pass(name);
            }
        };
    }

    function exposedName(prefix, prop) {
        return !prefix || /^fail/.test(prop)
            ? prop
            : prefix + stringSlice(prop, 0, 1).toUpperCase() + stringSlice(prop, 1);
    }

    assert = {
        failException: "AssertError",

        fail: function fail(message) {
            var error = new Error(message);
            error.name = this.failException || assert.failException;

            throw error;
        },

        pass: function pass() {
            return;
        },

        callOrder: function assertCallOrder() {
            verifyIsStub.apply(null, arguments);
            var expected = "";
            var actual = "";

            if (!calledInOrder(arguments)) {
                try {
                    expected = join(arguments, ", ");
                    var calls = arraySlice(arguments);
                    var i = calls.length;
                    while (i) {
                        if (!calls[--i].called) {
                            splice(calls, i, 1);
                        }
                    }
                    actual = join(orderByFirstCall(calls), ", ");
                } catch (e) {
                    // If this fails, we'll just fall back to the blank string
                }

                failAssertion(this, "expected " + expected + " to be called in order but were called as " + actual);
            } else {
                assert.pass("callOrder");
            }
        },

        callCount: function assertCallCount(method, count) {
            verifyIsStub(method);

            if (method.callCount !== count) {
                var msg = "expected %n to be called " + timesInWords(count) + " but was called %c%C";
                failAssertion(this, method.printf(msg));
            } else {
                assert.pass("callCount");
            }
        },

        expose: function expose(target, options) {
            if (!target) {
                throw new TypeError("target is null or undefined");
            }

            var o = options || {};
            var prefix = (typeof o.prefix === "undefined" && "assert") || o.prefix;
            var includeFail = typeof o.includeFail === "undefined" || Boolean(o.includeFail);
            var instance = this;

            forEach(Object.keys(instance), function(method) {
                if (method !== "expose" && (includeFail || !/^(fail)/.test(method))) {
                    target[exposedName(prefix, method)] = instance[method];
                }
            });

            return target;
        },

        match: function match(actual, expectation) {
            var matcher = createMatcher(expectation);
            if (matcher.test(actual)) {
                assert.pass("match");
            } else {
                var formatted = [
                    "expected value to match",
                    "    expected = " + format(expectation),
                    "    actual = " + format(actual)
                ];

                failAssertion(this, join(formatted, "\n"));
            }
        }
    };

    mirrorPropAsAssertion("called", "expected %n to have been called at least once but was never called");
    mirrorPropAsAssertion(
        "notCalled",
        function(spy) {
            return !spy.called;
        },
        "expected %n to not have been called but was called %c%C"
    );
    mirrorPropAsAssertion("calledOnce", "expected %n to be called once but was called %c%C");
    mirrorPropAsAssertion("calledTwice", "expected %n to be called twice but was called %c%C");
    mirrorPropAsAssertion("calledThrice", "expected %n to be called thrice but was called %c%C");
    mirrorPropAsAssertion("calledOn", "expected %n to be called with %1 as this but was called with %t");
    mirrorPropAsAssertion("alwaysCalledOn", "expected %n to always be called with %1 as this but was called with %t");
    mirrorPropAsAssertion("calledWithNew", "expected %n to be called with new");
    mirrorPropAsAssertion("alwaysCalledWithNew", "expected %n to always be called with new");
    mirrorPropAsAssertion("calledWith", "expected %n to be called with arguments %D");
    mirrorPropAsAssertion("calledWithMatch", "expected %n to be called with match %D");
    mirrorPropAsAssertion("alwaysCalledWith", "expected %n to always be called with arguments %D");
    mirrorPropAsAssertion("alwaysCalledWithMatch", "expected %n to always be called with match %D");
    mirrorPropAsAssertion("calledWithExactly", "expected %n to be called with exact arguments %D");
    mirrorPropAsAssertion("calledOnceWithExactly", "expected %n to be called once and with exact arguments %D");
    mirrorPropAsAssertion("calledOnceWithMatch", "expected %n to be called once and with match %D");
    mirrorPropAsAssertion("alwaysCalledWithExactly", "expected %n to always be called with exact arguments %D");
    mirrorPropAsAssertion("neverCalledWith", "expected %n to never be called with arguments %*%C");
    mirrorPropAsAssertion("neverCalledWithMatch", "expected %n to never be called with match %*%C");
    mirrorPropAsAssertion("threw", "%n did not throw exception%C");
    mirrorPropAsAssertion("alwaysThrew", "%n did not always throw exception%C");

    return assert;
}

module.exports = createAssertObject();
module.exports.createAssertObject = createAssertObject;
