import { shallowMount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import googleMap from '@/iss-components/google-map/google-map.vue';

// Supporting Files
import { getMountOptions } from '@/helpers/unit-test-helper.js';
import { useMainStore } from '@/store';

const mockFitBounds = jest.fn();
const mockMapInstance = {
    fitBounds: mockFitBounds
};
const mockMap = jest.fn(() => mockMapInstance);

const mockGeocode = jest.fn();
const mockGeocoderInstance1 = {
    geocode: mockGeocode
};
const mockGeocoderInstance2 = {
    geocode: mockGeocode
};
const mockGeocoder = jest.fn(() => (mockGeocoderInstance1));

const mockAdvancedMarkerInstance = {
    addListener: jest.fn()
};

const mockAdvancedMarkerElement = jest.fn(() => (mockAdvancedMarkerInstance));
const mockPinElement = jest.fn();
const mockInfoWindow = jest.fn();

const mockImportLibrary = jest.fn().mockImplementation(() => (Promise.resolve({
    Map: mockMap,
    Geocoder: mockGeocoder,
    AdvancedMarkerElement: mockAdvancedMarkerElement,
    PinElement: mockPinElement
})));

const mockExtend = jest.fn();
const mockLatLngBound = { extend: mockExtend };

const setupGoogleMock = () => {
    global.window.google = {
        maps: {
            importLibrary: mockImportLibrary,
            LatLngBounds: jest.fn(() => (mockLatLngBound)),
            InfoWindow: mockInfoWindow
        }
    };
};

beforeEach(() => {
    // clear constructors
    mockMap.mockClear();
    mockGeocoder.mockClear();
    mockAdvancedMarkerElement.mockClear();
    mockPinElement.mockClear();

    // clear methods
    mockExtend.mockClear();
    mockGeocode.mockClear();
    mockFitBounds.mockClear();

    setupGoogleMock();
});

function getMountedComponent(mainInitialState = {}, initialData = {}, propsData = {}) {
    const mountOptions = getMountOptions({
        router: {
            navigate: jest.fn()
        }
    });

    const testingPinia = createTestingPinia({
        initialState: {
            main: mainInitialState
        }
    });
    useMainStore(testingPinia);

    mountOptions.global.plugins = [testingPinia];
    mountOptions.data = () => (initialData);
    mountOptions.propsData = propsData;

    const wrapper = shallowMount(googleMap, mountOptions);
    wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => {});
    wrapper.vm.setCmsContent = jest.fn();
    return { wrapper };
}

async function awaitingSetupTicks(wrapper) {
    for (let i = 0; i < 11; i++) {
        // eslint-disable-next-line no-await-in-loop
        await wrapper.vm.$nextTick();
    }
}

describe('Google Map', () => {
    describe('should render', () => {
        test('expected initial data', async () => {
            // Arrange
            const wrapper = shallowMount(googleMap, {});
            await awaitingSetupTicks(wrapper);

            // Assert
            expect(wrapper.vm.$data).toMatchSnapshot();
        });
        test('map', async () => {
            // Arrange
            const mapReference = '#map';
            const wrapper = shallowMount(googleMap, {});
            await awaitingSetupTicks(wrapper);

            // Act
            const map = wrapper.find(mapReference);

            // Assert
            expect(map.exists()).toBeTruthy();
        });
    });
    test.each([
        ['13001', '13001 USA'],
        ['9', '9 USA'],
        ['', ' USA'],
        [null, ' USA']
    ])(
        'given that zipcode set to "%p", computed zipCodeAddress should return "%p"',
        async (zipCode, expected) => {
            // Arrange
            const { wrapper } = getMountedComponent({}, {}, { zipCode });
            await awaitingSetupTicks(wrapper);

            // Act
            const result = wrapper.vm.zipCodeAddress;

            // Assert
            expect(result).toBe(expected);
        }
    );
    test('before mount addresses creates map with expected markers', async () => {
        // Arrange
        const mockGetCenter = jest.fn();
        const mockNorthEast = jest.fn();
        const mockSouthWest = jest.fn();
        const zipCodeGeocodeResult = {
            results: [
                {
                    geometry: {
                        bounds:
                        {
                            getCenter: mockGetCenter,
                            getNorthEast: mockNorthEast,
                            getSouthWest: mockSouthWest
                        }
                    }
                }
            ]
        };

        const zipCode = '00882';
        const [address1, address2, position1, position2] = ['a1', 'a2', 'p1', 'p2'];
        const markers = [
            {
                fullAddress: address1
            },
            {
                fullAddress: address2
            }];
        mockGeocode.mockImplementation((obj) => {
            let result = {};
            if (obj.address.includes(zipCode)) {
                result = zipCodeGeocodeResult;
            } else if (obj.address === address1) {
                result = {
                    results: [
                        { geometry: { location: position1 } }
                    ]
                };
            } else if (obj.address === address2) {
                result = {
                    results: [
                        { geometry: { location: position2 } }
                    ]
                };
            }
            return Promise.resolve(result);
        });

        const propsData = { markers, zipCode };
        const { wrapper } = getMountedComponent({}, {}, propsData);
        await awaitingSetupTicks(wrapper);

        // Assert
        expect(mockMap).toBeCalledTimes(1);
        expect(mockAdvancedMarkerElement).toBeCalledTimes(2);
        expect(mockAdvancedMarkerElement).toBeCalledWith(expect.objectContaining({
            map: expect.anything(),
            position: position1
        }));
        expect(mockAdvancedMarkerElement).toBeCalledWith(expect.objectContaining({
            map: expect.anything(),
            position: position2
        }));
        expect(mockGeocoder).toBeCalledTimes(1);

        expect(mockExtend).toBeCalledTimes(4);
        expect(mockFitBounds).toBeCalledTimes(1);

        expect(mockGetCenter).toBeCalledTimes(1);
        expect(mockNorthEast).toBeCalledTimes(1);
        expect(mockSouthWest).toBeCalledTimes(1);
    });
    test('watch addresses creates map with expected markers', async () => {
        // Arrange
        const zipCode = '00882';

        const mockGetCenter = jest.fn();
        const mockNorthEast = jest.fn();
        const mockSouthWest = jest.fn();
        const zipCodeGeocodeResult = {
            results: [
                {
                    geometry: {
                        bounds:
                        {
                            getCenter: mockGetCenter,
                            getNorthEast: mockNorthEast,
                            getSouthWest: mockSouthWest
                        }
                    }
                }
            ]
        };
        const [address1, address2, position1, position2] = ['a1', 'a2', 'p1', 'p2'];
        mockGeocode.mockImplementation((obj) => {
            let result = {};
            if (obj.address.includes(zipCode)) {
                result = zipCodeGeocodeResult;
            } else if (obj.address === address1) {
                result = {
                    results: [
                        { geometry: { location: position1 } }
                    ]
                };
            } else if (obj.address === address2) {
                result = {
                    results: [
                        { geometry: { location: position2 } }
                    ]
                };
            }
            return Promise.resolve(result);
        });
        const markers = [
            {
                fullAddress: address1
            },
            {
                fullAddress: address2
            }];

        // Act
        const { wrapper } = getMountedComponent({}, { zipCode });
        await awaitingSetupTicks(wrapper);
        await wrapper.vm.$options.watch.markers.call(wrapper.vm, markers);
        await awaitingSetupTicks(wrapper);

        // Assert
        expect(mockMap).toBeCalledTimes(2);
        expect(mockAdvancedMarkerElement).toBeCalledTimes(2);
        expect(mockAdvancedMarkerElement).toBeCalledWith(expect.objectContaining({
            map: expect.anything(),
            position: position1
        }));
        expect(mockAdvancedMarkerElement).toBeCalledWith(expect.objectContaining({
            map: expect.anything(),
            position: position2
        }));
        expect(mockGeocoder).toBeCalledTimes(1);

        expect(mockExtend).toBeCalledTimes(6);
        expect(mockFitBounds).toBeCalledTimes(2);

        expect(mockGetCenter).toBeCalledTimes(2);
        expect(mockNorthEast).toBeCalledTimes(2);
        expect(mockSouthWest).toBeCalledTimes(2);
    });
    describe('method', () => {
        test('getMap calls map constructor with expected parameters', async () => {
            // Arrange
            const wrapper = shallowMount(googleMap, {});
            await awaitingSetupTicks(wrapper);
            const center = { lat: 90, lng: -5 };

            // Act
            const result = await wrapper.vm.getMap(center);

            // Assert
            expect(mockMap).toHaveBeenCalledWith(
                null,
                expect.objectContaining({ center, mapId: 'map_id', mapTypeControl: false })
            );
            expect(result).toBe(mockMapInstance);
        });
        test.each([
            [undefined, 0],
            [null, 0],
            [[], 0],
            [['loc1'], 1],
            [['loc1', 'loc1', 'loc1', 'loc1'], 4]
        ])(
            'given parameter %p, getBounds calls extend %p time(s)',
            async (locations, numberOfExtendCallsPostMount) => {
                // Arrange
                const wrapper = shallowMount(googleMap, {});
                await awaitingSetupTicks(wrapper);

                mockGeocode.mockImplementation(() => {
                    const result = {
                        results: [
                            { geometry: { location: 'p1' } }
                        ]
                    };
                    return Promise.resolve(result);
                });

                // Act
                const result = wrapper.vm.getBounds(locations);

                // Assert
                expect(mockExtend).toHaveBeenCalledTimes(numberOfExtendCallsPostMount);
                expect(result).toBe(mockLatLngBound);
            }
        );
        test.each([
            [1, null, mockGeocoderInstance1],
            [1, undefined, mockGeocoderInstance1],
            [0, mockGeocoderInstance2, mockGeocoderInstance2]
        ])(
            'setGeocoder calls Geocoder constructor %p time(s) when original geocoder %p and geocoder set to %p',
            async (numberOfCalls, originalGeocoder, newGeocoder) => {
                // Arrange
                const { wrapper } = getMountedComponent({}, { geocoder: originalGeocoder }, {});
                await awaitingSetupTicks(wrapper);

                // Act
                await wrapper.vm.setGeocoder();
                await wrapper.vm.$nextTick();

                // Assert
                expect(mockGeocoder).toHaveBeenCalledTimes(numberOfCalls);
                expect(wrapper.vm.geocoder).toEqual(newGeocoder);
            }
        );
        test.each([
            [null, 0],
            [undefined, 0],
            [[], 0],
            [['p1'], 1],
            [['p1', 'p2', 'p3'], 3]
        ])(
            'addMarkersToMap given positions argument %p, creates AdvancedMarkerElement %p time(s)',
            async (positions, numberOfCalls) => {
                // Arrange
                const wrapper = shallowMount(googleMap, {});
                await awaitingSetupTicks(wrapper);
                const map = jest.fn();

                // Act
                await wrapper.vm.addMarkersToMap(map, positions);

                // Assert
                expect(mockAdvancedMarkerElement).toHaveBeenCalledTimes(numberOfCalls);
            }
        );
        describe('getLocationsFromAddresses', () => {
            test.each([
                [null, 0],
                [undefined, 0],
                [[], 0],
                [['a1'], 1],
                [['a1', 'a2', 'a3'], 3]
            ])(
                'given address(es) %p calls geocode %p times',
                async (addresses, numberOfCallsPostMount) => {
                    // Arrange
                    const wrapper = shallowMount(googleMap, {});
                    await awaitingSetupTicks(wrapper);
                    const numberOfCallsFromMount = 1;
                    expect(mockGeocode).toHaveBeenCalledTimes(numberOfCallsFromMount);

                    // Act
                    await wrapper.vm.getLocationsFromAddresses(addresses?.map((a) => ({ fullAddress: a })));
                    await wrapper.vm.$nextTick();

                    // Assert
                    expect(mockGeocode).toHaveBeenCalledTimes(numberOfCallsFromMount + numberOfCallsPostMount);
                    expect(wrapper.vm.geocoder).not.toBeNull();
                }
            );
            test('only returns geocode results that are not null', async () => {
                // Arrange
                const wrapper = shallowMount(googleMap, {});
                await awaitingSetupTicks(wrapper);
                const addresses = ['a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8'].map((a) => ({ fullAddress: a }));
                mockGeocode.mockImplementationOnce(() => null)
                    .mockImplementationOnce(() => ({ results: null }))
                    .mockImplementationOnce(() => ({ results: [] }))
                    .mockImplementationOnce(() => ({ results: [{}] }))
                    .mockImplementationOnce(() => (
                        { results: [{ geometry: undefined }] }
                    ))
                    .mockImplementationOnce(() => (
                        { results: [{ geometry: {} }] }
                    ))
                    .mockImplementationOnce(() => (
                        {
                            results: [
                                { geometry: { location: 'some value' } }
                            ]
                        }
                    ))
                    .mockImplementationOnce(() => (
                        {
                            results: [
                                { geometry: { location: 'some other value' } }
                            ]
                        }
                    ));
                const expectedResult = ['some value', 'some other value'];

                // Act
                const result = (await wrapper.vm.getLocationsFromAddresses(addresses)).map((a) => a.position);

                // Assert
                expect(result.length).toBe(2);
                expect(result).toEqual(expectedResult);
            });
        });
        describe('getBoundsFromAddress', () => {
            test('returns expected when geocode returns valid result', async () => {
                // Arrange
                const wrapper = shallowMount(googleMap, {});
                await awaitingSetupTicks(wrapper);
                const address = 'random address';
                const expectedResult = 'bounds returned';
                const geocodeResult = {
                    results: [
                        {
                            geometry: { bounds: expectedResult }
                        }
                    ]
                };
                mockGeocode.mockImplementationOnce(() => Promise.resolve(geocodeResult));

                // Act
                const result = await wrapper.vm.getBoundsFromAddress(address);

                // Assert
                expect(result).toBe(expectedResult);
                expect(wrapper.vm.geocoder).not.toBeNull();
            });
            test('returns null when geocode returns invalid result', async () => {
                // Arrange
                const wrapper = shallowMount(googleMap, {});
                await awaitingSetupTicks(wrapper);
                const address = 'random address';
                mockGeocode.mockImplementationOnce(() => Promise.resolve({}));

                // Act
                const result = await wrapper.vm.getBoundsFromAddress(address);

                // Assert
                expect(result).toBeNull();
                expect(wrapper.vm.geocoder).not.toBeNull();
            });
        });
        describe('createMapWithMarkersForAddresses', () => {
            test('calls expected when addresses provided', async () => {
                // Arrange
                const mockGetCenter = jest.fn();
                const mockNorthEast = jest.fn();
                const mockSouthWest = jest.fn();
                const zipCodeGeocodeResult = {
                    results: [
                        {
                            geometry: {
                                bounds:
                                {
                                    getCenter: mockGetCenter,
                                    getNorthEast: mockNorthEast,
                                    getSouthWest: mockSouthWest
                                }
                            }
                        }
                    ]
                };
                const shopGeocodeResult = {
                    results: [
                        { geometry: { location: 'valid location' } }
                    ]
                };
                const zipCode = '00882';
                mockGeocode.mockImplementation((obj) =>
                    Promise.resolve(obj.address.includes(zipCode)
                        ? zipCodeGeocodeResult
                        : shopGeocodeResult));
                const addresses = [{ fullAddress: 'a1' }, { fullAddress: 'a2' }];
                const { wrapper } = getMountedComponent({}, { zipCode });
                await awaitingSetupTicks(wrapper);

                // Act
                await wrapper.vm.createMapWithMarkersForAddresses(addresses);
                await awaitingSetupTicks(wrapper);

                // Assert
                expect(mockMap).toBeCalledTimes(2);
                expect(mockAdvancedMarkerElement).toBeCalledTimes(2);
                expect(mockGeocoder).toBeCalledTimes(1);

                expect(mockExtend).toBeCalledTimes(6);
                expect(mockFitBounds).toBeCalledTimes(2);

                expect(mockGetCenter).toBeCalledTimes(2);
                expect(mockNorthEast).toBeCalledTimes(2);
                expect(mockSouthWest).toBeCalledTimes(2);
            });
            test.each([null, undefined, []])(
                'calls expected when addresses argument is "%p"',
                async (addresses) => {
                    // Arrange
                    const mockGetCenter = jest.fn();
                    const mockNorthEast = jest.fn();
                    const mockSouthWest = jest.fn();
                    const zipCodeGeocodeResult = {
                        results: [
                            {
                                geometry: {
                                    bounds:
                                    {
                                        getCenter: mockGetCenter,
                                        getNorthEast: mockNorthEast,
                                        getSouthWest: mockSouthWest
                                    }
                                }
                            }
                        ]
                    };
                    mockGeocode.mockImplementation(() => Promise.resolve(zipCodeGeocodeResult));
                    const zipCode = '00882';
                    const { wrapper } = getMountedComponent({}, { zipCode });
                    await awaitingSetupTicks(wrapper);

                    // Act
                    await wrapper.vm.createMapWithMarkersForAddresses(addresses);
                    await awaitingSetupTicks(wrapper);

                    // Assert
                    expect(mockMap).toBeCalledTimes(2);
                    expect(mockAdvancedMarkerElement).toBeCalledTimes(0);
                    expect(mockGeocoder).toBeCalledTimes(1);

                    expect(mockExtend).toBeCalledTimes(4);
                    expect(mockFitBounds).toBeCalledTimes(2);

                    expect(mockGetCenter).toBeCalledTimes(2);
                    expect(mockNorthEast).toBeCalledTimes(2);
                    expect(mockSouthWest).toBeCalledTimes(2);
                }
            );
        });
    });
});
