/*jslint onevar: false, eqeqeq: false*/
/*globals window sinon buster*/
/**
 * @author Christian Johansen (christian@cjohansen.no)
 * @author Maximilian Antoni (mail@maxantoni.de)
 * @license BSD
 *
 * Copyright (c) 2010-2013 Christian Johansen
 * Copyright (c) 2013 Maximilian Antoni
 */
"use strict";

if (typeof require === "function" && typeof module === "object") {
    var buster = require("../runner");
    var sinon = require("../../lib/sinon");
}

(function () {

    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]));
            }
        };
    }

    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);
    }

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

        "callObject": {
            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);
            }
        },

        "callCalledOn": {
            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(), "doIt()");
            },

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

                assert.equals(object.doIt.getCall(0).toString(), "doIt(42)");
            },

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

                assert.equals(object.doIt.getCall(0).toString(), "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(), "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(), "doIt(42, Hey) => ");
            },

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

                try {
                    object.doIt();
                } catch (e) {}

                assert.equals(object.doIt.getCall(0).toString(), "doIt() !TypeError");
            },

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

                try {
                    object.doIt();
                } catch (e) {}

                assert.equals(object.doIt.getCall(0).toString(), "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(), "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(), "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();

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

        "function": {
            "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;
                });
                try {
                    spy("a", "1");
                } catch (ignored) {}
                try {
                    spy("b", "2");
                } catch (ignored) {}
                try {
                    spy("b", "3");
                } catch (ignored) {}

                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) {}
                }

                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'), '\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'), 
                        '\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'),
                        '\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');
            }
        }
    });
}());

