import { fn } from '@vitest/spy';
import { setTimeout } from 'node:timers/promises';
import { select as d3_select } from 'd3-selection';
import { timerFlush as d3_timerFlush } from 'd3-timer';
import css from '../../../css/55_cursors.css?raw';

describe('iD.Map', function() {
    var content, context, map;

    beforeEach(function() {
        d3_select('head').append('style').html(css);
        content = d3_select('body').append('div');
        context = iD.coreContext().assetPath('../dist/').init().container(content);
        map = context.map();
        content.call(map);
        // Set default dimensions for map before zoom/center tests
        map.dimensions([1000, 1000]);
    });

    afterEach(function() {
        content.remove();
    });

    describe('#zoom', function() {
        it('gets and sets zoom level', function() {
            expect(map.zoom(4)).toEqual(map);
            expect(map.zoom()).toEqual(4);
        });

        it('dispatches move event when zoom changes', function() {
            const spy = fn();
            map.zoom(4);
            map.on('move', spy);
            map.zoom(5);
            expect(spy).toHaveBeenCalled();
        });

        it('dispatches no move event when zoom does not change', function() {
            const spy = fn();
            map.zoom(4);
            map.on('move', spy);
            map.zoom(4);
            expect(spy).not.toHaveBeenCalled();
        });

        it('respects minzoom', function() {
            map.minzoom(16);
            map.zoom(15);
            expect(map.zoom()).toEqual(16);
        });
    });

    describe('#zoomIn', function() {
        it('increments zoom', async () => {
            expect(map.zoom(4)).toEqual(map);
            map.zoomIn();
            await setTimeout(275);
            d3_timerFlush();
            expect(map.zoom()).toBeCloseTo(5, 6);
        });
    });

    describe('#zoomOut', function() {
        it('decrements zoom', async () => {
            expect(map.zoom(4)).toEqual(map);
            map.zoomOut();
            await setTimeout(275);
            d3_timerFlush();
            expect(map.zoom()).toBeCloseTo(3, 6);
        });
    });

    describe('#minzoom', function() {
        it('is zero by default', function() {
            expect(map.minzoom()).toEqual(0);
        });
    });

    describe('#center', function() {
        it('gets and sets center', function() {
            expect(map.center([0, 0])).toEqual(map);
            expect(map.center()[0]).toBeCloseTo(0, 6);
            expect(map.center()[1]).toBeCloseTo(0, 6);
            expect(map.center([10, 15])).toEqual(map);
            expect(map.center()[0]).toBeCloseTo(10, 6);
            expect(map.center()[1]).toBeCloseTo(15, 6);
        });

        it('dispatches move event when center changes', function() {
            const spy = fn();
            map.center([0, 0]);
            map.on('move', spy);
            map.center([1, 1]);
            expect(spy).toHaveBeenCalled();
        });

        it('dispatches no move event when center does not change', function() {
            const spy = fn();
            map.center([0, 0]);
            map.on('move', spy);
            map.center([0, 0]);
            expect(spy).not.toHaveBeenCalled();
        });
    });

    describe('#centerEase', function() {
        it('sets center', async () => {
            expect(map.center([10, 10])).toEqual(map);
            expect(map.centerEase([20, 20], 250)).toEqual(map);
            await setTimeout(275);
            d3_timerFlush();
            expect(map.center()[0]).toBeCloseTo(20, 6);
            expect(map.center()[1]).toBeCloseTo(20, 6);
        });
    });

    describe('#centerZoom', function() {
        it('gets and sets center and zoom', function() {
            expect(map.centerZoom([20, 25], 4)).toEqual(map);
            expect(map.center()[0]).toBeCloseTo(20, 6);
            expect(map.center()[1]).toBeCloseTo(25, 6);
            expect(map.zoom()).toBe(4);
        });
    });

    describe('#extent', function() {
        it('gets and sets extent', function() {
            map.dimensions([100, 100])
                .center([0, 0]);

            expect(map.extent()[0][0]).toBeCloseTo(-17.5, 0);
            expect(map.extent()[1][0]).toBeCloseTo(17.5, 0);
            map.extent([[10, 1], [30, 1]]);
            expect(map.extent()[0][0]).toBeCloseTo(10, 1);
            expect(map.extent()[1][0]).toBeCloseTo(30, 1);
            map.extent([[-1, -40], [1, -20]]);
            expect(map.extent()[0][1]).toBeGreaterThan(-41);
            expect(map.extent()[0][1]).toBeLessThan(-39);
            expect(map.extent()[1][1]).toBeGreaterThan(-21);
            expect(map.extent()[1][1]).toBeLessThan(-19);
        });
    });

    describe('surface', function() {
        it('is an SVG element', function() {
           expect(map.surface.node().tagName).toEqual('svg');
        });
    });

    describe('cursors', function() {
        var mode, behavior, point, vertex, line, area, midpoint;

        beforeEach(function() {
            mode = d3_select('body').append('div');
            behavior = mode.append('div');

            point    = behavior.append('div').attr('class', 'node point');
            vertex   = behavior.append('div').attr('class', 'node vertex');
            line     = behavior.append('div').attr('class', 'way line');
            area     = behavior.append('div').attr('class', 'way area');
            midpoint = behavior.append('div').attr('class', 'midpoint');
            // Ensure map dimensions set for any map instance created in nested tests
            if (typeof map?.dimensions === 'function') map.dimensions([1000, 1000]);
        });

        afterEach(function() {
            mode.remove();
        });

        function cursor(selection) {
            return window.getComputedStyle(selection.node()).cursor;
        }

        test('points use select-point cursor in browse and select modes', function() {
            mode.attr('class', 'ideditor mode-browse');
            expect(cursor(point)).to.match(/cursor\/select-point/);
            mode.attr('class', 'ideditor mode-select');
            expect(cursor(point)).to.match(/cursor\/select-point/);
        });

        test('vertices use select-vertex cursor in browse and select modes', function() {
            mode.attr('class', 'ideditor mode-browse');
            expect(cursor(vertex)).to.match(/cursor\/select-vertex/);
            mode.attr('class', 'ideditor mode-select');
            expect(cursor(vertex)).to.match(/cursor\/select-vertex/);
        });

        test('lines use select-line cursor in browse and select modes', function() {
            mode.attr('class', 'ideditor mode-browse');
            expect(cursor(line)).to.match(/cursor\/select-line/);
            mode.attr('class', 'ideditor mode-select');
            expect(cursor(line)).to.match(/cursor\/select-line/);
        });

        test('areas use select-area cursor in browse and select modes', function() {
            mode.attr('class', 'ideditor mode-browse');
            expect(cursor(area)).to.match(/cursor\/select-area/);
            mode.attr('class', 'ideditor mode-select');
            expect(cursor(area)).to.match(/cursor\/select-area/);
        });

        test('midpoints use select-split cursor in browse and select modes', function() {
            mode.attr('class', 'ideditor mode-browse');
            expect(cursor(midpoint)).to.match(/cursor\/select-split/);
            mode.attr('class', 'ideditor mode-select');
            expect(cursor(midpoint)).to.match(/cursor\/select-split/);
        });

        test('features use select-add cursor for adding to a selection', function() {
            mode.attr('class', 'ideditor mode-select');
            behavior.attr('class', 'behavior-multiselect');
            expect(cursor(point)).to.match(/cursor\/select-add/);
            expect(cursor(vertex)).to.match(/cursor\/select-add/);
            expect(cursor(line)).to.match(/cursor\/select-add/);
            expect(cursor(area)).to.match(/cursor\/select-add/);
        });

        test('features use select-remove cursor for removing from a selection', function() {
            mode.attr('class', 'ideditor mode-select');
            behavior.attr('class', 'behavior-multiselect');
            point.classed('selected', true);
            vertex.classed('selected', true);
            line.classed('selected', true);
            area.classed('selected', true);
            expect(cursor(point)).to.match(/cursor\/select-remove/);
            expect(cursor(vertex)).to.match(/cursor\/select-remove/);
            expect(cursor(line)).to.match(/cursor\/select-remove/);
            expect(cursor(area)).to.match(/cursor\/select-remove/);
        });

        test('targeted ways use draw-connect-line cursor in draw modes', function() {
            behavior.attr('class', 'behavior-hover');
            line.classed('target', true);
            area.classed('target', true);
            mode.attr('class', 'ideditor mode-draw-line');
            expect(cursor(line)).to.match(/cursor\/draw-connect-line/);
            expect(cursor(area)).to.match(/cursor\/draw-connect-line/);
            mode.attr('class', 'ideditor mode-draw-area');
            expect(cursor(line)).to.match(/cursor\/draw-connect-line/);
            expect(cursor(area)).to.match(/cursor\/draw-connect-line/);
            mode.attr('class', 'ideditor mode-add-line');
            expect(cursor(line)).to.match(/cursor\/draw-connect-line/);
            expect(cursor(area)).to.match(/cursor\/draw-connect-line/);
            mode.attr('class', 'ideditor mode-add-area');
            expect(cursor(line)).to.match(/cursor\/draw-connect-line/);
            expect(cursor(area)).to.match(/cursor\/draw-connect-line/);
            mode.attr('class', 'ideditor mode-drag-node');
            expect(cursor(line)).to.match(/cursor\/draw-connect-line/);
            expect(cursor(area)).to.match(/cursor\/draw-connect-line/);
        });

        test('targeted vertices use draw-connect-vertex cursor in draw modes', function() {
            behavior.attr('class', 'behavior-hover');
            vertex.classed('target', true);
            mode.attr('class', 'ideditor mode-draw-line');
            expect(cursor(vertex)).to.match(/cursor\/draw-connect-vertex/);
            mode.attr('class', 'ideditor mode-draw-area');
            expect(cursor(vertex)).to.match(/cursor\/draw-connect-vertex/);
            mode.attr('class', 'ideditor mode-add-line');
            expect(cursor(vertex)).to.match(/cursor\/draw-connect-vertex/);
            mode.attr('class', 'ideditor mode-add-area');
            expect(cursor(vertex)).to.match(/cursor\/draw-connect-vertex/);
            mode.attr('class', 'ideditor mode-drag-node');
            expect(cursor(vertex)).to.match(/cursor\/draw-connect-vertex/);
        });
    });
});
