/*
 * Copyright 2025 New Vector Ltd.
 *
 * 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 { type Page } from "@playwright/test";
import { closeReleaseAnnouncement, rejectToast } from "@element-hq/element-web-playwright-common";

import { expect, test } from "../../../element-web-test";
import { type Bot } from "../../../pages/bot";
import { type ElementAppPage } from "../../../pages/ElementAppPage";
import { getRoomList } from "./utils";

test.describe("Room list", () => {
    test.use({
        displayName: "Alice",
        botCreateOpts: {
            displayName: "BotBob",
        },
    });

    test.beforeEach(async ({ page, app, user }) => {
        // The toasts are displayed above the search section
        await rejectToast(page, "Verify this device");
        await rejectToast(page, "Notifications");

        // Close the release announcement about the new room list sections
        await closeReleaseAnnouncement(page, "Introducing Sections");

        // focus the user menu to avoid to have hover decoration
        await page.getByRole("button", { name: "User menu" }).focus();
    });

    test.describe("Room list", () => {
        test.slow();

        test.beforeEach(async ({ page, app, user }) => {
            for (let i = 0; i < 30; i++) {
                await app.client.createRoom({ name: `room${i}` });
            }
        });

        test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user, axe }) => {
            const roomListView = getRoomList(page);
            await expect(roomListView.getByRole("option", { name: "Open room room29" })).toBeVisible();
            await expect(roomListView).toMatchScreenshot("room-list.png");

            // Put focus on the room list
            await roomListView.getByRole("option", { name: "Open room room29" }).click();
            // Scroll to the end of the room list
            await app.scrollListToBottom(roomListView);

            // scrollListToBottom seems to leave the mouse hovered over the list, move it away.
            await page.getByRole("button", { name: "User menu" }).hover();

            await expect(axe).toHaveNoViolations();
            await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
        });

        test("should open the room when it is clicked", async ({ page, app, user }) => {
            const roomListView = getRoomList(page);
            await roomListView.getByRole("option", { name: "Open room room29" }).click();
            await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
        });

        test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
            const roomListView = getRoomList(page);
            await roomListView.getByRole("option", { name: "Open room room29" }).click({ button: "right" });
            await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
        });

        test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
            const roomListView = getRoomList(page);
            let roomItem = roomListView.getByRole("option", { name: "Open room room29" });
            await roomItem.hover();

            await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
            let roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
            await roomItemMenu.click();
            await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");

            // It should make the room favourited
            await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();

            // Check that the room is favourited
            roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
            await roomItem.hover();
            roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
            await roomItemMenu.click();
            await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
            // It should show the invite dialog
            await page.getByRole("menuitem", { name: "invite" }).click();
            await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
            await app.closeDialog();

            // It should leave the room
            await roomItem.hover();
            await roomItemMenu.click();
            await page.getByRole("menuitem", { name: "leave room" }).click();
            await expect(roomItem).not.toBeVisible();
        });

        test("should open the notification options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
            const roomListView = getRoomList(page);

            const roomItem = roomListView.getByRole("option", { name: "Open room room29" });
            await roomItem.hover();

            await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
            let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
            await roomItemMenu.click();

            // Default settings should be selected
            await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
                "aria-selected",
                "true",
            );
            await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png");

            // It should make the room muted
            await page.getByRole("menuitem", { name: "Mute room" }).click();

            // Put focus on the room list
            await roomListView.getByRole("option", { name: "Open room room28" }).click();
            await roomItem.scrollIntoViewIfNeeded();

            // Not hovered, the room decoration should be the muted icon
            await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();

            // During hover the room decoration should still be the muted icon
            await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();

            await roomItem.hover();

            // On hover, the room should show the muted icon
            await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png");

            roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
            await roomItemMenu.click();

            // The Mute room option should be selected
            await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true");
            await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png");
        });

        test("should scroll to the current room", async ({ page, app, user }) => {
            const roomListView = getRoomList(page);
            // Put focus on the room list
            await roomListView.getByRole("option", { name: "Open room room29" }).click();
            // Scroll to the end of the room list
            await app.scrollListToBottom(roomListView);

            await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
            await roomListView.getByRole("option", { name: "Open room room0" }).click();

            const filters = page.getByRole("listbox", { name: "Room list filters" });
            await filters.getByRole("option", { name: "People" }).click();
            await expect(roomListView.getByRole("option", { name: "Open room room0" })).not.toBeVisible();

            await filters.getByRole("option", { name: "People" }).click();
            await expect(roomListView.getByRole("option", { name: "Open room room0" })).toBeVisible();
        });

        test.describe("Shortcuts", () => {
            test("should select the next room", async ({ page, app, user }) => {
                const roomListView = getRoomList(page);
                await roomListView.getByRole("option", { name: "Open room room29" }).click();
                await page.keyboard.press("Alt+ArrowDown");

                await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
            });

            test("should select the previous room", async ({ page, app, user }) => {
                const roomListView = getRoomList(page);
                await roomListView.getByRole("option", { name: "Open room room28" }).click();
                await page.keyboard.press("Alt+ArrowUp");

                await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
            });

            test("should select the last room", async ({ page, app, user }) => {
                const roomListView = getRoomList(page);
                await roomListView.getByRole("option", { name: "Open room room29" }).click();
                await page.keyboard.press("Alt+ArrowUp");

                await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
            });

            test("should select the next unread room", async ({ page, app, user, bot }) => {
                const roomListView = getRoomList(page);

                const roomId = await app.client.createRoom({ name: "1 notification" });
                await app.client.inviteUser(roomId, bot.credentials.userId);
                await bot.joinRoom(roomId);
                await bot.sendMessage(roomId, "I am a robot. Beep.");

                await roomListView.getByRole("option", { name: "Open room room20" }).click();

                // Make sure the room with the unread is visible before we press the keyboard action to select it
                await expect(roomListView.getByRole("option", { name: "1 notification" })).toBeVisible();

                await page.keyboard.press("Alt+Shift+ArrowDown");

                await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
            });
        });

        test.describe("Keyboard navigation", () => {
            test("should navigate to the room list", async ({ page, app, user }) => {
                const roomListView = getRoomList(page);

                const room29 = roomListView.getByRole("option", { name: "Open room room29" });
                const room28 = roomListView.getByRole("option", { name: "Open room room28" });

                // open the room
                await room29.click();
                // put focus back on the room list item
                await room29.click();
                await expect(room29).toBeFocused();

                await page.keyboard.press("ArrowDown");
                await expect(room28).toBeFocused();
                await expect(room29).not.toBeFocused();

                await page.keyboard.press("ArrowUp");
                await expect(room29).toBeFocused();
                await expect(room28).not.toBeFocused();
            });

            test("should navigate to the notification menu", async ({ page, app, user }) => {
                const roomListView = getRoomList(page);
                const room29 = roomListView.getByRole("option", { name: "Open room room29" });
                const moreButton = room29.getByRole("button", { name: "More options" });
                const notificationButton = room29.getByRole("button", { name: "Notification options" });

                await room29.click();
                // put focus back on the room list item
                await room29.click();
                await page.keyboard.press("Tab");
                await expect(moreButton).toBeFocused();
                await page.keyboard.press("Tab");
                await expect(notificationButton).toBeFocused();

                // Open the menu
                await page.keyboard.press("Enter");
                // Wait for the menu to be open
                await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
                    "aria-selected",
                    "true",
                );

                await page.keyboard.press("ArrowDown");
                await page.keyboard.press("Escape");
                // Focus should be back on the notification button
                await expect(notificationButton).toBeFocused();
            });

            test("should reveal the options menu when a room is focused with the keyboard", async ({
                page,
                app,
                user,
            }) => {
                // Regression test: navigating the room list with the keyboard must reveal a room's hover
                // menu so the "More options" button is reachable by Tab, rather than focus escaping to
                // <body>. The reveal must depend on keyboard focus alone, so we move focus with the
                // keyboard to an adjacent room the pointer is NOT over — otherwise :hover would reveal
                // the menu and mask the behaviour (which is why the other keyboard tests don't catch it).
                const roomListView = getRoomList(page);
                const room29 = roomListView.getByRole("option", { name: "Open room room29" });
                const room28 = roomListView.getByRole("option", { name: "Open room room28" });
                const moreButton = room28.getByRole("button", { name: "More options" });

                // Open the room, then put focus back on the room list item.
                await room29.click();
                await room29.click();
                await expect(room29).toBeFocused();

                // Keyboard-focus the adjacent room (the pointer is still over room29, not room28), so the
                // menu's visibility depends purely on keyboard focus and not on :hover.
                await page.keyboard.press("ArrowDown");
                await expect(room28).toBeFocused();

                // The "More options" button must be revealed and reachable by Tab.
                await page.keyboard.press("Tab");
                await expect(moreButton).toBeFocused();

                // TODO: once menu-close focus restoration is fixed, extend this to open the menu
                // (Enter) and assert that Escape returns focus to a room list item rather than <body>.
                // Today that focus restoration is broken, so it isn't asserted here.
            });

            test("should navigate to the top and then bottom of the room list", async ({ page, app, user }) => {
                const roomListView = getRoomList(page);

                const topRoom = roomListView.getByRole("option", { name: "Open room room29" });

                // open the room
                await topRoom.click();
                // put focus back on the room list item
                await topRoom.click();
                await expect(topRoom).toBeFocused();

                await page.keyboard.press("End");
                const bottomRoom = roomListView.getByRole("option", { name: "Open room room0" });
                await expect(bottomRoom).toBeFocused();

                await page.keyboard.press("Home");
                const topRoomAgain = roomListView.getByRole("option", { name: "Open room room29" });
                await expect(topRoomAgain).toBeFocused();
            });
        });
    });

    test.describe("Avatar decoration", () => {
        test.use({ labsFlags: ["feature_video_rooms"] });

        test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
            // @ts-ignore Visibility enum is not accessible
            await app.client.createRoom({ name: "public room", visibility: "public" });

            // focus the user menu to avoid to have hover decoration
            await page.getByRole("button", { name: "User menu" }).focus();

            const roomListView = getRoomList(page);
            const publicRoom = roomListView.getByRole("option", { name: "public room" });

            await expect(publicRoom).toBeVisible();
            await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
        });

        test("should be a low priority room", { tag: "@screenshot" }, async ({ page, app, user }) => {
            // @ts-ignore Visibility enum is not accessible
            await app.client.createRoom({ name: "low priority room", visibility: "public" });
            const roomListView = getRoomList(page);
            let publicRoom = roomListView.getByRole("option", { name: "low priority room" });

            // Make room low priority
            await publicRoom.click({ button: "right" });
            await page.getByRole("menuitemcheckbox", { name: "Low priority" }).click();

            // Should have low priority decoration
            publicRoom = roomListView.getByRole("gridcell", { name: "low priority room" });
            await expect(publicRoom.locator(".mx_RoomAvatarView_icon")).toHaveAccessibleName(
                "This is a low priority room",
            );

            // focus the header to avoid to have hover decoration
            await page.getByTestId("room-list-header").click();
            await expect(publicRoom).toMatchScreenshot("room-list-item-low-priority.png");
        });

        test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
            await page
                .getByRole("navigation", { name: "Room list" })
                .getByRole("button", { name: "New conversation" })
                .click();
            await page.getByRole("menuitem", { name: "New video room" }).click();
            await page.getByRole("textbox", { name: "Name" }).fill("video room");
            // Make it public to avoid any crypto setup toasts
            await page.getByRole("button", { name: "Room visibility" }).click();
            await page.getByRole("option", { name: "Public room" }).click();
            await page.getByRole("textbox", { name: "Room address" }).fill("video-room");
            await page.getByRole("button", { name: "Create video room" }).click();

            const roomListView = getRoomList(page);
            const videoRoom = roomListView.getByRole("option", { name: "video room" });
            await expect(videoRoom).toHaveAttribute("aria-selected", "true"); // wait for room list update

            // Ensure we highlight the video
            await videoRoom.click();

            // focus the user menu to avoid to have hover decoration
            await page.getByRole("button", { name: "User menu" }).hover();

            await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
        });
    });

    test.describe("Notification decoration", () => {
        test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
            const roomListView = getRoomList(page);

            await bot.createRoom({
                name: "invited room",
                invite: [user.userId],
                is_direct: true,
            });
            const invitedRoom = roomListView.getByRole("option", { name: "invited room" });
            await expect(invitedRoom).toBeVisible();
            await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
        });

        test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
            const roomListView = getRoomList(page);

            const roomId = await app.client.createRoom({ name: "2 notifications" });
            await app.client.inviteUser(roomId, bot.credentials.userId);
            await bot.joinRoom(roomId);

            await bot.sendMessage(roomId, "I am a robot. Beep.");
            await bot.sendMessage(roomId, "I am a robot. Beep.");

            const room = roomListView.getByRole("option", { name: "2 notifications" });
            await expect(room).toBeVisible();
            await expect(room.getByTestId("notification-decoration")).toHaveText("2");
            await expect(room).toMatchScreenshot("room-list-item-notification.png");
        });

        test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
            const roomListView = getRoomList(page);

            const roomId = await app.client.createRoom({ name: "mention" });
            await app.client.inviteUser(roomId, bot.credentials.userId);
            await bot.joinRoom(roomId);

            const clientBot = await bot.prepareClient();
            await clientBot.evaluate(
                async (client, { roomId, userId }) => {
                    await client.sendMessage(roomId, {
                        // @ts-ignore ignore usage of MsgType.text
                        "msgtype": "m.text",
                        "body": "User",
                        "format": "org.matrix.custom.html",
                        "formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
                        "m.mentions": {
                            user_ids: [userId],
                        },
                    });
                },
                { roomId, userId: user.userId },
            );
            await bot.sendMessage(roomId, "I am a robot. Beep.");

            const room = roomListView.getByRole("option", { name: "mention" });
            await expect(room).toBeVisible();
            await expect(room).toMatchScreenshot("room-list-item-mention.png");
        });

        async function checkMessagePreview(page: Page, app: ElementAppPage, bot: Bot) {
            const roomListView = getRoomList(page);
            const roomId = await app.client.createRoom({ name: "activity" });

            // focus the user menu to avoid to have hover decoration
            await page.getByRole("button", { name: "User menu" }).focus();

            await app.client.inviteUser(roomId, bot.credentials.userId);
            await bot.joinRoom(roomId);
            await bot.sendMessage(roomId, "I am a robot. Beep.");

            const room = roomListView.getByRole("option", { name: "activity" });
            await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
            await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
        }

        test(
            "should render a message preview when enable in settings",
            { tag: "@screenshot" },
            async ({ page, app, user, bot }) => {
                await app.settings.openUserSettings("Preferences");
                await page.getByRole("switch", { name: "Show message previews" }).click();
                await app.closeDialog();

                await checkMessagePreview(page, app, bot);
            },
        );

        test(
            "should render a message preview when enabled in header",
            { tag: "@screenshot" },
            async ({ page, app, user, bot }) => {
                await page.getByRole("button", { name: "Room Options" }).click();
                await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click();

                await checkMessagePreview(page, app, bot);
            },
        );

        test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
            const roomListView = getRoomList(page);

            const otherRoomId = await app.client.createRoom({ name: "other room" });

            const roomId = await app.client.createRoom({ name: "activity" });
            await app.client.inviteUser(roomId, bot.credentials.userId);
            await bot.joinRoom(roomId);

            await app.viewRoomById(roomId);
            await app.settings.openRoomSettings("Notifications");
            await page.getByText("@mentions and replies only").click();
            await app.settings.closeDialog();

            await app.settings.openUserSettings("Notifications");
            await page
                .getByRole("switch", { name: "Show all activity in the room list (dots or number of unread messages)" })
                .check();
            await app.settings.closeDialog();

            // Switch to the other room to avoid the notification to be cleared
            await app.viewRoomById(otherRoomId);
            await bot.sendMessage(roomId, "I am a robot. Beep.");

            const room = roomListView.getByRole("option", { name: "activity" });
            await expect(room.getByTestId("notification-decoration")).toBeVisible();
            await expect(room).toMatchScreenshot("room-list-item-activity.png");
        });

        test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
            const roomListView = getRoomList(page);

            const roomId = await app.client.createRoom({ name: "mark as unread" });
            await app.client.inviteUser(roomId, bot.credentials.userId);
            await bot.joinRoom(roomId);

            const room = roomListView.getByRole("option", { name: "mark as unread" });
            await room.click({ button: "right" });
            await page.getByRole("menuitem", { name: "mark as unread" }).click();

            // focus the header to avoid to have hover decoration
            await page.getByTestId("room-list-header").click();

            await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
        });

        test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
            const roomListView = getRoomList(page);

            const roomId = await app.client.createRoom({ name: "silent" });
            await app.client.inviteUser(roomId, bot.credentials.userId);
            await bot.joinRoom(roomId);

            await app.viewRoomById(roomId);
            await app.settings.openRoomSettings("Notifications");
            await page.getByText("Mute").click();
            await app.settings.closeDialog();

            const room = roomListView.getByRole("option", { name: "silent" });
            await expect(room.getByTestId("notification-decoration")).toBeVisible();
            await expect(room).toMatchScreenshot("room-list-item-silent.png");
        });
    });
});
