/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { mocked } from "jest-mock";
import { RoomMember, RelationType, type MatrixClient, M_ASSET, LocationAssetType } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { act, fireEvent, render, type RenderResult } from "jest-matrix-react";
import * as maplibregl from "maplibre-gl";

import LocationShareMenu from "../../../../../src/components/views/location/LocationShareMenu";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { ChevronFace } from "../../../../../src/components/structures/ContextMenu";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { LocationShareType } from "../../../../../src/components/views/location/shareLocation";
import {
    flushPromisesWithFakeTimers,
    getMockClientWithEventEmitter,
    mockClientMethodsUser,
    setupAsyncStoreWithClient,
} from "../../../../test-utils";
import Modal from "../../../../../src/Modal";
import { DEFAULT_DURATION_MS } from "../../../../../src/components/views/location/LiveDurationDropdown";
import { OwnBeaconStore } from "../../../../../src/stores/OwnBeaconStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import QuestionDialog from "../../../../../src/components/views/dialogs/QuestionDialog";

jest.useFakeTimers();

jest.mock("../../../../../src/utils/location/findMapStyleUrl", () => ({
    findMapStyleUrl: jest.fn().mockReturnValue("test"),
}));

jest.mock("../../../../../src/settings/SettingsStore", () => ({
    getValue: jest.fn(),
    setValue: jest.fn(),
    monitorSetting: jest.fn(),
    watchSetting: jest.fn(),
    unwatchSetting: jest.fn(),
}));

jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
    OwnProfileStore: {
        instance: {
            displayName: "Ernie",
            getHttpAvatarUrl: jest.fn().mockReturnValue("image.com/img"),
        },
    },
}));

jest.mock("../../../../../src/Modal", () => ({
    createDialog: jest.fn(),
    on: jest.fn(),
    off: jest.fn(),
    ModalManagerEvent: { Opened: "opened" },
}));

describe("<LocationShareMenu />", () => {
    const userId = "@ernie:server.org";
    const mockClient = getMockClientWithEventEmitter({
        ...mockClientMethodsUser(userId),
        getClientWellKnown: jest.fn().mockResolvedValue({
            map_style_url: "maps.com",
        }),
        sendMessage: jest.fn(),
        unstable_createLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }),
        unstable_setLiveBeacon: jest.fn().mockResolvedValue({ event_id: "1" }),
        getVisibleRooms: jest.fn().mockReturnValue([]),
    });

    const defaultProps = {
        menuPosition: {
            top: 1,
            left: 1,
            chevronFace: ChevronFace.Bottom,
        },
        onFinished: jest.fn(),
        openMenu: jest.fn(),
        roomId: "!room:server.org",
        sender: new RoomMember("!room:server.org", userId),
    };

    const mockGeolocate = new maplibregl.GeolocateControl({});
    jest.spyOn(mockGeolocate, "on");
    const mapOptions = { container: {} as unknown as HTMLElement, style: "" };
    const mockMap = new maplibregl.Map(mapOptions);
    jest.spyOn(mockMap, "on");

    const position = {
        coords: {
            latitude: -36.24484561954707,
            longitude: 175.46884959563613,
            accuracy: 10,
        },
        timestamp: 1646305006802,
        type: "geolocate",
    };

    const makeOwnBeaconStore = async () => {
        const store = OwnBeaconStore.instance;

        await setupAsyncStoreWithClient(store, mockClient);
        return store;
    };

    const getComponent = (props = {}): RenderResult =>
        render(<LocationShareMenu {...defaultProps} {...props} />, {
            wrapper: ({ children }) => (
                <MatrixClientContext.Provider value={mockClient}>{children}</MatrixClientContext.Provider>
            ),
        });

    beforeEach(async () => {
        jest.spyOn(logger, "error").mockRestore();
        mocked(SettingsStore).getValue.mockReturnValue(false);
        mockClient.sendMessage.mockClear();
        mockClient.unstable_createLiveBeacon.mockClear().mockResolvedValue({ event_id: "1" });
        jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient as unknown as MatrixClient);
        mocked(Modal).createDialog.mockClear();

        jest.clearAllMocks();

        await makeOwnBeaconStore();
    });

    const getBackButton = (getByLabelText: RenderResult["getByLabelText"]) => getByLabelText("Back");

    const getCancelButton = (getByLabelText: RenderResult["getByLabelText"]) => getByLabelText("Close");

    const setLocationGeolocate = () => {
        // get the callback LocationShareMenu registered for geolocate
        expect(mocked(mockGeolocate.on)).toHaveBeenCalledWith("geolocate", expect.any(Function));
        const [, onGeolocateCallback] = mocked(mockGeolocate.on).mock.calls.find(([event]) => event === "geolocate")!;

        // set the location
        act(() => onGeolocateCallback(position));
    };

    const setLocationClick = () => {
        // get the callback LocationShareMenu registered for geolocate
        expect(mocked(mockMap.on)).toHaveBeenCalledWith("click", expect.any(Function));
        const [, onMapClickCallback] = mocked(mockMap.on).mock.calls.find(([event]) => event === "click")!;

        const event = {
            lngLat: { lng: position.coords.longitude, lat: position.coords.latitude },
        } as unknown as maplibregl.MapMouseEvent;
        // set the location
        act(() => onMapClickCallback(event));
    };

    const shareTypeLabels: Record<LocationShareType, string> = {
        [LocationShareType.Own]: "My current location",
        [LocationShareType.Live]: "My live location",
        [LocationShareType.Pin]: "Drop a Pin",
    };
    const setShareType = (getByText: RenderResult["getByText"], shareType: LocationShareType) => {
        fireEvent.click(getByText(shareTypeLabels[shareType]));
    };

    describe("when only Own share type is enabled", () => {
        beforeEach(() => enableSettings([]));

        it("renders own and live location options", () => {
            const { getByText } = getComponent();
            expect(getByText(shareTypeLabels[LocationShareType.Own])).toBeInTheDocument();
            expect(getByText(shareTypeLabels[LocationShareType.Live])).toBeInTheDocument();
        });

        it("renders back button from location picker screen", () => {
            const { getByText, getByLabelText } = getComponent();
            setShareType(getByText, LocationShareType.Own);

            expect(getBackButton(getByLabelText)).toBeInTheDocument();
        });

        it("clicking cancel button from location picker closes dialog", () => {
            const onFinished = jest.fn();
            const { getByLabelText } = getComponent({ onFinished });

            fireEvent.click(getCancelButton(getByLabelText));

            expect(onFinished).toHaveBeenCalled();
        });

        it("creates static own location share event on submission", () => {
            const onFinished = jest.fn();
            const { getByText } = getComponent({ onFinished });

            setShareType(getByText, LocationShareType.Own);

            setLocationGeolocate();

            fireEvent.click(getByText("Share location"));

            expect(onFinished).toHaveBeenCalled();
            const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
            expect(messageRoomId).toEqual(defaultProps.roomId);
            expect(relation).toEqual(null);
            expect(messageBody).toEqual(
                expect.objectContaining({
                    [M_ASSET.name]: {
                        type: LocationAssetType.Self,
                    },
                }),
            );
        });
    });

    describe("with pin drop share type enabled", () => {
        it("renders share type switch with own and pin drop options", () => {
            const { getByText } = getComponent();
            expect(document.querySelector(".mx_LocationPicker")).not.toBeInTheDocument();

            expect(getByText(shareTypeLabels[LocationShareType.Own])).toBeInTheDocument();
            expect(getByText(shareTypeLabels[LocationShareType.Pin])).toBeInTheDocument();
        });

        it("does not render back button on share type screen", () => {
            const { queryByLabelText } = getComponent();
            expect(queryByLabelText("Back")).not.toBeInTheDocument();
        });

        it("clicking cancel button from share type screen closes dialog", () => {
            const onFinished = jest.fn();
            const { getByLabelText } = getComponent({ onFinished });

            fireEvent.click(getCancelButton(getByLabelText));

            expect(onFinished).toHaveBeenCalled();
        });

        it("selecting own location share type advances to location picker", () => {
            const { getByText } = getComponent();

            setShareType(getByText, LocationShareType.Own);

            expect(document.querySelector(".mx_LocationPicker")).toBeInTheDocument();
        });

        it("clicking back button from location picker screen goes back to share screen", () => {
            const onFinished = jest.fn();
            const { getByText, getByLabelText } = getComponent({ onFinished });

            // advance to location picker
            setShareType(getByText, LocationShareType.Own);

            expect(document.querySelector(".mx_LocationPicker")).toBeInTheDocument();

            fireEvent.click(getBackButton(getByLabelText));

            // back to share type
            expect(getByText("What location type do you want to share?")).toBeInTheDocument();
        });

        it("creates pin drop location share event on submission", () => {
            const onFinished = jest.fn();
            const { getByText } = getComponent({ onFinished });

            // advance to location picker
            setShareType(getByText, LocationShareType.Pin);

            setLocationClick();

            fireEvent.click(getByText("Share location"));

            expect(onFinished).toHaveBeenCalled();
            const [messageRoomId, relation, messageBody] = mockClient.sendMessage.mock.calls[0];
            expect(messageRoomId).toEqual(defaultProps.roomId);
            expect(relation).toEqual(null);
            expect(messageBody).toEqual(
                expect.objectContaining({
                    [M_ASSET.name]: {
                        type: LocationAssetType.Pin,
                    },
                }),
            );
        });
    });

    describe("with live location disabled", () => {
        beforeEach(() => enableSettings([]));

        it("goes to labs flag screen after live options is clicked", () => {
            const onFinished = jest.fn();
            const { getByText, getByTestId } = getComponent({ onFinished });

            setShareType(getByText, LocationShareType.Live);

            expect(getByTestId("location-picker-enable-live-share")).toMatchSnapshot();
        });

        it("disables OK button when labs flag is not enabled", () => {
            const { getByText } = getComponent();

            setShareType(getByText, LocationShareType.Live);

            expect(getByText("OK").hasAttribute("disabled")).toBeTruthy();
        });

        it("enables OK button when labs flag is toggled to enabled", () => {
            const { getByText, getByLabelText } = getComponent();

            setShareType(getByText, LocationShareType.Live);

            fireEvent.click(getByLabelText("Enable live location sharing"));

            expect(getByText("OK").hasAttribute("disabled")).toBeFalsy();
        });

        it("enables live share setting on ok button submit", () => {
            const { getByText, getByLabelText } = getComponent();

            setShareType(getByText, LocationShareType.Live);

            fireEvent.click(getByLabelText("Enable live location sharing"));

            fireEvent.click(getByText("OK"));

            expect(SettingsStore.setValue).toHaveBeenCalledWith(
                "feature_location_share_live",
                null,
                SettingLevel.DEVICE,
                true,
            );
        });

        it("navigates to location picker when live share is enabled in settings store", () => {
            // @ts-ignore
            mocked(SettingsStore.watchSetting).mockImplementation((featureName, roomId, callback) => {
                callback(featureName, roomId, SettingLevel.DEVICE, "", "");
                window.setTimeout(() => {
                    callback(featureName, roomId, SettingLevel.DEVICE, "", "");
                }, 1000);
            });
            mocked(SettingsStore.getValue).mockReturnValue(false);
            const { getByText, getByLabelText } = getComponent();

            setShareType(getByText, LocationShareType.Live);

            // we're on enable live share screen
            expect(getByLabelText("Enable live location sharing")).toBeInTheDocument();

            act(() => {
                mocked(SettingsStore.getValue).mockReturnValue(true);
                // advance so watchSetting will update the value
                jest.runAllTimers();
            });

            // advanced to location picker
            expect(document.querySelector(".mx_LocationPicker")).toBeInTheDocument();
        });
    });

    describe("Live location share", () => {
        beforeEach(() => enableSettings(["feature_location_share_live"]));

        it("does not display live location share option when composer has a relation", () => {
            const relation = {
                rel_type: RelationType.Thread,
                event_id: "12345",
            };
            const { queryByText } = getComponent({ relation });

            expect(queryByText(shareTypeLabels[LocationShareType.Live])).not.toBeInTheDocument();
        });

        it("creates beacon info event on submission", async () => {
            const onFinished = jest.fn();
            const { getByText } = getComponent({ onFinished });

            // advance to location picker
            setShareType(getByText, LocationShareType.Live);
            setLocationGeolocate();

            fireEvent.click(getByText("Share location"));

            // flush stopping existing beacons promises
            await flushPromisesWithFakeTimers();

            expect(onFinished).toHaveBeenCalled();
            const [eventRoomId, eventContent] = mockClient.unstable_createLiveBeacon.mock.calls[0];
            expect(eventRoomId).toEqual(defaultProps.roomId);
            expect(eventContent).toEqual(
                expect.objectContaining({
                    // default timeout
                    timeout: DEFAULT_DURATION_MS,
                    description: `Ernie's live location`,
                    live: true,
                    [M_ASSET.name]: {
                        type: LocationAssetType.Self,
                    },
                }),
            );
        });

        it("opens error dialog when beacon creation fails", async () => {
            // stub logger to keep console clean from expected error
            const logSpy = jest.spyOn(logger, "error").mockReturnValue(undefined);
            const error = new Error("oh no");
            mockClient.unstable_createLiveBeacon.mockRejectedValue(error);
            const { getByText } = getComponent();

            // advance to location picker
            setShareType(getByText, LocationShareType.Live);
            setLocationGeolocate();

            fireEvent.click(getByText("Share location"));

            await flushPromisesWithFakeTimers();
            await flushPromisesWithFakeTimers();
            await flushPromisesWithFakeTimers();

            expect(logSpy).toHaveBeenCalledWith("We couldn't start sharing your live location", error);
            expect(mocked(Modal).createDialog).toHaveBeenCalledWith(
                QuestionDialog,
                expect.objectContaining({
                    button: "Try again",
                    description: "Element could not send your location. Please try again later.",
                    title: `We couldn't send your location`,
                    cancelButton: "Cancel",
                }),
            );
        });

        it("opens error dialog when beacon creation fails with permission error", async () => {
            // stub logger to keep console clean from expected error
            const logSpy = jest.spyOn(logger, "error").mockReturnValue(undefined);
            const error = { errcode: "M_FORBIDDEN" } as unknown as Error;
            mockClient.unstable_createLiveBeacon.mockRejectedValue(error);
            const { getByText } = getComponent();

            // advance to location picker
            setShareType(getByText, LocationShareType.Live);
            setLocationGeolocate();

            fireEvent.click(getByText("Share location"));

            await flushPromisesWithFakeTimers();
            await flushPromisesWithFakeTimers();
            await flushPromisesWithFakeTimers();

            expect(logSpy).toHaveBeenCalledWith("Insufficient permissions to start sharing your live location", error);
            expect(mocked(Modal).createDialog).toHaveBeenCalledWith(
                QuestionDialog,
                expect.objectContaining({
                    button: "OK",
                    description: "You need to have the right permissions in order to share locations in this room.",
                    title: `You don't have permission to share locations`,
                    hasCancelButton: false,
                }),
            );
        });
    });
});

function enableSettings(settings: string[]) {
    mocked(SettingsStore).getValue.mockReturnValue(false);
    mocked(SettingsStore).getValue.mockImplementation((settingName: string): any => settings.includes(settingName));
}
