var Shapes = require('@src/components/shapes');
var helpers = require('@src/components/shapes/helpers');
var constants = require('@src/components/shapes/constants');

var Plotly = require('@lib/index');
var PlotlyInternal = require('@src/plotly');
var Lib = require('@src/lib');

var Plots = PlotlyInternal.Plots;
var Axes = PlotlyInternal.Axes;

var d3 = require('d3');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var failTest = require('../assets/fail_test');
var drag = require('../assets/drag');


describe('Test shapes defaults:', function() {
    'use strict';

    function _supply(layoutIn, layoutOut) {
        layoutOut = layoutOut || {};
        layoutOut._has = Plots._hasPlotType.bind(layoutOut);

        Shapes.supplyLayoutDefaults(layoutIn, layoutOut);

        return layoutOut.shapes;
    }

    it('should skip non-array containers', function() {
        [null, undefined, {}, 'str', 0, false, true].forEach(function(cont) {
            var msg = '- ' + JSON.stringify(cont);
            var layoutIn = { shapes: cont };
            var out = _supply(layoutIn);

            expect(layoutIn.shapes).toBe(cont, msg);
            expect(out).toEqual([], msg);
        });
    });

    it('should make non-object item visible: false', function() {
        var shapes = [null, undefined, [], 'str', 0, false, true];
        var layoutIn = { shapes: shapes };
        var out = _supply(layoutIn);

        expect(layoutIn.shapes).toEqual(shapes);

        out.forEach(function(item, i) {
            expect(item).toEqual({
                visible: false,
                _input: {},
                _index: i
            });
        });
    });

    it('should provide the right defaults on all axis types', function() {
        var fullLayout = {
            xaxis: {type: 'linear', range: [0, 20]},
            yaxis: {type: 'log', range: [1, 5]},
            xaxis2: {type: 'date', range: ['2006-06-05', '2006-06-09']},
            yaxis2: {type: 'category', range: [-0.5, 7.5]}
        };

        Axes.setConvert(fullLayout.xaxis);
        Axes.setConvert(fullLayout.yaxis);
        Axes.setConvert(fullLayout.xaxis2);
        Axes.setConvert(fullLayout.yaxis2);

        var shape1In = {type: 'rect'},
            shape2In = {type: 'circle', xref: 'x2', yref: 'y2'};

        var layoutIn = {
            shapes: [shape1In, shape2In]
        };

        _supply(layoutIn, fullLayout);

        var shape1Out = fullLayout.shapes[0],
            shape2Out = fullLayout.shapes[1];

        // default positions are 1/4 and 3/4 of the full range of that axis
        expect(shape1Out.x0).toBe(5);
        expect(shape1Out.x1).toBe(15);

        // shapes use data values for log axes (like everyone will in V2.0)
        expect(shape1Out.y0).toBeWithin(100, 0.001);
        expect(shape1Out.y1).toBeWithin(10000, 0.001);

        // date strings also interpolate
        expect(shape2Out.x0).toBe('2006-06-06');
        expect(shape2Out.x1).toBe('2006-06-08');

        // categories must use serial numbers to get continuous values
        expect(shape2Out.y0).toBeWithin(1.5, 0.001);
        expect(shape2Out.y1).toBeWithin(5.5, 0.001);
    });
});

function countShapesInLowerLayer(gd) {
    return gd._fullLayout.shapes.filter(isShapeInLowerLayer).length;
}

function countShapesInUpperLayer(gd) {
    return gd._fullLayout.shapes.filter(isShapeInUpperLayer).length;
}

function countShapesInSubplots(gd) {
    return gd._fullLayout.shapes.filter(isShapeInSubplot).length;
}

function isShapeInUpperLayer(shape) {
    return shape.layer !== 'below';
}

function isShapeInLowerLayer(shape) {
    return (shape.xref === 'paper' && shape.yref === 'paper') &&
        !isShapeInUpperLayer(shape);
}

function isShapeInSubplot(shape) {
    return !isShapeInUpperLayer(shape) && !isShapeInLowerLayer(shape);
}

function countShapeLowerLayerNodes() {
    return d3.selectAll('.layer-below > .shapelayer').size();
}

function countShapeUpperLayerNodes() {
    return d3.selectAll('.layer-above > .shapelayer').size();
}

function countShapeLayerNodesInSubplots() {
    return d3.selectAll('.layer-subplot').size();
}

function countSubplots(gd) {
    return Object.keys(gd._fullLayout._plots || {}).length;
}

function countShapePathsInLowerLayer() {
    return d3.selectAll('.layer-below > .shapelayer > path').size();
}

function countShapePathsInUpperLayer() {
    return d3.selectAll('.layer-above > .shapelayer > path').size();
}

function countShapePathsInSubplots() {
    return d3.selectAll('.layer-subplot > .shapelayer > path').size();
}

describe('Test shapes:', function() {
    'use strict';

    var mock = require('@mocks/shapes.json');
    var gd;

    beforeEach(function(done) {
        gd = createGraphDiv();

        var mockData = Lib.extendDeep([], mock.data),
            mockLayout = Lib.extendDeep({}, mock.layout);

        Plotly.plot(gd, mockData, mockLayout).then(done);
    });

    afterEach(destroyGraphDiv);

    describe('*shapeLowerLayer*', function() {
        it('has one node', function() {
            expect(countShapeLowerLayerNodes()).toEqual(1);
        });

        it('has as many *path* nodes as shapes in the lower layer', function() {
            expect(countShapePathsInLowerLayer())
                .toEqual(countShapesInLowerLayer(gd));
        });

        it('should be able to get relayout', function(done) {
            Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
                expect(countShapeLowerLayerNodes()).toEqual(1);
                expect(countShapePathsInLowerLayer())
                    .toEqual(countShapesInLowerLayer(gd));
            })
            .catch(failTest)
            .then(done);
        });
    });

    describe('*shapeUpperLayer*', function() {
        it('has one node', function() {
            expect(countShapeUpperLayerNodes()).toEqual(1);
        });

        it('has as many *path* nodes as shapes in the upper layer', function() {
            expect(countShapePathsInUpperLayer())
                .toEqual(countShapesInUpperLayer(gd));
        });

        it('should be able to get relayout', function(done) {
            Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
                expect(countShapeUpperLayerNodes()).toEqual(1);
                expect(countShapePathsInUpperLayer())
                    .toEqual(countShapesInUpperLayer(gd));
            })
            .catch(failTest)
            .then(done);
        });
    });

    describe('each *subplot*', function() {
        it('has one *shapelayer*', function() {
            expect(countShapeLayerNodesInSubplots())
                .toEqual(countSubplots(gd));
        });

        it('has as many *path* nodes as shapes in the subplot', function() {
            expect(countShapePathsInSubplots())
                .toEqual(countShapesInSubplots(gd));
        });

        it('should be able to get relayout', function(done) {
            Plotly.relayout(gd, {height: 200, width: 400}).then(function() {
                expect(countShapeLayerNodesInSubplots())
                    .toEqual(countSubplots(gd));
                expect(countShapePathsInSubplots())
                    .toEqual(countShapesInSubplots(gd));
            })
            .catch(failTest)
            .then(done);
        });
    });

    function countShapes(gd) {
        return gd.layout.shapes ?
            gd.layout.shapes.length :
            0;
    }

    function getLastShape(gd) {
        return gd.layout.shapes ?
            gd.layout.shapes[gd.layout.shapes.length - 1] :
            null;
    }

    function getRandomShape() {
        return {
            x0: Math.random(),
            y0: Math.random(),
            x1: Math.random(),
            y1: Math.random()
        };
    }

    describe('Plotly.relayout', function() {
        it('should be able to add a shape', function(done) {
            var pathCount = countShapePathsInUpperLayer();
            var index = countShapes(gd);
            var shape = getRandomShape();

            Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1);
                expect(getLastShape(gd)).toEqual(shape);
                expect(countShapes(gd)).toEqual(index + 1);

                // add a shape not at the end of the array
                return Plotly.relayout(gd, 'shapes[0]', getRandomShape());
            })
            .then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(pathCount + 2);
                expect(getLastShape(gd)).toEqual(shape);
                expect(countShapes(gd)).toEqual(index + 2);
            })
            .catch(failTest)
            .then(done);
        });

        it('should be able to remove a shape', function(done) {
            var pathCount = countShapePathsInUpperLayer();
            var index = countShapes(gd);
            var shape = getRandomShape();

            Plotly.relayout(gd, 'shapes[' + index + ']', shape).then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(pathCount + 1);
                expect(getLastShape(gd)).toEqual(shape);
                expect(countShapes(gd)).toEqual(index + 1);

                return Plotly.relayout(gd, 'shapes[' + index + ']', 'remove');
            })
            .then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(pathCount);
                expect(countShapes(gd)).toEqual(index);

                return Plotly.relayout(gd, 'shapes[2].visible', false);
            })
            .then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(pathCount - 1);
                expect(countShapes(gd)).toEqual(index);

                return Plotly.relayout(gd, 'shapes[1]', null);
            })
            .then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(pathCount - 2);
                expect(countShapes(gd)).toEqual(index - 1);
            })
            .catch(failTest)
            .then(done);
        });

        it('should be able to remove all shapes', function(done) {
            Plotly.relayout(gd, { shapes: null }).then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(0);
                expect(countShapePathsInLowerLayer()).toEqual(0);
                expect(countShapePathsInSubplots()).toEqual(0);
            })
            .then(function() {
                return Plotly.relayout(gd, {'shapes[0]': getRandomShape()});
            })
            .then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(1);
                expect(countShapePathsInLowerLayer()).toEqual(0);
                expect(countShapePathsInSubplots()).toEqual(0);
                expect(gd.layout.shapes.length).toBe(1);

                return Plotly.relayout(gd, {'shapes[0]': null});
            })
            .then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(0);
                expect(countShapePathsInLowerLayer()).toEqual(0);
                expect(countShapePathsInSubplots()).toEqual(0);
                expect(gd.layout.shapes).toBeUndefined();
            })
            .catch(failTest)
            .then(done);
        });

        it('can replace the shapes array', function(done) {
            Plotly.relayout(gd, { shapes: [
                getRandomShape(),
                getRandomShape()
            ]}).then(function() {
                expect(countShapePathsInUpperLayer()).toEqual(2);
                expect(countShapePathsInLowerLayer()).toEqual(0);
                expect(countShapePathsInSubplots()).toEqual(0);
                expect(gd.layout.shapes.length).toBe(2);
            })
            .catch(failTest)
            .then(done);
        });

        it('should be able to update a shape layer', function(done) {
            var index = countShapes(gd),
                astr = 'shapes[' + index + ']',
                shape = getRandomShape(),
                shapesInLowerLayer = countShapePathsInLowerLayer(),
                shapesInUpperLayer = countShapePathsInUpperLayer();

            shape.xref = 'paper';
            shape.yref = 'paper';

            Plotly.relayout(gd, astr, shape).then(function() {
                expect(countShapePathsInLowerLayer())
                    .toEqual(shapesInLowerLayer);
                expect(countShapePathsInUpperLayer())
                    .toEqual(shapesInUpperLayer + 1);
                expect(getLastShape(gd)).toEqual(shape);
                expect(countShapes(gd)).toEqual(index + 1);
            }).then(function() {
                shape.layer = 'below';
                Plotly.relayout(gd, astr + '.layer', shape.layer);
            }).then(function() {
                expect(countShapePathsInLowerLayer())
                    .toEqual(shapesInLowerLayer + 1);
                expect(countShapePathsInUpperLayer())
                    .toEqual(shapesInUpperLayer);
                expect(getLastShape(gd)).toEqual(shape);
                expect(countShapes(gd)).toEqual(index + 1);
            }).then(function() {
                shape.layer = 'above';
                Plotly.relayout(gd, astr + '.layer', shape.layer);
            }).then(function() {
                expect(countShapePathsInLowerLayer())
                    .toEqual(shapesInLowerLayer);
                expect(countShapePathsInUpperLayer())
                    .toEqual(shapesInUpperLayer + 1);
                expect(getLastShape(gd)).toEqual(shape);
                expect(countShapes(gd)).toEqual(index + 1);
            })
            .catch(failTest)
            .then(done);
        });
    });
});

describe('shapes axis reference changes', function() {
    'use strict';

    var gd;

    beforeEach(function(done) {
        gd = createGraphDiv();

        Plotly.plot(gd, [
            {y: [1, 2, 3]},
            {y: [1, 2, 3], yaxis: 'y2'}
        ], {
            yaxis: {domain: [0, 0.4]},
            yaxis2: {domain: [0.6, 1]},
            shapes: [{
                xref: 'x', yref: 'paper', type: 'rect',
                x0: 0.8, x1: 1.2, y0: 0, y1: 1,
                fillcolor: '#eee', layer: 'below'
            }]
        }).then(done);
    });

    afterEach(destroyGraphDiv);

    function getShape(index) {
        var s = d3.selectAll('path[data-index="' + index + '"]');
        expect(s.size()).toBe(1);
        return s;
    }

    it('draws the right number of objects and updates clip-path correctly', function(done) {

        expect(getShape(0).attr('clip-path') || '').toMatch(/x\)$/);

        Plotly.relayout(gd, {
            'shapes[0].xref': 'paper',
            'shapes[0].x0': 0.2,
            'shapes[0].x1': 0.6
        })
        .then(function() {
            expect(getShape(0).attr('clip-path')).toBe(null);

            return Plotly.relayout(gd, {
                'shapes[0].yref': 'y2',
                'shapes[0].y0': 1.8,
                'shapes[0].y1': 2.2,
            });
        })
        .then(function() {
            expect(getShape(0).attr('clip-path') || '').toMatch(/^[^x]+y2\)$/);

            return Plotly.relayout(gd, {
                'shapes[0].xref': 'x',
                'shapes[0].x0': 1.5,
                'shapes[0].x1': 20
            });
        })
        .then(function() {
            expect(getShape(0).attr('clip-path') || '').toMatch(/xy2\)$/);
        })
        .catch(failTest)
        .then(done);
    });
});

describe('shapes edge cases', function() {
    'use strict';

    var gd;

    beforeEach(function() { gd = createGraphDiv(); });

    afterEach(destroyGraphDiv);

    it('falls back on shapeLowerLayer for below missing subplots', function(done) {
        Plotly.newPlot(gd, [
            {x: [1, 3], y: [1, 3]},
            {x: [1, 3], y: [1, 3], xaxis: 'x2', yaxis: 'y2'}
        ], {
            xaxis: {domain: [0, 0.5]},
            yaxis: {domain: [0, 0.5]},
            xaxis2: {domain: [0.5, 1], anchor: 'y2'},
            yaxis2: {domain: [0.5, 1], anchor: 'x2'},
            shapes: [{
                x0: 1, x1: 2, y0: 1, y1: 2, type: 'circle',
                layer: 'below',
                xref: 'x',
                yref: 'y2'
            }, {
                x0: 1, x1: 2, y0: 1, y1: 2, type: 'circle',
                layer: 'below',
                xref: 'x2',
                yref: 'y'
            }]
        }).then(function() {
            expect(countShapePathsInLowerLayer()).toBe(2);
            expect(countShapePathsInUpperLayer()).toBe(0);
            expect(countShapePathsInSubplots()).toBe(0);
        })
        .catch(failTest)
        .then(done);
    });
});

describe('shapes autosize', function() {
    'use strict';

    var gd;

    afterEach(destroyGraphDiv);

    it('should adapt to relayout calls', function(done) {
        gd = createGraphDiv();

        var mock = {
            data: [{}],
            layout: {
                shapes: [{
                    type: 'line',
                    x0: 0,
                    y0: 0,
                    x1: 1,
                    y1: 1
                }, {
                    type: 'line',
                    x0: 0,
                    y0: 0,
                    x1: 2,
                    y1: 2
                }]
            }
        };

        function assertRanges(x, y) {
            var fullLayout = gd._fullLayout;
            var PREC = 1;

            expect(fullLayout.xaxis.range).toBeCloseToArray(x, PREC, '- xaxis');
            expect(fullLayout.yaxis.range).toBeCloseToArray(y, PREC, '- yaxis');
        }

        Plotly.plot(gd, mock).then(function() {
            assertRanges([0, 2], [0, 2]);

            return Plotly.relayout(gd, { 'shapes[1].visible': false });
        })
        .then(function() {
            assertRanges([0, 1], [0, 1]);

            return Plotly.relayout(gd, { 'shapes[1].visible': true });
        })
        .then(function() {
            assertRanges([0, 2], [0, 2]);

            return Plotly.relayout(gd, { 'shapes[0].x1': 3 });
        })
        .then(function() {
            assertRanges([0, 3], [0, 2]);
        })
        .catch(failTest)
        .then(done);
    });
});

describe('Test shapes: a plot with shapes and an overlaid axis', function() {
    'use strict';

    var gd, data, layout;

    beforeEach(function() {
        gd = createGraphDiv();

        data = [{
            y: [1934.5, 1932.3, 1930.3],
            x: ['1947-01-01', '1947-04-01', '1948-07-01'],
            type: 'scatter'
        }];

        layout = {
            yaxis: {
                type: 'linear'
            },
            xaxis: {
                type: 'date'
            },
            yaxis2: {
                side: 'right',
                overlaying: 'y'
            },
            shapes: [{
                fillcolor: '#ccc',
                type: 'rect',
                x0: '1947-01-01',
                x1: '1947-04-01',
                xref: 'x',
                y0: 0,
                y1: 1,
                yref: 'paper',
                layer: 'below'
            }, {
                type: 'path',
                xref: 'x',
                yref: 'y2',
                path: 'M1947-01-01_12:00,2V4H1947-03-01Z'
            }, {
                type: 'rect',
                xref: 'x',
                yref: 'y2',
                x0: '1947-02-01',
                x1: '1947-03-01',
                y0: 3,
                y1: 5,
                layer: 'below'
            }, {
                type: 'circle',
                xref: 'x',
                yref: 'y',
                x0: '1947-01-15',
                x1: '1947-02-15',
                y0: 1931,
                y1: 1934
            }]
        };
    });

    afterEach(destroyGraphDiv);

    it('should not throw an exception', function(done) {
        Plotly.plot(gd, data, layout)
        .catch(failTest)
        .then(done);
    });
});

describe('Test shapes', function() {
    'use strict';

    var gd, data, layout, config;

    beforeEach(function() {
        gd = createGraphDiv();
        data = [{}];
        layout = {};
        config = {
            editable: true,
            displayModeBar: false
        };
    });

    afterEach(destroyGraphDiv);

    var testCases = [
        // xref: 'paper', yref: 'paper'
        {
            title: 'linked to paper'
        },

        // xaxis.type: 'linear', yaxis.type: 'log'
        {
            title: 'linked to linear and log axes',
            xaxis: { type: 'linear', range: [0, 10] },
            yaxis: { type: 'log', range: [Math.log10(1), Math.log10(1000)] }
        },

        // xaxis.type: 'date', yaxis.type: 'category'
        {
            title: 'linked to date and category axes',
            xaxis: {
                type: 'date',
                range: ['2000-01-01', '2000-02-02']
            },
            yaxis: { type: 'category', range: ['a', 'b'] }
        }
    ];

    testCases.forEach(function(testCase) {
        it(testCase.title + 'should be draggable', function(done) {
            setupLayout(testCase);
            testDragEachShape(done);
        });
    });

    testCases.forEach(function(testCase) {
        ['n', 's', 'w', 'e', 'nw', 'se', 'ne', 'sw'].forEach(function(direction) {
            var testTitle = testCase.title +
                'should be resizeable over direction ' +
                direction;
            it(testTitle, function(done) {
                setupLayout(testCase);
                testResizeEachShape(direction, done);
            });
        });
    });

    function setupLayout(testCase) {
        Lib.extendDeep(layout, testCase);

        var xrange = testCase.xaxis ? testCase.xaxis.range : [0.25, 0.75],
            yrange = testCase.yaxis ? testCase.yaxis.range : [0.25, 0.75],
            xref = testCase.xaxis ? 'x' : 'paper',
            yref = testCase.yaxis ? 'y' : 'paper',
            x0 = xrange[0],
            x1 = xrange[1],
            y0 = yrange[0],
            y1 = yrange[1];

        if(testCase.xaxis && testCase.xaxis.type === 'log') {
            x0 = Math.pow(10, x0);
            x1 = Math.pow(10, x1);
        }

        if(testCase.yaxis && testCase.yaxis.type === 'log') {
            y0 = Math.pow(10, y0);
            y1 = Math.pow(10, y1);
        }

        if(testCase.xaxis && testCase.xaxis.type === 'category') {
            x0 = 0;
            x1 = 1;
        }

        if(testCase.yaxis && testCase.yaxis.type === 'category') {
            y0 = 0;
            y1 = 1;
        }

        var x0y0 = x0 + ',' + y0,
            x1y1 = x1 + ',' + y1,
            x1y0 = x1 + ',' + y0;

        var layoutShapes = [
            { type: 'line' },
            { type: 'rect' },
            { type: 'circle' },
            {}  // path
        ];

        layoutShapes.forEach(function(s) {
            s.xref = xref;
            s.yref = yref;

            if(s.type) {
                s.x0 = x0;
                s.x1 = x1;
                s.y0 = y0;
                s.y1 = y1;
            }
            else {
                s.path = 'M' + x0y0 + 'L' + x1y1 + 'L' + x1y0 + 'Z';
            }
        });

        layout.shapes = layoutShapes;
    }

    function testDragEachShape(done) {
        var promise = Plotly.plot(gd, data, layout, config);

        var layoutShapes = gd.layout.shapes;

        expect(layoutShapes.length).toBe(4);  // line, rect, circle and path

        layoutShapes.forEach(function(layoutShape, index) {
            var dx = 100,
                dy = 100;
            promise = promise.then(function() {
                var node = getShapeNode(index);
                expect(node).not.toBe(null);

                return (layoutShape.path) ?
                    testPathDrag(dx, dy, layoutShape, node) :
                    testShapeDrag(dx, dy, layoutShape, node);
            });
        });

        return promise.then(done);
    }

    function testResizeEachShape(direction, done) {
        var promise = Plotly.plot(gd, data, layout, config);

        var layoutShapes = gd.layout.shapes;

        expect(layoutShapes.length).toBe(4);  // line, rect, circle and path

        var dxToShrinkWidth = {
                n: 0, s: 0, w: 10, e: -10, nw: 10, se: -10, ne: -10, sw: 10
            },
            dyToShrinkHeight = {
                n: 10, s: -10, w: 0, e: 0, nw: 10, se: -10, ne: 10, sw: -10
            };
        layoutShapes.forEach(function(layoutShape, index) {
            if(layoutShape.path) return;

            var dx = dxToShrinkWidth[direction],
                dy = dyToShrinkHeight[direction];

            promise = promise.then(function() {
                var node = getShapeNode(index);
                expect(node).not.toBe(null);

                return testShapeResize(direction, dx, dy, layoutShape, node);
            });

            promise = promise.then(function() {
                var node = getShapeNode(index);
                expect(node).not.toBe(null);

                return testShapeResize(direction, -dx, -dy, layoutShape, node);
            });
        });

        return promise.then(done);
    }

    function getShapeNode(index) {
        return d3.selectAll('.shapelayer path').filter(function() {
            return +this.getAttribute('data-index') === index;
        }).node();
    }

    function testShapeDrag(dx, dy, layoutShape, node) {
        var xa = Axes.getFromId(gd, layoutShape.xref),
            ya = Axes.getFromId(gd, layoutShape.yref),
            x2p = helpers.getDataToPixel(gd, xa),
            y2p = helpers.getDataToPixel(gd, ya, true);

        var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p);

        return drag(node, dx, dy).then(function() {
            var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p);

            expect(finalCoordinates.x0 - initialCoordinates.x0).toBeCloseTo(dx);
            expect(finalCoordinates.x1 - initialCoordinates.x1).toBeCloseTo(dx);
            expect(finalCoordinates.y0 - initialCoordinates.y0).toBeCloseTo(dy);
            expect(finalCoordinates.y1 - initialCoordinates.y1).toBeCloseTo(dy);
        });
    }

    function getShapeCoordinates(layoutShape, x2p, y2p) {
        return {
            x0: x2p(layoutShape.x0),
            x1: x2p(layoutShape.x1),
            y0: y2p(layoutShape.y0),
            y1: y2p(layoutShape.y1)
        };
    }

    function testPathDrag(dx, dy, layoutShape, node) {
        var xa = Axes.getFromId(gd, layoutShape.xref),
            ya = Axes.getFromId(gd, layoutShape.yref),
            x2p = helpers.getDataToPixel(gd, xa),
            y2p = helpers.getDataToPixel(gd, ya, true);

        var initialPath = layoutShape.path,
            initialCoordinates = getPathCoordinates(initialPath, x2p, y2p);

        expect(initialCoordinates.length).toBe(6);

        return drag(node, dx, dy).then(function() {
            var finalPath = layoutShape.path,
                finalCoordinates = getPathCoordinates(finalPath, x2p, y2p);

            expect(finalCoordinates.length).toBe(initialCoordinates.length);

            for(var i = 0; i < initialCoordinates.length; i++) {
                var initialCoordinate = initialCoordinates[i],
                    finalCoordinate = finalCoordinates[i];

                if(initialCoordinate.x) {
                    expect(finalCoordinate.x - initialCoordinate.x)
                        .toBeCloseTo(dx);
                }
                else {
                    expect(finalCoordinate.y - initialCoordinate.y)
                        .toBeCloseTo(dy);
                }
            }
        });
    }

    function testShapeResize(direction, dx, dy, layoutShape, node) {
        var xa = Axes.getFromId(gd, layoutShape.xref),
            ya = Axes.getFromId(gd, layoutShape.yref),
            x2p = helpers.getDataToPixel(gd, xa),
            y2p = helpers.getDataToPixel(gd, ya, true);

        var initialCoordinates = getShapeCoordinates(layoutShape, x2p, y2p);

        return drag(node, dx, dy, direction).then(function() {
            var finalCoordinates = getShapeCoordinates(layoutShape, x2p, y2p);

            var keyN, keyS, keyW, keyE;
            if(initialCoordinates.y0 < initialCoordinates.y1) {
                keyN = 'y0'; keyS = 'y1';
            }
            else {
                keyN = 'y1'; keyS = 'y0';
            }
            if(initialCoordinates.x0 < initialCoordinates.x1) {
                keyW = 'x0'; keyE = 'x1';
            }
            else {
                keyW = 'x1'; keyE = 'x0';
            }

            if(~direction.indexOf('n')) {
                expect(finalCoordinates[keyN] - initialCoordinates[keyN])
                    .toBeCloseTo(dy);
            }
            else if(~direction.indexOf('s')) {
                expect(finalCoordinates[keyS] - initialCoordinates[keyS])
                    .toBeCloseTo(dy);
            }

            if(~direction.indexOf('w')) {
                expect(finalCoordinates[keyW] - initialCoordinates[keyW])
                    .toBeCloseTo(dx);
            }
            else if(~direction.indexOf('e')) {
                expect(finalCoordinates[keyE] - initialCoordinates[keyE])
                    .toBeCloseTo(dx);
            }
        });
    }

    function getPathCoordinates(pathString, x2p, y2p) {
        var coordinates = [];

        pathString.match(constants.segmentRE).forEach(function(segment) {
            var paramNumber = 0,
                segmentType = segment.charAt(0),
                xParams = constants.paramIsX[segmentType],
                yParams = constants.paramIsY[segmentType],
                nParams = constants.numParams[segmentType],
                params = segment.substr(1).match(constants.paramRE);

            if(params) {
                params.forEach(function(param) {
                    if(paramNumber >= nParams) return;

                    if(xParams[paramNumber]) {
                        coordinates.push({ x: x2p(param) });
                    }
                    else if(yParams[paramNumber]) {
                        coordinates.push({ y: y2p(param) });
                    }

                    paramNumber++;
                });
            }
        });

        return coordinates;
    }
});
