var Plotly = require('@lib');
var Plots = require('@src/plots/plots');
var Lib = require('@src/lib');
var ScatterTernary = require('@src/traces/scatterternary');

var d3 = require('d3');
var createGraphDiv = require('../assets/create_graph_div');
var destroyGraphDiv = require('../assets/destroy_graph_div');
var fail = require('../assets/fail_test');
var customAssertions = require('../assets/custom_assertions');

var assertClip = customAssertions.assertClip;
var assertNodeDisplay = customAssertions.assertNodeDisplay;

describe('scatterternary defaults', function() {
    'use strict';

    var supplyDefaults = ScatterTernary.supplyDefaults;

    var traceIn, traceOut;

    var defaultColor = '#444',
        layout = {};

    beforeEach(function() {
        traceOut = {};
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (base case)', function() {
        traceIn = {
            a: [1, 2, 3],
            b: [1, 2, 3],
            c: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).not.toBe(true);
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'c\' is missing case)', function() {
        traceIn = {
            a: [1, 2, 3],
            b: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).not.toBe(true);
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'b\' is missing case)', function() {
        traceIn = {
            a: [1, 2, 3],
            c: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).not.toBe(true);
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\' is missing case)', function() {
        traceIn = {
            b: [1, 2, 3],
            c: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).not.toBe(true);
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'b\ and \'c\' are missing case)', function() {
        traceIn = {
            a: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).toBe(false);
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\ and \'c\' are missing case)', function() {
        traceIn = {
            b: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).toBe(false);
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\ and \'b\' are missing case)', function() {
        traceIn = {
            c: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).toBe(false);
    });

    it('should allow one of \'a\', \'b\' or \'c\' to be missing (all are missing case)', function() {
        traceIn = {};

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.visible).toBe(false);
    });

    it('should truncate data arrays to the same length (\'c\' is shortest case)', function() {
        traceIn = {
            a: [1, 2, 3],
            b: [1, 2],
            c: [1]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.a).toEqual([1]);
        expect(traceOut.b).toEqual([1]);
        expect(traceOut.c).toEqual([1]);
    });

    it('should truncate data arrays to the same length (\'a\' is shortest case)', function() {
        traceIn = {
            a: [1],
            b: [1, 2, 3],
            c: [1, 2]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.a).toEqual([1]);
        expect(traceOut.b).toEqual([1]);
        expect(traceOut.c).toEqual([1]);
    });

    it('should truncate data arrays to the same length (\'a\' is shortest case)', function() {
        traceIn = {
            a: [1, 2],
            b: [1],
            c: [1, 2, 3]
        };

        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.a).toEqual([1]);
        expect(traceOut.b).toEqual([1]);
        expect(traceOut.c).toEqual([1]);
    });

    it('should include \'name\' in \'hoverinfo\' default if multi trace graph', function() {
        traceIn = {
            type: 'scatterternary',
            a: [1, 2, 3],
            b: [1, 2, 3],
            c: [1, 2, 3]
        };

        var gd = {data: [traceIn, {}]};
        Plots.supplyDefaults(gd);

        expect(gd._fullData[0].hoverinfo).toBe('all');
    });

    it('should not include \'name\' in \'hoverinfo\' default if single trace graph', function() {
        traceIn = {
            type: 'scatterternary',
            a: [1, 2, 3],
            b: [1, 2, 3],
            c: [1, 2, 3]
        };

        var gd = {data: [traceIn]};
        Plots.supplyDefaults(gd);

        expect(gd._fullData[0].hoverinfo).toBe('a+b+c+text');
    });

    it('should correctly assign \'hoveron\' default', function() {
        traceIn = {
            a: [1, 2, 3],
            b: [1, 2, 3],
            c: [1, 2, 3],
            mode: 'lines+markers',
            fill: 'tonext'
        };

        // fills and markers, you get both hover types
        // you need visible: true here, as that normally gets set
        // outside of the module supplyDefaults
        traceOut = {visible: true};
        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.hoveron).toBe('points+fills');

        // but with only lines (or just fill) and fill tonext or toself
        // you get fills
        traceIn.mode = 'lines';
        traceOut = {visible: true};
        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.hoveron).toBe('fills');

        // without a fill you always get points. For scatterternary, unlike
        // scatter, every allowed fill but 'none' is an area fill (rather than
        // a vertical / horizontal fill) so they all should default to
        // hoveron points.
        traceIn.fill = 'none';
        traceOut = {visible: true};
        supplyDefaults(traceIn, traceOut, defaultColor, layout);
        expect(traceOut.hoveron).toBe('points');
    });
});

describe('scatterternary calc', function() {
    'use strict';

    var calc = ScatterTernary.calc;

    var gd, trace, cd;

    beforeEach(function() {
        gd = {
            _fullLayout: {
                ternary: { sum: 1 }
            }
        };

        trace = {
            subplot: 'ternary',
            sum: 1
        };
    });

    it('should fill in missing component (case \'c\')', function() {
        trace.a = [0.1, 0.3, 0.6];
        trace.b = [0.3, 0.6, 0.1];

        calc(gd, trace);
        expect(trace.c).toBeCloseToArray([0.6, 0.1, 0.3]);
    });

    it('should fill in missing component (case \'b\')', function() {
        trace.a = [0.1, 0.3, 0.6];
        trace.c = [0.1, 0.3, 0.2];

        calc(gd, trace);
        expect(trace.b).toBeCloseToArray([0.8, 0.4, 0.2]);
    });

    it('should fill in missing component (case \'a\')', function() {
        trace.b = [0.1, 0.3, 0.6];
        trace.c = [0.8, 0.4, 0.1];

        calc(gd, trace);
        expect(trace.a).toBeCloseToArray([0.1, 0.3, 0.3]);
    });

    it('should skip over non-numeric values', function() {
        trace.a = [0.1, 'a', 0.6];
        trace.b = [0.1, 0.3, null];
        trace.c = [8, 0.4, 0.1];

        cd = calc(gd, trace).map(function(obj) { delete obj.i; return obj; });

        expect(objectToArray(cd[0])).toBeCloseToArray([
            0.963414634, 0.012195121, 0.012195121, 0.012195121, 0.975609756
        ]);
        expect(cd[1]).toEqual({ x: false, y: false });
        expect(cd[2]).toEqual({ x: false, y: false });
    });

    function objectToArray(obj) {
        return Object.keys(obj).map(function(k) {
            return obj[k];
        });
    }

});

describe('scatterternary plot and hover', function() {
    'use strict';

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

    afterAll(destroyGraphDiv);

    beforeAll(function(done) {
        var gd = createGraphDiv();
        var mockCopy = Lib.extendDeep({}, mock);

        Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done);
    });

    it('should put scatterternary trace in \'frontplot\' node', function() {
        var nodes = d3.select('.frontplot').selectAll('.scatter');

        expect(nodes.size()).toEqual(1);
    });

    it('should generate one line path per trace', function() {
        var nodes = d3.selectAll('path.js-line');

        expect(nodes.size()).toEqual(mock.data.length);
    });

    it('should generate as many points as there are data points', function() {
        var nodes = d3.selectAll('path.point');

        expect(nodes.size()).toEqual(mock.data[0].a.length);
    });
});

describe('scatterternary hover', function() {
    'use strict';

    var gd;

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

        var data = [{
            type: 'scatterternary',
            a: [0.1, 0.2, 0.3],
            b: [0.3, 0.2, 0.1],
            c: [0.1, 0.4, 0.5],
            text: ['A', 'B', 'C']
        }];

        Plotly.plot(gd, data).then(done);
    });

    afterAll(destroyGraphDiv);

    function _hover(gd, xval, yval, hovermode) {
        var cd = gd.calcdata;
        var ternary = gd._fullLayout.ternary._subplot;

        var pointData = {
            index: false,
            distance: 20,
            cd: cd[0],
            trace: cd[0][0].trace,
            xa: ternary.xaxis,
            ya: ternary.yaxis
        };

        return ScatterTernary.hoverPoints(pointData, xval, yval, hovermode);
    }

    it('should generate extra text field on hover', function(done) {
        var xval = 0.42;
        var yval = 0.37;
        var hovermode = 'closest';
        var scatterPointData;

        scatterPointData = _hover(gd, xval, yval, hovermode);

        expect(scatterPointData[0].extraText).toEqual(
            'Component A: 0.3333333<br>Component B: 0.1111111<br>Component C: 0.5555556'
        );

        expect(scatterPointData[0].xLabelVal).toBeUndefined();
        expect(scatterPointData[0].yLabelVal).toBeUndefined();
        expect(scatterPointData[0].text).toEqual('C');

        Plotly.restyle(gd, {
            text: null,
            hovertext: [['apple', 'banana', 'orange']]
        })
        .then(function() {
            scatterPointData = _hover(gd, xval, yval, hovermode);

            expect(scatterPointData[0].extraText).toEqual(
                'Component A: 0.3333333<br>Component B: 0.1111111<br>Component C: 0.5555556'
            );

            expect(scatterPointData[0].xLabelVal).toBeUndefined();
            expect(scatterPointData[0].yLabelVal).toBeUndefined();
            expect(scatterPointData[0].text).toEqual('orange');
        })
        .then(done);
    });

});

describe('Test scatterternary *cliponaxis*', function() {
    afterEach(destroyGraphDiv);

    it('should show/hide point/text/errorbars in clipped and non-clipped layers', function(done) {
        var gd = createGraphDiv();
        var fig = Lib.extendDeep({}, require('@mocks/ternary_markers.json'));

        function _assert(layerClips, nodeDisplays, lineClips) {
            var frontLayer = d3.select('.frontplot');
            var scatterLayer = d3.select('.scatterlayer');

            assertClip(frontLayer, layerClips[0], 1, 'front layer');
            assertClip(scatterLayer, layerClips[1], 1, 'scatter layer');

            assertNodeDisplay(
                scatterLayer.selectAll('.point'),
                nodeDisplays,
                'scatter points'
            );
            assertNodeDisplay(
                scatterLayer.selectAll('.textpoint'),
                nodeDisplays,
                'scatter text points'
            );

            assertClip(
                scatterLayer.selectAll('.js-line'),
                lineClips[0], lineClips[1],
                'line clips'
            );
        }

        Plotly.plot(gd, fig)
        .then(function() {
            _assert(
                [false, false],
                [null, 'none', null, null, null, null, null, null, 'none', 'none', 'none'],
                [true, 1]
           );
            return Plotly.restyle(gd, 'visible', 'legendonly');
        })
        .then(function() {
            _assert(
                [false, false],
                [],
                [false, 0]
           );
            return Plotly.restyle(gd, {visible: true, cliponaxis: null});
        })
        .then(function() {
            _assert(
                [true, false],
                [null, null, null, null, null, null, null, null, null, null, null],
                [false, 1]
           );
            return Plotly.restyle(gd, 'cliponaxis', false);
        })
        .then(function() {
            _assert(
                [false, false],
                [null, 'none', null, null, null, null, null, null, 'none', 'none', 'none'],
                [true, 1]
           );
            return Plotly.relayout(gd, 'ternary.aaxis.min', 20);
        })
        .then(function() {
            _assert(
                [false, false],
                [null, 'none', null, 'none', 'none', 'none', null, 'none', 'none', 'none', 'none'],
                [true, 1]
           );
            return Plotly.relayout(gd, 'ternary.baxis.min', 40);
        })
        .then(function() {
            _assert(
                [false, false],
                ['none', 'none', 'none', 'none', 'none', 'none', null, 'none', 'none', 'none', 'none'],
                [true, 1]
           );
            return Plotly.relayout(gd, 'ternary.caxis.min', 30);
        })
        .then(function() {
            _assert(
                [false, false],
                ['none', 'none', 'none', 'none', 'none', 'none', 'none', 'none', 'none', 'none', 'none'],
                [true, 1]
           );
            return Plotly.relayout(gd, {
                'ternary.aaxis.min': null,
                'ternary.baxis.min': null,
                'ternary.caxis.min': null
            });
        })
        .then(function() {
            _assert(
                [false, false],
                [null, null, null, null, null, null, null, null, null, null, null],
                [true, 1]
           );
        })
        .catch(fail)
        .then(done);
    });
});
