/*global QUnit,sinon*/
sap.ui.define([
	"sap/ui/core/dnd/DragAndDrop",
	"sap/ui/core/dnd/DragInfo",
	"sap/ui/core/dnd/DropInfo",
	"sap/ui/core/dnd/DragDropInfo",
	"jquery.sap.global",
	"sap/ui/core/Control",
	"sap/ui/core/Element",
	"sap/ui/core/UIArea",
	"sap/ui/core/Core",
	"sap/ui/Device"
], function(DragAndDrop, DragInfo, DropInfo, DragDropInfo, jQuery, Control, Element, UIArea, Core, Device) {
	"use strict";

	var DivControl = Control.extend("sap.ui.core.dnd.test.DivControl", {
		metadata: {
			properties: {
				elementTag: {type: "string", defaultValue: "div"}
			}
		},
		renderer: {
			apiVersion: 2,
			render: function(rm, oControl) {
				var sElementTag = oControl.getElementTag(),
					bIsVoid = /^(?:area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i.test(sElementTag);

				rm.openStart("div").openEnd();
					if ( bIsVoid ) {
						rm.voidStart(sElementTag, oControl);
					} else {
						rm.openStart(sElementTag, oControl);
					}
					rm.attr("tabindex", 0);
					rm.style("width", "100px");
					rm.style("height", "50px");
					if ( bIsVoid ) {
						rm.voidEnd();
					} else {
						rm.openEnd();
						rm.close(sElementTag);
					}
				rm.close("div");
			}
		},
		getDragGhost: function() {
			return this.getDomRef().cloneNode(true);
		}
	});

	var DragAndDropControl = Control.extend("sap.ui.core.dnd.test.DragAndDropControl", {
		metadata: {
			dnd: true,
			properties : {
				showNoData : {type : "boolean", defaultValue : false}
			},
			aggregations: {
				topItems: {
					name: "topItems",
					type: "sap.ui.core.dnd.test.DivControl",
					multiple: true,
					singularName: "topItem",
					selector : "#{id}-topItems",
					dnd: true
				},
				bottomItems: {
					name: "bottomItems",
					type: "sap.ui.core.dnd.test.DivControl",
					multiple: true,
					singularName: "bottomItem",
					selector : "#{id}-bottomItems",
					dnd: true
				}
			}
		},
		renderer: {
			apiVersion: 2,
			render: function(rm, oControl) {

				var aTopItems = oControl.getTopItems();
				var aBottomItems = oControl.getBottomItems();
				var i;

				rm.openStart("div", oControl).attr("tabindex", 0).openEnd();
					rm.openStart("div", oControl.getId() + "-topItems").openEnd();
					if (!aTopItems.length) {
						rm.openStart("div", oControl.getId() + "-topNoData")
							.openEnd()
							.text("No top items")
							.close("div");
					} else {
						for (i = 0; i < aTopItems.length; i++) {
							rm.renderControl(aTopItems[i]);
						}
					}
					rm.close("div");

					rm.openStart("div", oControl.getId() + "-bottomItems").openEnd();
					if (!aBottomItems.length) {
						rm.openStart("div", oControl.getId() + "-bottomNoData")
							.openEnd()
							.text("No bottom items")
							.close("div");
					} else {
						for (i = 0; i < aBottomItems.length; i++) {
							rm.renderControl(aBottomItems[i]);
						}
					}
					rm.close("div");

				rm.close("div");
			}
		}
	});

	function createjQueryDragEventDummy(sEventType, oControl, bRemoveId, bRemoveDraggable) {
		var oEvent = jQuery.Event(sEventType);
		var oTarget = oControl.getDomRef();

		oEvent.target = oTarget;
		if (bRemoveId === true) {
			delete oTarget.dataset.sapUi;
			oTarget.removeAttribute("id");
		}
		if (bRemoveDraggable === true) {
			oTarget.draggable = false;
		}
		oEvent.originalEvent = createNativeDragEventDummy(sEventType);

		return oEvent;
	}

	function createNativeDragEventDummy(sEventType) {
		var oEvent;

		if (typeof Event === "function") {
			oEvent = new Event(sEventType, {
				bubbles: true,
				cancelable: true
			});
		} else { // IE
			oEvent = document.createEvent("Event");
			oEvent.initEvent(sEventType, true, true);
		}

		oEvent.dataTransfer = {
			types: [],
				dropEffect: "",
				setDragImage: function() {},
			setData: function() {}
		};

		return oEvent;
	}

	QUnit.module("DragSession", {
		beforeEach: function() {
			this.oControl = new DragAndDropControl({
				dragDropConfig: [
					new DragDropInfo({
						targetElement: "dummy"
					})
				]
			});
			this.oControl.addStyleClass("sapUiScrollDelegate").placeAt("qunit-fixture");
			Core.applyChanges();
		},
		afterEach: function() {
			this.oControl.destroy();
		}
	});

	QUnit.test("Non draggable elements", function(assert) {
		var oEvent = createjQueryDragEventDummy("dragstart", this.oControl, false, true);

		DragAndDrop.preprocessEvent(oEvent);
		assert.equal(oEvent.dragSession, null, "No drag session was created for a non draggable element");
	});

	QUnit.test("Text input elements", function(assert) {
		var oEvent;

		this.oControl.addTopItem(new DivControl({elementTag: "input"}));
		this.oControl.addTopItem(new DivControl({elementTag: "textarea"}));
		this.oControl.addDragDropConfig(new DragDropInfo({sourceAggregation: "topItems"}));
		Core.applyChanges();

		oEvent = createjQueryDragEventDummy("dragstart", this.oControl.getTopItems()[0]);
		oEvent.target.focus();
		DragAndDrop.preprocessEvent(oEvent);
		assert.equal(oEvent.dragSession, null, "No drag session was created for an input element");
		assert.ok(oEvent.isDefaultPrevented(), "Drag is not started for the input element");

		oEvent = createjQueryDragEventDummy("dragstart", this.oControl.getTopItems()[1]);
		oEvent.target.focus();
		DragAndDrop.preprocessEvent(oEvent);
		assert.equal(oEvent.dragSession, null, "No drag session was created for a textarea element");
	});

	QUnit.test("Text input elements - Workaround for a bug in Firefox", function(assert) {
		var oBrowserStub = sinon.stub(Device, "browser");
		var that = this;
		var sBrowser = "firefox";

		oBrowserStub.value({firefox: true});
		that.oControl.destroyTopItems();
		that.oControl.addTopItem(new DivControl({elementTag: "input"}));
		that.oControl.addTopItem(new DivControl({elementTag: "textarea"}));
		that.oControl.addDragDropConfig(new DragDropInfo());
		Core.applyChanges();

		assert.ok(that.oControl.getDomRef().draggable, sBrowser + " - Ancestor is draggable before mousedown");

		["input", "textarea"].forEach(function(sSelectableElementTagName) {
			that.oControl.$().find(sSelectableElementTagName).trigger("mousedown");
			assert.notOk(that.oControl.getDomRef().draggable, sBrowser + " - Ancestor is not draggable after mousedown to allow text selection");

			that.oControl.$().find(sSelectableElementTagName).trigger("mouseup");
			assert.ok(that.oControl.getDomRef().draggable, sBrowser + " - Ancestor is draggable again after mouseup to allow drag and drop again");
		});

		oBrowserStub.restore();
	});

	QUnit.test("Draggable elements without control id", function(assert) {
		var oEvent = createjQueryDragEventDummy("dragstart", this.oControl, true);

		oEvent.target.focus();
		DragAndDrop.preprocessEvent(oEvent);
		assert.equal(oEvent.dragSession, null, "No drag session was created for an element without relation to a UI5 control");
	});

	QUnit.test("Lifecycle", function(assert) {
		var oEvent;
		var oDragSession;

		oEvent = createjQueryDragEventDummy("dragstart", this.oControl);
		oEvent.target.focus();
		DragAndDrop.preprocessEvent(oEvent);
		assert.notEqual(oEvent.dragSession, null,
			"dragstart: A drag session was created for an element with a data-sap-ui control id");

		oDragSession = oEvent.dragSession;
		oEvent = createjQueryDragEventDummy("dragenter", this.oControl, false);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession === oDragSession, "dragenter: Drag session was preserved");

		oEvent = createjQueryDragEventDummy("dragover", this.oControl, false);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession === oDragSession, "dragover: Drag session was preserved");

		oEvent = createjQueryDragEventDummy("dragleave", this.oControl, false);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession === oDragSession, "dragleave: Drag session was preserved");

		oEvent = createjQueryDragEventDummy("drop", this.oControl, false);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession === oDragSession, "drop: Drag session was preserved");
		assert.notOk(oEvent.isDefaultPrevented(), "drop: Default is not prevented since there is no valid drop info");

		DragAndDrop.postprocessEvent(oEvent); // Postprocessing "drop" should not destroy the drag session.
		oEvent = createjQueryDragEventDummy("dragenter", this.oControl, false);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession != null, "Drag session is not destroyed on drop");

		oEvent = createjQueryDragEventDummy("dragstart", this.oControl);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession != null, "dragstart: A new drag session was created");

		DragAndDrop.postprocessEvent(oEvent);
		assert.ok(jQuery("html").hasClass("sapUiDnDNoScrolling"), "scrolling of the html element is blocked");

		oEvent = createjQueryDragEventDummy("dragstart", this.oControl, true);
		oEvent.target.dataset.sapUiRelated = this.oControl.getId();
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession != null && oEvent.dragSession != oDragSession,
			"dragstart: A new drag session was created for an element with a data-sap-ui-related control id");

		oDragSession = oEvent.dragSession;
		DragAndDrop.postprocessEvent(oEvent); // Postprocessing "dragstart" registers a global "dragend" event handler

		oEvent = createjQueryDragEventDummy("dragend", this.oControl, false);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession === oDragSession, "dragend: Drag session was preserved");

		DragAndDrop.postprocessEvent(oEvent);
		assert.notOk(jQuery("html").hasClass("sapUiDnDNoScrolling"), "scrolling of the html element is retained");

		DragAndDrop.postprocessEvent(oEvent); // Postprocessing "dragend" event. Drag session should be destroyed.
		oEvent = createjQueryDragEventDummy("dragenter", this.oControl, false);
		DragAndDrop.preprocessEvent(oEvent);
		assert.ok(oEvent.dragSession == null, "Drag session was destroyed");
	});

	QUnit.module("Events", {
		beforeEach: function() {
			this.oControl = new DragAndDropControl({
				topItems: [
					new DivControl(),
					new DivControl()
				],
				dragDropConfig: [
					new DragDropInfo({
						targetElement: "dummy"
					})
				]
			});
			this.oControl.placeAt("qunit-fixture");
			Core.applyChanges();
		},
		afterEach: function() {
			this.oControl.destroy();
		}
	});

	QUnit.test("Preprocessor", function(assert) {
		assert.ok(UIArea.getEventPreprocessors().indexOf(DragAndDrop.preprocessEvent) >= 0, "The UIArea contains the added preprocessor");
	});

	QUnit.test("Postprocessor", function(assert) {
		assert.ok(UIArea.getEventPostprocessors().indexOf(DragAndDrop.postprocessEvent) >= 0, "The UIArea contains the added postprocessor");
	});

	QUnit.test("Processing sequence", function(assert) {
		var aEventSequence = [];
		var oEventTarget = this.oControl.getDomRef();
		var i;

		oEventTarget.focus();

		this.oControl.ondragstart = function(oEvent) {aEventSequence.push({processor: "control", eventType: "dragstart"});};
		this.oControl.ondragenter = function(oEvent) {aEventSequence.push({processor: "control", eventType: "dragenter"});};
		this.oControl.ondragover = function(oEvent) {aEventSequence.push({processor: "control", eventType: "dragover"});};
		this.oControl.ondragleave = function(oEvent) {aEventSequence.push({processor: "control", eventType: "dragleave"});};
		this.oControl.ondragend = function(oEvent) {aEventSequence.push({processor: "control", eventType: "dragend"});};
		this.oControl.ondrop = function(oEvent) {aEventSequence.push({processor: "control", eventType: "drop"});};

		var fnOriginalDragAndDropEventPreprocessor;
		var fnOriginalDragAndDropEventPostprocessor;
		var aEventPreprocessors = UIArea.getEventPreprocessors();
		var aEventPostprocessors = UIArea.getEventPostprocessors();

		var wrappedEventPreprocessor = function(oEvent) {
			aEventSequence.push({processor: "dnd-preprocessor", eventType: oEvent.type});
			fnOriginalDragAndDropEventPreprocessor.apply(this, arguments);
		};

		var wrappedEventPostprocessor = function(oEvent) {
			aEventSequence.push({processor: "dnd-postprocessor", eventType: oEvent.type});
			fnOriginalDragAndDropEventPostprocessor.apply(this, arguments);
		};

		for (i = 0; i < aEventPreprocessors.length; i++) {
			if (aEventPreprocessors[i] === DragAndDrop.preprocessEvent) {
				fnOriginalDragAndDropEventPreprocessor = aEventPreprocessors[i];
				aEventPreprocessors[i] = wrappedEventPreprocessor;
			}
		}

		for (i = 0; i < aEventPostprocessors.length; i++) {
			if (aEventPostprocessors[i] === DragAndDrop.postprocessEvent) {
				fnOriginalDragAndDropEventPostprocessor = aEventPostprocessors[i];
				aEventPostprocessors[i] = wrappedEventPostprocessor;
			}
		}

		assert.strictEqual(fnOriginalDragAndDropEventPreprocessor, DragAndDrop.preprocessEvent,
			"The drag-and-drop event preprocessor is contained in the UIArea preprocessors");
		assert.strictEqual(fnOriginalDragAndDropEventPreprocessor, DragAndDrop.preprocessEvent,
			"The drag-and-drop event postprocessor is contained in the UIArea postprocessors");

		oEventTarget.dispatchEvent(createNativeDragEventDummy("dragstart"));
		oEventTarget.dispatchEvent(createNativeDragEventDummy("dragenter"));
		oEventTarget.dispatchEvent(createNativeDragEventDummy("dragover"));
		oEventTarget.dispatchEvent(createNativeDragEventDummy("dragleave"));
		oEventTarget.dispatchEvent(createNativeDragEventDummy("drop"));
		oEventTarget.dispatchEvent(createNativeDragEventDummy("dragend"));

		assert.deepEqual(aEventSequence, [
			{processor: "dnd-preprocessor", eventType: "dragstart"},
			{processor: "control", eventType: "dragstart"},
			{processor: "dnd-postprocessor", eventType: "dragstart"},
			{processor: "dnd-preprocessor", eventType: "dragenter"},
			{processor: "control", eventType: "dragenter"},
			{processor: "dnd-postprocessor", eventType: "dragenter"},
			{processor: "dnd-preprocessor", eventType: "dragover"},
			{processor: "control", eventType: "dragover"},
			{processor: "dnd-postprocessor", eventType: "dragover"},
			{processor: "dnd-preprocessor", eventType: "dragleave"},
			{processor: "control", eventType: "dragleave"},
			{processor: "dnd-postprocessor", eventType: "dragleave"},
			{processor: "dnd-preprocessor", eventType: "drop"},
			{processor: "control", eventType: "drop"},
			{processor: "dnd-postprocessor", eventType: "drop"},
			{processor: "dnd-preprocessor", eventType: "dragend"},
			{processor: "control", eventType: "dragend"},
			{processor: "dnd-postprocessor", eventType: "dragend"}
		], "The drag-and-drop events have been processed before and after they where dispatched to the controls");

		// Restore original dnd event preprocessor.
		fnOriginalDragAndDropEventPreprocessor = DragAndDrop.preprocessEvent;
	});

	QUnit.test("Simulated longdragover", function(assert) {
		var oEventTarget;
		var oOnLongDragOverSpy = sinon.spy(function(oEvent) {
			oOnLongDragOverSpy._oEventTarget = oEvent.target;
		});
		var iLastLongDragoverCount = 0;
		var oEvent;
		var oDateNowStub = sinon.stub(Date, "now");

		function assertLongdragover(iMsSinceDragEnter) {
			var iCallCount = Math.floor(iMsSinceDragEnter / 1000);

			oEventTarget.focus();
			oEventTarget.dispatchEvent(createNativeDragEventDummy("dragover"));
			assert.strictEqual(oOnLongDragOverSpy.callCount, iCallCount,
				"Time since dragenter: " + iMsSinceDragEnter + "ms - longdragover count: " + iCallCount);

			if (iCallCount > 0 && iCallCount !== iLastLongDragoverCount) {
				assert.strictEqual(oOnLongDragOverSpy._oEventTarget, oEventTarget, "The longdragover event was dispatched at the correct target");
			}
			iLastLongDragoverCount = iCallCount;
		}

		this.oControl.onlongdragover = oOnLongDragOverSpy;
		oEvent = createjQueryDragEventDummy("dragstart", this.oControl);
		oEvent.target.focus();
		DragAndDrop.preprocessEvent(oEvent); // Create drag session.

		function setTime(iTimeInMs) {
			oDateNowStub.returns(iTimeInMs);
		}

		setTime(0);
		oEventTarget = this.oControl.getTopItems()[0].getDomRef();
		oEventTarget.dispatchEvent(createNativeDragEventDummy("dragenter"));
		assertLongdragover(0);
		setTime(999);
		assertLongdragover(999);
		setTime(1000);
		assertLongdragover(1000);
		setTime(1999);
		assertLongdragover(1999);
		setTime(2000);
		assertLongdragover(2000);
		oEventTarget = this.oControl.getTopItems()[1].getDomRef();
		setTime(2999);
		oEventTarget.dispatchEvent(createNativeDragEventDummy("dragenter"));
		oOnLongDragOverSpy.reset();
		assertLongdragover(0);
		setTime(3000);
		assertLongdragover(1);
		setTime(3999);
		assertLongdragover(1000);
		oDateNowStub.restore();
	});

	QUnit.module("Between Indicator", {
		beforeEach: function() {
			this.oControl = new DragAndDropControl({
				topItems: [new DivControl(), new DivControl()],
				dragDropConfig: [
					new DragDropInfo({
						sourceAggregation: "topItems",
						targetAggregation: "topItems",
						dropPosition: "Between"
					})
				]
			});
			this.oControl.placeAt("qunit-fixture");
			Core.applyChanges();
		},
		afterEach: function() {
			this.oControl.destroy();
		}
	});

	QUnit.test("Indicator position with dropLayout property", function(assert) {
		var oEvent, $Indicator, mIndicatorOffset, mTargetOffset;
		var oDiv1 = this.oControl.getTopItems()[0];
		var oDiv2 = this.oControl.getTopItems()[1];

		// init drag session
		oEvent = createjQueryDragEventDummy("dragstart", oDiv1);
		oEvent.target.focus();
		DragAndDrop.preprocessEvent(oEvent);

		// validation
		oEvent = createjQueryDragEventDummy("dragenter", oDiv2);
		oEvent.target.focus();
		DragAndDrop.preprocessEvent(oEvent);

		// act for top indicator
		oEvent = createjQueryDragEventDummy("dragover", oDiv2);
		mTargetOffset = oDiv2.$().offset();
		oEvent.pageY = mTargetOffset.top + 1;
		oEvent.pageX = mTargetOffset.left + 1;
		oDiv2.$().trigger(oEvent);
		$Indicator = jQuery(oEvent.dragSession.getIndicator());
		mIndicatorOffset = $Indicator.offset();

		assert.strictEqual($Indicator.attr("data-drop-position"), "Between", "Indicator's data-drop-position attribute is set to between");
		assert.strictEqual($Indicator.attr("data-drop-layout"), "Vertical", "Indicator's data-drop-layout attribute is set to vertical.");
		assert.strictEqual($Indicator.width(), oDiv2.$().width() , "Indicator's width is equal to dropped item's width.");
		assert.strictEqual(mIndicatorOffset.top, mTargetOffset.top , "Indicator's top position is equal to dropped item's top position.");
		assert.strictEqual(mIndicatorOffset.left, mTargetOffset.left , "Indicator's left position is equal to dropped item's left position.");

		// act for bottom indicator
		oEvent = createjQueryDragEventDummy("dragover", oDiv2);
		mTargetOffset = oDiv2.$().offset();
		oEvent.pageX = mTargetOffset.left + 10;
		oEvent.pageY = mTargetOffset.top + oDiv2.$().height() - 1;
		oDiv2.$().trigger(oEvent);
		$Indicator = jQuery(oEvent.dragSession.getIndicator());
		mIndicatorOffset = $Indicator.offset();

		assert.strictEqual($Indicator.attr("data-drop-layout"), "Vertical", "Indicator's data-drop-layout attribute is still vertical.");
		assert.strictEqual($Indicator.width(), oDiv2.$().width() , "Indicator's width is equal to dropped item's width.");
		assert.strictEqual(mIndicatorOffset.left, mTargetOffset.left , "Indicator's left position is equal to dropped item's left position.");
		assert.strictEqual(mIndicatorOffset.top, mTargetOffset.top + oDiv2.$().height(), "Indicator's bottom position is equal to dropped item's bottom position.");

		// change the drop layout
		this.oControl.getDragDropConfig()[0].setDropLayout("Horizontal");

		// act for bottom indicator
		oEvent = createjQueryDragEventDummy("dragover", oDiv2);
		mTargetOffset = oDiv2.$().offset();
		oEvent.pageY = mTargetOffset.top + 1;
		oEvent.pageX = mTargetOffset.left + 1;
		oDiv2.$().trigger(oEvent);
		$Indicator = jQuery(oEvent.dragSession.getIndicator());
		mIndicatorOffset = $Indicator.offset();

		assert.strictEqual($Indicator.attr("data-drop-layout"), "Horizontal", "Indicator's data-drop-layout attribute is set to Horizontal.");
		assert.strictEqual($Indicator.height(), oDiv2.$().height() , "Indicator's height is equal to dropped item's height.");
		assert.strictEqual(mIndicatorOffset.top, mTargetOffset.top , "Indicator's top position is equal to dropped item's top position.");
		assert.strictEqual(mIndicatorOffset.left, mTargetOffset.left , "Indicator's left position is equal to dropped item's bottom position.");

		// act for right indicator
		oEvent = createjQueryDragEventDummy("dragover", oDiv2);
		mTargetOffset = oDiv2.$().offset();
		oEvent.pageY = mTargetOffset.top + 1;
		oEvent.pageX = mTargetOffset.left + oDiv2.$().width() - 1;
		oDiv2.$().trigger(oEvent);
		$Indicator = jQuery(oEvent.dragSession.getIndicator());
		mIndicatorOffset = $Indicator.offset();

		assert.strictEqual($Indicator.attr("data-drop-layout"), "Horizontal", "Indicator's data-drop-layout attribute is still Horizontal.");
		assert.strictEqual($Indicator.height(), oDiv2.$().height() , "Indicator's height is equal to dropped item's height.");
		assert.strictEqual(mIndicatorOffset.top, mTargetOffset.top , "Indicator's top position is equal to dropped item's top position.");
		assert.strictEqual(mIndicatorOffset.left, mTargetOffset.left + oDiv2.$().width(), "Indicator's left position is equal to dropped item's right position.");
		assert.strictEqual(oEvent.dragSession.getDropPosition(), "After", "Drop position is set correctly");

		// act for the RTL mode
		sinon.stub(sap.ui.getCore().getConfiguration(), "getRTL").callsFake(function() {
			return true;
		});

		oDiv2.$().trigger(oEvent);
		assert.strictEqual(oEvent.dragSession.getDropPosition(), "Before", "Drop position is set correctly for the RTL mode");

		// drop
		oDiv2.$().trigger("drop");
		assert.ok($Indicator.is(":visible"), "Indicator is still visible after drop");

		// cleanup
		oDiv2.$().trigger("dragend");
		assert.ok($Indicator.is(":hidden"), "Indicator is hidden after dragend");
	});

	QUnit.module("Drop on empty aggregation", {
		beforeEach: function() {
			this.oControl = new DragAndDropControl({
				topItems: [new DivControl(), new DivControl()],
				showNoData: true,
				dragDropConfig: [
					new DragDropInfo({
						sourceAggregation: "topItems",
						targetAggregation: "bottomItems",
						dropPosition: "OnOrBetween"
					})
				]
			});
			this.oControl.placeAt("qunit-fixture");
			Core.applyChanges();
		},
		afterEach: function() {
			this.oControl.destroy();
		}
	});

	QUnit.test("Indicator position for no data", function(assert) {
		var oEvent, $Indicator, mIndicatorOffset, mTargetOffset;
		var oBottomItemsDomRef = this.oControl.getDomRef("bottomItems");
		var oTargetDomRef = this.oControl.getDomRef("bottomNoData");
		var oSourceControl = this.oControl.getTopItems()[0];
		var oSourceDomRef = oSourceControl.getDomRef();

		// init drag session
		oSourceDomRef.focus();
		oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		// validation
		oTargetDomRef.focus();
		oTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));

		// act for the indicator
		oTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragover"));

		oEvent = jQuery.Event("dragover");
		oEvent.originalEvent = createNativeDragEventDummy("dragover");
		mTargetOffset = oTargetDomRef.getBoundingClientRect();
		oEvent.pageY = mTargetOffset.top + 1;
		oEvent.pageX = mTargetOffset.left + 1;
		jQuery(oTargetDomRef).trigger(oEvent);
		$Indicator = jQuery(oEvent.dragSession.getIndicator());
		mIndicatorOffset = $Indicator.offset();

		assert.strictEqual($Indicator.attr("data-drop-position"), "On", "Indicator's data-drop-position attribute is set to on");
		assert.strictEqual($Indicator.outerWidth(), oBottomItemsDomRef.offsetWidth , "Indicator's width is equal to dropped item's width.");
		assert.strictEqual($Indicator.outerHeight(), oBottomItemsDomRef.offsetHeight , "Indicator's height is equal to dropped item's height.");
		assert.strictEqual(mIndicatorOffset.top, mTargetOffset.top , "Indicator's top position is equal to dropped item's top position.");
		assert.strictEqual(mIndicatorOffset.left, mTargetOffset.left , "Indicator's left position is equal to dropped item's left position.");

		// clean up
		oTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragend"));
		assert.ok($Indicator.is(":hidden"), "Indicator is hidden after dragend");
	});

	QUnit.test("cleanup", function(assert) {
		var done = assert.async();
		var oEvent, $Indicator;
		var oTargetDomRef = this.oControl.getDomRef("bottomNoData");
		var oSourceControl = this.oControl.getTopItems()[0];
		var oSourceDomRef = oSourceControl.getDomRef();

		// init drag session
		oSourceDomRef.focus();
		oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		// validation
		oEvent = jQuery.Event("dragenter");
		oEvent.originalEvent = createNativeDragEventDummy("dragenter");
		jQuery(oTargetDomRef).trigger(oEvent);
		$Indicator = jQuery(oEvent.dragSession.getIndicator());
		assert.ok($Indicator.is(":visible"), "Indicator is visible after dragenter");

		// drop handling indicator
		oTargetDomRef.focus();
		oTargetDomRef.dispatchEvent(createNativeDragEventDummy("drop"));

		// assert
		window.requestAnimationFrame(function() {
			assert.ok($Indicator.is(":hidden"), "Indicator is hidden after drop without dragend");
			done();
		});
	});

	QUnit.module("dragSession", {
		beforeEach: function() {
			this.oContainer = new DragAndDropControl({
				topItems: [this.oSourceControl = new DivControl()],
				bottomItems: [this.oTargetControl = new DivControl(), this.oLastTargetControl = new DivControl()],
				dragDropConfig: [
					this.oDragInfo = new DragInfo({
						sourceAggregation: "topItems"
					}),
					this.oDropInfo = new DropInfo({
						targetAggregation: "bottomItems"
					})
				]
			});

			this.oContainer.placeAt("qunit-fixture");
			Core.applyChanges();

			this.oTargetDomRef = this.oTargetControl.getDomRef();
			this.oSourceDomRef = this.oSourceControl.getDomRef();
			this.oLastTargetDomRef = this.oLastTargetControl.getDomRef();
		},
		afterEach: function() {
			this.oContainer.destroy();
		}
	});

	QUnit.test("dataTransfer", function(assert) {
		var oDataTransfer = {
			data: "dataValue",
			textData : "textData",
			complexData: ["complexData"]
		};

		this.oSourceDomRef.focus();
		this.oDragInfo.attachDragStart(function(oEvent) {
			var oSession = oEvent.getParameter("dragSession");
			oSession.setData("data", oDataTransfer.data);
			oSession.setTextData(oDataTransfer.textData);
			oSession.setComplexData("complexData", oDataTransfer.complexData);
		});
		this.oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		function sessionTest(oEvent) {
			var oSession = oEvent.getParameter("dragSession");
			assert.strictEqual(oSession.getData("data"), oDataTransfer.data, "data is transfered over drag session");
			assert.strictEqual(oSession.getTextData(), oDataTransfer.textData, "text data is transfered over drag session");
			assert.strictEqual(oSession.getComplexData("complexData"), oDataTransfer.complexData, "complex data is transfered over drag session");
		}

		this.oLastTargetDomRef.focus();
		this.oDropInfo.attachDragEnter(sessionTest);
		this.oDropInfo.attachDragOver(sessionTest);
		this.oDropInfo.attachDrop(function() {
			this.oSourceControl.destroy();
		}.bind(this));
		this.oDragInfo.attachDragEnd(sessionTest);
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragover"));
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("drop"));
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragend"));
	});

	QUnit.test("dragged from outside the browser", function(assert) {
		this.oTargetDomRef.focus();
		this.oDropInfo.attachDragEnter(function(oEvent) {
			assert.ok(oEvent.getParameter("dragSession"), "drag session exists");
		});
		this.oTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));
	});

	QUnit.test("setDropControl", function(assert) {
		this.oSourceDomRef.focus();
		this.oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		this.oLastTargetDomRef.focus();
		this.oDropInfo.setDropPosition("OnOrBetween");
		this.oDropInfo.attachDragEnter(function(oEvent) {
			var oSession = oEvent.getParameter("dragSession");
			oSession.setDropControl(this.oTargetControl);
		}, this);
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));

		this.oDropInfo.attachDrop(function(oEvent) {
			assert.strictEqual(oEvent.getParameter("droppedControl"), this.oTargetControl, "Drop control is changed");
		}, this);
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("drop"));
	});

	QUnit.test("setIndicatorConfig", function(assert) {
		this.oSourceDomRef.focus();
		this.oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		this.oLastTargetDomRef.focus();
		this.oLastTargetControl.ondragenter = function(oEvent) {
			oEvent.dragSession.setIndicatorConfig({
				borderLeft: 0,
				paddingTop: 10,
				left: 100
			});
		};

		this.oDropInfo.attachDragOver(function(oEvent) {
			var oSession = oEvent.getParameter("dragSession");
			var $Indicator = jQuery(oSession.getIndicator());
			assert.strictEqual($Indicator.css("border-left-width"), "0px", "Custom borderLeft is set correctly");
			assert.strictEqual($Indicator.css("padding-top"), "10px", "Custom paddingTop is set correctly");
			assert.strictEqual($Indicator.css("left"), "100px", "Custom left is set correctly");
		}, this);

		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragover"));
		this.oLastTargetDomRef.dispatchEvent(createNativeDragEventDummy("drop"));
	});

	QUnit.module("groupName", {
		beforeEach: function() {
			this.oContainer = new DragAndDropControl({
				topItems: [this.oSourceControl = new DivControl()],
				bottomItems: [this.oTargetControl = new DivControl()],
				dragDropConfig: [
					this.oDragInfo = new DragInfo({
						sourceAggregation: "topItems",
						dragStart: this.fnDragStartSpy = sinon.spy(function() {})
					}),
					this.oDropInfo = new DropInfo({
						targetAggregation: "bottomItems",
						dragEnter: this.fnDragEnterSpy = sinon.spy(),
						drop: this.fnDropSpy = sinon.spy()
					})
				]
			});

			this.oContainer.placeAt("qunit-fixture");
			Core.applyChanges();

			this.oTargetDomRef = this.oTargetControl.getDomRef();
			this.oSourceDomRef = this.oSourceControl.getDomRef();
		},
		afterEach: function() {
			this.oContainer.destroy();
		}
	});

	QUnit.test("Matching groupNames", function(assert) {
		this.oDragInfo.setGroupName("abc");
		this.oDropInfo.setGroupName("abc");

		this.oSourceDomRef.focus();
		this.oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		this.oTargetDomRef.focus();
		this.oTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));
		assert.ok(this.fnDragEnterSpy.calledOnce, "dragEnter event is called once.");

		this.oTargetDomRef.dispatchEvent(createNativeDragEventDummy("drop"));
		assert.ok(this.fnDropSpy.calledOnce, "drop event is called once.");
	});

	QUnit.test("Unmatching groupNames", function(assert) {
		this.oDragInfo.setGroupName("abc");
		this.oDropInfo.setGroupName("xxx");

		this.oSourceDomRef.focus();
		this.oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		this.oTargetDomRef.focus();
		this.oTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));
		assert.equal(this.fnDragEnterSpy.callCount, 0, "dragEnter event is not called");

		this.oTargetDomRef.dispatchEvent(createNativeDragEventDummy("drop"));
		assert.equal(this.fnDropSpy.callCount, 0, "drop event is not called");
	});

	QUnit.test("Master groups", function(assert) {
		this.oDragInfo.setGroupName("abc");
		this.oDropInfo.setGroupName("");

		this.oSourceDomRef.focus();
		this.oSourceDomRef.dispatchEvent(createNativeDragEventDummy("dragstart"));

		this.oTargetDomRef.focus();
		this.oTargetDomRef.dispatchEvent(createNativeDragEventDummy("dragenter"));
		assert.ok(this.fnDragEnterSpy.calledOnce, "dragEnter event is called once.");

		this.oTargetDomRef.dispatchEvent(createNativeDragEventDummy("drop"));
		assert.ok(this.fnDropSpy.calledOnce, "drop event is called once.");
	});

});