define([
    'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', 'common/js/spec_helpers/template_helpers',
    'common/js/spec_helpers/view_helpers', 'js/models/course', 'js/models/group_configuration', 'js/models/group',
    'js/collections/group_configuration', 'js/collections/group', 'js/views/group_configuration_details',
    'js/views/group_configurations_list', 'js/views/group_configuration_editor', 'js/views/group_configuration_item',
    'js/views/experiment_group_edit', 'js/views/partition_group_list', 'js/views/partition_group_details',
    'js/views/content_group_editor', 'js/views/partition_group_item'
], function(
    _, AjaxHelpers, TemplateHelpers, ViewHelpers, Course, GroupConfigurationModel, GroupModel,
    GroupConfigurationCollection, GroupCollection, GroupConfigurationDetailsView, GroupConfigurationsListView,
    GroupConfigurationEditorView, GroupConfigurationItemView, ExperimentGroupEditView, GroupList,
    PartitionGroupDetailsView, ContentGroupEditorView, PartitionGroupItemView
) {
    'use strict';
    var SELECTORS = {
        detailsView: '.group-configuration-details',
        editView: '.group-configuration-edit',
        itemView: '.group-configurations-list-item',
        group: '.group',
        groupFields: '.groups-fields',
        name: '.group-configuration-name',
        description: '.group-configuration-description',
        groupsCount: '.group-configuration-groups-count',
        groupsAllocation: '.group-allocation',
        errorMessage: '.group-configuration-edit-error',
        inputGroupName: '.group-name',
        inputName: '.collection-name-input',
        inputDescription: '.group-configuration-description-input',
        usageCount: '.group-configuration-usage-count',
        usage: '.group-configuration-usage',
        usageText: '.group-configuration-usage-text',
        usageTextAnchor: '.group-configuration-usage-text > a',
        usageUnit: '.group-configuration-usage-unit',
        usageUnitAnchor: '.group-configuration-usage-unit a',
        usageUnitMessage: '.group-configuration-validation-message',
        usageUnitWarningIcon: '.group-configuration-usage-unit .fa-warning',
        usageUnitErrorIcon: '.group-configuration-usage-unit .fa-exclamation-circle',
        warningMessage: '.group-configuration-validation-text',
        warningIcon: '.wrapper-group-configuration-validation > .fa-warning',
        note: '.wrapper-delete-button'
    };

    var assertTheDetailsView = function(view, text) {
        expect(view.$el).toContainText(text);
        expect(view.$el).toContainText('ID: 0');
        expect(view.$('.delete')).toExist();
    };
    var assertShowEmptyUsages = function(view, usageText) {
        expect(view.$(SELECTORS.usageCount)).not.toExist();
        expect(view.$(SELECTORS.usageText)).toContainText(usageText);
        expect(view.$(SELECTORS.usageTextAnchor)).toExist();
        expect(view.$(SELECTORS.usageUnit)).not.toExist();
    };
    var assertHideEmptyUsages = function(view) {
        expect(view.$(SELECTORS.usageText)).not.toExist();
        expect(view.$(SELECTORS.usageUnit)).not.toExist();
        expect(view.$(SELECTORS.usageCount)).toContainText('Not in Use');
    };
    var assertShowNonEmptyUsages = function(view, usageText, toolTipText) {
        var usageUnitAnchors = view.$(SELECTORS.usageUnitAnchor);

        expect(view.$(SELECTORS.note)).toHaveAttr(
            'data-tooltip', toolTipText
        );
        expect(view.$('.delete')).toHaveClass('is-disabled');
        expect(view.$(SELECTORS.usageCount)).not.toExist();
        expect(view.$(SELECTORS.usageText)).toContainText(usageText);
        expect(view.$(SELECTORS.usageUnit).length).toBe(2);
        expect(usageUnitAnchors.length).toBe(2);
        expect(usageUnitAnchors.eq(0)).toContainText('label1');
        expect(usageUnitAnchors.eq(0).attr('href')).toBe('url1');
        expect(usageUnitAnchors.eq(1)).toContainText('label2');
        expect(usageUnitAnchors.eq(1).attr('href')).toBe('url2');
    };
    var assertHideNonEmptyUsages = function(view) {
        expect(view.$('.delete')).toHaveClass('is-disabled');
        expect(view.$(SELECTORS.usageText)).not.toExist();
        expect(view.$(SELECTORS.usageUnit)).not.toExist();
        expect(view.$(SELECTORS.usageCount)).toContainText('Used in 2 locations');
    };
    var setUsageInfo = function(model) {
        model.set('usage', [
            {'label': 'label1', 'url': 'url1'},
            {'label': 'label2', 'url': 'url2'}
        ]);
    };
    var assertHideValidationContent = function(view) {
        expect(view.$(SELECTORS.usageUnitMessage)).not.toExist();
        expect(view.$(SELECTORS.usageUnitWarningIcon)).not.toExist();
        expect(view.$(SELECTORS.usageUnitErrorIcon)).not.toExist();
    };
    var assertControllerView = function(view, detailsView, editView) {
        // Details view by default
        expect(view.$(detailsView)).toExist();
        view.$('.action-edit .edit').click();
        expect(view.$(editView)).toExist();
        expect(view.$(detailsView)).not.toExist();
        view.$('.action-cancel').click();
        expect(view.$(detailsView)).toExist();
        expect(view.$(editView)).not.toExist();
    };
    var assertAndDeleteItemError = function(that, url, promptText) {
        var requests = AjaxHelpers.requests(that),
            promptSpy = ViewHelpers.createPromptSpy(),
            notificationSpy = ViewHelpers.createNotificationSpy();

        ViewHelpers.clickDeleteItem(that, promptSpy, promptText);

        ViewHelpers.patchAndVerifyRequest(requests, url, notificationSpy);

        AjaxHelpers.respondWithNoContent(requests);
        ViewHelpers.verifyNotificationHidden(notificationSpy);
        expect($(SELECTORS.itemView)).not.toExist();
    };
    var assertAndDeleteItemWithError = function(that, url, listItemView, promptText) {
        var requests = AjaxHelpers.requests(that),
            promptSpy = ViewHelpers.createPromptSpy(),
            notificationSpy = ViewHelpers.createNotificationSpy();

        ViewHelpers.clickDeleteItem(that, promptSpy, promptText);
        ViewHelpers.patchAndVerifyRequest(requests, url, notificationSpy);

        AjaxHelpers.respondWithError(requests);
        ViewHelpers.verifyNotificationShowing(notificationSpy, /Deleting/);
        expect($(listItemView)).toExist();
    };
    var assertCannotDeleteUsed = function(that, toolTipText, warningText) {
        setUsageInfo(that.model);
        that.view.render();
        expect(that.view.$(SELECTORS.note)).toHaveAttr(
            'data-tooltip', toolTipText
        );
        expect(that.view.$(SELECTORS.warningMessage)).toContainText(warningText);
        expect(that.view.$(SELECTORS.warningIcon)).toExist();
        expect(that.view.$('.delete')).toHaveClass('is-disabled');
    };
    var assertUnusedOptions = function(that) {
        that.model.set('usage', []);
        that.view.render();
        expect(that.view.$(SELECTORS.warningMessage)).not.toExist();
        expect(that.view.$(SELECTORS.warningIcon)).not.toExist();
    };


    beforeEach(function() {
        window.course = new Course({
            id: '5',
            name: 'Course Name',
            url_name: 'course_name',
            org: 'course_org',
            num: 'course_num',
            revision: 'course_rev'
        });

        jasmine.addMatchers({
            toContainText: function() {
                return {
                    compare: function(actual, text) {
                        var trimmedText = $.trim(actual.text()),
                            passed;

                        if (text && $.isFunction(text.test)) {
                            passed = text.test(trimmedText);
                        } else {
                            passed = trimmedText.indexOf(text) !== -1;
                        }

                        return {
                            pass: passed
                        };
                    }
                };
            },
            toBeCorrectValuesInInputs: function() {
                return {
                    compare: function(actual, values) {
                        var expected = {
                            name: actual.$(SELECTORS.inputName).val(),
                            description: actual
                                .$(SELECTORS.inputDescription).val()
                        };

                        var passed = _.isEqual(values, expected);

                        return {
                            pass: passed
                        };
                    }
                };
            },
            toBeCorrectValuesInModel: function() {
                return {
                    compare: function(actual, values) {
                        var passed = _.every(values, function(value, key) {
                            return actual.get(key) === value;
                        }.bind(this));

                        return {
                            pass: passed
                        };
                    }
                };
            },
            toHaveDefaultNames: function() {
                return {
                    compare: function(actual, values) {
                        var actualValues = $.map(actual, function(item) {
                            return $(item).val();
                        });

                        var passed = _.isEqual(actualValues, values);

                        return {
                            pass: passed
                        };
                    }
                };
            }
        });
    });

    afterEach(function() {
        delete window.course;
    });

    describe('Experiment group configurations details view', function() {
        beforeEach(function() {
            TemplateHelpers.installTemplate('group-configuration-details', true);

            this.model = new GroupConfigurationModel({
                name: 'Configuration',
                description: 'Configuration Description',
                id: 0
            });

            this.collection = new GroupConfigurationCollection([this.model]);
            this.collection.outlineUrl = '/outline';
            this.view = new GroupConfigurationDetailsView({
                model: this.model
            });
            appendSetFixtures(this.view.render().el);
        });

        it('should render properly', function() {
            assertTheDetailsView(this.view, 'Configuration');
        });

        it('should show groups appropriately', function() {
            this.model.get('groups').add([{}]);
            this.model.set('showGroups', false);
            this.view.$('.show-groups').click();

            expect(this.model.get('showGroups')).toBeTruthy();
            expect(this.view.$(SELECTORS.group).length).toBe(3);
            expect(this.view.$(SELECTORS.groupsCount)).not.toExist();
            expect(this.view.$(SELECTORS.description))
                .toContainText('Configuration Description');
            expect(this.view.$(SELECTORS.groupsAllocation))
                .toContainText('33%');
        });

        it('should hide groups appropriately', function() {
            this.model.get('groups').add([{}]);
            this.model.set('showGroups', true);
            this.view.$('.hide-groups').click();

            expect(this.model.get('showGroups')).toBeFalsy();
            expect(this.view.$(SELECTORS.group)).not.toExist();
            expect(this.view.$(SELECTORS.groupsCount))
                .toContainText('Contains 3 groups');
            expect(this.view.$(SELECTORS.description)).not.toExist();
            expect(this.view.$(SELECTORS.groupsAllocation)).not.toExist();
        });

        it('should show empty usage appropriately', function() {
            this.model.set('showGroups', false);
            this.view.$('.show-groups').click();
            assertShowEmptyUsages(
                this.view,
                'This Group Configuration is not in use. ' +
                'Start by adding a content experiment to any Unit via the'
            );
        });

        it('should hide empty usage appropriately', function() {
            this.model.set('showGroups', true);
            this.view.$('.hide-groups').click();
            assertHideEmptyUsages(this.view);
        });

        it('should show non-empty usage appropriately', function() {
            setUsageInfo(this.model);
            this.model.set('showGroups', false);
            this.view.$('.show-groups').click();

            assertShowNonEmptyUsages(
                this.view,
                'This Group Configuration is used in:',
                'Cannot delete when in use by an experiment'
            );
        });

        it('should hide non-empty usage appropriately', function() {
            setUsageInfo(this.model);
            this.model.set('showGroups', true);
            this.view.$('.hide-groups').click();

            expect(this.view.$(SELECTORS.note)).toHaveAttr(
                'data-tooltip', 'Cannot delete when in use by an experiment'
            );
            assertHideNonEmptyUsages(this.view);
        });

        it('should show validation warning icon and message appropriately', function() {
            this.model.set('usage', [
                {
                    'label': 'label1',
                    'url': 'url1',
                    'validation': {
                        'text': 'Warning message',
                        'type': 'warning'
                    }
                }
            ]);
            this.model.set('showGroups', false);
            this.view.$('.show-groups').click();

            expect(this.view.$(SELECTORS.usageUnitMessage)).toContainText('Warning message');
            expect(this.view.$(SELECTORS.usageUnitWarningIcon)).toExist();
        });

        it('should show validation error icon and message appropriately', function() {
            this.model.set('usage', [
                {
                    'label': 'label1',
                    'url': 'url1',
                    'validation': {
                        'text': 'Error message',
                        'type': 'error'
                    }
                }
            ]);
            this.model.set('showGroups', false);
            this.view.$('.show-groups').click();

            expect(this.view.$(SELECTORS.usageUnitMessage)).toContainText('Error message');
            expect(this.view.$(SELECTORS.usageUnitErrorIcon)).toExist();
        });

        it('should hide validation icons and messages appropriately', function() {
            setUsageInfo(this.model);
            this.model.set('showGroups', true);
            this.view.$('.hide-groups').click();

            assertHideValidationContent(this.view);
        });
    });

    describe('Experiment group configurations editor view', function() {
        var setValuesToInputs = function(view, values) {
            _.each(values, function(value, selector) {
                if (SELECTORS[selector]) {
                    view.$(SELECTORS[selector]).val(value);
                }
            });
        };

        beforeEach(function() {
            ViewHelpers.installViewTemplates();
            TemplateHelpers.installTemplates([
                'group-configuration-editor', 'group-edit'
            ]);

            this.model = new GroupConfigurationModel({
                name: 'Configuration',
                description: 'Configuration Description',
                id: 0,
                editing: true
            });
            this.collection = new GroupConfigurationCollection([this.model]);
            this.collection.url = '/group_configurations';
            this.view = new GroupConfigurationEditorView({
                model: this.model
            });
            appendSetFixtures(this.view.render().el);
        });

        it('should render properly', function() {
            expect(this.view).toBeCorrectValuesInInputs({
                name: 'Configuration',
                description: 'Configuration Description'
            });
            expect(this.view.$('.delete')).toExist();
        });

        it('should allow you to create new groups', function() {
            var numGroups = this.model.get('groups').length;
            this.view.$('.action-add-group').click();
            expect(this.model.get('groups').length).toEqual(numGroups + 1);
        });

        it('should save properly', function() {
            var requests = AjaxHelpers.requests(this),
                notificationSpy = ViewHelpers.createNotificationSpy(),
                groups;

            this.view.$('.action-add-group').click();
            setValuesToInputs(this.view, {
                inputName: 'New Configuration',
                inputDescription: 'New Description'
            });

            ViewHelpers.submitAndVerifyFormSuccess(this.view, requests, notificationSpy);

            expect(this.model).toBeCorrectValuesInModel({
                name: 'New Configuration',
                description: 'New Description'
            });

            groups = this.model.get('groups');
            expect(groups.length).toBe(3);
            expect(groups.at(2).get('name')).toBe('Group C');
            expect(this.view.$el).not.toBeInDOM();
        });

        it('does not hide saving message if failure', function() {
            var requests = AjaxHelpers.requests(this),
                notificationSpy = ViewHelpers.createNotificationSpy();

            setValuesToInputs(this.view, {inputName: 'New Configuration'});
            ViewHelpers.submitAndVerifyFormError(this.view, requests, notificationSpy);
        });

        it('does not save on cancel', function() {
            this.view.$('.action-add-group').click();
            setValuesToInputs(this.view, {
                inputName: 'New Configuration',
                inputDescription: 'New Description'
            });

            expect(this.model.get('groups').length).toBe(3);

            this.view.$('.action-cancel').click();
            expect(this.model).toBeCorrectValuesInModel({
                name: 'Configuration',
                description: 'Configuration Description'
            });
            // Model is still exist in the collection
            expect(this.collection.indexOf(this.model)).toBeGreaterThan(-1);
            expect(this.collection.length).toBe(1);
            expect(this.model.get('groups').length).toBe(2);
        });

        it('should be removed on cancel if it is a new item', function() {
            spyOn(this.model, 'isNew').and.returnValue(true);
            setValuesToInputs(this.view, {
                inputName: 'New Configuration',
                inputDescription: 'New Description'
            });
            this.view.$('.action-cancel').click();
            // Model is removed from the collection
            expect(this.collection.length).toBe(0);
        });

        it('should be possible to correct validation errors', function() {
            var requests = AjaxHelpers.requests(this);

            // Set incorrect value
            setValuesToInputs(this.view, {inputName: ''});
            // Try to save
            this.view.$('form').submit();
            // See error message
            expect(this.view.$(SELECTORS.errorMessage)).toContainText(
                'Group Configuration name is required'
            );
            // No request
            AjaxHelpers.expectNoRequests(requests);
            // Set correct value
            setValuesToInputs(this.view, {inputName: 'New Configuration'});
            // Try to save
            this.view.$('form').submit();
            AjaxHelpers.respondWithJson(requests, {});
            // Model is updated
            expect(this.model).toBeCorrectValuesInModel({
                name: 'New Configuration'
            });
            // Error message disappear
            expect(this.view.$(SELECTORS.errorMessage)).not.toBeInDOM();
            AjaxHelpers.expectNoRequests(requests);
        });

        describe('removes all newly created groups on cancel', function() {
            it('if the model has a non-empty groups', function() {
                var groups = this.model.get('groups');

                this.view.render();
                groups.add([{name: 'non-empty'}]);
                expect(groups.length).toEqual(3);
                this.view.$('.action-cancel').click();
                // Restore to default state (2 groups by default).
                expect(groups.length).toEqual(2);
            });

            it('if the model has no non-empty groups', function() {
                var groups = this.model.get('groups');

                this.view.render();
                groups.add([{}, {}, {}]);
                expect(groups.length).toEqual(5);
                this.view.$('.action-cancel').click();
                // Restore to default state (2 groups by default).
                expect(groups.length).toEqual(2);
            });
        });

        it('groups have correct default names', function() {
            var group1 = new GroupModel({name: 'Group A'}),
                group2 = new GroupModel({name: 'Group B'}),
                collection = this.model.get('groups');

            collection.reset([group1, group2]); // Group A, Group B
            this.view.$('.action-add-group').click(); // Add Group C
            this.view.$('.action-add-group').click(); // Add Group D
            this.view.$('.action-add-group').click(); // Add Group E

            expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([
                'Group A', 'Group B', 'Group C', 'Group D', 'Group E'
            ]);

            // Remove Group B
            this.view.$('.group-1 .action-close').click();

            expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([
                'Group A', 'Group C', 'Group D', 'Group E'
            ]);

            this.view.$('.action-add-group').click(); // Add Group F
            this.view.$('.action-add-group').click(); // Add Group G

            expect(this.view.$(SELECTORS.inputGroupName)).toHaveDefaultNames([
                'Group A', 'Group C', 'Group D', 'Group E', 'Group F', 'Group G'
            ]);
        });

        it('cannot be deleted if it is in use', function() {
            assertCannotDeleteUsed(
                this,
                'Cannot delete when in use by an experiment',
                'This configuration is currently used in content ' +
                'experiments. If you make changes to the groups, you may ' +
                'need to edit those experiments.'
            );
        });

        it('does not contain warning message if it is not in use', function() {
            assertUnusedOptions(this);
        });
    });

    describe('Experiment group configurations list view', function() {
        var emptyMessage = 'You have not created any group configurations yet.';

        beforeEach(function() {
            TemplateHelpers.installTemplates(
                ['group-configuration-editor', 'group-edit', 'list']
            );

            this.model = new GroupConfigurationModel({id: 0});
            this.collection = new GroupConfigurationCollection();
            this.view = new GroupConfigurationsListView({
                collection: this.collection
            });
            appendSetFixtures(this.view.render().el);
        });

        describe('empty template', function() {
            it('should be rendered if no group configurations', function() {
                expect(this.view.$el).toContainText(emptyMessage);
                expect(this.view.$('.new-button')).toExist();
                expect(this.view.$(SELECTORS.itemView)).not.toExist();
            });

            it('should disappear if group configuration is added', function() {
                expect(this.view.$el).toContainText(emptyMessage);
                expect(this.view.$(SELECTORS.itemView)).not.toExist();
                this.collection.add(this.model);
                expect(this.view.$el).not.toContainText(emptyMessage);
                expect(this.view.$(SELECTORS.itemView)).toExist();
            });

            it('should appear if configurations were removed', function() {
                this.collection.add(this.model);
                expect(this.view.$(SELECTORS.itemView)).toExist();
                this.collection.remove(this.model);
                expect(this.view.$el).toContainText(emptyMessage);
                expect(this.view.$(SELECTORS.itemView)).not.toExist();
            });

            it('can create a new group configuration', function() {
                this.view.$('.new-button').click();
                expect($('.group-configuration-edit').length).toBeGreaterThan(0);
            });
        });
    });

    describe('Experiment group configurations controller view', function() {
        beforeEach(function() {
            TemplateHelpers.installTemplates([
                'group-configuration-editor', 'group-configuration-details'
            ], true);
            this.model = new GroupConfigurationModel({id: 0});
            this.collection = new GroupConfigurationCollection([this.model]);
            this.collection.url = '/group_configurations';
            this.view = new GroupConfigurationItemView({
                model: this.model
            });
            appendSetFixtures(this.view.render().el);
        });

        it('should render properly', function() {
            assertControllerView(this.view, SELECTORS.detailsView, SELECTORS.editView);
        });

        it('should destroy itself on confirmation of deleting', function() {
            assertAndDeleteItemError(this, '/group_configurations/0', 'Delete this group configuration?');
        });

        it('does not hide deleting message if failure', function() {
            assertAndDeleteItemWithError(
                this,
                '/group_configurations/0',
                SELECTORS.itemView,
                'Delete this group configuration?'
            );
        });
    });

    describe('Experiment group configurations group editor view', function() {
        beforeEach(function() {
            TemplateHelpers.installTemplate('group-edit', true);

            this.model = new GroupModel({
                name: 'Group A'
            });

            this.collection = new GroupCollection([this.model]);

            this.view = new ExperimentGroupEditView({
                model: this.model
            });
        });

        describe('Basic', function() {
            it('can render properly', function() {
                this.view.render();
                expect(this.view.$('.group-name').val()).toBe('Group A');
                expect(this.view.$('.group-allocation')).toContainText('100%');
            });

            it('can delete itself', function() {
                this.view.render().$('.action-close').click();
                expect(this.collection.length).toEqual(0);
            });
        });
    });

    describe('Content groups list view', function() {
        var newGroupCss = '.new-button',
            addGroupCss = '.action-add',
            inputCss = '.collection-name-input',
            saveButtonCss = '.action-primary',
            cancelButtonCss = '.action-cancel',
            validationErrorCss = '.content-group-edit-error',
            scopedGroupSelector, createGroups, renderView, saveOrCancel, editNewGroup, editExistingGroup,
            verifyEditingGroup, respondToSave, expectGroupsVisible, correctValidationError;

        scopedGroupSelector = function(groupIndex, additionalSelectors) {
            var groupSelector = '.partition-groups-list-item-' + groupIndex;
            if (additionalSelectors) {
                return groupSelector + ' ' + additionalSelectors;
            } else {
                return groupSelector;
            }
        };

        createGroups = function(groupNamesWithId) {
            var groups = new GroupCollection(_.map(groupNamesWithId, function(groupName, id) {
                    return {id: id, name: groupName};
                })),
                groupConfiguration = new GroupConfigurationModel({
                    id: 0,
                    name: 'Content Group Configuration',
                    groups: groups
                }, {canBeEmpty: true});
            groupConfiguration.urlRoot = '/mock_url';
            groupConfiguration.outlineUrl = '/mock_url';
            return groups;
        };

        renderView = function(groupNamesWithId) {
            var view = new GroupList({collection: createGroups(groupNamesWithId || {})}).render();
            appendSetFixtures(view.el);
            return view;
        };

        saveOrCancel = function(view, options, groupIndex) {
            if (options.save) {
                view.$(scopedGroupSelector(groupIndex, saveButtonCss)).click();
            } else if (options.cancel) {
                view.$(scopedGroupSelector(groupIndex, cancelButtonCss)).click();
            }
        };

        editNewGroup = function(view, options) {
            var newGroupIndex;
            if (view.collection.length === 0) {
                view.$(newGroupCss).click();
            } else {
                view.$(addGroupCss).click();
            }
            newGroupIndex = view.collection.length - 1;
            view.$(inputCss).val(options.newName);
            verifyEditingGroup(view, true, newGroupIndex);
            saveOrCancel(view, options, newGroupIndex);
        };

        editExistingGroup = function(view, options) {
            var groupIndex = options.groupIndex || 0;
            view.$(scopedGroupSelector(groupIndex, '.edit')).click();
            view.$(scopedGroupSelector(groupIndex, inputCss)).val(options.newName);
            saveOrCancel(view, options, groupIndex);
        };

        verifyEditingGroup = function(view, expectEditing, index) {
            // Should prevent the user from opening more than one edit
            // form at a time by removing the add button(s) when
            // editing a group.
            index = index || 0;
            if (expectEditing) {
                expect(view.$(scopedGroupSelector(index, '.content-group-edit'))).toExist();
                expect(view.$(newGroupCss)).not.toExist();
                expect(view.$(addGroupCss)).toHaveClass('is-hidden');
            } else {
                expect(view.$('.content-group-edit')).not.toExist();
                if (view.collection.length === 0) {
                    expect(view.$(newGroupCss)).toExist();
                    expect(view.$(addGroupCss)).not.toExist();
                } else {
                    expect(view.$(newGroupCss)).not.toExist();
                    expect(view.$(addGroupCss)).not.toHaveClass('is-hidden');
                }
            }
        };

        respondToSave = function(requests, view) {
            var request = AjaxHelpers.currentRequest(requests);
            expect(request.method).toBe('POST');
            expect(request.url).toBe('/mock_url/0');
            AjaxHelpers.respondWithJson(requests, {
                name: 'Content Group Configuration',
                groups: view.collection.map(function(groupModel, index) {
                    return _.extend(groupModel.toJSON(), {id: index});
                })
            });
        };

        correctValidationError = function(view, requests, newGroupName) {
            expect(view.$(validationErrorCss)).toExist();
            verifyEditingGroup(view, true);
            view.$(inputCss).val(newGroupName);
            view.$(saveButtonCss).click();
            respondToSave(requests, view);
            expect(view.$(validationErrorCss)).not.toExist();
        };

        expectGroupsVisible = function(view, groupNames) {
            _.each(groupNames, function(groupName) {
                expect(view.$('.partition-groups-list-item')).toContainText(groupName);
            });
        };

        beforeEach(function() {
            TemplateHelpers.installTemplates(
                ['content-group-editor', 'partition-group-details', 'list']
            );
        });

        it('shows a message when no groups are present', function() {
            expect(renderView().$('.no-content'))
                .toContainText('You have not created any content groups yet.');
        });

        it('can render groups', function() {
            var groupNames = ['Group 1', 'Group 2', 'Group 3'];
            renderView(groupNames).$('.partition-group-details').each(function(index) {
                expect($(this)).toContainText(groupNames[index]);
            });
        });

        it('can create an initial group and save', function() {
            var requests = AjaxHelpers.requests(this),
                newGroupName = 'New Group Name',
                view = renderView();
            editNewGroup(view, {newName: newGroupName, save: true});
            respondToSave(requests, view);
            verifyEditingGroup(view, false);
            expectGroupsVisible(view, [newGroupName]);
        });

        it('can add another group and save', function() {
            var requests = AjaxHelpers.requests(this),
                oldGroupName = 'Old Group Name',
                newGroupName = 'New Group Name',
                view = renderView({1: oldGroupName});
            editNewGroup(view, {newName: newGroupName, save: true});
            respondToSave(requests, view);
            verifyEditingGroup(view, false, 1);
            expectGroupsVisible(view, [oldGroupName, newGroupName]);
        });

        it('can cancel adding a group', function() {
            var requests = AjaxHelpers.requests(this),
                newGroupName = 'New Group Name',
                view = renderView();
            editNewGroup(view, {newName: newGroupName, cancel: true});
            AjaxHelpers.expectNoRequests(requests);
            verifyEditingGroup(view, false);
            expect(view.$()).not.toContainText(newGroupName);
        });

        it('can cancel editing a group', function() {
            var requests = AjaxHelpers.requests(this),
                originalGroupName = 'Original Group Name',
                view = renderView([originalGroupName]);
            editExistingGroup(view, {newName: 'New Group Name', cancel: true});
            verifyEditingGroup(view, false);
            AjaxHelpers.expectNoRequests(requests);
            expect(view.collection.at(0).get('name')).toBe(originalGroupName);
        });

        it('can show and correct a validation error', function() {
            var requests = AjaxHelpers.requests(this),
                newGroupName = 'New Group Name',
                view = renderView();
            editNewGroup(view, {newName: '', save: true});
            AjaxHelpers.expectNoRequests(requests);
            correctValidationError(view, requests, newGroupName);
        });

        it('can not invalidate an existing content group', function() {
            var requests = AjaxHelpers.requests(this),
                oldGroupName = 'Old Group Name',
                view = renderView([oldGroupName]);
            editExistingGroup(view, {newName: '', save: true});
            AjaxHelpers.expectNoRequests(requests);
            correctValidationError(view, requests, oldGroupName);
        });

        it('trims whitespace', function() {
            var requests = AjaxHelpers.requests(this),
                newGroupName = 'New Group Name',
                view = renderView();
            editNewGroup(view, {newName: '  ' + newGroupName + '  ', save: true});
            respondToSave(requests, view);
            expect(view.collection.at(0).get('name')).toBe(newGroupName);
        });

        it('only edits one form at a time', function() {
            var view = renderView();
            view.collection.add({name: 'Editing Group', editing: true});
            verifyEditingGroup(view, true);
        });
    });

    describe('Content groups details view', function() {
        beforeEach(function() {
            TemplateHelpers.installTemplate('partition-group-details', true);
            this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'});

            var saveableModel = new GroupConfigurationModel({
                name: 'Content Group Configuration',
                id: 0,
                scheme: 'cohort',
                groups: new GroupCollection([this.model])
            }, {canBeEmpty: true});

            saveableModel.urlRoot = '/mock_url';

            this.collection = new GroupConfigurationCollection([saveableModel]);
            this.collection.outlineUrl = '/outline';

            this.view = new PartitionGroupDetailsView({
                model: this.model
            });
            appendSetFixtures(this.view.render().el);
        });

        it('should render properly', function() {
            assertTheDetailsView(this.view, 'Content Group');
        });

        it('should show empty usage appropriately', function() {
            this.view.$('.show-groups').click();
            assertShowEmptyUsages(this.view, 'use this group to control access to a component');
        });

        it('should hide empty usage appropriately', function() {
            this.view.$('.hide-groups').click();
            assertHideEmptyUsages(this.view);
        });

        it('should show non-empty usage appropriately', function() {
            setUsageInfo(this.model);
            this.view.$('.show-groups').click();

            assertShowNonEmptyUsages(
                this.view,
                'This group controls access to:',
                'Cannot delete when in use by a unit'
            );
        });

        it('should hide non-empty usage appropriately', function() {
            setUsageInfo(this.model);
            this.view.$('.hide-groups').click();

            expect(this.view.$('li.action-delete')).toHaveAttr(
                'data-tooltip', 'Cannot delete when in use by a unit'
            );
            assertHideNonEmptyUsages(this.view);
        });

        it('should hide validation icons and messages appropriately', function() {
            setUsageInfo(this.model);
            this.view.$('.hide-groups').click();
            assertHideValidationContent(this.view);
        });
    });

    describe('Content groups editor view', function() {
        beforeEach(function() {
            ViewHelpers.installViewTemplates();
            TemplateHelpers.installTemplates(['content-group-editor']);

            this.model = new GroupModel({name: 'Content Group', id: 0});

            this.saveableModel = new GroupConfigurationModel({
                name: 'Content Group Configuration',
                id: 0,
                scheme: 'cohort',
                groups: new GroupCollection([this.model]),
                editing: true
            });

            this.collection = new GroupConfigurationCollection([this.saveableModel]);
            this.collection.outlineUrl = '/outline';
            this.collection.url = '/group_configurations';

            this.view = new ContentGroupEditorView({
                model: this.model
            });
            appendSetFixtures(this.view.render().el);
        });

        it('should save properly', function() {
            var requests = AjaxHelpers.requests(this),
                notificationSpy = ViewHelpers.createNotificationSpy();

            this.view.$('.action-add').click();
            this.view.$(SELECTORS.inputName).val('New Content Group');

            ViewHelpers.submitAndVerifyFormSuccess(this.view, requests, notificationSpy);

            expect(this.model).toBeCorrectValuesInModel({
                name: 'New Content Group'
            });
            expect(this.view.$el).not.toBeInDOM();
        });

        it('does not hide saving message if failure', function() {
            var requests = AjaxHelpers.requests(this),
                notificationSpy = ViewHelpers.createNotificationSpy();
            this.view.$(SELECTORS.inputName).val('New Content Group');

            ViewHelpers.submitAndVerifyFormError(this.view, requests, notificationSpy);
        });

        it('does not save on cancel', function() {
            expect(this.view.$('.action-add'));
            this.view.$('.action-add').click();
            this.view.$(SELECTORS.inputName).val('New Content Group');

            this.view.$('.action-cancel').click();
            expect(this.model).toBeCorrectValuesInModel({
                name: 'Content Group'
            });
            // Model is still exist in the collection
            expect(this.collection.indexOf(this.saveableModel)).toBeGreaterThan(-1);
            expect(this.collection.length).toBe(1);
        });

        it('cannot be deleted if it is in use', function() {
            assertCannotDeleteUsed(
                this,
                'Cannot delete when in use by a unit',
                'This content group is used in one or more units.'
            );
        });

        it('does not contain warning message if it is not in use', function() {
            assertUnusedOptions(this);
        });
    });

    describe('Content group controller view', function() {
        beforeEach(function() {
            TemplateHelpers.installTemplates([
                'content-group-editor', 'partition-group-details'
            ], true);

            this.model = new GroupModel({name: 'Content Group', id: 0, courseOutlineUrl: 'CourseOutlineUrl'});

            this.saveableModel = new GroupConfigurationModel({
                name: 'Content Group Configuration',
                id: 0,
                scheme: 'cohort',
                groups: new GroupCollection([this.model])
            });
            this.saveableModel.urlRoot = '/group_configurations';
            this.collection = new GroupConfigurationCollection([this.saveableModel]);
            this.collection.url = '/group_configurations';
            this.view = new PartitionGroupItemView({
                model: this.model
            });
            appendSetFixtures(this.view.render().el);
        });

        it('should render properly', function() {
            assertControllerView(this.view, '.partition-group-details', '.content-group-edit');
        });

        it('should destroy itself on confirmation of deleting', function() {
            assertAndDeleteItemError(this, '/group_configurations/0/0', 'Delete this content group');
        });

        it('does not hide deleting message if failure', function() {
            assertAndDeleteItemWithError(
                this,
                '/group_configurations/0/0',
                '.partition-groups-list-item',
                'Delete this content group'
            );
        });
    });
});
