/*jslint onevar: false*/
/*globals sinon buster*/
/**
 * @author Christian Johansen (christian@cjohansen.no)
 * @license BSD
 *
 * Copyright (c) 2010-2012 Christian Johansen
 */
"use strict";

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

buster.testCase("sinon.mock", {
    "create": {
        "returns function with expects method": function () {
            var mock = sinon.mock.create({});

            assert.isFunction(mock.expects);
        },

        "throws without object": function () {
            assert.exception(function () {
                sinon.mock.create();
            }, "TypeError");
        }
    },

    "expects": {
        setUp: function () {
            this.mock = sinon.mock.create({ someMethod: function () {} });
        },

        "throws without method": function () {
            var mock = this.mock;

            assert.exception(function () {
                mock.expects();
            }, "TypeError");
        },

        "returns expectation": function () {
            var result = this.mock.expects("someMethod");

            assert.isFunction(result);
            assert.equals(result.method, "someMethod");
        },

        "throws if expecting a non-existent method": function () {
            var mock = this.mock;

            assert.exception(function () {
                mock.expects("someMethod2");
            });
        }
    },

    "expectation": {
        setUp: function () {
            this.method = "myMeth";
            this.expectation = sinon.expectation.create(this.method);
        },

        "creates unnamed expectation": function () {
            var anonMock = sinon.expectation.create();
            anonMock.never();

            assert(anonMock.verify());
        },

        "uses 'anonymous mock expectation' for unnamed expectation": function () {
            var anonMock = sinon.expectation.create();
            anonMock.once();

            try {
                anonMock.verify();
            } catch (e) {
                assert.match(e.message, "anonymous mock expectation");
            }
        },

        "call expectation": function () {
            this.expectation();

            assert.isFunction(this.expectation.invoke);
            assert(this.expectation.called);
        },

        "is invokable": function () {
            var expectation = this.expectation;

            refute.exception(function () {
                expectation();
            });
        },

        "returns": {
            "returns configured return value": function () {
                var object = {};
                this.expectation.returns(object);

                assert.same(this.expectation(), object);
            }
        },

        "call": {
            "is called with correct this value": function () {
                var object = { method: this.expectation };
                object.method();

                assert(this.expectation.calledOn(object));
            }
        },

        "callCount": {
            "onlys be invokable once by default": function () {
                var expectation = this.expectation;
                expectation();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "throw readable error": function () {
                var expectation = this.expectation;
                expectation();

                try {
                    expectation();
                    buster.assertions.fail("Expected to throw");
                } catch (e) {
                    assert.equals(e.message, "myMeth already called once");
                }
            }
        },

        "callCountNever": {
            "is not callable": function () {
                var expectation = this.expectation;
                expectation.never();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "returns expectation for chaining": function () {
                assert.same(this.expectation.never(), this.expectation);
            }
        },

        "callCountOnce": {
            "allows one call": function () {
                var expectation = this.expectation;
                expectation.once();
                expectation();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "returns expectation for chaining": function () {
                assert.same(this.expectation.once(), this.expectation);
            }
        },

        "callCountTwice": {
            "allows two calls": function () {
                var expectation = this.expectation;
                expectation.twice();
                expectation();
                expectation();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "returns expectation for chaining": function () {
                assert.same(this.expectation.twice(), this.expectation);
            }
        },

        "callCountThrice": {
            "allows three calls": function () {
                var expectation = this.expectation;
                expectation.thrice();
                expectation();
                expectation();
                expectation();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "returns expectation for chaining": function () {
                assert.same(this.expectation.thrice(), this.expectation);
            }
        },

        "callCountExactly": {
            "allows specified number of calls": function () {
                var expectation = this.expectation;
                expectation.exactly(2);
                expectation();
                expectation();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "returns expectation for chaining": function () {
                assert.same(this.expectation.exactly(2), this.expectation);
            },

            "throws without argument": function () {
                var expectation = this.expectation;

                assert.exception(function () {
                    expectation.exactly();
                }, "TypeError");
            },

            "throws without number": function () {
                var expectation = this.expectation;

                assert.exception(function () {
                    expectation.exactly("12");
                }, "TypeError");
            }
        },

        "atLeast": {
            "throws without argument": function () {
                var expectation = this.expectation;

                assert.exception(function () {
                    expectation.atLeast();
                }, "TypeError");
            },

            "throws without number": function () {
                var expectation = this.expectation;

                assert.exception(function () {
                    expectation.atLeast({});
                }, "TypeError");
            },

            "returns expectation for chaining": function () {
                assert.same(this.expectation.atLeast(2), this.expectation);
            },

            "allows any number of calls": function () {
                var expectation = this.expectation;
                expectation.atLeast(2);
                expectation();
                expectation();

                refute.exception(function () {
                    expectation();
                    expectation();
                });
            },

            "should not be met with too few calls": function () {
                this.expectation.atLeast(2);
                this.expectation();

                assert.isFalse(this.expectation.met());
            },

            "is met with exact calls": function () {
                this.expectation.atLeast(2);
                this.expectation();
                this.expectation();

                assert(this.expectation.met());
            },

            "is met with excessive calls": function () {
                this.expectation.atLeast(2);
                this.expectation();
                this.expectation();
                this.expectation();

                assert(this.expectation.met());
            },

            "should not throw when exceeding at least expectation": function () {
                var obj = { foobar: function () {} };
                var mock = sinon.mock(obj);
                mock.expects("foobar").atLeast(1);

                obj.foobar();

                refute.exception(function () {
                    obj.foobar();
                    mock.verify();
                });
            }
        },

        "atMost": {
            "throws without argument": function () {
                var expectation = this.expectation;

                assert.exception(function () {
                    expectation.atMost();
                }, "TypeError");
            },

            "throws without number": function () {
                var expectation = this.expectation;

                assert.exception(function () {
                    expectation.atMost({});
                }, "TypeError");
            },

            "returns expectation for chaining": function () {
                assert.same(this.expectation.atMost(2), this.expectation);
            },

            "allows fewer calls": function () {
                var expectation = this.expectation;
                expectation.atMost(2);

                refute.exception(function () {
                    expectation();
                });
            },

            "is met with fewer calls": function () {
                this.expectation.atMost(2);
                this.expectation();

                assert(this.expectation.met());
            },

            "is met with exact calls": function () {
                this.expectation.atMost(2);
                this.expectation();
                this.expectation();

                assert(this.expectation.met());
            },

            "should not be met with excessive calls": function () {
                var expectation = this.expectation;
                this.expectation.atMost(2);
                this.expectation();
                this.expectation();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");

                assert.isFalse(this.expectation.met());
            }
        },

        "atMostAndAtLeast": {
            setUp: function () {
                this.expectation.atLeast(2);
                this.expectation.atMost(3);
            },

            "should not be met with too few calls": function () {
                this.expectation();

                assert.isFalse(this.expectation.met());
            },

            "is met with minimum calls": function () {
                this.expectation();
                this.expectation();

                assert(this.expectation.met());
            },

            "is met with maximum calls": function () {
                this.expectation();
                this.expectation();
                this.expectation();

                assert(this.expectation.met());
            },

            "throws with excessive calls": function () {
                var expectation = this.expectation;
                expectation();
                expectation();
                expectation();

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            }
        },

        "met": {
            "should not be met when not called enough times": function () {
                assert.isFalse(this.expectation.met());
            },

            "is met when called enough times": function () {
                this.expectation();

                assert(this.expectation.met());
            },

            "should not be met when called too many times": function () {
                this.expectation();

                try {
                    this.expectation();
                } catch (e) {}

                assert.isFalse(this.expectation.met());
            }
        },

        "withArgs": {
            "returns expectation for chaining": function () {
                assert.same(this.expectation.withArgs(1), this.expectation);
            },

            "accepts call with expected args": function () {
                this.expectation.withArgs(1, 2, 3);
                this.expectation(1, 2, 3);

                assert(this.expectation.met());
            },

            "throws when called without args": function () {
                var expectation = this.expectation;
                expectation.withArgs(1, 2, 3);

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "throws when called with too few args": function () {
                var expectation = this.expectation;
                expectation.withArgs(1, 2, 3);

                assert.exception(function () {
                    expectation(1, 2);
                }, "ExpectationError");
            },

            "throws when called with wrong args": function () {
                var expectation = this.expectation;
                expectation.withArgs(1, 2, 3);

                assert.exception(function () {
                    expectation(2, 2, 3);
                }, "ExpectationError");
            },

            "allows excessive args": function () {
                var expectation = this.expectation;
                expectation.withArgs(1, 2, 3);

                refute.exception(function () {
                    expectation(1, 2, 3, 4);
                });
            },

            "calls accept with no args": function () {
                this.expectation.withArgs();
                this.expectation();

                assert(this.expectation.met());
            },

            "allows no args called with excessive args": function () {
                var expectation = this.expectation;
                expectation.withArgs();

                refute.exception(function () {
                    expectation(1, 2, 3);
                });
            },
            "works with sinon matchers": function(){
                this.expectation.withArgs(sinon.match.number, sinon.match.string, sinon.match.func);
                this.expectation(1, "test", function(){});

                assert(this.expectation.met());
            },
            "throws when sinon matchers fail": function(){
                var expectation = this.expectation;
                
                this.expectation.withArgs(sinon.match.number, sinon.match.string, sinon.match.func);
                assert.exception(function () {
                    expectation(1, 2, 3);
                }, "ExpectationError");
                
            }
        },

        "withExactArgs": {
            "returns expectation for chaining": function () {
                assert.same(this.expectation.withExactArgs(1), this.expectation);
            },

            "accepts call with expected args": function () {
                this.expectation.withExactArgs(1, 2, 3);
                this.expectation(1, 2, 3);

                assert(this.expectation.met());
            },

            "throws when called without args": function () {
                var expectation = this.expectation;
                expectation.withExactArgs(1, 2, 3);

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            },

            "throws when called with too few args": function () {
                var expectation = this.expectation;
                expectation.withExactArgs(1, 2, 3);

                assert.exception(function () {
                    expectation(1, 2);
                }, "ExpectationError");
            },

            "throws when called with wrong args": function () {
                var expectation = this.expectation;
                expectation.withExactArgs(1, 2, 3);

                assert.exception(function () {
                    expectation(2, 2, 3);
                }, "ExpectationError");
            },

            "should not allow excessive args": function () {
                var expectation = this.expectation;
                expectation.withExactArgs(1, 2, 3);

                assert.exception(function () {
                    expectation(1, 2, 3, 4);
                }, "ExpectationError");
            },

            "accepts call with no expected args": function () {
                this.expectation.withExactArgs();
                this.expectation();

                assert(this.expectation.met());
            },

            "does not allow excessive args with no expected args": function () {
                var expectation = this.expectation;
                expectation.withExactArgs();

                assert.exception(function () {
                    expectation(1, 2, 3);
                }, "ExpectationError");
            }
        },

        "on": {
            "returns expectation for chaining": function () {
                assert.same(this.expectation.on({}), this.expectation);
            },

            "allows calls on object": function () {
                this.expectation.on(this);
                this.expectation();

                assert(this.expectation.met());
            },

            "throws if called on wrong object": function () {
                var expectation = this.expectation;
                expectation.on({});

                assert.exception(function () {
                    expectation();
                }, "ExpectationError");
            }
        },

        "verify": {
            "pass if met": function () {
                sinon.stub(sinon.expectation, "pass");
                var expectation = this.expectation;

                expectation();
                expectation.verify();

                assert.equals(sinon.expectation.pass.callCount, 1);
                sinon.expectation.pass.restore();
            },

            "throws if not called enough times": function () {
                var expectation = this.expectation;

                assert.exception(function () {
                    expectation.verify();
                }, "ExpectationError");
            },

            "throws readable error": function () {
                var expectation = this.expectation;

                try {
                    expectation.verify();
                    buster.assertions.fail("Expected to throw");
                } catch (e) {
                    assert.equals(e.message,
                                  "Expected myMeth([...]) once (never called)");
                }
            }
        }
    },

    "verify": {
        setUp: function () {
            this.method = function () {};
            this.object = { method: this.method };
            this.mock = sinon.mock.create(this.object);
        },

        "restores mocks": function () {
            this.object.method();
            this.object.method.call(this.thisValue);
            this.mock.verify();

            assert.same(this.object.method, this.method);
        },

        "passes verified mocks": function () {
            sinon.stub(sinon.expectation, "pass");

            this.mock.expects("method").once();
            this.object.method();
            this.mock.verify();

            assert.equals(sinon.expectation.pass.callCount, 1);
            sinon.expectation.pass.restore();
        },

        "restores if not met": function () {
            var mock = this.mock;
            mock.expects("method");

            assert.exception(function () {
                mock.verify();
            }, "ExpectationError");

            assert.same(this.object.method, this.method);
        },

        "includes all calls in error message": function () {
            var mock = this.mock;
            mock.expects("method").thrice();
            mock.expects("method").once().withArgs(42);
            var message;

            try {
                mock.verify();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message,
                          "Expected method([...]) thrice (never called)\nExpected method(42[, ...]) once (never called)");
        },

        "includes exact expected arguments in error message": function () {
            var mock = this.mock;
            mock.expects("method").once().withExactArgs(42);
            var message;

            try {
                mock.verify();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Expected method(42) once (never called)");
        },

        "includes received call count in error message": function () {
            var mock = this.mock;
            mock.expects("method").thrice().withExactArgs(42);
            this.object.method(42);
            var message;

            try {
                mock.verify();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Expected method(42) thrice (called once)");
        },

        "includes unexpected calls in error message": function () {
            var mock = this.mock;
            mock.expects("method").thrice().withExactArgs(42);
            var message;

            try {
                this.object.method();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message,
                          "Unexpected call: method()\n" +
                          "    Expected method(42) thrice (never called)");
        },

        "includes met expectations in error message": function () {
            var mock = this.mock;
            mock.expects("method").once().withArgs(1);
            mock.expects("method").thrice().withExactArgs(42);
            this.object.method(1);
            var message;

            try {
                this.object.method();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Unexpected call: method()\n" +
                          "    Expectation met: method(1[, ...]) once\n" +
                          "    Expected method(42) thrice (never called)");
        },

        "includes met expectations in error message from verify": function () {
            var mock = this.mock;
            mock.expects("method").once().withArgs(1);
            mock.expects("method").thrice().withExactArgs(42);
            this.object.method(1);
            var message;

            try {
                mock.verify();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Expected method(42) thrice (never called)\n" +
                          "Expectation met: method(1[, ...]) once");
        },

        "reports min calls in error message": function () {
            var mock = this.mock;
            mock.expects("method").atLeast(1);
            var message;

            try {
                mock.verify();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Expected method([...]) at least once (never called)");
        },

        "reports max calls in error message": function () {
            var mock = this.mock;
            mock.expects("method").atMost(2);
            var message;

            try {
                this.object.method();
                this.object.method();
                this.object.method();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Unexpected call: method()\n" +
                          "    Expectation met: method([...]) at most twice");
        },

        "reports min calls in met expectation": function () {
            var mock = this.mock;
            mock.expects("method").atLeast(1);
            mock.expects("method").withArgs(2).once();
            var message;

            try {
                this.object.method();
                this.object.method(2);
                this.object.method(2);
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Unexpected call: method(2)\n" +
                          "    Expectation met: method([...]) at least once\n" +
                          "    Expectation met: method(2[, ...]) once");
        },

        "reports max and min calls in error messages": function () {
            var mock = this.mock;
            mock.expects("method").atLeast(1).atMost(2);
            var message;

            try {
                mock.verify();
            } catch (e) {
                message = e.message;
            }

            assert.equals(message, "Expected method([...]) at least once and at most twice " +
                          "(never called)");
        }
    },

    "mockObject": {
        setUp: function () {
            this.method = function () {};
            this.object = { method: this.method };
            this.mock = sinon.mock.create(this.object);
        },

        "mocks object method": function () {
            this.mock.expects("method");

            assert.isFunction(this.object.method);
            refute.same(this.object.method, this.method);
        },

        "reverts mocked method": function () {
            this.mock.expects("method");
            this.object.method.restore();

            assert.same(this.object.method, this.method);
        },

        "reverts expectation": function () {
            var method = this.mock.expects("method");
            this.object.method.restore();

            assert.same(this.object.method, this.method);
        },

        "reverts mock": function () {
            var method = this.mock.expects("method");
            this.mock.restore();

            assert.same(this.object.method, this.method);
        },

        "verifies mock": function () {
            var method = this.mock.expects("method");
            this.object.method();
            var mock = this.mock;

            refute.exception(function () {
                assert(mock.verify());
            });
        },

        "verifies mock with unmet expectations": function () {
            var method = this.mock.expects("method");
            var mock = this.mock;

            assert.exception(function () {
                assert(mock.verify());
            }, "ExpectationError");
        }
    },

    "mock method multiple times": {
        setUp: function () {
            this.thisValue = {};
            this.method = function () {};
            this.object = { method: this.method };
            this.mock = sinon.mock.create(this.object);
            this.mock.expects("method");
            this.mock.expects("method").on(this.thisValue);
        },

        "queues expectations": function () {
            var object = this.object;

            refute.exception(function () {
                object.method();
            });
        },

        "starts on next expectation when first is met": function () {
            var object = this.object;
            object.method();

            assert.exception(function () {
                object.method();
            }, "ExpectationError");
        },

        "fails on last expectation": function () {
            var object = this.object;
            object.method();
            object.method.call(this.thisValue);

            assert.exception(function () {
                object.method();
            }, "ExpectationError");
        },

        "allows mock calls in any order": function () {
            var object = { method: function () {} };
            var mock = sinon.mock(object);
            mock.expects("method").once().withArgs(42);
            mock.expects("method").twice().withArgs("Yeah");

            refute.exception(function () {
                object.method("Yeah");
            });

            refute.exception(function () {
                object.method(42);
            });

            assert.exception(function () {
                object.method(1);
            });

            refute.exception(function () {
                object.method("Yeah");
            });

            assert.exception(function () {
                object.method(42);
            });
        }
    },

    "mock function": {
        "returns mock method": function () {
            var mock = sinon.mock();

            assert.isFunction(mock);
            assert.isFunction(mock.atLeast);
            assert.isFunction(mock.verify);
        },

        "returns mock object": function () {
            var mock = sinon.mock({});

            assert.isObject(mock);
            assert.isFunction(mock.expects);
            assert.isFunction(mock.verify);
        }
    },

    "yields": {
        "invokes only argument as callback": function () {
            var mock = sinon.mock().yields();
            var spy = sinon.spy();
            mock(spy);

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

        "throws understandable error if no callback is passed": function () {
            var mock = sinon.mock().yields();

            try {
                mock();
                throw new Error();
            } catch (e) {
                assert.equals(e.message, "stub expected to yield, but no callback was passed.");
            }
        }
    }
});
