(function (root) {
    "use strict";

    var buster = root.buster || require("buster");
    var sinon = root.sinon || require("../lib/sinon");
    var assert = buster.assert;
    var refute = buster.refute;

    function spyCallSetUp() {
        this.thisValue = {};
        this.args = [{}, [], new Error(), 3];
        this.returnValue = function () {};
        this.call = sinon.spyCall(function () {}, this.thisValue,
            this.args, this.returnValue, null, 0);
    }

    function spyCallCallSetup() {
        this.args = [];
        this.proxy = sinon.spy();
        this.call = sinon.spyCall(this.proxy, {}, this.args, null, null, 0);
    }

    function spyCallCalledTests(method) {
        return {
            setUp: spyCallSetUp,

            "returns true if all args match": function () {
                var args = this.args;

                assert(this.call[method](args[0], args[1], args[2]));
            },

            "returns true if first args match": function () {
                var args = this.args;

                assert(this.call[method](args[0], args[1]));
            },

            "returns true if first arg match": function () {
                var args = this.args;

                assert(this.call[method](args[0]));
            },

            "returns true for no args": function () {
                assert(this.call[method]());
            },

            "returns false for too many args": function () {
                var args = this.args;

                assert.isFalse(this.call[method](args[0], args[1], args[2], args[3], {}));
            },

            "returns false for wrong arg": function () {
                var args = this.args;

                assert.isFalse(this.call[method](args[0], args[2]));
            }
        };
    }

    function spyCallNotCalledTests(method) {
        return {
            setUp: spyCallSetUp,

            "returns false if all args match": function () {
                var args = this.args;

                assert.isFalse(this.call[method](args[0], args[1], args[2]));
            },

            "returns false if first args match": function () {
                var args = this.args;

                assert.isFalse(this.call[method](args[0], args[1]));
            },

            "returns false if first arg match": function () {
                var args = this.args;

                assert.isFalse(this.call[method](args[0]));
            },

            "returns false for no args": function () {
                assert.isFalse(this.call[method]());
            },

            "returns true for too many args": function () {
                var args = this.args;

                assert(this.call[method](args[0], args[1], args[2], args[3], {}));
            },

            "returns true for wrong arg": function () {
                var args = this.args;

                assert(this.call[method](args[0], args[2]));
            }
        };
    }


    buster.testCase("sinon.spy.call", {

        "call object": {
            setUp: spyCallSetUp,

            "gets call object": function () {
                var spy = sinon.spy.create();
                spy();
                var firstCall = spy.getCall(0);

                assert.isFunction(firstCall.calledOn);
                assert.isFunction(firstCall.calledWith);
                assert.isFunction(firstCall.returned);
            },

            "stores given call id": function () {
                var call = sinon.spyCall(function () {}, {}, [], null, null, 42);

                assert.same(call.callId, 42);
            },

            "throws if callId is undefined": function () {
                assert.exception(function () {
                    sinon.spyCall.create(function () {}, {}, []);
                });
            },

            // This is actually a spy test:
            "records ascending call id's": function () {
                var spy = sinon.spy();
                spy();

                assert(this.call.callId < spy.getCall(0).callId);
            },

            "exposes thisValue property": function () {
                var spy = sinon.spy();
                var obj = {};
                spy.call(obj);

                assert.same(spy.getCall(0).thisValue, obj);
            }
        },

        "call calledOn": {
            setUp: spyCallSetUp,

            "calledOn should return true": function () {
                assert(this.call.calledOn(this.thisValue));
            },

            "calledOn should return false": function () {
                assert.isFalse(this.call.calledOn({}));
            }
        },

        "call.calledWith": spyCallCalledTests("calledWith"),
        "call.calledWithMatch": spyCallCalledTests("calledWithMatch"),
        "call.notCalledWith": spyCallNotCalledTests("notCalledWith"),
        "call.notCalledWithMatch": spyCallNotCalledTests("notCalledWithMatch"),

        "call.calledWithExactly": {
            setUp: spyCallSetUp,

            "returns true when all args match": function () {
                var args = this.args;

                assert(this.call.calledWithExactly(args[0], args[1], args[2], args[3]));
            },

            "returns false for too many args": function () {
                var args = this.args;

                assert.isFalse(this.call.calledWithExactly(args[0], args[1], args[2], args[3], {}));
            },

            "returns false for too few args": function () {
                var args = this.args;

                assert.isFalse(this.call.calledWithExactly(args[0], args[1]));
            },

            "returns false for unmatching args": function () {
                var args = this.args;

                assert.isFalse(this.call.calledWithExactly(args[0], args[1], args[1]));
            },

            "returns true for no arguments": function () {
                var call = sinon.spyCall(function () {}, {}, [], null, null, 0);

                assert(call.calledWithExactly());
            },

            "returns false when called with no args but matching one": function () {
                var call = sinon.spyCall(function () {}, {}, [], null, null, 0);

                assert.isFalse(call.calledWithExactly({}));
            }
        },

        "call.callArg": {
            setUp: spyCallCallSetup,

            "calls argument at specified index": function () {
                var callback = sinon.spy();
                this.args.push(1, 2, callback);

                this.call.callArg(2);

                assert(callback.called);
            },

            "throws if argument at specified index is not callable": function () {
                this.args.push(1);
                var call = this.call;

                assert.exception(function () {
                    call.callArg(0);
                }, "TypeError");
            },

            "throws if no index is specified": function () {
                var call = this.call;

                assert.exception(function () {
                    call.callArg();
                }, "TypeError");
            },

            "throws if index is not number": function () {
                var call = this.call;

                assert.exception(function () {
                    call.callArg({});
                }, "TypeError");
            }
        },

        "call.callArgOn": {
            setUp: spyCallCallSetup,

            "calls argument at specified index": function () {
                var callback = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(1, 2, callback);

                this.call.callArgOn(2, thisObj);

                assert(callback.called);
                assert(callback.calledOn(thisObj));
            },

            "throws if argument at specified index is not callable": function () {
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(1);
                var call = this.call;

                assert.exception(function () {
                    call.callArgOn(0, thisObj);
                }, "TypeError");
            },

            "throws if index is not number": function () {
                var thisObj = { name1: "value1", name2: "value2" };
                var call = this.call;

                assert.exception(function () {
                    call.callArgOn({}, thisObj);
                }, "TypeError");
            }
        },

        "call.callArgWith": {
            setUp: spyCallCallSetup,

            "calls argument at specified index with provided args": function () {
                var object = {};
                var callback = sinon.spy();
                this.args.push(1, callback);

                this.call.callArgWith(1, object);

                assert(callback.calledWith(object));
            },

            "calls callback without args": function () {
                var callback = sinon.spy();
                this.args.push(1, callback);

                this.call.callArgWith(1);

                assert(callback.calledWith());
            },

            "calls callback wit multiple args": function () {
                var object = {};
                var array = [];
                var callback = sinon.spy();
                this.args.push(1, 2, callback);

                this.call.callArgWith(2, object, array);

                assert(callback.calledWith(object, array));
            },

            "throws if no index is specified": function () {
                var call = this.call;

                assert.exception(function () {
                    call.callArgWith();
                }, "TypeError");
            },

            "throws if index is not number": function () {
                var call = this.call;

                assert.exception(function () {
                    call.callArgWith({});
                }, "TypeError");
            }
        },

        "call.callArgOnWith": {
            setUp: spyCallCallSetup,

            "calls argument at specified index with provided args": function () {
                var object = {};
                var thisObj = { name1: "value1", name2: "value2" };
                var callback = sinon.spy();
                this.args.push(1, callback);

                this.call.callArgOnWith(1, thisObj, object);

                assert(callback.calledWith(object));
                assert(callback.calledOn(thisObj));
            },

            "calls callback without args": function () {
                var callback = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(1, callback);

                this.call.callArgOnWith(1, thisObj);

                assert(callback.calledWith());
                assert(callback.calledOn(thisObj));
            },

            "calls callback with multiple args": function () {
                var object = {};
                var array = [];
                var thisObj = { name1: "value1", name2: "value2" };
                var callback = sinon.spy();
                this.args.push(1, 2, callback);

                this.call.callArgOnWith(2, thisObj, object, array);

                assert(callback.calledWith(object, array));
                assert(callback.calledOn(thisObj));
            },

            "throws if index is not number": function () {
                var thisObj = { name1: "value1", name2: "value2" };
                var call = this.call;

                assert.exception(function () {
                    call.callArgOnWith({}, thisObj);
                }, "TypeError");
            }
        },

        "call.yieldTest": {
            setUp: spyCallCallSetup,

            "invokes only argument as callback": function () {
                var callback = sinon.spy();
                this.args.push(callback);

                this.call.yield();

                assert(callback.calledOnce);
                assert.equals(callback.args[0].length, 0);
            },

            "throws understandable error if no callback is passed": function () {
                var call = this.call;

                try {
                    call.yield();
                    throw new Error();
                } catch (e) {
                    assert.equals(e.message, "spy cannot yield since no callback was passed.");
                }
            },

            "includes stub name and actual arguments in error": function () {
                this.proxy.displayName = "somethingAwesome";
                this.args.push(23, 42);
                var call = this.call;

                try {
                    call.yield();
                    throw new Error();
                } catch (e) {
                    assert.equals(e.message, "somethingAwesome cannot yield since no callback was passed. " +
                                  "Received [23, 42]");
                }
            },

            "invokes last argument as callback": function () {
                var spy = sinon.spy();
                this.args.push(24, {}, spy);

                this.call.yield();

                assert(spy.calledOnce);
                assert.equals(spy.args[0].length, 0);
            },

            "invokes first of two callbacks": function () {
                var spy = sinon.spy();
                var spy2 = sinon.spy();
                this.args.push(24, {}, spy, spy2);

                this.call.yield();

                assert(spy.calledOnce);
                assert.isFalse(spy2.called);
            },

            "invokes callback with arguments": function () {
                var obj = { id: 42 };
                var spy = sinon.spy();
                this.args.push(spy);

                this.call.yield(obj, "Crazy");

                assert(spy.calledWith(obj, "Crazy"));
            },

            "throws if callback throws": function () {
                this.args.push(function () {
                    throw new Error("d'oh!");
                });
                var call = this.call;

                assert.exception(function () {
                    call.yield();
                });
            }
        },

        "call.invokeCallback": {

            "is alias for yield": function () {
                var call = sinon.spyCall(function () {}, {}, [], null, null, 0);

                assert.same(call.yield, call.invokeCallback);
            }

        },

        "call.yieldOnTest": {
            setUp: spyCallCallSetup,

            "invokes only argument as callback": function () {
                var callback = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(callback);

                this.call.yieldOn(thisObj);

                assert(callback.calledOnce);
                assert(callback.calledOn(thisObj));
                assert.equals(callback.args[0].length, 0);
            },

            "throws understandable error if no callback is passed": function () {
                var call = this.call;
                var thisObj = { name1: "value1", name2: "value2" };

                try {
                    call.yieldOn(thisObj);
                    throw new Error();
                } catch (e) {
                    assert.equals(e.message, "spy cannot yield since no callback was passed.");
                }
            },

            "includes stub name and actual arguments in error": function () {
                this.proxy.displayName = "somethingAwesome";
                this.args.push(23, 42);
                var call = this.call;
                var thisObj = { name1: "value1", name2: "value2" };

                try {
                    call.yieldOn(thisObj);
                    throw new Error();
                } catch (e) {
                    assert.equals(e.message, "somethingAwesome cannot yield since no callback was passed. " +
                                  "Received [23, 42]");
                }
            },

            "invokes last argument as callback": function () {
                var spy = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(24, {}, spy);

                this.call.yieldOn(thisObj);

                assert(spy.calledOnce);
                assert.equals(spy.args[0].length, 0);
                assert(spy.calledOn(thisObj));
            },

            "invokes first of two callbacks": function () {
                var spy = sinon.spy();
                var spy2 = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(24, {}, spy, spy2);

                this.call.yieldOn(thisObj);

                assert(spy.calledOnce);
                assert(spy.calledOn(thisObj));
                assert.isFalse(spy2.called);
            },

            "invokes callback with arguments": function () {
                var obj = { id: 42 };
                var spy = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(spy);

                this.call.yieldOn(thisObj, obj, "Crazy");

                assert(spy.calledWith(obj, "Crazy"));
                assert(spy.calledOn(thisObj));
            },

            "throws if callback throws": function () {
                this.args.push(function () {
                    throw new Error("d'oh!");
                });
                var call = this.call;
                var thisObj = { name1: "value1", name2: "value2" };

                assert.exception(function () {
                    call.yieldOn(thisObj);
                });
            }
        },

        "call.yieldTo": {
            setUp: spyCallCallSetup,

            "invokes only argument as callback": function () {
                var callback = sinon.spy();
                this.args.push({
                    success: callback
                });

                this.call.yieldTo("success");

                assert(callback.calledOnce);
                assert.equals(callback.args[0].length, 0);
            },

            "throws understandable error if no callback is passed": function () {
                var call = this.call;

                try {
                    call.yieldTo("success");
                    throw new Error();
                } catch (e) {
                    assert.equals(e.message, "spy cannot yield to 'success' since no callback was passed.");
                }
            },

            "includes stub name and actual arguments in error": function () {
                this.proxy.displayName = "somethingAwesome";
                this.args.push(23, 42);
                var call = this.call;

                try {
                    call.yieldTo("success");
                    throw new Error();
                } catch (e) {
                    assert.equals(
                        e.message,
                        "somethingAwesome cannot yield to 'success' since no callback was passed. Received [23, 42]"
                    );
                }
            },

            "invokes property on last argument as callback": function () {
                var spy = sinon.spy();
                this.args.push(24, {}, { success: spy });

                this.call.yieldTo("success");

                assert(spy.calledOnce);
                assert.equals(spy.args[0].length, 0);
            },

            "invokes first of two possible callbacks": function () {
                var spy = sinon.spy();
                var spy2 = sinon.spy();
                this.args.push(24, {}, { error: spy }, { error: spy2 });

                this.call.yieldTo("error");

                assert(spy.calledOnce);
                assert.isFalse(spy2.called);
            },

            "invokes callback with arguments": function () {
                var obj = { id: 42 };
                var spy = sinon.spy();
                this.args.push({ success: spy });

                this.call.yieldTo("success", obj, "Crazy");

                assert(spy.calledWith(obj, "Crazy"));
            },

            "throws if callback throws": function () {
                this.args.push({
                    success: function () {
                        throw new Error("d'oh!");
                    }
                });
                var call = this.call;

                assert.exception(function () {
                    call.yieldTo("success");
                });
            }
        },

        "call.yieldToOn": {
            setUp: spyCallCallSetup,

            "invokes only argument as callback": function () {
                var callback = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push({
                    success: callback
                });

                this.call.yieldToOn("success", thisObj);

                assert(callback.calledOnce);
                assert.equals(callback.args[0].length, 0);
                assert(callback.calledOn(thisObj));
            },

            "throws understandable error if no callback is passed": function () {
                var call = this.call;
                var thisObj = { name1: "value1", name2: "value2" };

                try {
                    call.yieldToOn("success", thisObj);
                    throw new Error();
                } catch (e) {
                    assert.equals(e.message, "spy cannot yield to 'success' since no callback was passed.");
                }
            },

            "includes stub name and actual arguments in error": function () {
                this.proxy.displayName = "somethingAwesome";
                this.args.push(23, 42);
                var call = this.call;
                var thisObj = { name1: "value1", name2: "value2" };

                try {
                    call.yieldToOn("success", thisObj);
                    throw new Error();
                } catch (e) {
                    assert.equals(
                        e.message,
                        "somethingAwesome cannot yield to 'success' since no callback was passed. Received [23, 42]"
                    );
                }
            },

            "invokes property on last argument as callback": function () {
                var spy = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(24, {}, { success: spy });

                this.call.yieldToOn("success", thisObj);

                assert(spy.calledOnce);
                assert(spy.calledOn(thisObj));
                assert.equals(spy.args[0].length, 0);
            },

            "invokes first of two possible callbacks": function () {
                var spy = sinon.spy();
                var spy2 = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push(24, {}, { error: spy }, { error: spy2 });

                this.call.yieldToOn("error", thisObj);

                assert(spy.calledOnce);
                assert(spy.calledOn(thisObj));
                assert.isFalse(spy2.called);
            },

            "invokes callback with arguments": function () {
                var obj = { id: 42 };
                var spy = sinon.spy();
                var thisObj = { name1: "value1", name2: "value2" };
                this.args.push({ success: spy });

                this.call.yieldToOn("success", thisObj, obj, "Crazy");

                assert(spy.calledWith(obj, "Crazy"));
                assert(spy.calledOn(thisObj));
            },

            "throws if callback throws": function () {
                this.args.push({
                    success: function () {
                        throw new Error("d'oh!");
                    }
                });
                var call = this.call;
                var thisObj = { name1: "value1", name2: "value2" };

                assert.exception(function () {
                    call.yieldToOn("success", thisObj);
                });
            }
        },

        "call.toString": {
            setUp: function () {
                this.format = sinon.format;
            },

            tearDown: function () {
                sinon.format = this.format;
            },

            "includes spy name": function () {
                var object = { doIt: sinon.spy() };
                object.doIt();

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt()");
            },

            "includes single argument": function () {
                var object = { doIt: sinon.spy() };
                object.doIt(42);

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt(42)");
            },

            "includes all arguments": function () {
                var object = { doIt: sinon.spy() };
                object.doIt(42, "Hey");

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt(42, Hey)");
            },

            "includes explicit return value": function () {
                var object = { doIt: sinon.stub().returns(42) };
                object.doIt(42, "Hey");

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt(42, Hey) => 42");
            },

            "includes empty string return value": function () {
                var object = { doIt: sinon.stub().returns("") };
                object.doIt(42, "Hey");

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt(42, Hey) => ");
            },

            "includes exception": function () {
                var object = { doIt: sinon.stub().throws("TypeError") };

                try {
                    object.doIt();
                }
                catch (e) {} // eslint-disable-line no-empty

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt() !TypeError");
            },

            "includes exception message if any": function () {
                var object = { doIt: sinon.stub().throws("TypeError", "Oh noes!") };

                try {
                    object.doIt();
                }
                catch (e) {} // eslint-disable-line no-empty

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt() !TypeError(Oh noes!)");
            },

            "formats arguments with sinon.format": function () {
                sinon.format = sinon.stub().returns("Forty-two");
                var object = { doIt: sinon.spy() };

                object.doIt(42);

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt(Forty-two)");
                assert(sinon.format.calledWith(42));
            },

            "formats return value with sinon.format": function () {
                sinon.format = sinon.stub().returns("Forty-two");
                var object = { doIt: sinon.stub().returns(42) };

                object.doIt();

                assert.equals(object.doIt.getCall(0).toString().replace(/ at.*/g, ""), "doIt() => Forty-two");
                assert(sinon.format.calledWith(42));
            }
        },

        constructor: {
            setUp: function () {
                this.CustomConstructor = function () {};
                this.customPrototype = this.CustomConstructor.prototype;
                sinon.spy(this, "CustomConstructor");
            },

            "creates original object": function () {
                var myInstance = new this.CustomConstructor();

                assert(this.customPrototype.isPrototypeOf(myInstance));
            },

            "does not interfere with instanceof": function () {
                var myInstance = new this.CustomConstructor();

                assert(myInstance instanceof this.CustomConstructor);
            },

            "records usage": function () {
                var myInstance = new this.CustomConstructor(); // eslint-disable-line no-unused-vars

                assert(this.CustomConstructor.called);
            }
        },

        functions: {
            "throws if spying on non-existent property": function () {
                var myObj = {};

                assert.exception(function () {
                    sinon.spy(myObj, "ouch");
                });

                refute.defined(myObj.ouch);
            },

            "throws if spying on non-existent object": function () {
                assert.exception(function () {
                    sinon.spy(undefined, "ouch");
                });
            },

            "haves toString method": function () {
                var obj = { meth: function () {} };
                sinon.spy(obj, "meth");

                assert.equals(obj.meth.toString(), "meth");
            },

            "toString should say 'spy' when unable to infer name": function () {
                var spy = sinon.spy();

                assert.equals(spy.toString(), "spy");
            },

            "toString should report name of spied function": function () {
                function myTestFunc() {}
                var spy = sinon.spy(myTestFunc);

                assert.equals(spy.toString(), "myTestFunc");
            },

            "toString should prefer displayName property if available": function () {
                function myTestFunc() {}
                myTestFunc.displayName = "My custom method";
                var spy = sinon.spy(myTestFunc);

                assert.equals(spy.toString(), "My custom method");
            },

            "toString should prefer property name if possible": function () {
                var obj = {};
                obj.meth = sinon.spy();
                obj.meth();

                assert.equals(obj.meth.toString(), "meth");
            }
        },

        ".reset": (function () {
            function assertReset(spy) {
                assert(!spy.called);
                assert(!spy.calledOnce);
                assert.equals(spy.args.length, 0);
                assert.equals(spy.returnValues.length, 0);
                assert.equals(spy.exceptions.length, 0);
                assert.equals(spy.thisValues.length, 0);
                assert.isNull(spy.firstCall);
                assert.isNull(spy.secondCall);
                assert.isNull(spy.thirdCall);
                assert.isNull(spy.lastCall);
            }

            return {
                "resets spy state": function () {
                    var spy = sinon.spy();
                    spy();

                    spy.reset();

                    assertReset(spy);
                },

                "resets call order state": function () {
                    var spies = [sinon.spy(), sinon.spy()];
                    spies[0]();
                    spies[1]();

                    spies[0].reset();

                    assert(!spies[0].calledBefore(spies[1]));
                },

                "resets fakes returned by withArgs": function () {
                    var spy = sinon.spy();
                    var fakeA = spy.withArgs("a");
                    var fakeB = spy.withArgs("b");
                    spy("a");
                    spy("b");
                    spy("c");
                    var fakeC = spy.withArgs("c");

                    spy.reset();

                    assertReset(fakeA);
                    assertReset(fakeB);
                    assertReset(fakeC);
                }
            };
        }()),

        ".withArgs": {
            "defines withArgs method": function () {
                var spy = sinon.spy();

                assert.isFunction(spy.withArgs);
            },

            "records single call": function () {
                var spy = sinon.spy().withArgs(1);
                spy(1);

                assert.equals(spy.callCount, 1);
            },

            "records non-matching call on original spy": function () {
                var spy = sinon.spy();
                var argSpy = spy.withArgs(1);
                spy(1);
                spy(2);

                assert.equals(spy.callCount, 2);
                assert.equals(argSpy.callCount, 1);
            },

            "records non-matching call with several arguments separately": function () {
                var spy = sinon.spy();
                var argSpy = spy.withArgs(1, "str", {});
                spy(1);
                spy(1, "str", {});

                assert.equals(spy.callCount, 2);
                assert.equals(argSpy.callCount, 1);
            },

            "records for partial argument match": function () {
                var spy = sinon.spy();
                var argSpy = spy.withArgs(1, "str", {});
                spy(1);
                spy(1, "str", {});
                spy(1, "str", {}, []);

                assert.equals(spy.callCount, 3);
                assert.equals(argSpy.callCount, 2);
            },

            "records filtered spy when original throws": function () {
                var spy = sinon.spy(function () {
                    throw new Error("Oops");
                });

                var argSpy = spy.withArgs({}, []);

                assert.exception(function () {
                    spy(1);
                });

                assert.exception(function () {
                    spy({}, []);
                });

                assert.equals(spy.callCount, 2);
                assert.equals(argSpy.callCount, 1);
            },

            "returns existing override for arguments": function () {
                var spy = sinon.spy();
                var argSpy = spy.withArgs({}, []);
                var another = spy.withArgs({}, []);
                spy();
                spy({}, []);
                spy({}, [], 2);

                assert.same(another, argSpy);
                refute.same(another, spy);
                assert.equals(spy.callCount, 3);
                assert.equals(spy.withArgs({}, []).callCount, 2);
            },

            "chains withArgs calls on original spy": function () {
                var spy = sinon.spy();
                var numArgSpy = spy.withArgs({}, []).withArgs(3);
                spy();
                spy({}, []);
                spy(3);

                assert.equals(spy.callCount, 3);
                assert.equals(numArgSpy.callCount, 1);
                assert.equals(spy.withArgs({}, []).callCount, 1);
            },

            "initializes filtered spy with callCount": function () {
                var spy = sinon.spy();
                spy("a");
                spy("b");
                spy("b");
                spy("c");
                spy("c");
                spy("c");

                var argSpy1 = spy.withArgs("a");
                var argSpy2 = spy.withArgs("b");
                var argSpy3 = spy.withArgs("c");

                assert.equals(argSpy1.callCount, 1);
                assert.equals(argSpy2.callCount, 2);
                assert.equals(argSpy3.callCount, 3);
                assert(argSpy1.called);
                assert(argSpy2.called);
                assert(argSpy3.called);
                assert(argSpy1.calledOnce);
                assert(argSpy2.calledTwice);
                assert(argSpy3.calledThrice);
            },

            "initializes filtered spy with first, second, third and last call": function () {
                var spy = sinon.spy();
                spy("a", 1);
                spy("b", 2);
                spy("b", 3);
                spy("b", 4);

                var argSpy1 = spy.withArgs("a");
                var argSpy2 = spy.withArgs("b");

                assert(argSpy1.firstCall.calledWithExactly("a", 1));
                assert(argSpy1.lastCall.calledWithExactly("a", 1));
                assert(argSpy2.firstCall.calledWithExactly("b", 2));
                assert(argSpy2.secondCall.calledWithExactly("b", 3));
                assert(argSpy2.thirdCall.calledWithExactly("b", 4));
                assert(argSpy2.lastCall.calledWithExactly("b", 4));
            },

            "initializes filtered spy with arguments": function () {
                var spy = sinon.spy();
                spy("a");
                spy("b");
                spy("b", "c", "d");

                var argSpy1 = spy.withArgs("a");
                var argSpy2 = spy.withArgs("b");

                assert(argSpy1.getCall(0).calledWithExactly("a"));
                assert(argSpy2.getCall(0).calledWithExactly("b"));
                assert(argSpy2.getCall(1).calledWithExactly("b", "c", "d"));
            },

            "initializes filtered spy with thisValues": function () {
                var spy = sinon.spy();
                var thisValue1 = {};
                var thisValue2 = {};
                var thisValue3 = {};
                spy.call(thisValue1, "a");
                spy.call(thisValue2, "b");
                spy.call(thisValue3, "b");

                var argSpy1 = spy.withArgs("a");
                var argSpy2 = spy.withArgs("b");

                assert(argSpy1.getCall(0).calledOn(thisValue1));
                assert(argSpy2.getCall(0).calledOn(thisValue2));
                assert(argSpy2.getCall(1).calledOn(thisValue3));
            },

            "initializes filtered spy with return values": function () {
                var spy = sinon.spy(function (value) {
                    return value;
                });
                spy("a");
                spy("b");
                spy("b");

                var argSpy1 = spy.withArgs("a");
                var argSpy2 = spy.withArgs("b");

                assert(argSpy1.getCall(0).returned("a"));
                assert(argSpy2.getCall(0).returned("b"));
                assert(argSpy2.getCall(1).returned("b"));
            },

            "initializes filtered spy with call order": function () {
                var spy = sinon.spy();
                spy("a");
                spy("b");
                spy("b");

                var argSpy1 = spy.withArgs("a");
                var argSpy2 = spy.withArgs("b");

                assert(argSpy2.getCall(0).calledAfter(argSpy1.getCall(0)));
                assert(argSpy2.getCall(1).calledAfter(argSpy1.getCall(0)));
            },

            "initializes filtered spy with exceptions": function () {
                var spy = sinon.spy(function (x, y) {
                    var error = new Error();
                    error.name = y;
                    throw error;
                });
                /*eslint-disable no-empty*/
                try {
                    spy("a", "1");
                } catch (ignored) {}
                try {
                    spy("b", "2");
                } catch (ignored) {}
                try {
                    spy("b", "3");
                } catch (ignored) {}
                /*eslint-enable no-empty*/

                var argSpy1 = spy.withArgs("a");
                var argSpy2 = spy.withArgs("b");

                assert(argSpy1.getCall(0).threw("1"));
                assert(argSpy2.getCall(0).threw("2"));
                assert(argSpy2.getCall(1).threw("3"));
            }
        },

        ".printf": {
            name: {
                named: function () {
                    var named = sinon.spy(function cool() { });
                    assert.equals(named.printf("%n"), "cool");
                },
                anon: function () {
                    var anon = sinon.spy(function () {});
                    assert.equals(anon.printf("%n"), "spy");

                    var noFn = sinon.spy();
                    assert.equals(noFn.printf("%n"), "spy");
                }
            },

            count: function () {
                // Throwing just to make sure it has no effect.
                var spy = sinon.spy(sinon.stub().throws());
                function call() {
                    try {
                        spy();
                    }
                    catch (e) {} // eslint-disable-line no-empty
                }

                call();
                assert.equals(spy.printf("%c"), "once");
                call();
                assert.equals(spy.printf("%c"), "twice");
                call();
                assert.equals(spy.printf("%c"), "thrice");
                call();
                assert.equals(spy.printf("%c"), "4 times");
            },

            calls: {
                oneLine: function () {
                    function test(arg, expected) {
                        var spy = sinon.spy();
                        spy(arg);
                        assert.equals(spy.printf("%C").replace(/ at.*/g, ""), "\n    " + expected);
                    }

                    test(true, "spy(true)");
                    test(false, "spy(false)");
                    test(undefined, "spy(undefined)");
                    test(1, "spy(1)");
                    test(0, "spy(0)");
                    test(-1, "spy(-1)");
                    test(-1.1, "spy(-1.1)");
                    test(Infinity, "spy(Infinity)");
                    test(["a"], "spy([\"a\"])");
                    test({ a: "a" }, "spy({ a: \"a\" })");
                },

                multiline: function () {
                    var str = "spy\ntest";
                    var spy = sinon.spy();

                    spy(str);
                    spy(str);
                    spy(str);

                    assert.equals(spy.printf("%C").replace(/ at.*/g, ""),
                        "\n    spy(" + str + ")" +
                        "\n\n    spy(" + str + ")" +
                        "\n\n    spy(" + str + ")");

                    spy.reset();

                    spy("test");
                    spy("spy\ntest");
                    spy("spy\ntest");

                    assert.equals(spy.printf("%C").replace(/ at.*/g, ""),
                        "\n    spy(test)" +
                        "\n    spy(" + str + ")" +
                        "\n\n    spy(" + str + ")");
                }
            },

            thisValues: function () {
                var spy = sinon.spy();
                spy();
                assert.equals(spy.printf("%t"), "undefined");

                spy.reset();
                spy.call(true);
                assert.equals(spy.printf("%t"), "true");
            }
        }
    });
}(this));
