define([
    'backbone-brace',
    'bitbucket/internal/util/deprecation'
], function(
    Brace,
    deprecate
) {

    'use strict';

    var oldConsole;
    module('util-deprecation');

    function withFakeConsoleLog(fn) {
        var oldConsole = window.console;
        window.console = {
            warn : sinon.spy()
        };
        try {
            fn();
        } finally {
            window.console = oldConsole;

        }
    }

    function assertDeprecationMessageContainsInputs(message, display, alternate, since, remove) {
        if (!message) {
            ok(false, 'No message was logged.');
            return;
        }
        message = message.split("\n")[0];
        ok(~message.indexOf(display || "DISPLAY"), "Logged the given display name in message: " + message);
        ok(~message.indexOf(alternate || "ALTERNATE"), "Logged the given alternate name in message: " + message);
        ok(~message.indexOf(since || "SINCE"), "Logged the given since version in message: " + message);
        ok(~message.indexOf(remove || "REMOVE"), "Logged the given removal version in message: " + message);
    }

    function assertFunctionDeprecated(deprecatedFn, self, arg, display, alternate, since, remove) {
        withFakeConsoleLog(function() {
            ok(!console.warn.called, "console.warn should not yet be called");
            deprecatedFn.call(self, arg);
            ok(console.warn.calledOnce, "console.warn should have been called once");
            assertDeprecationMessageContainsInputs(console.warn.args[0] && console.warn.args[0][0], display, alternate, since, remove);
            deprecatedFn.call(self, arg);
            ok(console.warn.calledOnce, "console.warn should not have been called again");
        });
    }

    function assertNoncallablePropertyDeprecated(obj, deprecatedProp, initialVal, display, alternate, since, remove, initialValueIsCopy) {
        withFakeConsoleLog(function() {
            ok(!console.warn.called, "console.warn should not yet be called");
            var actualValue = obj[deprecatedProp];
            if (initialValueIsCopy) {
                deepEqual(actualValue, initialVal, "accessing property should return real value");
            } else {
                strictEqual(actualValue, initialVal, "accessing property should return real value");
            }

            ok(console.warn.calledOnce, "console.warn should have been called once");
            assertDeprecationMessageContainsInputs(console.warn.args[0] && console.warn.args[0][0], display, alternate, since, remove);
            strictEqual(obj[deprecatedProp] = 'a', 'a', "setting property should return set value");
            ok(console.warn.calledOnce, "console.warn should not have been called again for setting");
            strictEqual(obj[deprecatedProp], 'a', "setting property should set real value");
            obj[deprecatedProp] = actualValue;
        });
    }

    function assertNoncallablePropertyNotDeprecated(obj, deprecatedProp, initialVal) {
        withFakeConsoleLog(function() {
            ok(!console.warn.called, "console.warn should not yet be called");
            strictEqual(obj[deprecatedProp], initialVal, "accessing property should return real value");
            ok(!console.warn.called, "console.warn should not be called for non-deprecated");
            strictEqual(obj[deprecatedProp] = 'a', 'a', "setting property should return set value");
            ok(!console.warn.called, "console.warn should not be called for non-deprecated");
            strictEqual(obj[deprecatedProp], 'a', "setting property should set real value");
            obj[deprecatedProp] = initialVal;
            ok(!console.warn.called, "console.warn should not be called for non-deprecated");
        });
    }

    test('deprecate.fn', function() {
        expect(12);

        var self = {};
        var arg = {};

        function fn(param) {
            strictEqual(this, self, 'function was called with right context');
            strictEqual(param, arg, 'function was called with right args');
        }
        var deprecatedFn = deprecate.fn(fn, 'DISPLAY', 'ALTERNATE', 'SINCE', 'REMOVE');
        equal(typeof deprecatedFn, 'function', 'deprecate.fn returns a function');

        assertFunctionDeprecated(deprecatedFn, self, arg);
    });

    test('deprecate.prop', function() {
        expect(21);
        var arg = {};

        var prop = {};
        var obj = {
            prop : prop,
            method : function(param) {
                strictEqual(this, obj, 'method was called with right context');
                strictEqual(param, arg, 'method was called with right args');
            }
        };

        deprecate.prop(obj, 'prop', 'DISPLAY', 'ALTERNATE', 'SINCE', 'REMOVE');
        assertNoncallablePropertyDeprecated(obj, 'prop', prop);

        deprecate.prop(obj, 'method', 'DISPLAY', 'ALTERNATE', 'SINCE', 'REMOVE');
        assertFunctionDeprecated(obj.method, obj, arg);
    });

    test('deprecate.obj', function() {
        expect(21);
        var arg = {};

        var prop = {};
        var method = function(param) {
            strictEqual(this, obj, 'method was called with right context');
            strictEqual(param, arg, 'method was called with right args');
        };
        var obj = {
            prop : prop,
            method : method
        };

        deprecate.obj(obj, "OBJ:", "ALTERNATIVE:", "SINCE", "REMOVE");
        assertNoncallablePropertyDeprecated(obj, 'prop', prop, "OBJ:prop", "ALTERNATIVE:prop", "SINCE", "REMOVE");
        assertFunctionDeprecated(obj.method, obj, arg, "OBJ:method", "ALTERNATIVE:method", "SINCE", "REMOVE");
    });

    function testBraceAsJson(namedAttributesType) {
        expect(39);
        var Model = Brace.Model.extend({
            namedAttributes : namedAttributesType === 'object' ? {
                propA : null,
                propB : null
            } : [
                'propA',
                'propB'
            ]
        });
        deprecate.braceAsJson(Model, 'Model', 'SINCE', 'REMOVE');

        var aData = {
            propA : 'string',
            propB : 3
        };
        var modelA = new Model(aData);
        var bData = {
            propA : 'charArray',
            propB : 2
        };
        var modelB = new Model(bData);

        // The same property on modelB shouldn't show the deprecation message again after it's fired on modelA.
        assertNoncallablePropertyDeprecated(modelA, 'propA', aData.propA, "Model::propA", "Model::get|set('propA')", "SINCE", "REMOVE");
        assertNoncallablePropertyNotDeprecated(modelB, 'propA', bData.propA);
        assertNoncallablePropertyDeprecated(modelB, 'propB', bData.propB, "Model::propB", "Model::get|set('propB')", "SINCE", "REMOVE");
        assertNoncallablePropertyNotDeprecated(modelA, 'propB', aData.propB);

        equal(modelA.propA, modelA.getPropA(), 'deprecated value matches real value.');
        equal(modelB.propA, modelB.getPropA(), 'deprecated value matches real value.');
        equal(modelA.propB, modelA.getPropB(), 'deprecated value matches real value.');
        equal(modelB.propB, modelB.getPropB(), 'deprecated value matches real value.');

        modelB.setPropB('x');
        equal(modelB.propB, 'x', 'Deprecated property always takes on real value.');
    }

    test('deprecate.braceAsJson', function() {
        testBraceAsJson('object');
    });

    test('deprecate.braceAsJson with array namedAttributes', function() {
        testBraceAsJson('array');
    });

    test('deprecate.braceAttribute', function(){
        var Model = Brace.Model.extend({
                namedAttributes : {
                    propA : null,
                    propB : null
                }
            });

        deprecate.braceAttribute(Model, 'propA', 'propB', 'SINCE', 'REMOVE');

        var modelA = new Model({
            propA : 'foo',
            propB : true
        });

        assertFunctionDeprecated(modelA.getPropA, modelA, null, 'PropA', 'propB', 'SINCE', 'REMOVE');
        assertFunctionDeprecated(modelA.setPropA, modelA, 'bar', 'PropA', 'propB', 'SINCE', 'REMOVE');
        equal('bar', modelA.getPropA(), 'Deprecated attribute is still accessed correctly');
    });

    test('deprecate.jsonAsBrace', function() {
        var Model = Brace.Model.extend({
            namedAttributes : {
                propA : null,
                propB : null
            }
        });
        var json = {
            propA : 'foo',
            propB : true
        };
        var model = new Model(json);

        function assertFuncDeprecated(attr, arg) {
            var deprecatedModel = deprecate.jsonAsBrace(model, 'SINCE', 'REMOVE');
            assertFunctionDeprecated(deprecatedModel[attr], deprecatedModel, arg, 'Backbone', 'JSON', 'SINCE', 'REMOVE');
        }

        // needs a deepEqual check instead of strict equal
        function assertAttributesDeprecated() {
            var deprecatedModel = deprecate.jsonAsBrace(model, 'SINCE', 'REMOVE');
            assertNoncallablePropertyDeprecated(deprecatedModel, 'attributes', json, 'Backbone', 'JSON', 'SINCE', 'REMOVE', true);
        }

        assertAttributesDeprecated();
        assertFuncDeprecated('getPropA');
        assertFuncDeprecated('setPropA');
        assertFuncDeprecated('getPropB');
        assertFuncDeprecated('setPropB');
        assertFuncDeprecated('get', 'propA');
        assertFuncDeprecated('set', 'propB');
        assertFuncDeprecated('toJSON');

        var deprecatedModel = deprecate.jsonAsBrace(model, 'SINCE', 'REMOVE');
        assertNoncallablePropertyNotDeprecated(deprecatedModel, 'propA', 'foo');
        assertNoncallablePropertyNotDeprecated(deprecatedModel, 'propB', true);
    });
});
