/*
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 { shuffle } from "lodash";

import type { Room } from "matrix-js-sdk/src/matrix";
import type { Sorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters";
import type { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
import { mkMessage, stubClient } from "../../../../test-utils";
import { RoomSkipList } from "../../../../../src/stores/room-list-v3/skip-list/RoomSkipList";
import { RecencySorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
import { AlphabeticSorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
import { getMockedRooms } from "./getMockedRooms";
import { MetaSpace } from "../../../../../src/stores/spaces";
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
import { FavouriteFilter } from "../../../../../src/stores/room-list-v3/skip-list/filters/FavouriteFilter";
import { FilterEnum } from "../../../../../src/stores/room-list-v3/skip-list/filters";
import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";

describe("RoomSkipList", () => {
    function generateSkipList(roomCount?: number): {
        skipList: RoomSkipList;
        rooms: Room[];
        totalRooms: number;
        sorter: Sorter;
    } {
        const client = stubClient();
        const sorter = new RecencySorter(client.getSafeUserId());
        const skipList = new RoomSkipList(sorter);
        const rooms = getMockedRooms(client, roomCount);
        skipList.seed(rooms);
        return { skipList, rooms, totalRooms: rooms.length, sorter };
    }

    beforeEach(() => {
        jest.spyOn(SDKContextClass.instance.spaceStore, "isRoomInSpace").mockImplementation(
            (space) => space === MetaSpace.Home,
        );
        jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpace", "get").mockImplementation(() => MetaSpace.Home);
        jest.spyOn(SDKContextClass.instance.spaceStore, "storeReadyPromise", "get").mockImplementation(() =>
            Promise.resolve(),
        );
        jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation(() => {
            const state = {
                mute: false,
            } as unknown as RoomNotificationState;
            return state;
        });
    });

    it("Rooms are in sorted order after initial seed", () => {
        const { skipList, totalRooms } = generateSkipList();
        expect(skipList.size).toEqual(totalRooms);
        const sortedRooms = [...skipList];
        for (let i = 0; i < totalRooms; ++i) {
            expect(sortedRooms[i].roomId).toEqual(`!foo${totalRooms - i - 1}:matrix.org`);
        }
    });

    it("Tolerates multiple, repeated inserts of existing rooms", () => {
        const { skipList, rooms, totalRooms } = generateSkipList();
        // Let's choose 5 rooms from the list
        const toInsert = [23, 76, 2, 90, 66].map((i) => rooms[i]);
        for (const room of toInsert) {
            // Insert this room 10 times
            for (let i = 0; i < 10; ++i) {
                skipList.reInsertRoom(room);
            }
        }
        // Sorting order should be the same as before
        const sortedRooms = [...skipList];
        for (let i = 0; i < totalRooms; ++i) {
            expect(sortedRooms[i].roomId).toEqual(`!foo${totalRooms - i - 1}:matrix.org`);
        }
    });

    it("Sorting order is maintained when rooms are inserted", () => {
        const { skipList, rooms, totalRooms } = generateSkipList();
        // To simulate the worst case, let's say the order gets reversed one by one
        for (let i = 0; i < rooms.length; ++i) {
            const room = rooms[i];
            const event = mkMessage({
                room: room.roomId,
                user: `@foo${i}:matrix.org`,
                ts: totalRooms - i,
                event: true,
            });
            jest.spyOn(room.getLiveTimeline(), "getEvents").mockReturnValue([event]);
            skipList.reInsertRoom(room);
            expect(skipList.size).toEqual(rooms.length);
        }
        const sortedRooms = [...skipList];
        for (let i = 0; i < totalRooms; ++i) {
            expect(sortedRooms[i].roomId).toEqual(`!foo${i}:matrix.org`);
        }
    });

    it("Room is not duplicated when same room is added via addNewRoom", () => {
        const { skipList, rooms } = generateSkipList();
        const room = rooms[5];
        const sizeBefore = skipList.size;
        skipList.addNewRoom(room);

        expect(skipList.size).toEqual(sizeBefore);
    });

    it("Filters are applied to existing nodes when useNewFilters is called", () => {
        const { skipList, rooms } = generateSkipList(10);

        // Mark some rooms as favourite
        const favouriteRooms = [rooms[2], rooms[5], rooms[8]];
        for (const room of favouriteRooms) {
            room.tags = { [DefaultTagID.Favourite]: { order: 0 } };
        }

        // No filters yet — all rooms are in the list
        expect(skipList.size).toEqual(10);

        // Apply the favourite filter
        skipList.useNewFilters([new FavouriteFilter()]);

        // Only favourite rooms should be returned when filtering by favourite
        const filteredRooms = Array.from(skipList.getRoomsInActiveSpace([FilterEnum.FavouriteFilter]));
        expect(filteredRooms).toHaveLength(favouriteRooms.length);
    });

    it("Re-sort works when sorter is swapped", () => {
        const { skipList, rooms, sorter } = generateSkipList();
        const sortedByRecency = [...rooms].sort((a, b) => sorter.comparator(a, b));
        expect(sortedByRecency).toEqual([...skipList]);
        // Now switch over to alphabetic sorter
        const newSorter = new AlphabeticSorter();
        skipList.useNewSorter(newSorter, rooms);
        const sortedByAlphabet = [...rooms].sort((a, b) => newSorter.comparator(a, b));
        expect(sortedByAlphabet).toEqual([...skipList]);
    });

    describe("Empty skip list functionality", () => {
        it("Insertions into empty skip list works", () => {
            // Create an empty skip list
            const client = stubClient();
            const sorter = new RecencySorter(client.getSafeUserId());
            const roomSkipList = new RoomSkipList(sorter);
            expect(roomSkipList.size).toEqual(0);
            roomSkipList.seed([]);
            expect(roomSkipList.size).toEqual(0);

            // Create some rooms
            const totalRooms = 10;
            const rooms = getMockedRooms(client, totalRooms);

            // Shuffle and insert the rooms
            for (const room of shuffle(rooms)) {
                roomSkipList.addNewRoom(room);
            }

            expect(roomSkipList.size).toEqual(totalRooms);
            const sortedRooms = [...roomSkipList];
            for (let i = 0; i < totalRooms; ++i) {
                expect(sortedRooms[i].roomId).toEqual(`!foo${totalRooms - i - 1}:matrix.org`);
            }
        });

        it("Tolerates deletions until skip list is empty", () => {
            const { skipList, rooms } = generateSkipList(10);
            const sorted = [...skipList];
            for (const room of shuffle(rooms)) {
                skipList.removeRoom(room);
                const i = sorted.findIndex((r) => r.roomId === room.roomId);
                sorted.splice(i, 1);
                expect([...skipList]).toEqual(sorted);
            }
            expect(skipList.size).toEqual(0);
        });
    });
});
