(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;

    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.referee.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();
                    });
                },

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

                    mock.expects("foobar").withArgs("arg1");
                    mock.expects("foobar").atLeast(1).withArgs("arg2");

                    obj.foobar("arg1");
                    obj.foobar("arg2");
                    obj.foobar("arg2");

                    assert(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) {} // eslint-disable-line no-empty

                    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.referee.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)");
            },

            "does not call pass if no expectations": function () {
                var pass = sinon.stub(sinon.expectation, "pass");

                var mock = this.mock;
                mock.expects("method").never();
                delete mock.expectations;

                mock.verify();

                refute(pass.called, "expectation.pass should not be called");

                pass.restore();
            }
        },

        "mock object": {
            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 () {
                this.mock.expects("method");
                this.object.method.restore();

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

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

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

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

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

            "verifies mock with unmet expectations": function () {
                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.");
                }
            }
        }
    });
}(this));
