$(function() {

    var Grid = jsGrid.Grid,

        JSGRID = "JSGrid",
        JSGRID_DATA_KEY = JSGRID;

    Grid.prototype.updateOnResize = false;

    module("basic");

    test("default creation", function() {
        var gridOptions = {
                simpleOption: "test",
                complexOption: {
                    a: "subtest",
                    b: 1,
                    c: {}
                }
            },

            grid = new Grid("#jsGrid", gridOptions);

        equal(grid._container[0], $("#jsGrid")[0], "container saved");
        equal(grid.simpleOption, "test", "primitive option extended");
        equal(grid.complexOption, gridOptions.complexOption, "non-primitive option extended");
    });

    test("jquery adapter creation", function() {
        var gridOptions = {
                option: "test"
            },
            $element = $("#jsGrid"),
            result = $element.jsGrid(gridOptions),
            grid = $element.data(JSGRID_DATA_KEY);

        equal(result, $element, "jquery fn returned source jQueryElement");
        ok(grid instanceof Grid, "jsGrid saved to jquery data");
        equal(grid.option, "test", "options provided");
    });

    test("destroy", function() {
        var $element = $("#jsGrid"),
            grid;

        $element.jsGrid({});
        grid = $element.data(JSGRID_DATA_KEY);

        grid.destroy();

        strictEqual($element.html(), "", "content is removed");
        strictEqual($element.data(JSGRID_DATA_KEY), undefined, "jquery data is removed");
    });

    test("jquery adapter second call changes option value", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                option: "test"
            },
            grid;

        $element.jsGrid(gridOptions);
        grid = $element.data(JSGRID_DATA_KEY);

        gridOptions.option = "new test";
        $element.jsGrid(gridOptions);

        equal(grid, $element.data(JSGRID_DATA_KEY), "instance was not changed");
        equal(grid.option, "new test", "option changed");
    });

    test("jquery adapter invokes jsGrid method", function() {
        var methodResult = "",
            $element = $("#jsGrid"),
            gridOptions = {
                method: function(str) {
                    methodResult = "test_" + str;
                }
            };

        $element.jsGrid(gridOptions);
        $element.jsGrid("method", "invoke");

        equal(methodResult, "test_invoke", "method invoked");
    });

    test("onInit callback", function() {
        var $element = $("#jsGrid"),
            onInitArguments,
            gridOptions = {
                onInit: function(args) {
                    onInitArguments = args;
                }
            };

        var grid = new Grid($element, gridOptions);

        equal(onInitArguments.grid, grid, "grid instance is provided in onInit callback arguments");
    });

    test("controller methods are $.noop when not specified", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                controller: {}
            },
            testOption;

        $element.jsGrid(gridOptions);

        deepEqual($element.data(JSGRID_DATA_KEY)._controller, {
            loadData: $.noop,
            insertItem: $.noop,
            updateItem: $.noop,
            deleteItem: $.noop
        }, "controller has stub methods");
    });

    test("option method", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                test: "value"
            },
            testOption;

        $element.jsGrid(gridOptions);

        testOption = $element.jsGrid("option", "test");
        equal(testOption, "value", "read option value");

        $element.jsGrid("option", "test", "new_value");
        testOption = $element.jsGrid("option", "test");
        equal(testOption, "new_value", "set option value");
    });

    test("fieldOption method", function() {
        var dataLoadedCount = 0;
        var $element = $("#jsGrid"),
            gridOptions = {
                loadMessage: "",

                autoload: true,

                controller: {
                    loadData: function() {
                        dataLoadedCount++;
                        return [{ prop1: "value1", prop2: "value2", prop3: "value3" }];
                    }
                },

                fields: [
                    { name: "prop1", title: "_" }
                ]
            };

        $element.jsGrid(gridOptions);

        var fieldOptionValue = $element.jsGrid("fieldOption", "prop1", "name");
        equal(fieldOptionValue, "prop1", "read field option");

        $element.jsGrid("fieldOption", "prop1", "name", "prop2");
        equal($element.text(), "_value2", "set field option by field name");
        equal(dataLoadedCount, 1, "data not reloaded on field option change");

        $element.jsGrid("fieldOption", 0, "name", "prop3");
        equal($element.text(), "_value3", "set field option by field index");
    });

    test("option changing event handlers", function() {
        var $element = $("#jsGrid"),
            optionChangingEventArgs,
            optionChangedEventArgs,

            gridOptions = {
                test: "testValue",
                another: "anotherValue",
                onOptionChanging: function(e) {
                    optionChangingEventArgs = $.extend({}, e);

                    e.option = "another";
                    e.newValue = e.newValue + "_" + this.another;
                },
                onOptionChanged: function(e) {
                    optionChangedEventArgs = $.extend({}, e);
                }
            },
            anotherOption;

        $element.jsGrid(gridOptions);

        $element.jsGrid("option", "test", "newTestValue");
        equal(optionChangingEventArgs.option, "test", "option name is provided in args of optionChanging");
        equal(optionChangingEventArgs.oldValue, "testValue", "old option value is provided in args of optionChanging");
        equal(optionChangingEventArgs.newValue, "newTestValue", "new option value is provided in args of optionChanging");

        anotherOption = $element.jsGrid("option", "another");
        equal(anotherOption, "newTestValue_anotherValue", "option changing handler changed option and value");

        equal(optionChangedEventArgs.option, "another", "option name is provided in args of optionChanged");
        equal(optionChangedEventArgs.value, "newTestValue_anotherValue", "option value is provided in args of optionChanged");
    });

    test("common layout rendering", function() {
        var $element = $("#jsGrid"),
            grid = new Grid($element, {}),
            $headerGrid,
            $headerGridTable,
            $bodyGrid,
            $bodyGridTable;

        ok($element.hasClass(grid.containerClass), "container class attached");
        ok($element.children().eq(0).hasClass(grid.gridHeaderClass), "grid header");
        ok($element.children().eq(1).hasClass(grid.gridBodyClass), "grid body");
        ok($element.children().eq(2).hasClass(grid.pagerContainerClass), "pager container");

        $headerGrid = $element.children().eq(0);
        $headerGridTable = $headerGrid.children().first();
        ok($headerGridTable.hasClass(grid.tableClass), "header table");
        equal($headerGrid.find("." + grid.headerRowClass).length, 1, "header row");
        equal($headerGrid.find("." + grid.filterRowClass).length, 1, "filter row");
        equal($headerGrid.find("." + grid.insertRowClass).length, 1, "insert row");
        ok(grid._headerRow.hasClass(grid.headerRowClass), "header row class");
        ok(grid._filterRow.hasClass(grid.filterRowClass), "filter row class");
        ok(grid._insertRow.hasClass(grid.insertRowClass), "insert row class");

        $bodyGrid = $element.children().eq(1);
        $bodyGridTable = $bodyGrid.children().first();
        ok($bodyGridTable.hasClass(grid.tableClass), "body table");
        equal(grid._content.parent()[0], $bodyGridTable[0], "content is tbody in body table");
        equal($bodyGridTable.find("." + grid.noDataRowClass).length, 1, "no data row");
        equal($bodyGridTable.text(), grid.noDataContent, "no data text");
    });

    test("set default options with setDefaults", function() {
        jsGrid.setDefaults({
            defaultOption: "test"
        });

        var $element = $("#jsGrid").jsGrid({});

        equal($element.jsGrid("option", "defaultOption"), "test", "default option set");
    });


    module("loading");

    test("loading with controller", function() {
        var $element = $("#jsGrid"),
            data = [
                { test: "test1" },
                { test: "test2" }
            ],

            gridOptions = {
                controller: {
                    loadData: function() {
                        return data;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.loadData();

        equal(grid.option("data"), data, "loadData loads data");
    });

    test("loadData throws exception when controller method not found", function() {
        var $element = $("#jsGrid");
        var grid = new Grid($element);
        grid._controller = {};

        throws(function() {
            grid.loadData();
        }, /loadData/, "loadData threw an exception");
    });

    test("onError event should be fired on controller fail", function() {
        var errorArgs,
            errorFired = 0,
            $element = $("#jsGrid"),

            gridOptions = {
                controller: {
                    loadData: function() {
                        return $.Deferred().reject({ value: 1 }, "test").promise();
                    }
                },
                onError: function(args) {
                    errorFired++;
                    errorArgs = args;
                }
            },

            grid = new Grid($element, gridOptions);

        grid.loadData();

        equal(errorFired, 1, "onError handler fired");
        deepEqual(errorArgs, { grid: grid, args: [{ value: 1 }, "test"] }, "error has correct params");
    });

    asyncTest("autoload should call loadData after render", 1, function() {
        new Grid($("#jsGrid"), {
            autoload: true,
            controller: {
                loadData: function() {
                    ok(true, "autoload calls loadData on creation");
                    start();
                    return [];
                }
            }
        });
    });

    test("loading filtered data", function() {
        var filteredData,
            loadingArgs,
            loadedArgs,
            $element = $("#jsGrid"),
            data = [
                { field: "test" },
                { field: "test_another" },
                { field: "test_another" },
                { field: "test" }
            ],

            gridOptions = {
                filtering: true,
                fields: [
                    {
                        name: "field",
                        filterValue: function(value) {
                            return "test";
                        }
                    }
                ],
                onDataLoading: function(e) {
                    loadingArgs = $.extend(true, {}, e);
                },
                onDataLoaded: function(e) {
                    loadedArgs = $.extend(true, {}, e);
                },
                controller: {
                    loadData: function(filter) {
                        filteredData = $.grep(data, function(item) {
                            return item.field === filter.field;
                        });
                        return filteredData;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.loadData();

        equal(loadingArgs.filter.field, "test");
        equal(grid.option("data").length, 2, "filtered data loaded");
        deepEqual(loadedArgs.data, filteredData);
    });

    asyncTest("loading indication", function() {
        var timeout = 10,
            stage = "initial",
            $element = $("#jsGrid"),

            gridOptions = {
                loadIndication: true,
                loadIndicationDelay: timeout,
                loadMessage: "loading...",

                loadIndicator: function(config) {
                    equal(config.message, gridOptions.loadMessage, "message provided");
                    ok(config.container.jquery, "grid container is provided");

                    return {
                        show: function() {
                            stage = "started";
                        },
                        hide: function() {
                            stage = "finished";
                        }
                    };
                },

                fields: [
                    { name: "field" }
                ],

                controller: {
                    loadData: function() {
                        var deferred = $.Deferred();

                        equal(stage, "initial", "initial stage");

                        setTimeout(function() {
                            equal(stage, "started", "loading started");

                            deferred.resolve([]);
                            equal(stage, "finished", "loading finished");

                            start();
                        }, timeout);

                        return deferred.promise();
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.loadData();
    });

    asyncTest("loadingIndication=false should not show loading", 0, function() {
        var $element = $("#jsGrid"),
            timeout = 10,

            gridOptions = {
                loadIndication: false,
                loadIndicationDelay: timeout,

                loadIndicator: function() {
                    return {
                        show: function() {
                            ok(false, "should not call show");
                        },
                        hide: function() {
                            ok(false, "should not call hide");
                        }
                    };
                },

                fields: [
                    { name: "field" }
                ],

                controller: {
                    loadData: function() {
                        var deferred = $.Deferred();

                        setTimeout(function() {
                            deferred.resolve([]);
                            start();
                        }, timeout);

                        return deferred.promise();
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.loadData();
    });

    test("search", function() {
        var $element = $("#jsGrid"),
            data = [
                { field: "test" },
                { field: "test_another" },
                { field: "test_another" },
                { field: "test" }
            ],

            gridOptions = {
                pageIndex: 2,
                _sortField: "field",
                _sortOrder: "desc",
                filtering: true,
                fields: [
                    {
                        name: "field",
                        filterValue: function(value) {
                            return "test";
                        }
                    }
                ],
                controller: {
                    loadData: function(filter) {
                        var filteredData = $.grep(data, function(item) {
                            return item.field === filter.field;
                        });
                        return filteredData;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.search();
        equal(grid.option("data").length, 2, "data filtered");
        strictEqual(grid.option("pageIndex"), 1, "pageIndex reset");
        strictEqual(grid._sortField, null, "sortField reset");
        strictEqual(grid._sortOrder, "asc", "sortOrder reset");
    });

    test("change loadStrategy on the fly", function() {
        var $element = $("#jsGrid");

        var gridOptions = {
            controller: {
                loadData: function() {
                    return [];
                }
            }
        };

        var grid = new Grid($element, gridOptions);

        grid.option("loadStrategy", {
            firstDisplayIndex: function() {
                return 0;
            },

            lastDisplayIndex: function() {
                return 1;
            },

            loadParams: function() {
                return [];
            },

            finishLoad: function() {
                grid.option("data", [{}]);
            }
        });

        grid.loadData();

        equal(grid.option("data").length, 1, "new load strategy is applied");
    });


    module("filtering");

    test("filter rendering", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                filtering: true,
                fields: [
                    {
                        name: "test",
                        align: "right",
                        filtercss: "filter-class",
                        filterTemplate: function() {
                            var result = this.filterControl = $("<input>").attr("type", "text").addClass("filter-input");
                            return result;
                        }
                    }
                ]
            },
            grid = new Grid($element, gridOptions);

        equal(grid._filterRow.find(".filter-class").length, 1, "filtercss class is attached");
        equal(grid._filterRow.find(".filter-input").length, 1, "filter control rendered");
        equal(grid._filterRow.find("." + grid.cellClass).length, 1, "cell class is attached");
        ok(grid._filterRow.find(".filter-class").hasClass("jsgrid-align-right"), "align class is attached");
        ok(grid.fields[0].filterControl.is("input[type=text]"), "filter control saved in field");
    });

    test("filter get/clear", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                filtering: true,
                controller: {
                    loadData: function() {
                        return [];
                    }
                },
                fields: [
                    {
                        name: "field",
                        filterTemplate: function() {
                            return this.filterControl = $("<input>").attr("type", "text");
                        },
                        filterValue: function() {
                            return this.filterControl.val();
                        }
                    }
                ]
            },
            grid = new Grid($element, gridOptions);

        grid.fields[0].filterControl.val("test");
        deepEqual(grid.getFilter(), { field: "test" }, "get filter");

        grid.clearFilter();
        deepEqual(grid.getFilter(), { field: "" }, "filter cleared");
        equal(grid.fields[0].filterControl.val(), "", "grid field filterControl cleared");
    });

    test("field without filtering", function() {
        var $element = $("#jsGrid"),
            jsGridFieldConfig = {
                filterTemplate: function() {
                    var result = this.filterControl = $("<input>").attr("type", "text");
                    return result;
                },
                filterValue: function(value) {
                    if(!arguments.length) {
                        return this.filterControl.val();
                    }
                    this.filterControl.val(value);
                }
            },

            gridOptions = {
                filtering: true,
                fields: [
                    $.extend({}, jsGridFieldConfig, {
                        name: "field1",
                        filtering: false
                    }),
                    $.extend({}, jsGridFieldConfig, {
                        name: "field2"
                    })
                ]
            },

            grid = new Grid($element, gridOptions);

        grid.fields[0].filterControl.val("test1");
        grid.fields[1].filterControl.val("test2");
        deepEqual(grid.getFilter(), { field2: "test2" }, "field with filtering=false is not included in filter");
    });

    test("search with filter", function() {
        var $element = $("#jsGrid"),
            data = [
                { field: "test" },
                { field: "test_another" },
                { field: "test_another" },
                { field: "test" }
            ],

            gridOptions = {
                fields: [
                    {
                        name: "field"
                    }
                ],
                controller: {
                    loadData: function(filter) {
                        var filteredData = $.grep(data, function(item) {
                            return item.field === filter.field;
                        });
                        return filteredData;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.search({ field: "test" });
        equal(grid.option("data").length, 2, "data filtered");
    });

    test("filtering with static data should not do actual filtering", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                filtering: true,
                fields: [
                    { type: "text", name: "field" }
                ],
                data: [
                    { name: "value1" },
                    { name: "value2" }
                ]
            },
            grid = new Grid($element, gridOptions);

        grid._filterRow.find("input").val("1");

        grid.search();
        equal(grid.option("data").length, 2, "data is not filtered");
    });


    module("nodatarow");

    test("nodatarow after bind on empty array", function() {
        var $element = $("#jsGrid"),
            gridOptions = {},
            grid = new Grid($element, gridOptions);

        grid.option("data", []);

        equal(grid._content.find("." + grid.noDataRowClass).length, 1, "no data row rendered");
        equal(grid._content.find("." + grid.cellClass).length, 1, "grid cell class attached");
        equal(grid._content.text(), grid.noDataContent, "no data text rendered");
    });

    test("nodatarow customize content", function() {
        var noDataMessage = "NoData Custom Content",
            $element = $("#jsGrid"),
            gridOptions = {
                noDataContent: function() {
                    return noDataMessage;
                }
            },
            grid = new Grid($element, gridOptions);

        grid.option("data", []);

        equal(grid._content.find("." + grid.cellClass).length, 1, "grid cell class attached");
        equal(grid._content.text(), noDataMessage, "custom noDataContent");
    });


    module("row rendering", {
        setup: function() {
            this.testData = [
                { id: 1, text: "test1" },
                { id: 2, text: "test2" },
                { id: 3, text: "test3" }
            ];
        }
    });

    test("rows rendered correctly", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                data: this.testData
            },
            grid = new Grid($element, gridOptions);

        equal(grid._content.children().length, 3, "rows rendered");
        equal(grid._content.find("." + grid.oddRowClass).length, 2, "two odd rows for 3 items");
        equal(grid._content.find("." + grid.evenRowClass).length, 1, "one even row for 3 items");
    });

    test("custom rowClass", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                data: this.testData,
                rowClass: "custom-row-cls"
            },
            grid = new Grid($element, gridOptions);

        equal(grid._content.find("." + grid.oddRowClass).length, 2);
        equal(grid._content.find("." + grid.evenRowClass).length, 1);
        equal(grid._content.find(".custom-row-cls").length, 3, "custom row class");
    });

    test("custom rowClass callback", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                data: this.testData,
                rowClass: function(item, index) {
                    return item.text;
                }
            },
            grid = new Grid($element, gridOptions);

        equal(grid._content.find("." + grid.oddRowClass).length, 2);
        equal(grid._content.find("." + grid.evenRowClass).length, 1);
        equal(grid._content.find(".test1").length, 1, "custom row class");
        equal(grid._content.find(".test2").length, 1, "custom row class");
        equal(grid._content.find(".test3").length, 1, "custom row class");
    });

    test("rowClick standard handler", function() {
        var $element = $("#jsGrid"),
            $secondRow,
            grid = new Grid($element, { editing: true });

        grid.option("data", this.testData);

        $secondRow = grid._content.find("." + grid.evenRowClass);
        $secondRow.trigger("click", $.Event($secondRow));

        equal(grid._editingRow.get(0), $secondRow.get(0), "clicked row is editingRow");
    });

    test("rowClick handler", function() {
        var rowClickArgs,
            $element = $("#jsGrid"),
            $secondRow,
            gridOptions = {
                rowClick: function(args) {
                    rowClickArgs = args;
                }
            },
            grid = new Grid($element, gridOptions);

        grid.option("data", this.testData);

        $secondRow = grid._content.find("." + grid.evenRowClass);
        $secondRow.trigger("click", $.Event($secondRow));

        ok(rowClickArgs.event instanceof jQuery.Event, "jquery event arg");
        equal(rowClickArgs.item, this.testData[1], "item arg");
        equal(rowClickArgs.itemIndex, 1, "itemIndex arg");
    });

    test("row selecting with selectedRowClass", function() {
        var $element = $("#jsGrid"),
            $secondRow,
            gridOptions = {
                selecting: true
            },
            grid = new Grid($element, gridOptions);

        grid.option("data", this.testData);

        $secondRow = grid._content.find("." + grid.evenRowClass);

        $secondRow.trigger("mouseenter", $.Event($secondRow));
        ok($secondRow.hasClass(grid.selectedRowClass), "mouseenter adds selectedRowClass");

        $secondRow.trigger("mouseleave", $.Event($secondRow));
        ok(!$secondRow.hasClass(grid.selectedRowClass), "mouseleave removes selectedRowClass");
    });

    test("no row selecting while selection is disabled", function() {
        var $element = $("#jsGrid"),
            $secondRow,
            gridOptions = {
                selecting: false
            },
            grid = new Grid($element, gridOptions);

        grid.option("data", this.testData);

        $secondRow = grid._content.find("." + grid.evenRowClass);
        $secondRow.trigger("mouseenter", $.Event($secondRow));
        ok(!$secondRow.hasClass(grid.selectedRowClass), "mouseenter doesn't add selectedRowClass");
    });

    test("refreshing and refreshed callbacks", function() {
        var refreshingEventArgs,
            refreshedEventArgs,
            $element = $("#jsGrid"),
            grid = new Grid($element, {});

        grid.onRefreshing = function(e) {
            refreshingEventArgs = e;
            equal(grid._content.find("." + grid.oddRowClass).length, 0, "no items before refresh");
        };

        grid.onRefreshed = function(e) {
            refreshedEventArgs = e;
            equal(grid._content.find("." + grid.oddRowClass).length, 2, "items rendered after refresh");
        };

        grid.option("data", this.testData);

        equal(refreshingEventArgs.grid, grid, "grid provided in args for refreshing event");
        equal(refreshedEventArgs.grid, grid, "grid provided in args for refreshed event");
        equal(grid._content.find("." + grid.oddRowClass).length, 2, "items rendered");
    });

    test("grid fields normalization", function() {
        var CustomField = function(config) {
            $.extend(true, this, config);
        };

        jsGrid.fields.custom = CustomField;

        try {
            var $element = $("#jsGrid"),
                gridOptions = {
                    fields: [
                        new jsGrid.Field({
                            name: "text1",
                            title: "title1"
                        }),
                        {
                            name: "text2",
                            title: "title2"
                        },
                        {
                            name: "text3",
                            type: "custom"
                        }
                    ]
                },
                grid = new Grid($element, gridOptions);

            var field1 = grid.fields[0];
            ok(field1 instanceof jsGrid.Field);
            equal(field1.name, "text1", "name is set for field");
            equal(field1.title, "title1", "title field");

            var field2 = grid.fields[1];
            ok(field2 instanceof jsGrid.Field);
            equal(field2.name, "text2", "name is set for field");
            equal(field2.title, "title2", "title field");

            var field3 = grid.fields[2];
            ok(field3 instanceof CustomField);
            equal(field3.name, "text3", "name is set for field");
        } finally {
            delete jsGrid.fields.custom;
        }
    });

    test("'0' itemTemplate should be rendered", function() {
        var $element = $("#jsGrid"),
            grid = new Grid($element, {
                data: [{}],
                fields: [
                    new jsGrid.Field({ name: "id", itemTemplate: function() { return 0; } })
                ]
            });

        equal(grid._bodyGrid.text(), "0", "item template is rendered");
    });

    test("grid field name used for header if title is not specified", function() {
        var $element = $("#jsGrid"),
            grid = new Grid($element, {
                fields: [
                    new jsGrid.Field({ name: "id" })
                ]
            });

        equal(grid._headerRow.text(), "id", "name is rendered in header");
    });

    test("grid fields header and item rendering", function() {
        var $element = $("#jsGrid"),
            $secondRow,
            gridOptions = {
                fields: [
                    new jsGrid.Field({
                        name: "text",
                        title: "title",
                        css: "cell-class",
                        headercss: "header-class",
                        align: "right"
                    })
                ]
            },
            grid = new Grid($element, gridOptions);

        grid.option("data", this.testData);

        equal(grid._headerRow.text(), "title", "header rendered");
        equal(grid._headerRow.find("." + grid.headerCellClass).length, 1, "header cell class is attached");
        equal(grid._headerRow.find(".header-class").length, 1, "headercss class is attached");
        ok(grid._headerRow.find(".header-class").hasClass("jsgrid-align-right"), "align class is attached");

        $secondRow = grid._content.find("." + grid.evenRowClass);
        equal($secondRow.text(), "test2", "item rendered");
        equal($secondRow.find(".cell-class").length, 1, "css class added to cell");
        equal($secondRow.find("." + grid.cellClass).length, 1, "cell class is attached");
        ok($secondRow.find(".cell-class").hasClass("jsgrid-align-right"), "align class added to cell");
    });

    test("grid field cellRenderer", function() {
        var testItem = { text: "test" },
            args;

        var $grid = $("#jsGrid");

        var gridOptions = {
            data: [testItem],
            fields: [
                {
                    name: "text",
                    cellRenderer: function(value, item) {
                        args = {
                            value: value,
                            item: item
                        };
                        return $("<td>").addClass("custom-class").text(value);
                    }
                }
            ]
        };

        var grid = new Grid($grid, gridOptions);
        var $customCell = $grid.find(".custom-class");

        equal($customCell.length, 1, "custom cell rendered");
        equal($customCell.text(), "test");
        deepEqual(args, { value: "test", item: testItem }, "cellRenderer args provided");
    });

    test("grid field 'visible' option", function() {
        var $grid = $("#jsGrid");

        var gridOptions = {
            editing: true,
            fields: [
                { name: "id", visible: false },
                { name: "test" }
            ]
        };

        var grid = new Grid($grid, gridOptions);

        equal($grid.find("." + grid.noDataRowClass).children().eq(0).prop("colspan"), 1, "no data row colspan only for visible cells");

        grid.option("data", this.testData);
        grid.editItem(this.testData[2]);

        equal($grid.find("." + grid.headerRowClass).children().length, 1, "header single cell");
        equal($grid.find("." + grid.filterRowClass).children().length, 1, "filter single cell");
        equal($grid.find("." + grid.insertRowClass).children().length, 1, "insert single cell");
        equal($grid.find("." + grid.editRowClass).children().length, 1, "edit single cell");
        equal($grid.find("." + grid.oddRowClass).eq(0).children().length, 1, "odd data row single cell");
        equal($grid.find("." + grid.evenRowClass).eq(0).children().length, 1, "even data row single cell");
    });


    module("inserting");

    test("inserting rendering", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                inserting: true,
                fields: [
                    {
                        name: "test",
                        align: "right",
                        insertcss: "insert-class",
                        insertTemplate: function() {
                            var result = this.insertControl = $("<input>").attr("type", "text").addClass("insert-input");
                            return result;
                        }
                    }
                ]
            },
            grid = new Grid($element, gridOptions);

        equal(grid._insertRow.find(".insert-class").length, 1, "insertcss class is attached");
        equal(grid._insertRow.find(".insert-input").length, 1, "insert control rendered");
        equal(grid._insertRow.find("." + grid.cellClass).length, 1, "cell class is attached");
        ok(grid._insertRow.find(".insert-class").hasClass("jsgrid-align-right"), "align class is attached");
        ok(grid.fields[0].insertControl.is("input[type=text]"), "insert control saved in field");
    });

    test("field without inserting", function() {
        var $element = $("#jsGrid"),
            jsGridFieldConfig = {
                insertTemplate: function() {
                    var result = this.insertControl = $("<input>").attr("type", "text");
                    return result;
                },
                insertValue: function() {
                    return this.insertControl.val();
                }
            },

            gridOptions = {
                inserting: true,
                fields: [
                    $.extend({}, jsGridFieldConfig, {
                        name: "field1",
                        inserting: false
                    }),
                    $.extend({}, jsGridFieldConfig, {
                        name: "field2"
                    })
                ]
            },

            grid = new Grid($element, gridOptions);

        grid.fields[0].insertControl.val("test1");
        grid.fields[1].insertControl.val("test2");
        deepEqual(grid._getInsertItem(), { field2: "test2" }, "field with inserting=false is not included in inserting item");
    });

    test("insert data", function() {
        var $element = $("#jsGrid"),

            inserted = false,
            insertingArgs,
            insertedArgs,

            gridOptions = {
                inserting: true,
                data: [],
                fields: [
                    {
                        name: "field",
                        insertTemplate: function() {
                            var result = this.insertControl = $("<input>").attr("type", "text");
                            return result;
                        },
                        insertValue: function() {
                            return this.insertControl.val();
                        }
                    }
                ],
                onItemInserting: function(e) {
                    insertingArgs = $.extend(true, {}, e);
                },
                onItemInserted: function(e) {
                    insertedArgs = $.extend(true, {}, e);
                },
                controller: {
                    insertItem: function() {
                        inserted = true;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.fields[0].insertControl.val("test");
        grid.insertItem();

        equal(insertingArgs.item.field, "test", "field is provided in inserting args");
        equal(grid.option("data").length, 1, "data is inserted");
        ok(inserted, "controller insertItem was called");
        deepEqual(grid.option("data")[0], { field: "test" }, "correct data is inserted");
        equal(insertedArgs.item.field, "test", "field is provided in inserted args");
    });

    test("insertItem accepts item to insert", function() {
        var $element = $("#jsGrid"),
            itemToInsert = { field: "test" },
            insertedItem,

            gridOptions = {
                data: [],
                fields: [
                    {
                        name: "field"
                    }
                ],
                controller: {
                    insertItem: function(item) {
                        insertedItem = item;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.insertItem(itemToInsert);
        deepEqual(grid.option("data")[0], itemToInsert, "data is inserted");
        deepEqual(insertedItem, itemToInsert, "controller insertItem was called with correct item");
    });


    module("editing");

    test("editing rendering", function() {
        var $element = $("#jsGrid"),
            $editRow,
            data = [{
                test: "value"
            }],

            gridOptions = {
                editing: true,
                fields: [
                    {
                        name: "test",
                        align: "right",
                        editcss: "edit-class",
                        editTemplate: function(value) {
                            var result = this.editControl = $("<input>").attr("type", "text").val(value).addClass("edit-input");
                            return result;
                        }
                    }
                ]
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        equal(grid._content.find("." + grid.editRowClass).length, 0, "no edit row after initial rendering");

        grid.editItem(data[0]);

        $editRow = grid._content.find("." + grid.editRowClass);
        equal($editRow.length, 1, "edit row rendered");
        equal($editRow.find(".edit-class").length, 1, "editcss class is attached");
        equal($editRow.find(".edit-input").length, 1, "edit control rendered");
        equal($editRow.find("." + grid.cellClass).length, 1, "cell class is attached");
        ok($editRow.find(".edit-class").hasClass("jsgrid-align-right"), "align class is attached");

        ok(grid.fields[0].editControl.is("input[type=text]"), "edit control saved in field");
        equal(grid.fields[0].editControl.val(), "value", "edit control value");
    });

    test("editItem accepts row to edit", function() {
        var $element = $("#jsGrid"),
            $editRow,
            data = [
                { test: "value" }
            ],

            gridOptions = {
                editing: true,
                fields: [
                    { name: "test" }
                ]
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        var $row = $element.find("." + grid.oddRowClass).eq(0);

        grid.editItem($row);
        $editRow = grid._content.find("." + grid.editRowClass);
        equal($editRow.length, 1, "edit row rendered");

        grid.cancelEdit();

        grid.editItem($row.get(0));
        $editRow = grid._content.find("." + grid.editRowClass);
        equal($editRow.length, 1, "edit row rendered");
    });

    test("edit item", function() {
        var $element = $("#jsGrid"),
            editingArgs,
            editingRow,
            updated = false,
            updatingArgs,
            updatingRow,
            updatedRow,
            updatedArgs,
            data = [{
                field: "value"
            }],

            gridOptions = {
                editing: true,
                fields: [
                    {
                        name: "field",
                        editTemplate: function(value) {
                            var result = this.editControl = $("<input>").attr("type", "text").val(value);
                            return result;
                        },
                        editValue: function() {
                            return this.editControl.val();
                        }
                    }
                ],
                controller: {
                    updateItem: function(updatingItem) {
                        updated = true;
                    }
                },
                onItemEditing: function(e) {
                    editingArgs = $.extend(true, {}, e);
                    editingRow = grid.rowByItem(data[0])[0];
                },
                onItemUpdating: function(e) {
                    updatingArgs = $.extend(true, {}, e);
                    updatingRow = grid.rowByItem(data[0])[0];
                },
                onItemUpdated: function(e) {
                    updatedArgs = $.extend(true, {}, e);
                    updatedRow = grid.rowByItem(data[0])[0];
                }
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        grid.editItem(data[0]);

        deepEqual(editingArgs.item, { field: "value" }, "item before editing is provided in editing event args");
        equal(editingArgs.itemIndex, 0, "itemIndex is provided in editing event args");
        equal(editingArgs.row[0], editingRow, "row element is provided in editing event args");

        grid.fields[0].editControl.val("new value");
        grid.updateItem();

        deepEqual(updatingArgs.previousItem, { field: "value" }, "item before editing is provided in updating event args");
        deepEqual(updatingArgs.item, { field: "new value" }, "updating item is provided in updating event args");
        equal(updatingArgs.itemIndex, 0, "itemIndex is provided in updating event args");
        equal(updatingArgs.row[0], updatingRow, "row element is provided in updating event args");
        ok(updated, "controller updateItem called");
        deepEqual(grid.option("data")[0], { field: "new value" }, "correct data updated");
        equal(grid._content.find("." + grid.editRowClass).length, 0, "edit row removed");
        equal(grid._content.find("." + grid.oddRowClass).length, 1, "data row rendered");
        deepEqual(updatedArgs.previousItem, { field: "value" }, "item before editing is provided in updated event args");
        deepEqual(updatedArgs.item, { field: "new value" }, "updated item is provided in updated event args");
        equal(updatedArgs.itemIndex, 0, "itemIndex is provided in updated event args");
        equal(updatedArgs.row[0], updatedRow, "row element is provided in updated event args");
    });

    test("failed update should not change original item", function() {
        var $element = $("#jsGrid"),
            data = [{
                field: "value"
            }],

            gridOptions = {
                editing: true,
                fields: [
                    {
                        name: "field",
                        editTemplate: function(value) {
                            var result = this.editControl = $("<input>").attr("type", "text").val(value);
                            return result;
                        },
                        editValue: function() {
                            return this.editControl.val();
                        }
                    }
                ],
                controller: {
                    updateItem: function(updatingItem) {
                        return $.Deferred().reject();
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        grid.editItem(data[0]);

        grid.fields[0].editControl.val("new value");
        grid.updateItem();

        deepEqual(grid.option("data")[0], { field: "value" }, "value is not updated");
    });

    test("cancel edit", function() {
        var $element = $("#jsGrid"),
            updated = false,
            cancellingArgs,
            cancellingRow,
            data = [{
                field: "value"
            }],

            gridOptions = {
                editing: true,
                fields: [
                    {
                        name: "field",
                        editTemplate: function(value) {
                            var result = this.editControl = $("<input>").attr("type", "text").val(value);
                            return result;
                        },
                        editValue: function() {
                            return this.editControl.val();
                        }
                    }
                ],
                controller: {
                    updateData: function(updatingItem) {
                        updated = true;
                    }
                },
                onItemEditCancelling: function(e) {
                    cancellingArgs = $.extend(true, {}, e);
                    cancellingRow = grid.rowByItem(data[0])[0];
                }
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        grid.editItem(data[0]);
        grid.fields[0].editControl.val("new value");
        grid.cancelEdit();

        deepEqual(cancellingArgs.item, { field: "value" }, "item before cancel is provided in cancelling event args");
        equal(cancellingArgs.itemIndex, 0, "itemIndex is provided in cancelling event args");
        equal(cancellingArgs.row[0], cancellingRow, "row element is provided in cancelling event args");

        ok(!updated, "controller updateItem was not called");
        deepEqual(grid.option("data")[0], { field: "value" }, "data were not updated");
        equal(grid._content.find("." + grid.editRowClass).length, 0, "edit row removed");
        equal(grid._content.find("." + grid.oddRowClass).length, 1, "data row restored");
    });

    test("updateItem accepts item to update and new item", function() {
        var $element = $("#jsGrid"),
            updatingItem,
            data = [{
                field: "value"
            }],

            gridOptions = {
                fields: [
                    { name: "field" }
                ],
                controller: {
                    updateItem: function(item) {
                        return updatingItem = item;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        grid.updateItem(data[0], { field: "new value" });

        deepEqual(updatingItem, { field: "new value" }, "controller updateItem called correctly");
        deepEqual(grid.option("data")[0], { field: "new value" }, "correct data updated");
    });

    test("updateItem accepts single argument - item to update", function() {
        var $element = $("#jsGrid"),
            updatingItem,
            data = [{
                field: "value"
            }],

            gridOptions = {
                fields: [
                    { name: "field" }
                ],
                controller: {
                    updateItem: function(item) {
                        return updatingItem = item;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        data[0].field = "new value";

        grid.updateItem(data[0]);

        deepEqual(updatingItem, { field: "new value" }, "controller updateItem called correctly");
        deepEqual(grid.option("data")[0], { field: "new value" }, "correct data updated");
    });

    test("editRowRenderer", function() {
        var $element = $("#jsGrid"),

            data = [
                { value: "test" }
            ],

            gridOptions = {
                data: data,
                editing: true,

                editRowRenderer: function(item, itemIndex) {
                    return $("<tr>").addClass("custom-edit-row").append($("<td>").text(itemIndex + ":" + item.value));
                },

                fields: [
                    { name: "value" }
                ]
            },

            grid = new Grid($element, gridOptions);

        grid.editItem(data[0]);

        var $editRow = grid._content.find(".custom-edit-row");
        equal($editRow.length, 1, "edit row rendered");
        equal($editRow.text(), "0:test", "custom edit row renderer rendered");
    });


    module("deleting");

    test("delete item", function() {
        var $element = $("#jsGrid"),
            deleted = false,
            deletingArgs,
            deletedArgs,
            data = [{
                field: "value"
            }],

            gridOptions = {
                confirmDeleting: false,
                fields: [
                    { name: "field" }
                ],
                controller: {
                    deleteItem: function(deletingItem) {
                        deleted = true;
                    }
                },
                onItemDeleting: function(e) {
                    deletingArgs = $.extend(true, {}, e);
                },
                onItemDeleted: function(e) {
                    deletedArgs = $.extend(true, {}, e);
                }
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        grid.deleteItem(data[0]);

        deepEqual(deletingArgs.item, { field: "value" }, "field and value is provided in deleting event args");
        equal(deletingArgs.itemIndex, 0, "itemIndex is provided in updating event args");
        equal(deletingArgs.row.length, 1, "row element is provided in updating event args");
        ok(deleted, "controller deleteItem called");
        equal(grid.option("data").length, 0, "data row deleted");
        deepEqual(deletedArgs.item, { field: "value" }, "item is provided in updating event args");
        equal(deletedArgs.itemIndex, 0, "itemIndex is provided in updating event args");
        equal(deletedArgs.row.length, 1, "row element is provided in updating event args");
    });

    test("deleteItem accepts row", function() {
        var $element = $("#jsGrid"),
            deletedItem,
            itemToDelete = {
                field: "value"
            },
            data = [itemToDelete],

            gridOptions = {
                confirmDeleting: false,
                fields: [
                    { name: "field" }
                ],
                controller: {
                    deleteItem: function(deletingItem) {
                        deletedItem = deletingItem;
                    }
                }
            },

            grid = new Grid($element, gridOptions);

        grid.option("data", data);

        var $row = $element.find("." + grid.oddRowClass).eq(0);

        grid.deleteItem($row);

        deepEqual(deletedItem, itemToDelete, "controller deleteItem called correctly");
        equal(grid.option("data").length, 0, "data row deleted");
    });


    module("paging");

    test("pager is rendered if necessary", function() {
        var $element = $("#jsGrid"),

            grid = new Grid($element, {
                data: [{}, {}, {}],
                paging: false,
                pageSize: 2
            });

        ok(grid._pagerContainer.is(":hidden"), "pager is hidden when paging=false");
        equal(grid._pagerContainer.html(), "", "pager is not rendered when paging=false");

        grid.option("paging", true);

        ok(grid._pagerContainer.is(":visible"), "pager is visible when paging=true");
        ok(grid._pagerContainer.html(), "pager is rendered when paging=true");

        grid.option("data", [{}, {}]);
        ok(grid._pagerContainer.is(":hidden"), "pager is hidden for single page");
        ok(grid._pagerContainer.html(), "pager is rendered for single page when paging=true");
    });

    test("external pagerContainer", function() {
        var $pagerContainer = $("<div>").appendTo("#qunit-fixture").hide(),
            $element = $("#jsGrid");

        new Grid($element, {
            data: [{}, {}, {}],
            pagerContainer: $pagerContainer,
            paging: true,
            pageSize: 2
        });

        ok($pagerContainer.is(":visible"), "external pager shown");
        ok($pagerContainer.html(), "external pager rendered");
    });

    test("pager functionality", function() {
        var $element = $("#jsGrid"),
            pager,
            pageChangedArgs,
            grid = new Grid($element, {
                data: [{}, {}, {}, {}, {}, {}, {}, {}, {}],
                onPageChanged: function(args) {
                    pageChangedArgs = args;
                },
                paging: true,
                pageSize: 2,
                pageButtonCount: 3
            });

        equal(grid._pagesCount(), 5, "correct page count");
        equal(grid.option("pageIndex"), 1, "pageIndex is initialized");
        equal(grid._firstDisplayingPage, 1, "_firstDisplayingPage is initialized");

        pager = grid._pagerContainer;
        equal(pager.find("." + grid.currentPageClass).length, 1, "there is one current page");
        ok(pager.find("." + grid.pageClass).eq(0).hasClass(grid.currentPageClass), "first page is current");
        equal(pager.find("." + grid.pageClass).length, 3, "three pages displayed");
        equal(pager.find("." + grid.pagerNavButtonClass).length, 5, "five nav buttons displayed: Fisrt Prev Next Last ...");
        equal(pager.find("." + grid.pagerNavButtonInactiveClass).length, 2, "two nav buttons inactive: Fisrt Prev");

        grid.openPage(2);
        equal(pager.find("." + grid.currentPageClass).length, 1, "there is one current page");
        ok(pager.find("." + grid.pageClass).eq(1).hasClass(grid.currentPageClass), "second page is current");
        equal(pager.find("." + grid.pageClass).length, 3, "three pages displayed");
        equal(pager.find("." + grid.pagerNavButtonClass).length, 5, "five nav buttons displayed: First Prev Next Last and ...");
        equal(pageChangedArgs.pageIndex, 2, "onPageChanged callback provides pageIndex in arguments");

        grid.showNextPages();
        equal(grid._firstDisplayingPage, 3, "navigate by pages forward");

        grid.showPrevPages();
        equal(grid._firstDisplayingPage, 1, "navigate by pages backward");

        grid.openPage(5);
        equal(grid._firstDisplayingPage, 3, "opening next non-visible page moves first displaying page forward");

        grid.openPage(2);
        equal(grid._firstDisplayingPage, 2, "opening prev non-visible page moves first displaying page backward");
    });

    test("pager format", function() {
        var $element = $("#jsGrid"),
            grid = new Grid($element, {
                data: [{}, {}, {}, {}, {}, {}],
                paging: true,
                pageSize: 2,
                pageIndex: 2,
                pageButtonCount: 1,
                pagerFormat: "a {pageIndex} {first} {prev} {pages} {next} {last} {pageCount} {itemCount} z",
                pagePrevText: "<",
                pageNextText: ">",
                pageFirstText: "<<",
                pageLastText: ">>",
                pageNavigatorNextText: "np",
                pageNavigatorPrevText: "pp"
            });

        grid._firstDisplayingPage = 2;
        grid._refreshPager();

        equal($.trim(grid._pagerContainer.text()), "a 2 << < pp2np > >> 3 6 z", "pager text follows the format specified");
    });

    test("pagerRenderer", function() {
        var $element = $("#jsGrid");
        var pagerRendererConfig;
        var pageSize = 2;
        var items = [{}, {}, {}, {}, {}, {}, {}];
        var pageCount = Math.ceil(items.length / pageSize);

        var grid = new Grid($element, {
            data: items,
            paging: true,
            pageSize: pageSize,
            pagerRenderer: function(pagerConfig) {
                pagerRendererConfig = pagerConfig;
            }
        });

        deepEqual(pagerRendererConfig, { pageIndex: 1, pageCount: pageCount });

        grid.openPage(2);
        deepEqual(pagerRendererConfig, { pageIndex: 2, pageCount: pageCount });
    });

    test("loading by page", function() {
        var $element = $("#jsGrid"),
            data = [],
            itemCount = 20;

        for(var i = 1; i <= itemCount; i += 1) {
            data.push({
                value: i
            });
        }

        var gridOptions = {
            pageLoading: true,
            paging: true,
            pageSize: 7,
            fields: [
                { name: "value" }
            ],
            controller: {
                loadData: function(filter) {
                    var startIndex = (filter.pageIndex - 1) * filter.pageSize,
                        result = data.slice(startIndex, startIndex + filter.pageSize);
                    return {
                        data: result,
                        itemsCount: data.length
                    };
                }
            }
        };

        var grid = new Grid($element, gridOptions);

        grid.loadData();

        var pager = grid._pagerContainer;
        var gridData = grid.option("data");

        equal(gridData.length, 7, "loaded one page of data");
        equal(gridData[0].value, 1, "loaded right data start value");
        equal(gridData[gridData.length - 1].value, 7, "loaded correct data end value");
        ok(pager.find("." + grid.pageClass).eq(0).hasClass(grid.currentPageClass), "first page is current");
        equal(pager.find("." + grid.pageClass).length, 3, "three pages displayed");

        grid.openPage(3);
        gridData = grid.option("data");

        equal(gridData.length, 6, "loaded last page of data");
        equal(gridData[0].value, 15, "loaded right data start value");
        equal(gridData[gridData.length - 1].value, 20, "loaded right data end value");
        ok(pager.find("." + grid.pageClass).eq(2).hasClass(grid.currentPageClass), "third page is current");
        equal(pager.find("." + grid.pageClass).length, 3, "three pages displayed");
    });

    test("'openPage' method ignores indexes out of range", function() {
        var $element = $("#jsGrid"),
            grid = new Grid($element, {
                data: [{}, {}],
                paging: true,
                pageSize: 1
            });

        grid.openPage(0);
        equal(grid.option("pageIndex"), 1, "too small index is ignored");

        grid.openPage(3);
        equal(grid.option("pageIndex"), 1, "too big index is ignored");
    });


    module("sorting");

    test("sorting", function() {
        var $element = $("#jsGrid"),

            gridOptions = {
                sorting: true,
                data: [
                    { value: 3 },
                    { value: 2 },
                    { value: 1 }
                ],
                fields: [
                    { name: "value", sorter: "number" }
                ]
            },
            grid = new Grid($element, gridOptions);

        var gridData = grid.option("data");

        var $th = grid._headerRow.find("th").eq(0);
        $th.trigger("click");

        equal(grid._sortOrder, "asc", "asc sorting order for first click");
        equal(grid._sortField, grid.fields[0], "sort field is set");
        equal(gridData[0].value, 1);
        equal(gridData[1].value, 2);
        equal(gridData[2].value, 3);
        ok($th.hasClass(grid.sortableClass));
        ok($th.hasClass(grid.sortAscClass));

        $th.trigger("click");

        equal(grid._sortOrder, "desc", "desc sorting order for second click");
        equal(grid._sortField, grid.fields[0], "sort field is set");
        equal(gridData[0].value, 3);
        equal(gridData[1].value, 2);
        equal(gridData[2].value, 1);
        ok(!$th.hasClass(grid.sortAscClass));
        ok($th.hasClass(grid.sortDescClass));
    });

    test("sorting with pageLoading", function() {
        var $element = $("#jsGrid"),
            loadFilter,

            gridOptions = {
                sorting: true,
                pageLoading: true,
                data: [
                    { value: 3 },
                    { value: 2 },
                    { value: 1 }
                ],
                controller: {
                    loadData: function(filter) {
                        loadFilter = filter;
                        return {
                            itemsCount: 0,
                            data: []
                        };
                    }
                },
                fields: [
                    { name: "value", sorter: "number" }
                ]
            },

            grid = new Grid($element, gridOptions);

        var $th = grid._headerRow.find("th").eq(0);
        $th.trigger("click");

        equal(grid._sortOrder, "asc", "asc sorting order for first click");
        equal(grid._sortField, grid.fields[0], "sort field is set");
        equal(loadFilter.sortOrder, "asc", "sort direction is provided in loadFilter");
        equal(loadFilter.sortField, "value", "sort field is provided in loadFilter");

        $th.trigger("click");

        equal(grid._sortOrder, "desc", "desc sorting order for second click");
        equal(grid._sortField, grid.fields[0], "sort field is set");
        equal(loadFilter.sortOrder, "desc", "sort direction is provided in loadFilter");
        equal(loadFilter.sortField, "value", "sort field is provided in loadFilter");
    });

    test("no sorting for column with sorting = false", function() {
        var $element = $("#jsGrid");

        var gridOptions = {
            sorting: true,
            data: [
                { value: 3 },
                { value: 2 },
                { value: 1 }
            ],
            fields: [
                { name: "value", sorting: false }
            ]
        };

        var grid = new Grid($element, gridOptions);

        var gridData = grid.option("data");

        var $th = grid._headerRow.find("th").eq(0);
        $th.trigger("click");

        equal(grid._sortField, null, "sort field is not set for the field with sorting=false");
        equal(gridData[0].value, 3);
        equal(gridData[1].value, 2);
        equal(gridData[2].value, 1);
        equal($th.hasClass(grid.sortableClass), false, "no sorting css for field with sorting=false");
        equal($th.hasClass(grid.sortAscClass), false, "no sorting css for field with sorting=false");
    });

    test("sort accepts sorting config", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                sorting: true,
                data: [
                    { value: 3 },
                    { value: 2 },
                    { value: 1 }
                ],
                fields: [
                    { name: "value", sorter: "number" }
                ]
            },
            grid = new Grid($element, gridOptions);

        var gridData = grid.option("data");

        grid.sort({ field: "value", order: "asc" });
        equal(grid._sortOrder, "asc", "asc sorting order is set");
        equal(grid._sortField, grid.fields[0], "sort field is set");
        equal(gridData[0].value, 1);
        equal(gridData[1].value, 2);
        equal(gridData[2].value, 3);

        grid.sort({ field: 0 });
        equal(grid._sortOrder, "desc", "desc sorting order for already set asc sorting");
        equal(grid._sortField, grid.fields[0], "sort field is set");
        equal(gridData[0].value, 3);
        equal(gridData[1].value, 2);
        equal(gridData[2].value, 1);

        grid.sort("value", "asc");
        equal(grid._sortOrder, "asc", "asc sorting order is set");
        equal(grid._sortField, grid.fields[0], "sort field is set");

        grid.sort(0);
        equal(grid._sortOrder, "desc", "desc sorting order for already set asc sorting");
        equal(grid._sortField, grid.fields[0], "sort field is set");
    });

    test("getSorting returns current sorting", function() {
        var $element = $("#jsGrid"),
            gridOptions = {
                sorting: true,
                data: [
                    { value: 3 },
                    { value: 2 },
                    { value: 1 }
                ],
                fields: [
                    { name: "value", sorter: "number" }
                ]
            },
            grid = new Grid($element, gridOptions);

        deepEqual(grid.getSorting(), { field: undefined, order: undefined }, "undefined field and order before sorting");

        grid.sort("value");
        deepEqual(grid.getSorting(), { field: "value", order: "asc" }, "current sorting returned");
    });

    test("sorting css attached correctly when a field is hidden", function() {
        var $element = $("#jsGrid");

        var gridOptions = {
            sorting: true,
            data: [],
            fields: [
                { name: "field1", visible: false },
                { name: "field2" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        var gridData = grid.option("data");

        var $th = grid._headerRow.find("th").eq(0);
        $th.trigger("click");

        equal($th.hasClass(grid.sortAscClass), true, "sorting css is attached to first field");
    });

    module("canceling events");

    test("cancel item edit", function() {
        var $element = $("#jsGrid");
        var data = [{}];

        var gridOptions = {
            editing: true,

            onItemEditing: function(e) {
                e.cancel = true;
            },

            controller: {
                loadData: function() {
                    return data;
                }
            },

            fields: [
                { name: "test" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.loadData();
        grid.editItem(data[0]);
        strictEqual(grid._editingRow, null, "no editing row");
    });

    test("cancel controller.loadData", function() {
        var $element = $("#jsGrid");

        var gridOptions = {

            onDataLoading: function(e) {
                e.cancel = true;
            },

            controller: {
                loadData: function() {
                    return [{}];
                }
            },

            fields: [
                { name: "test" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.loadData();

        equal(grid.option("data").length, 0, "no data loaded");
    });

    test("cancel controller.insertItem", function() {
        var $element = $("#jsGrid");
        var insertedItem = null;

        var gridOptions = {

            onItemInserting: function(e) {
                e.cancel = true;
            },

            controller: {
                insertItem: function(item) {
                    insertedItem = item;
                }
            },

            fields: [
                { name: "test" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.insertItem({ test: "value" });

        strictEqual(insertedItem, null, "item was not inserted");
    });

    test("cancel controller.updateItem", function() {
        var $element = $("#jsGrid");
        var updatedItem = null;
        var existingItem = { test: "value" };

        var gridOptions = {

            data: [
                existingItem
            ],

            onItemUpdating: function(e) {
                e.cancel = true;
            },

            controller: {
                updateItem: function(item) {
                    updatedItem = item;
                }
            },

            fields: [
                { name: "test" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.updateItem(existingItem, { test: "new_value" });

        strictEqual(updatedItem, null, "item was not updated");
    });

    test("cancel controller.deleteItem", function() {
        var $element = $("#jsGrid");
        var deletingItem = { test: "value" };
        var deletedItem = null;

        var gridOptions = {

            data: [
                deletingItem
            ],

            confirmDeleting: false,

            onItemDeleting: function(e) {
                e.cancel = true;
            },

            controller: {
                deleteItem: function(item) {
                    deletedItem = item;
                }
            },

            fields: [
                { name: "test" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.deleteItem(deletingItem);

        strictEqual(deletedItem, null, "item was not deleted");
    });


    module("complex properties binding");

    test("rendering", function() {
        var $element = $("#jsGrid");

        var gridOptions = {

            loadMessage: "",

            data: [
                { complexProp: { prop: "test" } }
            ],

            fields: [
                { name: "complexProp.prop", title: "" }
            ]
        };

        new Grid($element, gridOptions);

        equal($element.text(), "test", "complex property value rendered");
    });

    test("editing", function() {
        var $element = $("#jsGrid");

        var gridOptions = {

            editing: true,

            data: [
                { complexProp: { prop: "test" } }
            ],

            fields: [
                { type: "text", name: "complexProp.prop" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.editItem(gridOptions.data[0]);
        equal(grid.fields[0].editControl.val(), "test", "complex property value set in editor");
    });

    test("should not fail if property is absent", function() {
        var $element = $("#jsGrid");

        var gridOptions = {

            loadMessage: "",

            data: [
                { complexProp: { } }
            ],

            fields: [
                { name: "complexProp.subprop.prop", title: "" }
            ]
        };

        new Grid($element, gridOptions);

        equal($element.text(), "", "rendered empty value");
    });

    test("inserting", function() {
        var $element = $("#jsGrid");
        var insertingItem;

        var gridOptions = {
            inserting: true,

            fields: [
                { type: "text", name: "complexProp.prop" }
            ],

            onItemInserting: function(args) {
                insertingItem = args.item;
            }
        };

        var grid = new Grid($element, gridOptions);

        grid.fields[0].insertControl.val("test");
        grid.insertItem();

        deepEqual(insertingItem, { complexProp: { prop: "test" } }, "inserting item has complex properties");
    });

    test("filtering", function() {
        var $element = $("#jsGrid");
        var loadFilter;

        var gridOptions = {
            filtering: true,

            fields: [
                { type: "text", name: "complexProp.prop" }
            ],

            controller: {
                loadData: function(filter) {
                    loadFilter = filter;
                }
            }
        };

        var grid = new Grid($element, gridOptions);

        grid.fields[0].filterControl.val("test");
        grid.search();

        deepEqual(loadFilter, { complexProp: { prop: "test" } }, "filter has complex properties");
    });

    test("updating", function() {
        var $element = $("#jsGrid");
        var updatingItem;

        var gridOptions = {
            editing: true,

            data: [
                { complexProp: { } }
            ],

            fields: [
                { type: "text", name: "complexProp.prop" }
            ],

            onItemUpdating: function(args) {
                updatingItem = args.item;
            }
        };

        var grid = new Grid($element, gridOptions);

        grid.editItem(gridOptions.data[0]);
        grid.fields[0].editControl.val("test");
        grid.updateItem();

        deepEqual(updatingItem, { complexProp: { prop: "test" } }, "updating item has complex properties");
    });

    test("update nested prop", function() {
        var $element = $("#jsGrid");
        var updatingItem;
        var previousItem;

        var gridOptions = {
            editing: true,

            data: [
                { prop: { subprop1: "test1", subprop2: "test2" } }
            ],

            fields: [
                { type: "text", name: "prop.subprop1" },
                { type: "text", name: "prop.subprop2" }
            ],

            onItemUpdating: function(args) {
                updatingItem = args.item;
            }
        };

        var grid = new Grid($element, gridOptions);

        grid.editItem(gridOptions.data[0]);
        grid.fields[0].editControl.val("new_test1");
        grid.updateItem();

        var expectedUpdatingItem = {
            prop: {
                subprop1: "new_test1",
                subprop2: "test2"
            }
        };

        deepEqual(updatingItem, expectedUpdatingItem, "updating item has nested properties");
    });

    test("updating deeply nested prop", function() {
        var $element = $("#jsGrid");
        var updatingItem;
        var previousItem;

        var gridOptions = {
            editing: true,

            data: [
                { complexProp: { subprop1: { another_prop: "test" } } }
            ],

            fields: [
                { type: "text", name: "complexProp.subprop1.prop1" },
                { type: "text", name: "complexProp.subprop1.subprop2.prop12" }
            ],

            onItemUpdating: function(args) {
                updatingItem = $.extend(true, {}, args.item);
                previousItem = $.extend(true, {}, args.previousItem);
            }
        };

        var grid = new Grid($element, gridOptions);

        grid.editItem(gridOptions.data[0]);
        grid.fields[0].editControl.val("test1");
        grid.fields[1].editControl.val("test2");
        grid.updateItem();

        var expectedUpdatingItem = {
            complexProp: {
                subprop1: {
                    another_prop: "test",
                    prop1: "test1",
                    subprop2: { prop12: "test2" }
                }
            }
        };

        var expectedPreviousItem = {
            complexProp: {
                subprop1: {
                    another_prop: "test"
                }
            }
        };

        deepEqual(updatingItem, expectedUpdatingItem, "updating item has deeply nested properties");
        deepEqual(previousItem, expectedPreviousItem, "previous item preserved correctly");
    });


    module("validation");

    test("insertItem should call validation.validate", function() {
        var $element = $("#jsGrid");
        var fieldValidationRules = { test: "value" };
        var validatingArgs;

        var gridOptions = {
            data: [],
            inserting: true,
            invalidNotify: $.noop,
            validation: {
                validate: function(args) {
                    validatingArgs = args;
                    return [];
                }
            },
            fields: [
                { type: "text", name: "Name", validate: fieldValidationRules }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.fields[0].insertControl.val("test");
        grid.insertItem();

        deepEqual(validatingArgs, { value: "test", item: { Name: "test" }, itemIndex: -1,
            row: grid._insertRow, rules: fieldValidationRules }, "validating args is provided");
    });

    test("insertItem rejected when data is not valid", function() {
        var $element = $("#jsGrid");

        var gridOptions = {
            data: [],
            inserting: true,
            invalidNotify: $.noop,
            validation: {
                validate: function() {
                    return ["Error"];
                }
            },
            fields: [
                { type: "text", name: "Name", validate: true }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.fields[0].insertControl.val("test");
        grid.insertItem().done(function() {
            ok(false, "insertItem should not be completed");
        }).fail(function() {
            ok(true, "insertItem should fail");
        });
    });

    test("invalidClass is attached on invalid cell on inserting", function() {
        var $element = $("#jsGrid");

        var gridOptions = {
            data: [],
            inserting: true,
            invalidNotify: $.noop,
            validation: {
                validate: function() {
                    return ["Error"];
                }
            },
            fields: [
                { type: "text", name: "Id", visible: false },
                { type: "text", name: "Name", validate: true }
            ]
        };

        var grid = new Grid($element, gridOptions);
        var $insertCell = grid._insertRow.children().eq(0);

        grid.insertItem();

        ok($insertCell.hasClass(grid.invalidClass), "invalid class is attached");
        equal($insertCell.attr("title"), "Error", "cell tooltip contains error message");
    });

    test("onItemInvalid callback", function() {
        var $element = $("#jsGrid");
        var errors = ["Error"];
        var onItemInvalidCalled = 0;
        var onItemInvalidArgs;

        var gridOptions = {
            data: [],
            inserting: true,
            invalidNotify: $.noop,
            onItemInvalid: function(args) {
                onItemInvalidCalled++;
                onItemInvalidArgs = args;
            },

            validation: {
                validate: function(args) {
                    return !args.value ? errors : [];
                }
            },
            fields: [
                { type: "text", name: "Name", validate: true }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.insertItem();

        equal(onItemInvalidCalled, 1, "onItemInvalid is called, when item data is invalid");
        deepEqual(onItemInvalidArgs, { grid: grid, errors: [{ field: grid.fields[0], message: "Error" }],
            item: { Name: "" }, itemIndex: -1, row: grid._insertRow }, "arguments provided");

        grid.fields[0].insertControl.val("test");
        grid.insertItem();

        equal(onItemInvalidCalled, 1, "onItemInvalid was not called, when data is valid");
    });

    test("invalidNotify", function() {
        var $element = $("#jsGrid");
        var errors = ["Error"];
        var invalidNotifyCalled = 0;
        var invalidNotifyArgs;

        var gridOptions = {
            data: [],
            inserting: true,

            invalidNotify: function(args) {
                invalidNotifyCalled++;
                invalidNotifyArgs = args;
            },

            validation: {
                validate: function(args) {
                    return !args.value ? errors : [];
                }
            },
            fields: [
                { type: "text", name: "Name", validate: true }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.insertItem();

        equal(invalidNotifyCalled, 1, "invalidNotify is called, when item data is invalid");
        deepEqual(invalidNotifyArgs, { grid: grid, errors: [{ field: grid.fields[0], message: "Error" }],
            row: grid._insertRow, item: { Name: "" }, itemIndex: -1 }, "arguments provided");

        grid.fields[0].insertControl.val("test");
        grid.insertItem();

        equal(invalidNotifyCalled, 1, "invalidNotify was not called, when data is valid");
    });

    test("invalidMessage", function() {
        var $element = $("#jsGrid");
        var invalidMessage;
        var originalAlert = window.alert;

        window.alert = function(message) {
            invalidMessage = message;
        };

        try {
            Grid.prototype.invalidMessage = "InvalidTest";
            Grid.prototype.invalidNotify({ errors: [{ message: "Message1" }, { message: "Message2" }] });

            var expectedInvalidMessage = ["InvalidTest", "Message1", "Message2"].join("\n");
            equal(invalidMessage, expectedInvalidMessage, "message contains invalidMessage and field error messages");
        } finally {
            window.alert = originalAlert;
        }
    });

    test("updateItem should call validation.validate", function() {
        var $element = $("#jsGrid");
        var validatingArgs;

        var gridOptions = {
            data: [{ Name: "" }],
            editing: true,

            invalidNotify: $.noop,
            validation: {
                validate: function(args) {
                    validatingArgs = args;
                    return ["Error"];
                }
            },

            fields: [
                { type: "text", name: "Name", validate: "required" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.editItem(gridOptions.data[0]);

        grid.fields[0].editControl.val("test");
        grid.updateItem();

        deepEqual(validatingArgs, { value: "test", item: { Name: "test" }, itemIndex: 0,
            row: grid._getEditRow(), rules: "required" }, "validating args is provided");
    });

    test("invalidClass is attached on invalid cell on updating", function() {
        var $element = $("#jsGrid");

        var gridOptions = {
            data: [{}],
            editing: true,
            invalidNotify: $.noop,
            validation: {
                validate: function() {
                    return ["Error"];
                }
            },
            fields: [
                { type: "text", name: "Name", validate: true }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.editItem(gridOptions.data[0]);
        var $editCell = grid._getEditRow().children().eq(0);

        grid.updateItem();

        ok($editCell.hasClass(grid.invalidClass), "invalid class is attached");
        equal($editCell.attr("title"), "Error", "cell tooltip contains error message");
    });

    test("validation should ignore not editable fields", function() {
        var invalidNotifyCalled = 0;
        var $element = $("#jsGrid");
        var validatingArgs;

        var gridOptions = {
            data: [{ Name: "" }],
            editing: true,

            invalidNotify: function() {
                invalidNotifyCalled++;
            },
            validation: {
                validate: function() {
                    return ["Error"];
                }
            },

            fields: [
                { type: "text", name: "Name", editing: false, validate: "required" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.editItem(gridOptions.data[0]);
        grid.updateItem();
        equal(invalidNotifyCalled, 0, "data is valid");
    });

    module("api");

    test("reset method should go the first page when pageLoading is truned on", function() {
        var items = [{ Name: "1" }, { Name: "2" }];
        var $element = $("#jsGrid");

        var gridOptions = {
            paging: true,
            pageSize: 1,
            pageLoading: true,
            autoload: true,
            controller: {
                loadData: function(args) {
                    return {
                        data: [items[args.pageIndex - 1]],
                        itemsCount: items.length
                    };
                }
            },

            fields: [
                { type: "text", name: "Name" }
            ]
        };

        var grid = new Grid($element, gridOptions);

        grid.openPage(2);
        grid.reset();
        equal(grid._bodyGrid.text(), "1", "grid content reset");
    });


    module("i18n");

    test("set locale by name", function() {
        jsGrid.locales.my_lang = {
            grid: {
                test: "test_text"
            }
        };

        jsGrid.locale("my_lang");

        var $element = $("#jsGrid").jsGrid({});

        equal($element.jsGrid("option", "test"), "test_text", "option localized");
    });

    test("set locale by config", function() {
        jsGrid.locale( {
            grid: {
                test: "test_text"
            }
        });

        var $element = $("#jsGrid").jsGrid({});

        equal($element.jsGrid("option", "test"), "test_text", "option localized");
    });

    test("locale throws exception for unknown locale", function() {
        throws(function() {
            jsGrid.locale("unknown_lang");
        }, /unknown_lang/, "locale threw an exception");
    });


    module("controller promise");

    asyncTest("should support jQuery promise success callback", 1, function() {
        var data = [];
        var gridOptions = {
            autoload: false,
            controller: {
                loadData: function() {
                    var d = $.Deferred();

                    setTimeout(function() {
                        d.resolve(data);
                        start();
                    });

                    return d.promise();
                }
            }
        };

        var grid = new Grid($("#jsGrid"), gridOptions);

        var promise = grid._controllerCall("loadData", {}, false, $.noop);
        promise.done(function(result) {
            equal(result, data, "data provided to done callback");
        });
    });

    asyncTest("should support jQuery promise fail callback", 1, function() {
        var failArgs = {};
        var gridOptions = {
            autoload: false,
            controller: {
                loadData: function() {
                    var d = $.Deferred();

                    setTimeout(function() {
                        d.reject(failArgs);
                        start();
                    });

                    return d.promise();
                }
            }
        };

        var grid = new Grid($("#jsGrid"), gridOptions);

        var promise = grid._controllerCall("loadData", {}, false, $.noop);
        promise.fail(function(result) {
            equal(result, failArgs, "fail args provided to fail callback");
        });
    });

    asyncTest("should support JS promise success callback", 1, function() {
        if(typeof Promise === "undefined") {
            ok(true, "Promise not supported");
            start();
            return;
        }

        var data = [];
        var gridOptions = {
            autoload: false,
            controller: {
                loadData: function() {
                    return new Promise(function(resolve, reject) {
                        setTimeout(function() {
                            resolve(data);
                            start();
                        });
                    });
                }
            }
        };

        var grid = new Grid($("#jsGrid"), gridOptions);

        var promise = grid._controllerCall("loadData", {}, false, $.noop);
        promise.done(function(result) {
            equal(result, data, "data provided to done callback");
        });
    });

    asyncTest("should support JS promise fail callback", 1, function() {
        if(typeof Promise === "undefined") {
            ok(true, "Promise not supported");
            start();
            return;
        }

        var failArgs = {};
        var gridOptions = {
            autoload: false,
            controller: {
                loadData: function() {
                    return new Promise(function(resolve, reject) {
                        setTimeout(function() {
                            reject(failArgs);
                            start();
                        });
                    });
                }
            }
        };

        var grid = new Grid($("#jsGrid"), gridOptions);

        var promise = grid._controllerCall("loadData", {}, false, $.noop);
        promise.fail(function(result) {
            equal(result, failArgs, "fail args provided to fail callback");
        });
    });

    test("should support non-promise result", 1, function() {
        var data = [];
        var gridOptions = {
            autoload: false,
            controller: {
                loadData: function() {
                    return data;
                }
            }
        };

        var grid = new Grid($("#jsGrid"), gridOptions);

        var promise = grid._controllerCall("loadData", {}, false, $.noop);
        promise.done(function(result) {
            equal(result, data, "data provided to done callback");
        });
    });


    module("renderTemplate");

    test("should pass undefined and null arguments to the renderer", function() {
        var rendererArgs;
        var rendererContext;
        var context = {};

        var renderer = function() {
            rendererArgs = arguments;
            rendererContext = this;
        };

        Grid.prototype.renderTemplate(renderer, context, { arg1: undefined, arg2: null, arg3: "test" });

        equal(rendererArgs.length, 3);
        strictEqual(rendererArgs[0], undefined, "undefined passed");
        strictEqual(rendererArgs[1], null, "null passed");
        strictEqual(rendererArgs[2], "test", "null passed");
        strictEqual(rendererContext, context, "context is preserved");
    });

});
