/*
Copyright 2024 New Vector Ltd.
Copyright 2021 Robin Townsend <robin@robin.town>

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 {
    MatrixEvent,
    EventType,
    LocationAssetType,
    M_ASSET,
    M_LOCATION,
    M_TIMESTAMP,
    M_TEXT,
} from "matrix-js-sdk/src/matrix";
import { act, fireEvent, getByTestId, render, type RenderResult, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { sleep } from "matrix-js-sdk/src/utils";

import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import ForwardDialog from "../../../../../src/components/views/dialogs/ForwardDialog";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import {
    getMockClientWithEventEmitter,
    makeBeaconEvent,
    makeLegacyLocationEvent,
    makeLocationEvent,
    mkEvent,
    mkMessage,
    mkStubRoom,
    mockPlatformPeg,
} from "../../../../test-utils";
import { TILE_SERVER_WK_KEY } from "../../../../../src/utils/WellKnownUtils";
import SettingsStore from "../../../../../src/settings/SettingsStore";

// mock offsetParent
Object.defineProperty(HTMLElement.prototype, "offsetParent", {
    get() {
        return this.parentNode;
    },
});

describe("ForwardDialog", () => {
    const sourceRoom = "!111111111111111111:example.org";
    const aliceId = "@alice:example.org";
    const defaultMessage = mkMessage({
        room: sourceRoom,
        user: aliceId,
        msg: "Hello world!",
        event: true,
    });
    const accountDataEvent = new MatrixEvent({
        type: EventType.Direct,
        sender: aliceId,
        content: {},
    });
    const mockClient = getMockClientWithEventEmitter({
        getUserId: jest.fn().mockReturnValue(aliceId),
        getSafeUserId: jest.fn().mockReturnValue(aliceId),
        isGuest: jest.fn().mockReturnValue(false),
        getVisibleRooms: jest.fn().mockReturnValue([]),
        getRoom: jest.fn(),
        getAccountData: jest.fn().mockReturnValue(accountDataEvent),
        getPushActionsForEvent: jest.fn(),
        mxcUrlToHttp: jest.fn().mockReturnValue(""),
        getProfileInfo: jest.fn().mockResolvedValue({
            displayname: "Alice",
        }),
        decryptEventIfNeeded: jest.fn(),
        sendEvent: jest.fn(),
        getClientWellKnown: jest.fn().mockReturnValue({
            [TILE_SERVER_WK_KEY.name]: { map_style_url: "maps.com" },
        }),
    });
    const defaultRooms = ["a", "A", "b"].map((name) => mkStubRoom(name, name, mockClient));

    const mountForwardDialog = (message = defaultMessage, rooms = defaultRooms, stubSource = false) => {
        mockClient.getVisibleRooms.mockReturnValue(rooms);

        const sourceRoomStub = mkStubRoom(sourceRoom, sourceRoom, mockClient);

        mockClient.getRoom.mockImplementation((roomId) => {
            if (stubSource && roomId === sourceRoom) {
                return sourceRoomStub; // Return the source room stub, if enabled
            }
            return rooms.find((room) => room.roomId === roomId) || null;
        });

        const wrapper: RenderResult = render(
            <ForwardDialog
                matrixClient={mockClient}
                event={message}
                permalinkCreator={new RoomPermalinkCreator(undefined!, sourceRoom)}
                onFinished={jest.fn()}
            />,
        );

        return wrapper;
    };

    beforeEach(() => {
        DMRoomMap.makeShared(mockClient);
        jest.clearAllMocks();
        mockClient.getUserId.mockReturnValue("@bob:example.org");
        mockClient.getSafeUserId.mockReturnValue("@bob:example.org");
        mockClient.sendEvent.mockReset();
    });

    afterAll(() => {
        jest.spyOn(MatrixClientPeg, "get").mockRestore();
    });

    it("shows a preview with us as the sender", async () => {
        const { container } = mountForwardDialog();

        expect(screen.queryByText("Hello world!")).toBeInTheDocument();

        // We would just test SenderProfile for the user ID, but it's stubbed
        const previewAvatar = container.querySelector(".mx_EventTile_avatar .mx_BaseAvatar");
        expect(previewAvatar?.getAttribute("title")).toBe("@bob:example.org");
    });

    it("filters the rooms", async () => {
        const { container } = mountForwardDialog();

        expect(container.querySelectorAll(".mx_ForwardList_entry")).toHaveLength(3);

        const searchInput = getByTestId(container, "searchbox-input");
        await userEvent.type(searchInput, "a");

        expect(container.querySelectorAll(".mx_ForwardList_entry")).toHaveLength(2);
    });

    it("should be navigable using arrow keys", async () => {
        const { container } = mountForwardDialog();

        const searchBox = getByTestId(container, "searchbox-input");
        searchBox.focus();
        await waitFor(() =>
            expect(container.querySelectorAll(".mx_ForwardList_entry")[0]).toHaveClass("mx_ForwardList_entry_active"),
        );

        await userEvent.keyboard("[ArrowDown]");
        await waitFor(() =>
            expect(container.querySelectorAll(".mx_ForwardList_entry")[1]).toHaveClass("mx_ForwardList_entry_active"),
        );

        await userEvent.keyboard("[ArrowDown]");
        await waitFor(() =>
            expect(container.querySelectorAll(".mx_ForwardList_entry")[2]).toHaveClass("mx_ForwardList_entry_active"),
        );

        await userEvent.keyboard("[ArrowUp]");
        await waitFor(() =>
            expect(container.querySelectorAll(".mx_ForwardList_entry")[1]).toHaveClass("mx_ForwardList_entry_active"),
        );

        await userEvent.keyboard("[Enter]");
        expect(mockClient.sendEvent).toHaveBeenCalledWith("A", "m.room.message", {
            "body": "Hello world!",
            "msgtype": "m.text",
            "m.mentions": {},
        });
    });

    it("tracks message sending progress across multiple rooms", async () => {
        mockPlatformPeg();
        const { container } = mountForwardDialog();

        // Make sendEvent require manual resolution so we can see the sending state
        let finishSend: (arg?: any) => void;
        let cancelSend: () => void;
        mockClient.sendEvent.mockImplementation(
            <T extends {}>() =>
                new Promise<T>((resolve, reject) => {
                    finishSend = resolve;
                    cancelSend = reject;
                }),
        );

        let firstButton!: Element;
        let secondButton!: Element;
        const update = () => {
            [firstButton, secondButton] = container.querySelectorAll(".mx_ForwardList_sendButton");
        };
        update();

        expect(firstButton.className).toContain("mx_ForwardList_canSend");

        act(() => {
            fireEvent.click(firstButton);
        });
        update();
        expect(firstButton.className).toContain("mx_ForwardList_sending");

        await act(async () => {
            cancelSend();
            // Wait one tick for the button to realize the send failed
            await sleep(0);
        });
        update();
        expect(firstButton.className).toContain("mx_ForwardList_sendFailed");

        expect(secondButton.className).toContain("mx_ForwardList_canSend");

        act(() => {
            fireEvent.click(secondButton);
        });
        update();
        expect(secondButton.className).toContain("mx_ForwardList_sending");

        await act(async () => {
            finishSend();
            // Wait one tick for the button to realize the send succeeded
            await sleep(0);
        });
        update();
        expect(secondButton.className).toContain("mx_ForwardList_sent");
    });

    it("can render replies", async () => {
        const replyMessage = mkEvent({
            type: "m.room.message",
            room: "!111111111111111111:example.org",
            user: "@alice:example.org",
            content: {
                "msgtype": "m.text",
                "body": "> <@bob:example.org> Hi Alice!\n\nHi Bob!",
                "m.relates_to": {
                    "m.in_reply_to": {
                        event_id: "$2222222222222222222222222222222222222222222",
                    },
                },
            },
            event: true,
        });

        mountForwardDialog(replyMessage);

        expect(screen.queryByText("Hi Alice!", { exact: false })).toBeInTheDocument();
    });

    it("disables buttons for rooms without send permissions", async () => {
        const readOnlyRoom = mkStubRoom("a", "a", mockClient);
        readOnlyRoom.maySendMessage = jest.fn().mockReturnValue(false);
        const rooms = [readOnlyRoom, mkStubRoom("b", "b", mockClient)];

        const { container } = mountForwardDialog(undefined, rooms);

        const [firstButton, secondButton] = container.querySelectorAll<HTMLButtonElement>(".mx_ForwardList_sendButton");

        expect(firstButton.getAttribute("aria-disabled")).toBeTruthy();
        expect(secondButton.getAttribute("aria-disabled")).toBeFalsy();
    });

    describe("Mention recalculation", () => {
        const roomId = "a";
        const sendClick = (container: HTMLElement): void =>
            act(() => {
                const sendButton = container.querySelector(".mx_ForwardList_sendButton");
                fireEvent.click(sendButton!);
            });
        const makeMessage = (body: string, mentions: object, formattedBody?: string) => {
            return mkEvent({
                type: "m.room.message",
                room: sourceRoom,
                user: "@bob:example.org",
                content: {
                    "msgtype": "m.text",
                    "body": body,
                    "m.mentions": mentions,
                    ...(formattedBody && {
                        format: "org.matrix.custom.html",
                        formatted_body: formattedBody,
                    }),
                },
                event: true,
            });
        };

        it("strips extra mentions", async () => {
            const message = makeMessage("Hi Alice", { user_ids: [aliceId] });
            const { container } = mountForwardDialog(message);
            sendClick(container);
            // Expected content should have mentions empty.
            expect(mockClient.sendEvent).toHaveBeenCalledWith(roomId, message.getType(), {
                ...message.getContent(),
                "m.mentions": {},
            });
        });

        it("recalculates mention pills", async () => {
            const message = makeMessage(
                "Hi Alice",
                { user_ids: [aliceId] },
                `Hi <a href="https://matrix.to/#/${aliceId}">Alice</a>`,
            );
            const { container } = mountForwardDialog(message, defaultRooms, true);
            sendClick(container);
            // Expected content should have mentions empty.
            expect(mockClient.sendEvent).toHaveBeenCalledWith(roomId, message.getType(), {
                ...message.getContent(),
                "m.mentions": { user_ids: [aliceId] },
            });
        });
    });

    describe("Location events", () => {
        // 14.03.2022 16:15
        const now = 1647270879403;
        const roomId = "a";
        const geoUri = "geo:51.5076,-0.1276";
        const legacyLocationEvent = makeLegacyLocationEvent(geoUri);
        const modernLocationEvent = makeLocationEvent(geoUri);
        const pinDropLocationEvent = makeLocationEvent(geoUri, LocationAssetType.Pin);

        beforeEach(() => {
            // legacy events will default timestamp to Date.now()
            // mock a stable now for easy assertion
            jest.spyOn(Date, "now").mockReturnValue(now);
        });

        afterAll(() => {
            jest.spyOn(Date, "now").mockRestore();
        });

        const sendToFirstRoom = (container: HTMLElement): void =>
            act(() => {
                const sendToFirstRoomButton = container.querySelector(".mx_ForwardList_sendButton");
                fireEvent.click(sendToFirstRoomButton!);
            });

        it("converts legacy location events to pin drop shares", async () => {
            const { container } = mountForwardDialog(legacyLocationEvent);

            await waitFor(() => expect(container.querySelector(".mx_MLocationBody")).toBeTruthy());
            sendToFirstRoom(container);

            // text and description from original event are removed
            // text gets new default message from event values
            // timestamp is defaulted to now
            const text = `Location ${geoUri} at ${new Date(now).toISOString()}`;
            const expectedStrippedContent = {
                ...modernLocationEvent.getContent(),
                body: text,
                [M_TEXT.name]: text,
                [M_TIMESTAMP.name]: now,
                [M_ASSET.name]: { type: LocationAssetType.Pin },
                [M_LOCATION.name]: {
                    uri: geoUri,
                },
            };
            expect(mockClient.sendEvent).toHaveBeenCalledWith(
                roomId,
                legacyLocationEvent.getType(),
                expectedStrippedContent,
            );
        });

        it("removes personal information from static self location shares", async () => {
            const { container } = mountForwardDialog(modernLocationEvent);

            await waitFor(() => expect(container.querySelector(".mx_MLocationBody")).toBeTruthy());
            sendToFirstRoom(container);

            const timestamp = M_TIMESTAMP.findIn<number>(modernLocationEvent.getContent())!;
            // text and description from original event are removed
            // text gets new default message from event values
            const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`;
            const expectedStrippedContent = {
                ...modernLocationEvent.getContent(),
                body: text,
                [M_TEXT.name]: text,
                [M_ASSET.name]: { type: LocationAssetType.Pin },
                [M_LOCATION.name]: {
                    uri: geoUri,
                },
            };
            expect(mockClient.sendEvent).toHaveBeenCalledWith(
                roomId,
                modernLocationEvent.getType(),
                expectedStrippedContent,
            );
        });

        it("forwards beacon location as a pin drop event", async () => {
            const timestamp = 123456;
            const beaconEvent = makeBeaconEvent("@alice:server.org", { geoUri, timestamp });
            const text = `Location ${geoUri} at ${new Date(timestamp).toISOString()}`;
            const expectedContent = {
                msgtype: "m.location",
                body: text,
                [M_TEXT.name]: text,
                [M_ASSET.name]: { type: LocationAssetType.Pin },
                [M_LOCATION.name]: {
                    uri: geoUri,
                },
                geo_uri: geoUri,
                [M_TIMESTAMP.name]: timestamp,
            };
            const { container } = mountForwardDialog(beaconEvent);

            await waitFor(() => expect(container.querySelector(".mx_MLocationBody")).toBeTruthy());

            sendToFirstRoom(container);

            expect(mockClient.sendEvent).toHaveBeenCalledWith(roomId, EventType.RoomMessage, expectedContent);
        });

        it("forwards pin drop event", async () => {
            const { container } = mountForwardDialog(pinDropLocationEvent);

            await waitFor(() => expect(container.querySelector(".mx_MLocationBody")).toBeTruthy());

            sendToFirstRoom(container);

            const expectedContent = {
                ...pinDropLocationEvent.getContent(),
                "m.mentions": {}, // Add mentions (explicitly set to empty)
            };

            expect(mockClient.sendEvent).toHaveBeenCalledWith(roomId, pinDropLocationEvent.getType(), expectedContent);
        });
    });

    describe("If the feature_dynamic_room_predecessors is not enabled", () => {
        beforeEach(() => {
            jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
        });

        it("Passes through the dynamic predecessor setting", async () => {
            mockClient.getVisibleRooms.mockClear();
            mountForwardDialog();
            expect(mockClient.getVisibleRooms).toHaveBeenCalledWith(false);
        });
    });

    describe("If the feature_dynamic_room_predecessors is enabled", () => {
        beforeEach(() => {
            // Turn on feature_dynamic_room_predecessors setting
            jest.spyOn(SettingsStore, "getValue").mockImplementation(
                (settingName) => settingName === "feature_dynamic_room_predecessors",
            );
        });

        it("Passes through the dynamic predecessor setting", async () => {
            mockClient.getVisibleRooms.mockClear();
            mountForwardDialog();
            expect(mockClient.getVisibleRooms).toHaveBeenCalledWith(true);
        });
    });
});
