define([
    'jquery',
    'lodash',
    'Squire',
    'test-helper',
    'bitbucket/mock/mirroring/upstream-project-provider',
    'bitbucket/internal/util/events'
], function (
    $,
    _,
    Squire,
    testHelper,
    MockUpstreamProvider,
    events
) {
    'use strict';

    /**
     * A mock template for the unit test. We can't rely on SOY being there.
     *
     * @returns {string} the mock html for a project selector.
     */
    function template() {
        return '' +
            '<form>' +
            '   <input type="text">' +
            '   <span class="project-selector-spinner"></span>' +
            '   <button class="project-selector-add">Add</button>' +
            '</form>';
    }

    function createSelector(ProjectSelector, element) {
        return new ProjectSelector({target: element}).init();
    }

    /**
     * Mock out a jQuery function. Returns a function that can be called to restore the original function.
     *
     * @param name the name of the function to mock.
     * @param fn the mock
     * @returns {Function} to restore the original function.
     */
    function mockJquery(name, fn) {
        var old = $.fn[name];
        $.fn[name] = fn;
        return function () {
            if (old) {
                $.fn[name] = old;
            } else {
                delete $.fn[name]
            }
        }
    }

    var projects = [{
        id: 1,
        name: "Project One"
    }, {
        id: 2,
        name: "Project Two"
    }, {
        id: 3,
        name: "Project Three"
    }, {
        id: 4,
        name: "Project Four"
    }, {
        id: 5,
        name: "Project Five"
    }];

    /**
     * Mock implementation of the `SearchableMultiSelector`
     *
     * @param field the DOM element to make into a selector.
     * @param options
     * @constructor
     */
    function MockSearchableMultiSelector(field, options) {
        this.$field = $(field);
        this.projects = [];
        this.dataSource = options.dataSource;
        this.$field.data('selector', this);
    }

    events.addLocalEventMixin(MockSearchableMultiSelector.prototype);

    MockSearchableMultiSelector.prototype.addProject = function (project) {
        this.projects = this.projects.concat(project);
        this.trigger('change');
        return this;
    };

    MockSearchableMultiSelector.prototype.removeProject = function (project) {
        this.projects = _.without(this.projects, project);
        this.trigger('change');
        return this;
    };

    MockSearchableMultiSelector.prototype.getSelectedItems = function () {
        return this.projects;
    };

    MockSearchableMultiSelector.prototype.clearSelectedItems = function () {
        this.projects = [];
        return this;
    };

    MockSearchableMultiSelector.prototype.clear = function () {
        this.dataSource.clear();
        return this;
    };

    MockSearchableMultiSelector.prototype.nextPage = function (filter) {
        return this.dataSource.nextPage(filter);
    };

    /**
     * Page object for the Project Selector.
     *
     * @param el the element that contains the initialized selector.
     * @constructor
     */
    function ProjectSelectorPageObject(el) {
        this.$el = $(el);
        this.$add = this.$el.find('.project-selector-add');
        this.$spinner = this.$el.find('.project-selector-spinner');
        this.$input = this.$el.find('input');
        this.selector = this.$input.data('selector');
    }

    ProjectSelectorPageObject.prototype.isAddEnabled = function() {
        return !this.$add.prop('disabled') && !this.$add.hasClass('disabled');
    };

    ProjectSelectorPageObject.prototype.isSelectorEnabled = function() {
        return !this.$input.prop('disabled');
    };

    ProjectSelectorPageObject.prototype.isDisabled = function () {
        return !this.isSelectorEnabled() && !this.isAddEnabled();
    };

    ProjectSelectorPageObject.prototype.addProject = function(project) {
        this.selector.addProject(project);
        return this;
    };

    ProjectSelectorPageObject.prototype.addProjects = function(projects) {
        var self = this;
        projects.forEach(function (item) {
            self.addProject(item);
        });
        return this;
    };

    ProjectSelectorPageObject.prototype.removeProject = function(project) {
        this.selector.removeProject(project);
        return this;
    };

    ProjectSelectorPageObject.prototype.clickAdd = function() {
        this.$add.click();
        return this;
    };

    ProjectSelectorPageObject.prototype.isSpinnerShown = function() {
        return this.$spinner.hasClass('spinning');
    };

    ProjectSelectorPageObject.prototype.isElementEmpty = function() {
        return !this.$el.children().length;
    };

    ProjectSelectorPageObject.prototype.clear = function() {
        this.selector.clear();
        return this;
    };

    ProjectSelectorPageObject.prototype.nextPage = function(filter) {
        return this.selector.nextPage(filter);
    };

    testHelper.setTemplates({
        'bitbucket.internal.mirroring.project.projectSelector': template
    });

    var injector = new Squire();
    injector.mock('jquery', $);
    injector.mock('bitbucket/internal/widget/searchable-multi-selector', MockSearchableMultiSelector);
    injector.mock('bitbucket/internal/mirroring/upstream-project-provider', function (options) {
        return new MockUpstreamProvider(_.assign(options, {
            projects: projects,
            limit: 2
        }));
    });
    injector.setDefaultDeps(['bitbucket/internal/mirroring/project-selector']);

    QUnit.module('project-selector', {
        beforeEach: function () {
            this.$fixture = $("#qunit-fixture");
            this.restoreSpin = mockJquery('spin', function () {
                this.addClass('spinning');
                return this;
            });

            this.restoreSpinStop = mockJquery('spinStop', function () {
                this.removeClass('spinning');
                return this;
            });
        },
        afterEach: function () {
            this.restoreSpin();
            this.restoreSpinStop();
        }
    });

    injector.test('Add button is disabled when no projects selected', function (assert, ProjectSelector) {
        createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        assert.ok(!selectorDriver.isAddEnabled(), "Add is disabled when no projects selected.");

        selectorDriver.addProject("project1");
        assert.ok(selectorDriver.isAddEnabled(), "Adding project enables add.");

        selectorDriver.addProject("project2");
        assert.ok(selectorDriver.isAddEnabled(), "Adding project leaves add enabled.");

        selectorDriver.removeProject("project1");
        assert.ok(selectorDriver.isAddEnabled(), "Removing project leaves add enabled.");

        selectorDriver.removeProject("project2");
        assert.ok(!selectorDriver.isAddEnabled(), "Removing all projects disables add.");
    });

    injector.test('Add button triggers `projectsSelected`', function (assert, ProjectSelector) {
        assert.expect(1);

        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);
        var selectedProjects = ["project1", "project2"];

        selector.on('projectsSelected', function (projects) {
            assert.deepEqual(projects, selectedProjects, "Event fired.");
        });

        selectorDriver.addProjects(selectedProjects).clickAdd();
    });

    injector.test('Disable disables the control', function (assert, ProjectSelector) {

        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        selector.disable();

        assert.ok(selectorDriver.isDisabled(), "Selector is disabled");
    });

    injector.test('Enable enables both selector and add button if project selected', function (assert, ProjectSelector) {

        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        selectorDriver.addProject("project1");

        selector.disable();

        assert.ok(selectorDriver.isDisabled(), "Selector is disabled");

        selector.enable();

        assert.ok(selectorDriver.isAddEnabled(), "Add is enabled.");
        assert.ok(selectorDriver.isSelectorEnabled(), "Selector is enabled.");
    });

    injector.test('Enable enables only enabled selector if no project selected', function (assert, ProjectSelector) {

        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        selector.disable();

        assert.ok(selectorDriver.isDisabled(), "Selector is disabled");

        selector.enable();

        assert.ok(!selectorDriver.isAddEnabled(), "Add is not enabled.");
        assert.ok(selectorDriver.isSelectorEnabled(), "Selector is enabled.");
    });

    injector.test('Get selected items returns the selected projects', function (assert, ProjectSelector) {
        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        assert.deepEqual([], selector.getSelected(), "No projects selected");

        selectorDriver.addProject("project1");

        assert.deepEqual(["project1"], selector.getSelected(), "Project 1 selected.");

        selectorDriver.addProject("project2");

        assert.deepEqual(["project1", "project2"], selector.getSelected(), "Project 2 also selected.");

        selectorDriver.removeProject("project1");

        assert.deepEqual(["project2"], selector.getSelected(), "Project 1 removed.");
    });

    injector.test('Clear removes selected projects add disables add', function (assert, ProjectSelector) {
        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        selectorDriver.addProjects(["project1", "project2"]);

        selector.clear();

        assert.deepEqual([], selector.getSelected(), "No projects selected.");
        assert.ok(!selectorDriver.isAddEnabled(), "Add should not be enabled.");
    });

    injector.test('Destroy clears html', function (assert, ProjectSelector) {
        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        selector.destroy();

        assert.ok(selectorDriver.isElementEmpty(), "All content removed?");
    });

    injector.test('Spinner show and hides when requested', function (assert, ProjectSelector) {
        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        assert.ok(!selectorDriver.isSpinnerShown(), "Spinner not shown initially");

        selector.showSpinner();

        assert.ok(selectorDriver.isSpinnerShown(), "Spinner shown?");

        selector.hideSpinner();

        assert.ok(!selectorDriver.isSpinnerShown(), "Spinner hidden?");
    });

    injector.test('Search queries the server for projects', function (assert, ProjectSelector) {

        assert.expect(3);

        createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        selectorDriver.nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, projects.slice(0, 2) , "First page correct.");
        });

        selectorDriver.nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, projects.slice(2, 4), "Second page correct.");
        });

        selectorDriver.nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, projects.slice(4, 5), "Third page correct.");
        });
    });

    injector.test('Search error is reported as `loadError` event', function (assert, ProjectSelector) {

        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);
        var loadError = sinon.spy();

        selector.on('loadError', loadError);

        ok(!loadError.called, "Error not reported");
        selectorDriver.nextPage('error');
        ok(loadError.calledOnce, "Error reported");
    });

    injector.test('Clear resets the search', function (assert, ProjectSelector) {

        assert.expect(3);

        createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);

        selectorDriver.nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, projects.slice(0, 2), "First page correct.");
        });

        selectorDriver.clear();

        selectorDriver.nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, projects.slice(0, 2), "First page returned after clear.");
        });


        selectorDriver.nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, projects.slice(2, 4), "Second page returned after clear.");
        });
    });

    injector.test('Predicate filters out projects', function (assert, ProjectSelector) {
        var selector = createSelector(ProjectSelector, this.$fixture);
        var selectorDriver = new ProjectSelectorPageObject(this.$fixture);
        var project1 = projects[0];
        var project2 = projects[1];
        var project3 = projects[2];
        var project4 = projects[3];
        var project5 = projects[4];

        selector.addIgnoredProjects([project1, project2]);
        selector.addIgnoredProjectIds([project3.id]);

        selectorDriver.nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, [project4, project5], "Filtered projects ignored.");
        });

        selector.removeIgnoredProjects([project3, project4]);
        selector.removeIgnoredProjectIds([project1.id]);

        selectorDriver.clear().nextPage('Project').done(function (data) {
            assert.deepEqual(data.values, [project1, project3], "Projects appear again");
        });

        selectorDriver.clear().nextPage('Three').done(function (data) {
            assert.deepEqual(data.values, [project3], "Projects filtered by name also");
        });
    })
});