/*global QUnit, sinon*/
sap.ui.define([
	"sap/ui/core/util/MockServer",
	"sap/ui/model/odata/v2/ODataModel",
	"sap/ui/thirdparty/jquery",
	"sap/ui/model/FilterOperator",
	"sap/ui/model/json/JSONModel",
	"sap/ui/model/Filter",
	"sap/ui/model/Sorter",
	"sap/m/List",
	"sap/m/StandardListItem",
	"sap/m/InputListItem",
	"sap/m/CustomListItem",
	"sap/m/Text",
	"sap/m/VBox",
	"sap/m/Input",
	"sap/base/strings/capitalize",
	"sap/ui/core/Core",
	"sap/m/GrowingEnablement"
], function(
	MockServer,
	ODataModel,
	jQuery,
	FilterOperator,
	JSONModel,
	Filter,
	Sorter,
	List,
	StandardListItem,
	InputListItem,
	CustomListItem,
	Text,
	VBox,
	Input,
	capitalize,
	oCore,
	GrowingEnablement
) {
	"use strict";

	// global service URL keep the path relative not to have cross domain issue.
	var sServiceURI = "/service/";

	function createODataModel(sURL, mSettings) {
		sURL = sURL || sServiceURI;
		var oModel = new ODataModel(sURL, true);

		mSettings = mSettings || {};
		jQuery.each(mSettings, function(sProperty, vValue) {
			oModel["set" + capitalize(sProperty)](vValue);
		});

		return oModel;
	}

	// convenience helper for synchronous ajax calls
	function syncFetch(options) {
		var oResult;
		jQuery.ajax(Object.assign(options, {
			async: false,
			success : function(data) {
				oResult = data;
			}
		}));
		return oResult;
	}

	function createJSONModel(oData) {
		oData = oData || {
			Products : syncFetch({
				url : "test-resources/sap/m/qunit/data/Product.json",
				dataType:"json"
			})
		};

		var oModel = new JSONModel();
		oModel.setData(oData);
		return oModel;
	}

	function startMockServer(iRespondAfter) {
		// configure respond to requests delay
		MockServer.config({
			autoRespond : true,
			autoRespondAfter : iRespondAfter || 10
		});

		// create mockserver
		var oMockServer = new MockServer({
			rootUri : sServiceURI
		});

		// start and return
		oMockServer.simulate("test-resources/sap/m/qunit/data/metadata.xml", "test-resources/sap/m/qunit/data");
		oMockServer.start();
		return oMockServer;
	}

	function createList(oListConfig, oBindConfig, oModel) {
		// init
		var oDeferred = jQuery.Deferred();
		var oList = new List(oListConfig || {});
		var oItemTemplate = new StandardListItem({
			title : "{Name}",
			description : "{Category}"
		});

		// render the list
		oList.placeAt("qunit-fixture");
		oCore.applyChanges();

		// set the list as a promise
		oDeferred.promise(oList);
		oList.attachUpdateFinished(oDeferred.resolve);

		// build binding confing
		oBindConfig = jQuery.extend({
			path : "/Products",
			template : oItemTemplate
		}, oBindConfig);

		// set model and bind items
		oModel = oModel || createODataModel();
		oList.setModel(oModel).bindItems(oBindConfig);

		return oList;
	}

	QUnit.module("BusyIndicator");
	QUnit.test("List should show busy indicator during the binding update", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer(0),
			oList = createList({
				busyIndicatorDelay: 0
			}, {
				events: {
					dataRequested: function() {
						var $BusyIndicator = jQuery(".sapUiLocalBusyIndicator");
						assert.strictEqual($BusyIndicator.length, 1, "Busy indicator should be visible after data has been requested");
					},
					dataReceived: function() {
						window.setTimeout(function() {
							var $BusyIndicator = oList.$().find(".sapUiLocalBusyIndicator");
							assert.strictEqual($BusyIndicator.length, 0, "Busy indicator should be removed after data has been received");
							done();
						}, 0);
					}
				}
			});

		// when initial binding is completed
		oList.done(function () {
			oMockServer.stop();
			oList.destroy();
		});
	});

	QUnit.test("List should show busy indicator during the binding update when growing is true", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer(),
			oList = createList({
				growing : true,
				growingThreshold : 10,
				busyIndicatorDelay : 0,
				updateFinished : function(oEvent) {
					assert.strictEqual(oEvent.getParameter("actual"), 10, "Because of the growingThreshold we should see only first 10 items.");
					assert.ok(oList.getDomRef("listUl").classList.contains("sapMListHasGrowing"), "sapMListHasGrowing class is added");
				}
			}, {
				events: {
					dataRequested: function() {
						var $BusyIndicator = jQuery(".sapUiLocalBusyIndicator");
						assert.strictEqual($BusyIndicator.length, 1, "Busy indicator should be visible after data has been requested");
					},
					dataReceived: function() {
						window.setTimeout(function() {
							var $BusyIndicator = oList.$().find(".sapUiLocalBusyIndicator");
							assert.strictEqual($BusyIndicator.length, 0, "Busy indicator should be removed after data has been received");
							done();
						}, 0);
					}
				}
			});

		// when initial binding is completed
		oList.done(function () {
			oMockServer.stop();
			oList.destroy();
		});
	});

	QUnit.test("List should show busy indicator when there is no data found", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer(),
			oList = createList({
				busyIndicatorDelay : 0,
				updateFinished : function(oEvent) {
					assert.strictEqual(oEvent.getParameter("total"), 0, "Because of the filter total binding length should be 0");
					assert.notOk(oList.getDomRef("listUl").classList.contains("sapMListHasGrowing"), "sapMListHasGrowing class is removed");
				}
			}, {
				filters : [new Filter("Name", FilterOperator.EQ, "ThisTextShouldNotBeFound")],
				events: {
					dataRequested: function() {
						var $BusyIndicator = jQuery(".sapUiLocalBusyIndicator");
						assert.strictEqual($BusyIndicator.length, 1, "Busy indicator should be visible after data has been requested");
					},
					dataReceived: function() {
						window.setTimeout(function() {
							var $BusyIndicator = oList.$().find(".sapUiLocalBusyIndicator");
							assert.strictEqual($BusyIndicator.length, 0, "Busy indicator should be removed after data has been received");
							done();
						}, 0);
					}
				}
			});

		// when initial binding is completed
		oList.done(function () {
			oMockServer.stop();
			oList.destroy();
		});
	});

	QUnit.test("List should respect busyIndicatorDelay property to show busy indicator.", function(assert) {
		var done = assert.async();
		var iRespondDelay = 10,
			oMockServer = startMockServer(iRespondDelay),
			oList = createList({
				busyIndicatorDelay : iRespondDelay + 1,
				updateStarted : function() {
					var $BusyIndicator = this.$().find(".sapUiLocalBusyIndicator");
					assert.strictEqual($BusyIndicator.length, 0, "Delay is longer than response time busy indicator should not be visible");
				}
			});

		// when initial binding is completed
		oList.done(function() {
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.test("List should respect enableBusyIndicator property to show busy indicator.", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer(),
			oList = createList({
				enableBusyIndicator : false,
				updateStarted : function() {
					var $BusyIndicator = this.$().find(".sapUiLocalBusyIndicator");
					assert.strictEqual($BusyIndicator.length, 0, "enableBusyIndicator is false List should not show busy indicator.");
				}
			});

		// when initial binding is completed
		oList.done(function() {
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.test("List should clear busy state when binding error has occured", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer(),
			oList = createList({
				busyIndicatorDelay : 0,
				updateFinished : function(oEvent) {
					assert.strictEqual(oEvent.getParameter("total"), 0, "Because on error data is cleared, binding length should be 0");
					assert.strictEqual(oEvent.getParameter("actual"), 0, "Because on error data is cleared, current item count should be 0");
					assert.strictEqual(this.$().find(".sapMListNoData").length, 1, "No Data indication is shown after error");
				}
			}, {
				path :"/ThereIsNoSuchPath",
				events: {
					dataRequested: function() {
						var $BusyIndicator = jQuery(".sapUiLocalBusyIndicator");
						assert.strictEqual($BusyIndicator.length, 1, "Busy indicator should be visible after data has been requested");
					},
					dataReceived: function() {
						window.setTimeout(function() {
							var $BusyIndicator = oList.$().find(".sapUiLocalBusyIndicator");
							assert.strictEqual($BusyIndicator.length, 0, "Busy indicator should be removed after data has been received");
							done();
						}, 0);
					}
				}
			});

		// when initial binding is completed
		oList.done(function () {
			oMockServer.stop();
			oList.destroy();
		});
	});

	QUnit.test("Growing List should clear busy state and all data when binding error has occured", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer(),
			oDeferred = jQuery.Deferred(),
			oList = createList({
				growing : true,
				growingThreshold : 10,
				busyIndicatorDelay : 0
			});

		// when initial binding is completed
		oList.done(function () {
			// attach to the update binding events
			oList.attachUpdateStarted(function(oEvent) {
				assert.strictEqual(oEvent.getParameter("actual"), 10, "There are 10 items after initial loading");
			}).attachUpdateFinished(function(oEvent) {
				assert.strictEqual(oEvent.getParameter("total"), 0, "Because on error data is cleared, binding length should be 0");
				assert.strictEqual(oEvent.getParameter("actual"), 0, "Because on error data is cleared, current item count should be 0");
				assert.strictEqual(this.$().find(".sapMListNoData").length, 1, "No Data indication is shown after error");
			});

			var oBinding = oList.getBinding("items");
			oBinding.attachEvents({
				dataRequested: function() {
					var $BusyIndicator = oList.$().find(".sapUiLocalBusyIndicator");
					assert.strictEqual($BusyIndicator.length, 1, "Busy indicator should be visible after data has been requested");
				},
				dataReceived: function() {
					window.setTimeout(function() {
						var $BusyIndicator = oList.$().find(".sapUiLocalBusyIndicator");
						assert.strictEqual($BusyIndicator.length, 0, "Busy indicator should be removed after data has been received");
						oDeferred.resolve();
					}, 0);
				}
			});

			window.setTimeout(function() { // first let the list finish rendering
				// create http error with unkown sorter field
				oBinding.sort(new Sorter("NoSuchField"));
			}, 0);
		});

		// when test is completed
		jQuery.when(oDeferred).done(function () {
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.test("Update events should not be fired when metadata is not loaded", function(assert) {
		var oMockServer = startMockServer(),
			oModel = createODataModel("/there/is/no/such/service"),
			fnUpdateFinishedSpy = this.spy(),
			fnUpdateStartedSpy = this.spy(),
			oList = createList({
				updateStarted : fnUpdateStartedSpy,
				updateFinished : fnUpdateFinishedSpy
			}, {}, oModel);

		assert.strictEqual(fnUpdateStartedSpy.callCount, 0, "updateStarted is not fired because metadata is not loaded");
		assert.strictEqual(fnUpdateFinishedSpy.callCount, 0, "updateFinished is not fired because metadata is not loaded");

		// cleanup
		oMockServer.stop();
		oModel.destroy();
		oList.destroy();
	});

	QUnit.test("Should not have a static after busy", function(assert) {
		var done = assert.async();
		// Arrange
		var oMockServer = startMockServer();

		// System under Test
		var oList = createList({
				growing : true,
				growingThreshold : 10,
				busyIndicatorDelay : 0
			});

		// Act
		oList.done(function () {
			var sPosition = oList.$().css("position");
			// Assert
			assert.notStrictEqual(sPosition, "static", "position was not static but: " + sPosition);

			// cleanup
			oMockServer.stop();
			oList.destroy();

			done();
		});
	});

	QUnit.module("Abort");
	QUnit.test("List should abort multiple requests and runs only the last one", function(assert) {
		var done = assert.async();

		// arrange
		var fnUpdateStartedSpy = this.spy(),
			fnUpdateFinishedSpy = this.spy(),
			oMockServer = startMockServer(),
			oDeferred = jQuery.Deferred(),
			oList = createList();

		// when initial binding is completed
		oList.done(function () {
			// attach to the update binding events
			oList.attachUpdateStarted(fnUpdateStartedSpy);
			oList.attachUpdateFinished(fnUpdateFinishedSpy);
			oList.attachUpdateFinished(oDeferred.resolve);

			// sort and immediately filter then sort request should be aborted
			// but even though sort stays in the model and next filter should have sort info
			this.getBinding("items").sort(new Sorter("ProductId", true));
			this.getBinding("items").filter(new Filter("SupplierName", FilterOperator.Contains, "Very Best Screens"));

		});

		// when all requests are completed
		jQuery.when(oDeferred).done(function () {
			// assert
			assert.strictEqual(fnUpdateStartedSpy.callCount, 1, "update started event is called once");
			assert.strictEqual(fnUpdateFinishedSpy.callCount, 1, "update finished event is called once");
			assert.strictEqual(fnUpdateFinishedSpy.args[0][0].getParameter("total"), 3, "Event reported there are 3 records are found: Very Best Screens");
			assert.strictEqual(oList.getItems().length, 3, "List has 3 records: Very Best Screens");

			// find all product ids
			var sProductIds = oList.getItems().map(function(oItem) {
				return oItem.getBindingContext().getProperty("ProductId");
			}).join(" > ");

			assert.strictEqual(sProductIds, "id_13 > id_12 > id_11", "Aborted descending sort information is applied to the filter");

			// clean up
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.module("CountMode");
	QUnit.test("CountMode.None should show growing indicator when list is not complete yet", function(assert) {
		var done = assert.async();
		var iThreshold = 10,
			oMockServer = startMockServer(),
			oList = createList({
				growing : true,
				growingThreshold : iThreshold,
				updateFinished : function(oEvent) {
					assert.strictEqual(oEvent.getParameter("total") > oEvent.getParameter("actual"), true, "Total items is higher than actual items");
					setTimeout(function() {
						assert.ok(oList.$().find(".sapMGrowingList").is(":visible"), "Growing indicator is visible");
					}, 0);
				}
			}, {
				parameters: {
					countMode: "None"
				}
			});

		// when initial binding is completed
		oList.done(function () {
			setTimeout(function() {
				oMockServer.stop();
				oList.destroy();
				done();
			}, 0);
		});
	});

	QUnit.test("CountMode.None should not show growing indicator when all items are shown", function(assert) {
		var done = assert.async();
		var iThreshold = 20,
			oMockServer = startMockServer(),
			oList = createList({
				growing : true,
				growingThreshold : iThreshold,
				updateFinished : function(oEvent) {
					assert.strictEqual(oEvent.getParameter("total"), 16, "Total number equals amount of entries");
					assert.strictEqual(oEvent.getParameter("actual"), 16, "Actual number equals amount of entries");
					assert.ok(!oList.$().find(".sapMGrowingList").is(":visible"), "Growing indicator is not visible");
				}
			}, {
				parameters: {
					countMode: "None"
				}
			});

		// when initial binding is completed
		oList.done(function () {
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.module("NonAbsoluteAndUnresolveable Binding Path");
	QUnit.test("Should not fire update started if a binding cannot be resolved", function(assert) {
		var oMockServer = startMockServer(),
			oList = createList({
				updateStarted : function () {
					assert.ok(false, "Started an update! Why should we? There should no request fired.");
				}
			}, {
				path : "nonAbsoluteAndUnresolveable"
			});

		assert.ok(!oList.getBusy(), "Why should the list be busy? There should not be any request");

		// clean up
		oMockServer.stop();
		oList.destroy();
	});

	QUnit.module("FocusAfterBindingUpdate");
	document.hasFocus() && QUnit.test("Focus should be retained after binding update", function(assert) {
		var done = assert.async();
		var sFocusedControlId,
			oMockServer = startMockServer(),
			oDeferred = jQuery.Deferred(),
			oList = createList({
				growing : false
			}, {
				template : new InputListItem({
					label: "Product Name",
					content : new Input({
						value: "{Name}"
					})
				})
			});

		// when initial binding is completed
		oList.done(function () {

			// focus to the input field
			sFocusedControlId = oList.$().find("input").eq(5).trigger("focus").attr("id");

			// rerender list
			oList.rerender();

			setTimeout(function() {
				// check focus after rerender
				assert.strictEqual(document.activeElement.id, sFocusedControlId, "Focus is retained after list rerender");

				// recheck focus after binding update
				oList.attachUpdateFinished(function(oEvent) {
					assert.strictEqual(document.activeElement.id, sFocusedControlId, "Focus is retained after binding update");
					oDeferred.resolve();
				});

				// update the list binding
				oList.updateItems();
			}, 0);

		});

		// when test is completed
		jQuery.when(oDeferred).done(function () {
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.test("Last setGrowingThreshold should be respected before data fetching", function(assert) {
		var done = assert.async();
		var iInitThreshold = 5,
			iLastThreshold = 10,
			oMockServer = startMockServer(),
			oList = createList({
				growing : true,
				growingThreshold : iInitThreshold,
				updateFinished : function(oEvent) {
					assert.strictEqual(oEvent.getParameter("actual"), iLastThreshold, "Last growing threshold is respected");
				}
			});

		oList.setGrowingThreshold(iLastThreshold);

		// when initial binding is completed
		oList.done(function () {
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.module("Upwards Growing Direction");
	QUnit.test("Should fetch the data and render in reverse order", function(assert) {
		var done = assert.async();
		var iThreshold = 5,
			oDeferred = jQuery.Deferred(),
			oMockServer = startMockServer(),
			oList = createList({
				growing : true,
				growingThreshold : iThreshold,
				growingDirection : "Upwards",
				updateFinished : function(oEvent) {
					var oNavigationRoot = oList.getNavigationRoot(),
						$GrowingButton = oList.$("trigger");

					assert.ok($GrowingButton.parent().next().get(0) === oList.getDomRef("before"), "Growing button is inserted before the first item.");
					assert.ok(oList.getItems().pop().getDomRef() === oNavigationRoot.children[0], "Last aggregation DOM is the first child of the list");

					if (document.hasFocus() && oList.getItemNavigation()) {
						oList.focus();
						assert.ok(document.activeElement === oNavigationRoot.children[iThreshold - 1], "First focus jumps to last item");
					}

					// act: request new page
					$GrowingButton.trigger("focus").trigger("tap");

					oList.attachUpdateFinished(function(oEvent) {
						oNavigationRoot = oList.getNavigationRoot();
						assert.ok(oList.getItems().pop().getDomRef() === oNavigationRoot.children[0], "Last aggregation DOM is the first child of the list");

						if (document.hasFocus()) {
							assert.ok(document.activeElement === $GrowingButton.get(0), "Focus is not changed after growing");
						}

						oDeferred.resolve();
					});
				}
			});

		// when test is completed
		jQuery.when(oDeferred).done(function () {
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.module("TwoWayBinding And Growing");
	QUnit.test("Property changes updates the list binding when growing feature is enabled", function(assert) {
		var done = assert.async();
		// arrange
		var fnUpdateStartedSpy = this.spy(),
			oMockServer = startMockServer(),
			oModel = createJSONModel(),
			oList = createList({
				growing: true
			}, {}, oModel);

		// when initial binding is completed
		oList.done(function () {
			oList.attachUpdateStarted(fnUpdateStartedSpy);

			oModel.setProperty("/Products/0/Name", ":(");
			assert.strictEqual(fnUpdateStartedSpy.callCount, 1, "Property update also updates the list binding");

			// clean up
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.test("Property changes should not update the list binding when unique key is defined", function(assert) {
		var done = assert.async();
		// arrange
		var fnUpdateStartedSpy = this.spy(),
			oMockServer = startMockServer(),
			oModel = createJSONModel(),
			oList = createList({
				growing: true
			}, {
				key: "ProductId"
			}, oModel);

		// when initial binding is completed
		oList.done(function () {
			oList.attachUpdateStarted(fnUpdateStartedSpy);

			oModel.setProperty("/Products/0/Name", ":)");
			assert.strictEqual(fnUpdateStartedSpy.callCount, 0, "Property update did not update the list binding.");

			// clean up
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.module("ExtendedChangeDetection");
	QUnit.test("Replace option when template is available", function(assert) {
		var done = assert.async();

		// arrange
		var oMockServer = startMockServer(),
			oDeferred = jQuery.Deferred(),
			oCreateListItemSpy,
			oSetBindingContextSpy,
			oList = createList({
				growing: true,
				growingThreshold: 4
			});

		// when initial binding has been completed
		oList.done(function () {
			oList.attachUpdateFinished(oDeferred.resolve);
			oCreateListItemSpy = sinon.spy(StandardListItem.prototype, "init");
			oSetBindingContextSpy = sinon.spy(StandardListItem.prototype, "setBindingContext");
			this.getBinding("items").sort(new Sorter("ProductId", true));
		});

		// when sorting has been completed
		jQuery.when(oDeferred).done(function () {

			assert.equal(oCreateListItemSpy.callCount, 0, "No List Item is created because of sort");
			assert.equal(oSetBindingContextSpy.callCount, 4, "Only binding contexts are set for list items");

			// clean up
			oSetBindingContextSpy.restore();
			oCreateListItemSpy.restore();
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.test("No replace option when factory is available", function(assert) {
		var done = assert.async();

		// arrange
		var oMockServer = startMockServer(),
			oDeferred = jQuery.Deferred(),
			oCreateListItemSpy,
			oList = createList({
				growing: true,
				growingThreshold: 4
			}, {
				template: null,
				factory: function() {
					return new StandardListItem({
						title : "{Name}",
						description : "{Category}"
					});
				}
			});

		// when initial binding has been completed
		oList.done(function () {
			oList.attachUpdateFinished(oDeferred.resolve);
			oCreateListItemSpy = sinon.spy(StandardListItem.prototype, "init");
			this.getBinding("items").sort(new Sorter("ProductId", true));
		});

		// when sorting has been completed
		jQuery.when(oDeferred).done(function () {

			assert.equal(oCreateListItemSpy.callCount, 4, "4 List Items are created");

			// clean up
			oCreateListItemSpy.restore();
			oMockServer.stop();
			oList.destroy();
			done();
		});
	});

	QUnit.module("Virtual Context", {
		beforeEach: function() {
		},

		afterEach: function() {
			this.list.destroy();
		},

		testVirtualContext: function(assert, bWithGrowing) {

			this.list = createList({growing: bWithGrowing}, null, createJSONModel());

			var iInitialItemCount = this.list.getItems().length;
			var oUpdateItemsSpy = sinon.spy(this.list, "_updateFinished");
			var oInvalidateSpy = sinon.spy(this.list, "invalidate");
			var oBinding = this.list.getBinding("items");

			// Fake the virtual context process.

			oBinding.fireEvent("change", {
				detailedReason: "AddVirtualContext",
				reason: "change"
			});

			var oVirtualItem = this.list._oVirtualItem;

			assert.ok(oInvalidateSpy.notCalled, "AddVirtualContext: List is not invalidated");
			assert.ok(oUpdateItemsSpy.notCalled, "AddVirtualContext: Update hook is not called");
			assert.ok(this.list.indexOfDependent(oVirtualItem) >= 0, "AddVirtualContext: Virtual item added to dependents aggregation");
			assert.ok(oVirtualItem.getId().indexOf("virtual") > 0, "AddVirtualContext: Virtual item has the correct ID");
			assert.strictEqual(this.list.getItems().length, iInitialItemCount, "AddVirtualContext: Number of items is correct");
			assert.strictEqual(oVirtualItem.getBindingContext(), oBinding.getContexts(0, 1)[0],
				"AddVirtualContext: Virtual item has the correct context");
			assert.notOk(oVirtualItem.bIsDestroyed, "AddVirtualContext: Virtual item is not destroyed");
			oInvalidateSpy.resetHistory();
			oUpdateItemsSpy.resetHistory();

			oBinding.fireEvent("change", {
				detailedReason: "RemoveVirtualContext",
				reason: "change"
			});

			assert.ok(oInvalidateSpy.notCalled, "RemoveVirtualContext: List is not invalidated");
			assert.ok(oUpdateItemsSpy.notCalled, "RemoveVirtualContext: Update hook is not called");
			assert.ok(this.list.indexOfDependent(oVirtualItem) === -1, "RemoveVirtualContext: Virtual item removed from dependents aggregation");
			assert.strictEqual(this.list.getItems().length, iInitialItemCount, "AddVirtualContext: Number of items is correct");
			assert.ok(oVirtualItem.bIsDestroyed, "RemoveVirtualContext: Virtual row is destroyed");
			assert.notOk("_oVirtualItem" in this.list, "RemoveVirtualContext: Reference to virtual item removed from list");

			oBinding.fireEvent("change", {
				detailedReason: "AddVirtualContext",
				reason: "change"
			});
			oVirtualItem = this.list._oVirtualItem;
			this.list.bindItems(this.list.getBindingInfo("items"));

			assert.ok(this.list.indexOfDependent(oVirtualItem) === -1, "BindItems: Virtual item removed from dependents aggregation");
			assert.ok(oVirtualItem.bIsDestroyed, "BindItems: Virtual item is destroyed");
			assert.notOk("_oVirtualItem" in this.list, "BindItems: Reference to virtual item removed from list");
		}
	});

	QUnit.test("Virtual Context Handling - Without Growing", function(assert) {
		this.testVirtualContext(assert, false);
	});

	QUnit.test("Virtual Context Handling - With Growing", function(assert) {
		this.testVirtualContext(assert, true);
	});

	QUnit.test("Growing should reset if list contains items and there is a AddVirtualContext binding change", function(assert) {
		var done = assert.async(),
			oMockServer = startMockServer(0),
			oBinding,
			oInitialLoadDeferred = jQuery.Deferred(),
			oAfterGrowingTriggered = jQuery.Deferred(),
			oAfterBindingChangeSimulation = jQuery.Deferred();

		this.list = createList({
			growing: true,
			growingThreshold: 5
		});

		this.list.attachEventOnce("updateFinished", oInitialLoadDeferred.resolve);

		jQuery.when(oInitialLoadDeferred).then(function() {
			assert.strictEqual(this.list._oGrowingDelegate._iLimit, 5, "GrowingDelegate limit is 5");
			this.list.$("trigger").trigger("focus").trigger("tap");
			this.list.attachEventOnce("updateFinished", oAfterGrowingTriggered.resolve);
		}.bind(this));

		jQuery.when(oAfterGrowingTriggered).then(function() {
			assert.strictEqual(this.list._oGrowingDelegate._iLimit, 10, "GrowingDelegate limit is 10 due to growing");
			oBinding = this.list.getBinding("items");

			// simulate AddVirtualContext
			oBinding.fireEvent("change", {
				detailedReason: "AddVirtualContext",
				reason: "context"
			});
			oBinding.fireEvent("change", {
				detailedReason: "RemoveVirtualContext",
				reason: "change"
			});
			oBinding.refresh();

			this.list.attachEventOnce("updateFinished", oAfterBindingChangeSimulation.resolve);
		}.bind(this));

		jQuery.when(oAfterBindingChangeSimulation).then(function() {
			assert.strictEqual(this.list._oGrowingDelegate._iLimit, 5, "GrowingDelegate limit is 5, reset due to AddVirtualContext");

			oMockServer.stop();
			done();
		}.bind(this));
	});

	QUnit.module("Rebind");
	QUnit.test("List should not invalidate before update on rebind", function(assert) {
		var oMockServer = startMockServer(0),
			oList = createList(),
			oInvalidateSpy = this.spy(oList, "invalidate");

		return new Promise(function(resolve) {
			oList.attachEventOnce("updateFinished", resolve);
		}).then(function() {
			oInvalidateSpy.resetHistory();
			oList.bindItems(oList.getBindingInfo("items"));
			return new Promise(function(resolve) {
				oList.updateItems = resolve;
			});
		}).then(function() {
			assert.ok(oInvalidateSpy.notCalled, "The list is not invalidated");
			oList.destroy();
			oMockServer.stop();
		});
	});

	QUnit.test("List should invalidate on update if all items are removed after rebind", function(assert) {
		var oMockServer = startMockServer(0),
			oList = createList(),
			oInvalidateSpy = this.spy(oList, "invalidate");

		return new Promise(function(resolve) {
			oList.attachEventOnce("updateFinished", resolve);
		}).then(function() {
			oInvalidateSpy.resetHistory();
			oList.getBindingInfo("items").filters = [new Filter("Name", FilterOperator.EQ, "ThisTextShouldNotBeFound")];
			oList.bindItems(oList.getBindingInfo("items"));
			return new Promise(function(resolve) {
				var fnUpdateItems = oList.updateItems;
				oList.updateItems = function() {
					fnUpdateItems.apply(oList, arguments);
					resolve();
				};
			});
		}).then(function() {
			assert.ok(oInvalidateSpy.called, "The list is invalidated");
			oList.destroy();
			oMockServer.stop();
		});
	});

	QUnit.test("List should invalidate on update if items are updated after rebind", function(assert) {
		var oMockServer = startMockServer(0),
			oList = createList(),
			oInvalidateSpy = this.spy(oList, "invalidate");

		return new Promise(function(resolve) {
			oList.attachEventOnce("updateFinished", resolve);
		}).then(function() {
			oInvalidateSpy.resetHistory();
			oList.getBindingInfo("items").filters = [new Filter("Name", FilterOperator.EQ, "Gladiator MX")];
			oList.bindItems(oList.getBindingInfo("items"));
			return new Promise(function(resolve) {
				var fnUpdateItems = oList.updateItems;
				oList.updateItems = function() {
					fnUpdateItems.apply(oList, arguments);
					resolve();
				};
			});
		}).then(function() {
			assert.ok(oInvalidateSpy.called, "The list is invalidated");
			oList.destroy();
			oMockServer.stop();
		});
	});

	QUnit.test("Growing list should invalidate on update if all items are removed after rebind", function(assert) {
		var oMockServer = startMockServer(0),
			oList = createList({
				growing: true
			}),
			oInvalidateSpy = this.spy(oList, "invalidate");

		return new Promise(function(resolve) {
			oList.attachEventOnce("updateFinished", resolve);
		}).then(function() {
			oInvalidateSpy.resetHistory();
			oList.getBindingInfo("items").filters = [new Filter("Name", FilterOperator.EQ, "ThisTextShouldNotBeFound")];
			oList.bindItems(oList.getBindingInfo("items"));
			return new Promise(function(resolve) {
				var fnUpdateItems = oList.updateItems;
				oList.updateItems = function() {
					fnUpdateItems.apply(oList, arguments);
					resolve();
				};
			});
		}).then(function() {
			assert.ok(oInvalidateSpy.called, "The list is invalidated");
			oList.destroy();
			oMockServer.stop();
		});
	});

	QUnit.test("Growing list should not invalidate on update if items are updated after rebind", function(assert) {
		var oMockServer = startMockServer(0),
			oList = createList({
				growing: true
			}),
			oInvalidateSpy = this.spy(oList, "invalidate");

		return new Promise(function(resolve) {
			oList.attachEventOnce("updateFinished", resolve);
		}).then(function() {
			oInvalidateSpy.resetHistory();
			oList.getBindingInfo("items").filters = [new Filter("Name", FilterOperator.EQ, "Gladiator MX")];
			oList.bindItems(oList.getBindingInfo("items"));
			return new Promise(function(resolve) {
				var fnUpdateItems = oList.updateItems;
				oList.updateItems = function() {
					fnUpdateItems.apply(oList, arguments);
					resolve();
				};
			});
		}).then(function() {
			assert.ok(oInvalidateSpy.notCalled, "The list is not invalidated");
			oList.destroy();
			oMockServer.stop();
		});
	});

	QUnit.module("ItemsPool");

	QUnit.test("Array should be available", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer();
		var oModel = createODataModel(sServiceURI, {useBatch: false});
		var fnDone, oControl, fnFillItemsPoolStub;
		var pPromise = new Promise(function(resolve) {
			fnDone = resolve;
		});
		var oList = createList({
			growing: true,
			growingThreshold: 8,
			updateStarted: function(oEvent) {
				oControl = oEvent.getSource();
				assert.ok(oControl._oGrowingDelegate._aItemsPool, "itemsPool was created");
				// reference to the original "fillItemsPool" function
				var fnFillItemsPool = oControl._oGrowingDelegate.fillItemsPool;
				// "fillItemsPool" is called in a timeout, which causes unstable unit tests due to timing issues,
				// instead stub the function which calls the original function and also take care of assertions here
				fnFillItemsPoolStub = sinon.stub(oControl._oGrowingDelegate, "fillItemsPool").callsFake(function() {
					// simulate no items are rendered
					oControl._oGrowingDelegate._iRenderedDataItems = 0;
					// call the original function
					fnFillItemsPool.call(oControl._oGrowingDelegate);
					assert.strictEqual(oControl._oGrowingDelegate._aItemsPool.length, 8, "8 items are available in the itemsPool");
					// inform that the "fillItemsPool" has done its job by resolving the promise
					fnDone();
				});
			}
		}, {
			template: new CustomListItem({
				content: [
					new VBox({
						items: [
							new Text({text: "{ProductId}"}),
							new Text({text: "{Name}"}),
							new Text({text: "{Category}"})
						]
					})
				]
			})
		},
		oModel);

		pPromise.then(function() {
			// clean up
			fnFillItemsPoolStub.restore();
			oList.destroy();
			oMockServer.stop();
			done();
		});
	});

	QUnit.test("Array should be limited to 100 items", function(assert) {
		var done = assert.async();
		var oMockServer = startMockServer();
		var oModel = createODataModel(sServiceURI, {useBatch: false});
		var fnDone, oControl, fnFillItemsPoolStub;
		var pPromise = new Promise(function(resolve) {
			fnDone = resolve;
		});
		var oList = createList({
			growing: true,
			growingThreshold: 200, // sut - high threshold value
			updateStarted: function(oEvent) {
				oControl = oEvent.getSource();
				assert.ok(oControl._oGrowingDelegate._aItemsPool, "itemsPool was created");
				// reference to the original "fillItemsPool" function
				var fnFillItemsPool = oControl._oGrowingDelegate.fillItemsPool;
				// "fillItemsPool" is called in a timeout, which causes unstable unit tests due to timing issues,
				// instead stub the function which calls the original function and also take care of assertions here
				fnFillItemsPoolStub = sinon.stub(oControl._oGrowingDelegate, "fillItemsPool").callsFake(function() {
					// simulate no items are rendered
					oControl._oGrowingDelegate._iRenderedDataItems = 0;
					// call the original function
					fnFillItemsPool.call(oControl._oGrowingDelegate);
					assert.strictEqual(oControl._oGrowingDelegate._aItemsPool.length, 100, "itemPool limited to 100 items, inspite of the growingThreshold=" + oControl.getGrowingThreshold());
					// inform that the "fillItemsPool" has done its job by resolving the promise
					fnDone();
				});
			}
		}, {
			template: new CustomListItem({
				content: [
					new VBox({
						items: [
							new Text({text: "{ProductId}"}),
							new Text({text: "{Name}"}),
							new Text({text: "{Category}"})
						]
					})
				]
			})
		},
		oModel);

		pPromise.then(function() {
			// clean up
			fnFillItemsPoolStub.restore();
			oList.destroy();
			oMockServer.stop();
			done();
		});
	});
});