import {afterEach, beforeEach, describe, test, expect} from 'vitest';
import {createMap, beforeMapTest, createIdentityCrsAnsweringInside, createPolarStereographicCrs, createRotatedCrs, createWidePlateCarreeCrs} from '../../util/test/util.ts';
import {addProjection, removeProjection} from '../../geo/projection/projection_crud.ts';
import {MAX_VALID_LATITUDE} from '../../util/util.ts';
import {LngLat} from '../../geo/lng_lat.ts';

beforeEach(() => {
    beforeMapTest();
    global.fetch = null;
});

describe('Map in the identity projection', () => {
    test('loads a style that declares the identity projection', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: 'identity'}}});
        await map.once('style.load');

        expect(map.getProjection()).toEqual({type: 'identity'});
    });

    test('keeps an initial center north of the mercator latitude limit', async () => {
        const latitudeNorthOfMercatorLimit = MAX_VALID_LATITUDE + 1;
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: 'identity'}}, center: [0, latitudeNorthOfMercatorLimit], zoom: 6});
        await map.once('style.load');

        expect(map.getCenter().lat).toBeCloseTo(latitudeNorthOfMercatorLimit, 6);
    });

    test('returns to the identity projection after a round trip through globe', async () => {
        const map = createMap();
        await map.once('style.load');

        map.setProjection({type: 'identity'});
        map.setProjection({type: 'globe'});
        expect(map.getProjection()).toEqual({type: 'globe'});

        map.setProjection({type: 'identity'});
        expect(map.getProjection()).toEqual({type: 'identity'});
    });

    test('keeps mercator between its latitude limits after max bounds were cleared in the identity projection', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: 'identity'}}, zoom: 3});
        await map.once('style.load');
        map.setMaxBounds(null);
        map.setProjection({type: 'mercator'});

        map.setCenter([0, 89]);

        expect(map.getCenter().lat).toBeCloseTo(84.231948, 6);
    });

    test('stops the center at the east edge of the world square', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: 'identity'}}, zoom: 3});
        await map.once('style.load');

        map.setCenter([170, 0]);

        expect(map.getCenter().lng).toBeCloseTo(90, 6);
    });
});

describe('Map in a CRS registered with addProjection', () => {
    const crs = createRotatedCrs();

    beforeEach(() => {
        addProjection(crs);
    });

    afterEach(() => {
        removeProjection(crs.name);
    });

    test('setProjection accepts the registered name', async () => {
        const map = createMap();
        await map.once('style.load');

        map.setProjection({type: crs.name});

        expect(map.getProjection()).toEqual({type: crs.name});
    });

    test('projects lng/lat to the screen through the registered CRS', async () => {
        const map = createMap();
        await map.once('style.load');
        map.setProjection({type: crs.name});
        const worldSizeAtZoom0 = 512;
        const worldOffsetInContainer = (worldSizeAtZoom0 - map.getContainer().clientWidth) / 2;
        const worldFractionOfCrsPoint = {x: 0.7, y: 0.5};
        const {origin, extentAtZoom0} = crs.tileMatrixSet;
        const crsPoint = {x: origin[0] + worldFractionOfCrsPoint.x * extentAtZoom0, y: origin[1] - worldFractionOfCrsPoint.y * extentAtZoom0};

        const [lng, lat] = crs.projection.inverse([crsPoint.x, crsPoint.y]);
        const screenPoint = map.project([lng, lat]);

        expect(screenPoint.x).toBeCloseTo(worldFractionOfCrsPoint.x * worldSizeAtZoom0 - worldOffsetInContainer, 6);
        expect(screenPoint.y).toBeCloseTo(worldFractionOfCrsPoint.y * worldSizeAtZoom0 - worldOffsetInContainer, 6);
    });
});

describe('Map in a polar stereographic CRS', () => {
    const crs = createPolarStereographicCrs();

    beforeEach(() => {
        addProjection(crs);
    });

    afterEach(() => {
        removeProjection(crs.name);
    });

    test('fits the cap north of 60N with the pole at the center', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: crs.name}}});
        await map.once('style.load');

        const camera = map.cameraForBounds([[-180, 60], [180, 90]]);

        expect(LngLat.convert(camera.center).lat).toBeCloseTo(90, 6);
    });

    test('keeps a center on the far side of the pole inside maxBounds of the cap north of 60N', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: crs.name}}, maxBounds: [[-180, 60], [180, 90]], zoom: 3});
        await map.once('style.load');

        map.setCenter([45, 70]);

        expect(map.getCenter().lng).toBeCloseTo(45, 6);
        expect(map.getCenter().lat).toBeCloseTo(70, 6);
    });
});

describe('Map in a CRS whose converter answers only inside its tile matrix set', () => {
    const crs = createIdentityCrsAnsweringInside(null);

    beforeEach(() => {
        addProjection(crs);
    });

    afterEach(() => {
        removeProjection(crs.name);
    });

    test('unprojects a point past the west edge of the world square to the edge', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: crs.name}}});
        await map.once('style.load');
        const containerMiddle = 100;

        const pastTheWestEdge = map.unproject([-1000, containerMiddle]);

        expect(pastTheWestEdge.lng).toBeCloseTo(-90, 6);
        expect(pastTheWestEdge.lat).toBeCloseTo(0, 6);
    });
});

describe('Map in a CRS wider than ±180°', () => {
    const crs = createWidePlateCarreeCrs();

    beforeEach(() => {
        addProjection(crs);
    });

    afterEach(() => {
        removeProjection(crs.name);
    });

    test('eases to a center past 180° without wrapping it', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: crs.name}}});
        await map.once('style.load');

        map.easeTo({center: [200, 0], duration: 0});

        expect(map.getCenter().lng).toBeCloseTo(200, 6);
    });

    test('flies to a center past 180° without wrapping it', async () => {
        const map = createMap({style: {version: 8, sources: {}, layers: [], projection: {type: crs.name}}});
        await map.once('style.load');

        map.flyTo({center: [200, 0], duration: 0});

        expect(map.getCenter().lng).toBeCloseTo(200, 6);
    });
});
