/* global QUnit */
define([
    'Squire',
    'aui',
    'jquery',
    'lodash',
    'bitbucket/internal/util/function',
    'bitbucket/internal/util/text',
    'exports'
], function (
    Squire,
    AJS,
    $,
    _,
    fn,
    text,
    exports
) {

    'use strict';

    /**
     * Helper function that sets the dependencies to be required when using
     * injectedCall / injectedTest
     *
     * @param {array} deps - array of module names that every call to injectedCall/injectedTest will need
     */
    Squire.prototype.setDefaultDeps = function (deps) {
        // This is a necessary evil to stop Squire reloading AUI in every test, which causes problems with Skate. Long
        // term we should refactor this test helper so that has a method that returns a Squire instance, instead of
        // monkey-patching Squire itself.  By doing it here we only need to do it once, instead of making every test that
        // uses Squire have a depencency / mock on AUI as well. https://jira.atlassian.com/browse/STASHDEV-8715
        this.mock('aui', AJS);
        this.mock('underscore', window._);
        this.mock('jquery', $);
        this.defaultDeps = deps;
    };

    /**
     * Calls a function after requiring the set of defaultDeps for this instance
     * @param {function} func - the function to call after dependencies are resolved
     * @param {*} ctx - value of this when func is called
     * @param {function} [errorHandler] - optional function to call with any errors that occur when running func
     */
    Squire.prototype.injectedCall = function (func, ctx, errorHandler /*, ...args */) {
        var self = this;
        var callArgs = Array.prototype.splice.call(arguments, 3);
        this.require(this.defaultDeps, function () {
            try {
                func.apply(ctx || self, callArgs.concat(Array.prototype.slice.apply(arguments)));
            } catch (err) {
                if (errorHandler) {
                    errorHandler.call(ctx || self, err);
                } else {
                    console.error(err.stack);
                    throw err;
                }
            }
        });
    };

    /**
     * Function that implements a unit test body
     *
     * @callback testFunction
     * @param {object} assert - QUnit assert
     * @param {function} done - QUnit async done function
     */

    /**
     * Run an injected async QUnit test
     * @param {string} name - the name of the test
     * @param {testFunction} testFunction - the test function
     */
    Squire.prototype.test = function (name, testFunction) {
        var self = this;
        QUnit.test(name, function (assert) {
            var done = assert.async();
            self.injectedCall(function () {
                testFunction.apply(this, arguments);
                done();
            }, this, null, assert);
        });
    };

    /**
     * Helper for defining mock Soy templates.
     * @param {object} templates - an object containing template names as keys and either a function or a string as the value. If
     *  value is a string then it will be wrapped in a constant function.
     */
    function setTemplates(templates) {
        _.forOwn(templates, function (func, key) {
            fn.set(key, _.isString(func) ? fn.constant(func) : func, true)(window);
        });
    }

    /**
     * Helper for defining a mock Stash model/user object
     * @param props - user properties
     * @returns {function}
     */
    function mockUser(props) {
        return fn.constant({
            getName: fn.constant(props.username || 'username'),
            getSlug: fn.constant(props.slug || 'username'),
            getId: fn.constant(props.id || 1),
            id: props.id || 1,
            isAdmin: fn.constant(props.isAdmin || false),
            toJSON: fn.constant({
                id: props.id || 1,
                name: props.username || 'username',
                isAdmin: props.isAdmin || false
            })
        });
    }

    /**
     * Returns a mock page state API
     * @param {object} props - properties of the page state
     * @return {object}
     */
    function mockPageStateApi(props) {
        props = props || {};
        var propList = [
            'pullRequest',
            'project',
            'repository',
            'currentUser',
            'filePath',
            'commit'
        ];

        var api = {};
        _.each(propList, function (propName) {
            api['get' + text.toSentenceCase(propName)] = fn.constant(props[propName] || {});
        });
        return api;
    }

    /**
     * Returns a mock page state model
     */
    function mockPageState(props) {
        props = props || {};
        return {
            getPullRequest: fn.constant({
                id: props.pullRequestId || 1,
                getId: fn.constant(props.pullRequestId || 1),
                getAuthor: fn.constant({
                        getUser: props.pullRequestAuthor || mockUser({
                            name: 'author'
                        })
                }),
                getReviewers: fn.constant({
                    models: props.reviewers || []
                }),
                getParticipants: fn.constant({
                    models: props.participants || []
                })
            }),
            getProject: fn.constant({
               getKey: fn.constant(props.projectKey || 'PROJ')
            }),
            getRepository: fn.constant({
                id: props.repoId || 1,
                getId: fn.constant(props.repoId || 1),
                getSlug: fn.constant(props.repoSlug || 'REPO')
            }),
            getCurrentUser: props.currentUser || mockUser({
                getName: fn.constant(props.currentUserName || 'username')
            })
        };
    }

    /**
     * Returns a mock WRM implementation
     * @param {object} props - key value pairs containing WRM data
     * @returns {object}
     */
    function mockWRM(props) {
        return {
            data: {
                claim: function (key) {
                    return _.has(props, key) ? props[key] : undefined;
                },
                debug: function () {
                    return props;
                }
            },
            require: sinon.spy(function () {
                var deferred = $.Deferred();
                deferred.resolve();
                return deferred.promise();
            })
        };
    }

    /**
     * Mocks a util/property instance
     * @param {object} values - property values to provide
     * @returns {{getFromProvider: Function}}
     */
    function mockPropertyUtil(values) {
        return {
            getFromProvider: function (prop) {
                var defer = $.Deferred();
                defer.resolve(values[prop]);
                return defer.promise();
            }
        };
    }

    /**
     * An object that can be used as a simple bitbucket/util/events mock in most instances
     * @constructor
     */
    function SimpleEvents() {
        this.handlers = {};
    }

    SimpleEvents.prototype.on = function (eventName, handler) {
        this.handlers[eventName] = handler;
        return this;
    };

    SimpleEvents.prototype.trigger = function (eventName, data) {
        if (_.has(this.handlers, eventName)) {
            this.handlers[eventName](data);
        } else {
            console.error("SimpleEvents - missing handler for " + eventName);
        }
    };

    SimpleEvents.prototype.reset = function () {
        this.handlers = {};
    };

    exports.mockPageStateApi = mockPageStateApi;
    exports.mockPageState = mockPageState;
    exports.mockPropertyUtil = mockPropertyUtil;
    exports.mockUser = mockUser;
    exports.mockWRM = mockWRM;
    exports.setTemplates = setTemplates;
    exports.SimpleEvents = SimpleEvents;
});