import {describe, test, expect} from 'vitest';
import {LngLat} from './lng_lat';
import {LngLatBounds} from './lng_lat_bounds';
import {tileIdToLngLatBounds} from '../tile/tile_id_to_lng_lat_bounds';
import {CanonicalTileID} from '../tile/tile_id';
import {EXTENT} from '../data/extent';

describe('LngLatBounds', () => {
    test('constructor', () => {
        const sw = new LngLat(0, 0);
        const ne = new LngLat(-10, 10);
        const bounds = new LngLatBounds(sw, ne);
        expect(bounds.getSouth()).toBe(0);
        expect(bounds.getWest()).toBe(0);
        expect(bounds.getNorth()).toBe(10);
        expect(bounds.getEast()).toBe(-10);
    });

    test('constructor across dateline', () => {
        const sw = new LngLat(170, 0);
        const ne = new LngLat(-170, 10);
        const bounds = new LngLatBounds(sw, ne);
        expect(bounds.getSouth()).toBe(0);
        expect(bounds.getWest()).toBe(170);
        expect(bounds.getNorth()).toBe(10);
        expect(bounds.getEast()).toBe(-170);
    });

    test('constructor across pole', () => {
        const sw = new LngLat(0, 85);
        const ne = new LngLat(-10, -85);
        const bounds = new LngLatBounds(sw, ne);
        expect(bounds.getSouth()).toBe(85);
        expect(bounds.getWest()).toBe(0);
        expect(bounds.getNorth()).toBe(-85);
        expect(bounds.getEast()).toBe(-10);
    });

    test('constructor no args', () => {
        const bounds = new LngLatBounds();
        const t1 = () => {
            bounds.getCenter();
        };
        expect(t1).toThrow();
    });

    test('extend with coordinate', () => {
        const bounds = new LngLatBounds([0, 0], [10, 10]);
        bounds.extend([-10, -10]);

        expect(bounds.getSouth()).toBe(-10);
        expect(bounds.getWest()).toBe(-10);
        expect(bounds.getNorth()).toBe(10);
        expect(bounds.getEast()).toBe(10);

        bounds.extend(new LngLat(-15, -15));

        expect(bounds.getSouth()).toBe(-15);
        expect(bounds.getWest()).toBe(-15);
        expect(bounds.getNorth()).toBe(10);
        expect(bounds.getEast()).toBe(10);

        bounds.extend([-80, -80, 80, 80]);

        expect(bounds.getSouth()).toBe(-80);
        expect(bounds.getWest()).toBe(-80);
        expect(bounds.getNorth()).toBe(80);
        expect(bounds.getEast()).toBe(80);

        bounds.extend({lng: -90, lat: -90});

        expect(bounds.getSouth()).toBe(-90);
        expect(bounds.getWest()).toBe(-90);
        expect(bounds.getNorth()).toBe(80);
        expect(bounds.getEast()).toBe(80);

        bounds.extend({lon: 90, lat: 90});

        expect(bounds.getSouth()).toBe(-90);
        expect(bounds.getWest()).toBe(-90);
        expect(bounds.getNorth()).toBe(90);
        expect(bounds.getEast()).toBe(90);
    });

    test('extend with bounds', () => {
        const bounds1 = new LngLatBounds([0, 0], [10, 10]);
        const bounds2 = new LngLatBounds([-10, -10], [10, 10]);

        bounds1.extend(bounds2);

        expect(bounds1.getSouth()).toBe(-10);
        expect(bounds1.getWest()).toBe(-10);
        expect(bounds1.getNorth()).toBe(10);
        expect(bounds1.getEast()).toBe(10);

        const bounds4 = new LngLatBounds([-20, -20, 20, 20]);
        bounds1.extend(bounds4);

        expect(bounds1.getSouth()).toBe(-20);
        expect(bounds1.getWest()).toBe(-20);
        expect(bounds1.getNorth()).toBe(20);
        expect(bounds1.getEast()).toBe(20);

        const bounds5 = new LngLatBounds();
        bounds1.extend(bounds5);

        expect(bounds1.getSouth()).toBe(-20);
        expect(bounds1.getWest()).toBe(-20);
        expect(bounds1.getNorth()).toBe(20);
        expect(bounds1.getEast()).toBe(20);
    });

    test('extend with null', () => {
        const bounds = new LngLatBounds([0, 0], [10, 10]);

        bounds.extend(null);

        expect(bounds.getSouth()).toBe(0);
        expect(bounds.getWest()).toBe(0);
        expect(bounds.getNorth()).toBe(10);
        expect(bounds.getEast()).toBe(10);
    });

    test('extend undefined bounding box', () => {
        const bounds1 = new LngLatBounds(undefined, undefined);
        const bounds2 = new LngLatBounds([-10, -10], [10, 10]);

        bounds1.extend(bounds2);

        expect(bounds1.getSouth()).toBe(-10);
        expect(bounds1.getWest()).toBe(-10);
        expect(bounds1.getNorth()).toBe(10);
        expect(bounds1.getEast()).toBe(10);
    });

    test('extend same LngLat instance', () => {
        const point = new LngLat(0, 0);
        const bounds = new LngLatBounds(point, point);

        bounds.extend(new LngLat(15, 15));

        expect(bounds.getSouth()).toBe(0);
        expect(bounds.getWest()).toBe(0);
        expect(bounds.getNorth()).toBe(15);
        expect(bounds.getEast()).toBe(15);
    });

    test('accessors', () => {
        const sw = new LngLat(0, 0);
        const ne = new LngLat(-10, -20);
        const bounds = new LngLatBounds(sw, ne);
        expect(bounds.getCenter()).toEqual(new LngLat(-5, -10));
        expect(bounds.getSouth()).toBe(0);
        expect(bounds.getWest()).toBe(0);
        expect(bounds.getNorth()).toBe(-20);
        expect(bounds.getEast()).toBe(-10);
        expect(bounds.getSouthWest()).toEqual(new LngLat(0, 0));
        expect(bounds.getSouthEast()).toEqual(new LngLat(-10, 0));
        expect(bounds.getNorthEast()).toEqual(new LngLat(-10, -20));
        expect(bounds.getNorthWest()).toEqual(new LngLat(0, -20));
    });

    test('convert', () => {
        const sw = new LngLat(0, 0);
        const ne = new LngLat(-10, 10);
        const bounds = new LngLatBounds(sw, ne);
        expect(LngLatBounds.convert(undefined)).toBeUndefined();
        expect(LngLatBounds.convert(bounds)).toEqual(bounds);
        expect(LngLatBounds.convert([sw, ne])).toEqual(bounds);
        expect(
            LngLatBounds.convert([bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()])
        ).toEqual(bounds);
    });

    test('toArray', () => {
        const llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]);
        expect(llb.toArray()).toEqual([[-73.9876, 40.7661], [-73.9397, 40.8002]]);
    });

    test('toString', () => {
        const llb = new LngLatBounds([-73.9876, 40.7661], [-73.9397, 40.8002]);
        expect(llb.toString()).toBe('LngLatBounds(LngLat(-73.9876, 40.7661), LngLat(-73.9397, 40.8002))');
    });

    test('isEmpty', () => {
        const nullBounds = new LngLatBounds();
        expect(nullBounds.isEmpty()).toBe(true);

        const sw = new LngLat(0, 0);
        const ne = new LngLat(-10, 10);
        const bounds = new LngLatBounds(sw, ne);
        expect(bounds.isEmpty()).toBe(false);
    });

    test('fromLngLat', () => {
        const center0 = new LngLat(0, 0);
        const center1 = new LngLat(-73.9749, 40.7736);

        const center0Radius10 = LngLatBounds.fromLngLat(center0, 10);
        const center1Radius10 = LngLatBounds.fromLngLat(center1, 10);
        const center1Radius0 = LngLatBounds.fromLngLat(center1);

        expect(center0Radius10.toArray()).toEqual(
            [[-0.00008983152770714982, -0.00008983152770714982], [0.00008983152770714982, 0.00008983152770714982]]
        );
        expect(center1Radius10.toArray()).toEqual(
            [[-73.97501862141328, 40.77351016847229], [-73.97478137858673, 40.77368983152771]]
        );
        expect(center1Radius0.toArray()).toEqual([[-73.9749, 40.7736], [-73.9749, 40.7736]]);
    });

    describe('LngLatBounds adjustAntiMeridian tests', () => {
        test('kenya', () => {
            const sw = new LngLat(32.958984, -5.353521);
            const ne = new LngLat(43.50585, 5.615985);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(-5.353521);
            expect(bounds.getWest()).toBe(32.958984);
            expect(bounds.getNorth()).toBe(5.615985);
            expect(bounds.getEast()).toBe(43.50585);
        });

        test('normal cross (crossing antimeridian)', () => {
            const sw = new LngLat(170, 0);
            const ne = new LngLat(-170, 10);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(0);
            expect(bounds.getWest()).toBe(170);
            expect(bounds.getNorth()).toBe(10);
            expect(bounds.getEast()).toBe(190);
        });

        test('exactly meridian (crossing antimeridian)', () => {
            const sw = new LngLat(180, -20);
            const ne = new LngLat(-180, 20);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(-20);
            expect(bounds.getWest()).toBe(180);
            expect(bounds.getNorth()).toBe(20);
            expect(bounds.getEast()).toBe(180);
        });

        test('small cross (crossing antimeridian)', () => {
            const sw = new LngLat(179, -5);
            const ne = new LngLat(-179, 5);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(-5);
            expect(bounds.getWest()).toBe(179);
            expect(bounds.getNorth()).toBe(5);
            expect(bounds.getEast()).toBe(181);
        });

        test('large cross (crossing antimeridian)', () => {
            const sw = new LngLat(100, -30);
            const ne = new LngLat(-100, 30);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(-30);
            expect(bounds.getWest()).toBe(100);
            expect(bounds.getNorth()).toBe(30);
            expect(bounds.getEast()).toBe(260);
        });

        test('reverse cross (crossing antimeridian)', () => {
            const sw = new LngLat(-170, 0);
            const ne = new LngLat(170, 10);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(0);
            expect(bounds.getWest()).toBe(-170);
            expect(bounds.getNorth()).toBe(10);
            expect(bounds.getEast()).toBe(170);
        });

        test('reverse not cross', () => {
            const sw = new LngLat(150, 0);
            const ne = new LngLat(170, 10);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(0);
            expect(bounds.getWest()).toBe(150);
            expect(bounds.getNorth()).toBe(10);
            expect(bounds.getEast()).toBe(170);
        });

        test('same longitude', () => {
            const sw = new LngLat(175, -10);
            const ne = new LngLat(175, 10);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(-10);
            expect(bounds.getWest()).toBe(175);
            expect(bounds.getNorth()).toBe(10);
            expect(bounds.getEast()).toBe(175);
        });

        test('full world', () => {
            const sw = new LngLat(-180, -90);
            const ne = new LngLat(180, 90);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(-90);
            expect(bounds.getWest()).toBe(-180);
            expect(bounds.getNorth()).toBe(90);
            expect(bounds.getEast()).toBe(180);
        });

        test('across pole', () => {
            const sw = new LngLat(0, 85);
            const ne = new LngLat(-10, -85);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(85);
            expect(bounds.getWest()).toBe(0);
            expect(bounds.getNorth()).toBe(-85);
            expect(bounds.getEast()).toBe(350);
        });

        test('across pole reverse', () => {
            const sw = new LngLat(-10, -85);
            const ne = new LngLat(0, 85);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(-85);
            expect(bounds.getWest()).toBe(-10);
            expect(bounds.getNorth()).toBe(85);
            expect(bounds.getEast()).toBe(0);
        });

        test('across dateline', () => {
            const sw = new LngLat(170, 0);
            const ne = new LngLat(-170, 10);
            const bounds = new LngLatBounds(sw, ne).adjustAntiMeridian();
            expect(bounds.getSouth()).toBe(0);
            expect(bounds.getWest()).toBe(170);
            expect(bounds.getNorth()).toBe(10);
            expect(bounds.getEast()).toBe(190);
        });
    });

    describe('contains', () => {
        describe('point', () => {
            test('point is in bounds', () => {
                const llb = new LngLatBounds([-1, -1], [1, 1]);
                const ll = {lng: 0, lat: 0};
                expect(llb.contains(ll)).toBeTruthy();
            });

            test('point is not in bounds', () => {
                const llb = new LngLatBounds([-1, -1], [1, 1]);
                const ll = {lng: 3, lat: 3};
                expect(llb.contains(ll)).toBeFalsy();
            });

            test('point is in bounds that spans dateline', () => {
                const llb = new LngLatBounds([190, -10], [170, 10]);
                const ll = {lng: 180, lat: 0};
                expect(llb.contains(ll)).toBeTruthy();
            });

            test('point is not in bounds that spans dateline', () => {
                const llb = new LngLatBounds([190, -10], [170, 10]);
                const ll = {lng: 0, lat: 0};
                expect(llb.contains(ll)).toBeFalsy();
            });
        });
    });

    describe('intersects', () => {
        test('bounds intersect', () => {
            const bounds1 = new LngLatBounds([0, 0], [10, 10]);
            const bounds2 = new LngLatBounds([5, 5], [15, 15]);
            expect(bounds1.intersects(bounds2)).toBe(true);
        });

        test('bounds do not intersect', () => {
            const bounds1 = new LngLatBounds([0, 0], [10, 10]);
            const bounds2 = new LngLatBounds([20, 20], [30, 30]);
            expect(bounds1.intersects(bounds2)).toBe(false);
        });

        describe('dateline crossing', () => {
            test('both bounds wrap around dateline - always intersect', () => {
                const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
                const bounds2 = new LngLatBounds([160, 5], [-160, 15]);
                expect(bounds1.intersects(bounds2)).toBe(true);
            });

            test('only first bounds wraps - intersects on east side', () => {
                const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
                const bounds2 = new LngLatBounds([165, 0], [175, 10]);
                expect(bounds1.intersects(bounds2)).toBe(true);
            });

            test('only first bounds wraps - intersects on west side', () => {
                const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
                const bounds2 = new LngLatBounds([-175, 0], [-165, 10]);
                expect(bounds1.intersects(bounds2)).toBe(true);
            });

            test('only first bounds wraps - does not intersect (in gap)', () => {
                const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
                const bounds2 = new LngLatBounds([0, 0], [10, 10]);
                expect(bounds1.intersects(bounds2)).toBe(false);
            });

            test('only second bounds wraps - intersects on east side', () => {
                const bounds1 = new LngLatBounds([165, 0], [175, 10]);
                const bounds2 = new LngLatBounds([170, 0], [-170, 10]);
                expect(bounds1.intersects(bounds2)).toBe(true);
            });

            test('only second bounds wraps - intersects on west side', () => {
                const bounds1 = new LngLatBounds([-175, 0], [-165, 10]);
                const bounds2 = new LngLatBounds([170, 0], [-170, 10]);
                expect(bounds1.intersects(bounds2)).toBe(true);
            });

            test('only second bounds wraps - does not intersect (in gap)', () => {
                const bounds1 = new LngLatBounds([0, 0], [10, 10]);
                const bounds2 = new LngLatBounds([170, 0], [-170, 10]);
                expect(bounds1.intersects(bounds2)).toBe(false);
            });

            test('wrapping bounds with no latitude overlap', () => {
                const bounds1 = new LngLatBounds([170, 0], [-170, 10]);
                const bounds2 = new LngLatBounds([160, 20], [-160, 30]);
                expect(bounds1.intersects(bounds2)).toBe(false);
            });

            test('wrapping tile bounds at dateline intersects with negative longitude bounds', () => {
                const tileBounds = new LngLatBounds([170, 0], [-170, 10]);
                const bounds = new LngLatBounds([-180, 5], [-175, 10]);
                expect(tileBounds.intersects(bounds)).toBe(true);
            });

            test('entire worlds tile should return true', () => {
                const tileBounds = tileIdToLngLatBounds(new CanonicalTileID(0, 0, 0), 2048 / EXTENT);
                const bounds = new LngLatBounds([[-8.290589217651302, 44.47966524518165], [20.566067150212803, 50.98693819014929]]);
                expect(tileBounds.intersects(bounds)).toBe(true);
            });

            test('point feature outside bounds does not intersect', () => {
                const bounds = new LngLatBounds([0, 0], [10, 10]);
                const point = new LngLatBounds([20, 5], [20, 5]);
                expect(bounds.intersects(point)).toBe(false);
            });
        });
    });
});

