describe("Ext.event.gesture.Drag", function() {
    var helper = Ext.testHelper,
        recognizer = Ext.event.gesture.Drag.instance,
        minDistance = recognizer.getMinDistance(),
        targetEl, dragstartHandler, dragHandler, dragendHandler, dragcancelHandler,
        dragstartEvent, dragEvent, dragendEvent, dragcancelEvent;

    function start(cfg, target) {
        helper.touchStart(target || targetEl, cfg);
    }

    function move(cfg, target) {
        helper.touchMove(target || targetEl, cfg);
    }

    function end(cfg, target) {
        helper.touchEnd(target || targetEl, cfg);
    }

    function cancel(cfg, target) {
        helper.touchCancel(target || targetEl, cfg);
    }

    function expectInfo(e, info) {
        var name;

        for (name in info) {
            expect(e[name]).toBe(info[name]);
        }
    }

    beforeEach(function() {
        targetEl = Ext.getBody().createChild({
            id: 'target',
            style: 'width: 200px; height: 200px; border: 1px solid red;'
        });
        dragstartHandler = jasmine.createSpy();
        dragHandler = jasmine.createSpy();
        dragendHandler = jasmine.createSpy();
        dragcancelHandler = jasmine.createSpy();

        dragstartHandler.andCallFake(function(event) {
            dragstartEvent = event;
        });

        dragHandler.andCallFake(function(event) {
            dragEvent = event;
        });

        dragendHandler.andCallFake(function(event) {
            dragendEvent = event;
        });

        dragcancelHandler.andCallFake(function(event) {
            dragcancelEvent = event;
        });

        targetEl.on('dragstart', dragstartHandler);
        targetEl.on('drag', dragHandler);
        targetEl.on('dragend', dragendHandler);
        targetEl.on('dragcancel', dragcancelHandler);
    });

    afterEach(function() {
        targetEl.destroy();
    });

    it("should fire dragstart, drag, and dragend when the distance exceeds minDistance", function() {
        runs(function() {
            start({ id: 1, x: 100, y: 101 });
            move({ id: 1, x: 99, y: 101 - minDistance });
        });

        waitsForAnimation();

        runs(function() {
            expect(dragstartHandler).toHaveBeenCalled();
            expect(dragHandler).toHaveBeenCalled();

            expectInfo(dragstartEvent, {
                x: 99,
                y: 101 - minDistance,
                pageX: 99,
                pageY: 101 - minDistance,
                startX: 100,
                startY: 101,
                previousX: 100,
                previousY: 101,
                deltaX: -1,
                deltaY: -minDistance,
                absDeltaX: 1,
                absDeltaY: minDistance,
                previousDeltaX: 0,
                previousDeltaY: 0,
                longpress: false
            });

            expectInfo(dragEvent, {
                x: 99,
                y: 101 - minDistance,
                pageX: 99,
                pageY: 101 - minDistance,
                startX: 100,
                startY: 101,
                previousX: 100,
                previousY: 101,
                deltaX: -1,
                deltaY: -minDistance,
                absDeltaX: 1,
                absDeltaY: minDistance,
                previousDeltaX: 0,
                previousDeltaY: 0,
                longpress: false
            });

            move({ id: 1, x: 97, y: 100 - minDistance });
        });

        waitsForAnimation();

        runs(function() {
            expect(dragHandler.callCount).toBe(2);

            expectInfo(dragEvent, {
                x: 97,
                y: 100 - minDistance,
                pageX: 97,
                pageY: 100 - minDistance,
                startX: 100,
                startY: 101,
                previousX: 99,
                previousY: 101 - minDistance,
                deltaX: -3,
                deltaY: -(minDistance + 1),
                absDeltaX: 3,
                absDeltaY: minDistance + 1,
                previousDeltaX: -1,
                previousDeltaY: -minDistance,
                longpress: false
            });

            end({ id: 1, x: 96, y: 99 - minDistance });
        });

        waitsForAnimation();

        runs(function() {
            expect(dragendHandler).toHaveBeenCalled();

            expectInfo(dragendEvent, {
                x: 96,
                y: 99 - minDistance,
                pageX: 96,
                pageY: 99 - minDistance,
                startX: 100,
                startY: 101,
                previousX: 97,
                previousY: 100 - minDistance,
                deltaX: -4,
                deltaY: -(minDistance + 2),
                absDeltaX: 4,
                absDeltaY: minDistance + 2,
                previousDeltaX: -3,
                previousDeltaY: -(minDistance + 1),
                longpress: false
            });
        });
    });

    it("should not fire dragstart, drag, and dragend when the distance is less than minDistance", function() {
        runs(function() {
            start({ id: 1, x: 100, y: 101 });
            move({ id: 1, x: 99, y: 99 + minDistance });
            end({ id: 1, x: 99, y: 99 + minDistance });
        });

        waitsForAnimation();

        runs(function() {
            expect(dragstartHandler).not.toHaveBeenCalled();
            expect(dragHandler).not.toHaveBeenCalled();
            expect(dragendHandler).not.toHaveBeenCalled();
        });
    });

    it("should not fire dragstart when touchstart is stopped and the sequence tap in, tap out, tap in is followed", function() {
        var touchStart = jasmine.createSpy();
        targetEl.on('touchstart', touchStart.andCallFake(function(e) {
            e.stopPropagation();
        }));

        start({id: 1, x: 100, y: 101});
        end({id: 1, x: 100, y: 100});

        expect(touchStart.callCount).toBe(1);
        expect(dragstartHandler).not.toHaveBeenCalled();

        start({id: 2, x: 400, y: 400}, Ext.getBody());
        end({id: 2, x: 400, y: 400}, Ext.getBody());

        expect(touchStart.callCount).toBe(1);
        expect(dragstartHandler).not.toHaveBeenCalled();

        start({id: 3, x: 100, y: 101});
        end({id: 3, x: 100, y: 100});

        expect(touchStart.callCount).toBe(2);
        expect(dragstartHandler).not.toHaveBeenCalled();
    });

    if (Ext.supports.Touch) {
        it("should fire dragcancel and not dragend if the touch is canceled after dragstart", function() {
            runs(function() {
                start({ id: 1, x: 100, y: 101 });
                move({ id: 1, x: 99, y: 101 - minDistance });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragstartHandler).toHaveBeenCalled();
                expect(dragHandler).toHaveBeenCalled();
                move({ id: 1, x: 97, y: 100 - minDistance });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragHandler.callCount).toBe(2);
                cancel({ id: 1, x: 96, y: 99 - minDistance });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragendHandler).not.toHaveBeenCalled();
                expect(dragcancelHandler).toHaveBeenCalled();

                expectInfo(dragcancelEvent, {
                    x: 96,
                    y: 99 - minDistance,
                    pageX: 96,
                    pageY: 99 - minDistance,
                    startX: 100,
                    startY: 101,
                    previousX: 97,
                    previousY: 100 - minDistance,
                    deltaX: -4,
                    deltaY: -(minDistance + 2),
                    absDeltaX: 4,
                    absDeltaY: minDistance + 2,
                    previousDeltaX: -3,
                    previousDeltaY: -(minDistance + 1),
                    longpress: false
                });
            });
        });

        it("should fire dragcancel when a second touch begins", function() {
            runs(function() {
                start({ id: 1, x: 100, y: 101 });
                move({ id: 1, x: 99, y: 101 - minDistance });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragstartHandler).toHaveBeenCalled();
                expect(dragHandler).toHaveBeenCalled();
                move({ id: 1, x: 97, y: 100 - minDistance });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragHandler.callCount).toBe(2);
                start({ id: 2, x: 200, y: 300 });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragendHandler).not.toHaveBeenCalled();
                expect(dragcancelHandler).toHaveBeenCalled();

                expectInfo(dragcancelEvent, {
                    x: 97,
                    y: 100 - minDistance,
                    pageX: 97,
                    pageY: 100 - minDistance,
                    startX: 100,
                    startY: 101,
                    previousX: 99,
                    previousY: 101 - minDistance,
                    deltaX: -3,
                    deltaY: -(minDistance + 1),
                    absDeltaX: 3,
                    absDeltaY: minDistance + 1,
                    previousDeltaX: -1,
                    previousDeltaY: -minDistance,
                    longpress: false
                });
            });
        });
    }

    it("should have the correct e.target if the mouse is moved off of the target", function() {
        runs(function() {
            start({ id: 1, x: 500, y: 300 });
            helper.touchMove(document.body, { id: 1, x: 200, y: 700 });
        });

        waitsForAnimation();

        runs(function() {
            expect(dragEvent.target).toBe(targetEl.dom);
            end({ id: 1, x: 200, y: 700 });
        });

        waitsForAnimation();
    });

    describe("longpress to drag", function() {
        it("should not initiate drag with longpress by default", function() {
            var longpressed = false;

            targetEl.on('longpress', function() {
                longpressed = true;
            });

            runs(function() {
                start({ id: 1, x: 100, y: 101 });
            });

            waitsFor(function() {
                return longpressed;
            }, "longpress handler was never called", 3000);

            runs(function() {
                expect(dragstartHandler).not.toHaveBeenCalled();
            });
        });

        it("should initiate drag with longpress when e.startDrag() is invoked", function() {
            var longpressed = false;

            targetEl.on('longpress', function(e) {
                e.startDrag();
                longpressed = true;
            });

            runs(function() {
                start({ id: 1, x: 100, y: 101 });
            });

            waitsFor(function() {
                return longpressed;
            }, "longpress handler was never called", 3000);

            runs(function() {
                expect(dragstartHandler).toHaveBeenCalled();

                expectInfo(dragstartEvent, {
                    x: 100,
                    y: 101,
                    pageX: 100,
                    pageY: 101,
                    startX: 100,
                    startY: 101,
                    previousX: 100,
                    previousY: 101,
                    deltaX: 0,
                    deltaY: 0,
                    absDeltaX: 0,
                    absDeltaY: 0,
                    previousDeltaX: 0,
                    previousDeltaY: 0,
                    longpress: true
                });
            });

            runs(function() {
                move({ id: 1, x: 99, y: 97 });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragHandler).toHaveBeenCalled();

                expectInfo(dragEvent, {
                    x: 99,
                    y: 97,
                    pageX: 99,
                    pageY: 97,
                    startX: 100,
                    startY: 101,
                    previousX: 100,
                    previousY: 101,
                    deltaX: -1,
                    deltaY: -4,
                    absDeltaX: 1,
                    absDeltaY: 4,
                    previousDeltaX: 0,
                    previousDeltaY: 0,
                    longpress: true
                });

                move({ id: 1, x: 97, y: 100 - minDistance });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragHandler.callCount).toBe(2);

                expectInfo(dragEvent, {
                    x: 97,
                    y: 100 - minDistance,
                    pageX: 97,
                    pageY: 100 - minDistance,
                    startX: 100,
                    startY: 101,
                    previousX: 99,
                    previousY: 97,
                    deltaX: -3,
                    deltaY: -(minDistance + 1),
                    absDeltaX: 3,
                    absDeltaY: minDistance + 1,
                    previousDeltaX: -1,
                    previousDeltaY: -4,
                    longpress: true
                });

                end({ id: 1, x: 96, y: 99 - minDistance });
            });

            waitsForAnimation();

            runs(function() {
                expect(dragendHandler).toHaveBeenCalled();

                expectInfo(dragendEvent, {
                    x: 96,
                    y: 99 - minDistance,
                    pageX: 96,
                    pageY: 99 - minDistance,
                    startX: 100,
                    startY: 101,
                    previousX: 97,
                    previousY: 100 - minDistance,
                    deltaX: -4,
                    deltaY: -(minDistance + 2),
                    absDeltaX: 4,
                    absDeltaY: minDistance + 2,
                    previousDeltaX: -3,
                    previousDeltaY: -(minDistance + 1),
                    longpress: true
                });
            });
        });

        it("should claim the drag gesture when startDrag is called", function() {
            var longpressHandled = false;

            targetEl.on('longpress', function(e) {
                e.startDrag();
                longpressHandled = true;
            });

            helper.touchStart(targetEl, {id: 1, x: 10, y: 15});

            waitsFor(function() {
                return longpressHandled;
            });

            runs(function() {
                expect(Ext.event.gesture.Drag.instance.isActive).toBe(true);

                expect(Ext.event.gesture.DoubleTap.instance.isActive).toBe(false);
                expect(Ext.event.gesture.EdgeSwipe.instance.isActive).toBe(false);
                expect(Ext.event.gesture.LongPress.instance.isActive).toBe(false);
                expect(Ext.event.gesture.Pinch.instance.isActive).toBe(false);
                expect(Ext.event.gesture.Rotate.instance.isActive).toBe(false);
                expect(Ext.event.gesture.Swipe.instance.isActive).toBe(false);
                expect(Ext.event.gesture.Tap.instance.isActive).toBe(false);
            });
        });
    });

    function makeRemoveSuite(useRemoveChild) {
        describe("when the target element is removed from the dom mid-drag " + (useRemoveChild ? "(using removeChild)" : "(using innerHTML)"), function() {
            var parent, target, firingTarget;

            function removeTarget() {
                if (useRemoveChild) {
                    parent.dom.removeChild(target.dom);
                } else {
                    parent.dom.innerHTML = '';
                }
            }

            beforeEach(function() {
                parent = Ext.getBody().createChild({
                    id: 'parent'
                });
                target = parent.createChild({
                    id: 'child'
                });

                // the "firingTarget" attempts to emulate the target that the browser uses
                // after an element is removed from the dom
                if (Ext.supports.TouchEvents) {
                    // with touch events, the element remains the target of current touches
                    // even after the element is removed from the dom
                    firingTarget = target;
                } else {
                    // with mouse and pointer events, once the element is removed from the dom
                    // we get a new target.  Assume the worst - we removed the element AND
                    // moved the mouse or pointer off of the parent element which has the
                    // listeners.  This is primarily to ensure that things like buffered grids
                    // work correctly when using the touch scroller and dragging past the
                    // edge of the buffer zone.
                    firingTarget = document.body;
                }
            });

            afterEach(function() {
                parent.destroy();
                target.destroy();
            });

            it("should recover gracefully when the listener is attached above the target", function() {
                runs(function() {
                    parent.on('drag', dragHandler);
                    parent.on('dragend', dragendHandler);

                    start({ id: 1, x: 100, y: 100 }, target);
                    move({ id: 1, x: 100, y: 100 + minDistance }, target);
                });

                waitsForAnimation();

                runs(function() {
                    expect(dragHandler.callCount).toBe(1);
                    removeTarget();
                    move({ id: 1, x: 120, y: 150 + minDistance }, firingTarget);
                });

                waitsForAnimation();

                runs(function() {
                    expect(dragHandler.callCount).toBe(2);
                    expect(dragEvent.target).toBe(target.dom);

                    end({ id: 1, x: 120, y: 150 + minDistance }, firingTarget);
                });

                waitsForAnimation();

                runs(function() {
                    expect(dragendHandler).toHaveBeenCalled();
                    expect(dragendEvent.target).toBe(target.dom);
                });
            });

            it("should recover gracefully when the listener is attached to the target", function() {
                runs(function() {
                    target.on('drag', dragHandler);
                    target.on('dragend', dragendHandler);

                    start({ id: 1, x: 100, y: 100 }, target);
                    move({ id: 1, x: 100, y: 100 + minDistance }, target);
                });

                waitsForAnimation();

                runs(function() {
                    expect(dragHandler.callCount).toBe(1);
                    removeTarget();
                    move({ id: 1, x: 120, y: 150 + minDistance }, firingTarget);
                });

                waitsForAnimation();

                runs(function() {
                    expect(dragHandler.callCount).toBe(2);
                    expect(dragEvent.target).toBe(target.dom);

                    end({ id: 1, x: 120, y: 150 + minDistance }, firingTarget);
                });

                waitsForAnimation();

                runs(function() {
                    expect(dragendHandler).toHaveBeenCalled();
                    expect(dragendEvent.target).toBe(target.dom);
                });
            });
        });
    }

    makeRemoveSuite(true); // using removeChild
    makeRemoveSuite(false); // using innerHTML = '' on an ancestor

});