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

    'use strict';

    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"
    }];
    var project1 = projects[0];
    var project2 = projects[1];
    var project3 = projects[2];
    var project4 = projects[3];
    var project5 = projects[4];

    /**
     * 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]
            }
        }
    }

    function tableRowTemplate(project) {
        return "" +
            "<tr data-project-id='" + project.id + "'>" +
                "<td class='project-name'><a href='#'>" + project.name + "</td>" +
                "<td class='remove-project'><button>Delete Me</button></td>" +
            "</tr>";
    }

    function tableRowsTemplate(projects) {
        var rows = [];
        projects.forEach(function (project) {
            rows.push(tableRowTemplate(project));
        });
        return rows.join('');
    }

    function emptyTemplate() {
        return $("<div><div class='empty-table-message'></div>");
    }

    function spinnerTemplate() {
        return $("<div class='remove-spinner'>");
    }

    testHelper.setTemplates({
        'bitbucket.internal.mirroring.project.table.empty': emptyTemplate,
        'bitbucket.internal.mirroring.project.table.done': _.noop,
        'bitbucket.internal.mirroring.project.table.rows': function (data) {
            return tableRowsTemplate(data.projects);
        },
        'bitbucket.internal.mirroring.project.table.spinner': spinnerTemplate
    });

    /**
     * Mock of `PagedTable`.
     *
     * @param options
     * @constructor
     */
    function MockPagedTable (options) {
        this.$table = $(options.target);
    }

    MockPagedTable.prototype.add = function (projects) {
        this.projects = projects;
        this.handleNewRows({values: projects}, 'append');
        return true;
    };

    MockPagedTable.prototype.handleNoData = function () {
        this.$table.parent().append(emptyTemplate());
    };

    MockPagedTable.prototype.init = function () {
    };

    /**
     * Page object for the table. Can be used to drive interaction with the table.
     *
     * @param table the table element.
     * @constructor
     */
    function PageTableDriver (table) {
        this.$table = $(table);
        this.$container = this.$table.parent();
    }

    PageTableDriver.prototype.addEmptyMessage = function () {
        this.$container.append(emptyTemplate());
        return this;
    };

    PageTableDriver.prototype.hasEmptyMessage = function () {
        return !!this.$container.find(".empty-table-message").length;
    };

    PageTableDriver.prototype.getProjects = function () {
        var projects = [];
        this.$table.find('tr').each(function () {
            var $project = $(this);
            var name = $project.find(".project-name a").text();
            var id = $project.attr("data-project-id");

            projects.push({
                id: id,
                name : name
            });
        });
        return projects;
    };

    function getRow($table, projectId) {
        return $table.find('tbody > tr[data-project-id="' + projectId + '"]');
    }

    PageTableDriver.prototype.isSpinning = function (projectId) {
        var $row = getRow(this.$table, projectId);
        var $spinner = $row.find('.remove-spinner');
        return Boolean($spinner.length && $spinner.hasClass('spinning'));
    };

    PageTableDriver.prototype.clickRemoveProject = function (projectId) {
        var $row = getRow(this.$table, projectId);
        if (!$row.length) {
            throw new Error("Row for project '" + projectId + "' not found.");
        }
        $row.find('.remove-project > button').click();
        return this;
    };

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

    QUnit.module('mirror-project-table', {
        beforeEach: function () {
            this.$fixture = $("#qunit-fixture");
            this.$table = $("<table><tbody></tbody></table>").appendTo(this.$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("Gets projects from server", function (assert, ProjectTable) {

        assert.expect(3);

        var projectTable = new ProjectTable({target: this.$table, selectedProjects: _.pluck(projects, 'id')});

        projectTable.performAjax(0, 2).done(function (data) {
            assert.deepEqual(data.values, projects.slice(0, 2), "First page returned.");
        });

        projectTable.performAjax(2, 2).done(function (data) {
            assert.deepEqual(data.values, projects.slice(2, 4), "Second page returned.");
        });

        projectTable.performAjax(4, 2).done(function (data) {
            assert.deepEqual(data.values, projects.slice(4, 5), "Third page returned.");
        });
    });

    injector.test("Filters projects from server", function (assert, ProjectTable) {

        assert.expect(2);

        var projectTable = new ProjectTable({target: this.$table, selectedProjects: ["1", "2", "3"]});

        projectTable.performAjax(0, 2).done(function (data) {
            assert.deepEqual(data.values, projects.slice(0, 2), "First page returned.");
        });

        projectTable.performAjax(2, 2).done(function (data) {
            assert.deepEqual(data.values, projects.slice(2, 3), "Second page returned.");
        });
    });

    injector.test("Add delegates to parent and removes empty message", function (assert, ProjectTable) {
        var projectTable = new ProjectTable({target: this.$table});
        var driver = new PageTableDriver(this.$table);

        driver.addEmptyMessage();

        assert.ok(driver.hasEmptyMessage());
        projectTable.add(projects);
        assert.ok(!driver.hasEmptyMessage(), "Empty message removed");
        assert.deepEqual(projectTable.projects, projects, "Projects added");
    });

    injector.test("Handle new rows adds new rows.", function (assert, ProjectTable) {
        var projectTable = new ProjectTable({target: this.$table});
        var driver = new PageTableDriver(this.$table);

        projectTable.handleNewRows({values: projects}, 'append');
        assert.deepEqual(driver.getProjects(), projects, "Projects correctly rendered.");
    });

    injector.test("Remove project removes correct row ", function (assert, ProjectTable) {
        var projectTable = new ProjectTable({target: this.$table});
        var driver = new PageTableDriver(this.$table);

        projectTable.add(projects);

        assert.deepEqual(driver.getProjects(), projects, "Projects correctly rendered.");
        projectTable.removeProject(project1.id);
        assert.deepEqual(driver.getProjects(), projects.slice(1), "Project removed.");
        projectTable.removeProject(project5.id);
        assert.deepEqual(driver.getProjects(), projects.slice(1, 4), "Project removed.");
    });

    injector.test("Remove last project adds empty state", function (assert, ProjectTable) {
        var projectTable = new ProjectTable({target: this.$table});
        var driver = new PageTableDriver(this.$table);
        var project1 = projects[0];

        projectTable.add([project1]);

        assert.deepEqual(driver.getProjects(), [project1], "Projects correctly rendered.");
        projectTable.removeProject(project1.id);
        assert.deepEqual(driver.getProjects(), [], "Project removed.");
        assert.ok(driver.hasEmptyMessage(), "Empty message shown?");
    });

    injector.test("Spinner shown/hidden in correct row", function (assert, ProjectTable) {
        var projectTable = new ProjectTable({target: this.$table});
        var driver = new PageTableDriver(this.$table);

        projectTable.add(projects);

        projectTable.showSpinner(project3.id);
        assert.ok(!driver.isSpinning(project1.id));
        assert.ok(!driver.isSpinning(project2.id));
        assert.ok(driver.isSpinning(project3.id));
        assert.ok(!driver.isSpinning(project4.id));
        assert.ok(!driver.isSpinning(project5.id));

        projectTable.hideSpinner(project3.id);
        assert.ok(!driver.isSpinning(project1.id));
        assert.ok(!driver.isSpinning(project2.id));
        assert.ok(!driver.isSpinning(project3.id));
        assert.ok(!driver.isSpinning(project4.id));
        assert.ok(!driver.isSpinning(project5.id));
    });

    injector.test("Remove triggers a remove event but does not remove row", function (assert, ProjectTable) {
        var projectTable = new ProjectTable({target: this.$table});
        var driver = new PageTableDriver(this.$table);
        var callback = sinon.spy();

        projectTable.init();

        projectTable.on('projectRemoved', callback);
        projectTable.add(projects);

        assert.deepEqual(driver.getProjects(), projects, "Projects correctly rendered.");
        driver.clickRemoveProject(project1.id);

        assert.deepEqual(driver.getProjects(), projects, "Project not removed.");
        assert.ok(callback.calledWith(project1), "Callback triggered");
    });

    injector.test("Table error reported as loadError", function (assert, ProjectTable) {
        var projectTable = new ProjectTable({target: this.$table});
        var callback = sinon.stub();

        projectTable.on('loadError', callback);

        assert.ok(!callback.called, "Callback not called before error.");
        projectTable.performAjax(-1, 2);
        assert.ok(callback.called, "Callback not called on error.");
    });
});
