/*
Copyright 2022 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import React from "react";
import ReactTestUtils from "react-dom/test-utils";
import ReactDOM from "react-dom";
import { PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";

import * as TestUtils from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import dis from "../../../../src/dispatcher/dispatcher";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { DefaultTagID } from "../../../../src/stores/room-list/models";
import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
import RoomList from "../../../../src/components/views/rooms/RoomList";
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
import { RoomTile } from "../../../../src/components/views/rooms/RoomTile";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";

function generateRoomId() {
    return "!" + Math.random().toString().slice(2, 10) + ":domain";
}

describe("RoomList", () => {
    function createRoom(opts) {
        const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
            // The room list now uses getPendingEvents(), so we need a detached ordering.
            pendingEventOrdering: PendingEventOrdering.Detached,
        });
        if (opts) {
            Object.assign(room, opts);
        }
        return room;
    }

    let parentDiv = null;
    let root = null;
    const myUserId = "@me:domain";

    const movingRoomId = "!someroomid";
    let movingRoom: Room | undefined;
    let otherRoom: Room | undefined;

    let myMember: RoomMember | undefined;
    let myOtherMember: RoomMember | undefined;

    const client = getMockClientWithEventEmitter({
        ...mockClientMethodsUser(myUserId),
        getRooms: jest.fn(),
        getVisibleRooms: jest.fn(),
        getRoom: jest.fn(),
    });

    const defaultProps = {
        onKeyDown: jest.fn(),
        onFocus: jest.fn(),
        onBlur: jest.fn(),
        onResize: jest.fn(),
        resizeNotifier: {} as unknown as ResizeNotifier,
        isMinimized: false,
        activeSpace: "",
    };

    beforeEach(async function (done) {
        RoomListStoreClass.TEST_MODE = true;
        jest.clearAllMocks();

        client.credentials = { userId: myUserId };

        DMRoomMap.makeShared();

        parentDiv = document.createElement("div");
        document.body.appendChild(parentDiv);

        const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
        root = ReactDOM.render(<WrappedRoomList {...defaultProps} />, parentDiv);
        ReactTestUtils.findRenderedComponentWithType(root, RoomList);

        movingRoom = createRoom({ name: "Moving room" });
        expect(movingRoom.roomId).not.toBe(null);

        // Mock joined member
        myMember = new RoomMember(movingRoomId, myUserId);
        myMember.membership = "join";
        movingRoom.updateMyMembership("join");
        movingRoom.getMember = (userId) =>
            ({
                [client.credentials.userId]: myMember,
            }[userId]);

        otherRoom = createRoom({ name: "Other room" });
        myOtherMember = new RoomMember(otherRoom.roomId, myUserId);
        myOtherMember.membership = "join";
        otherRoom.updateMyMembership("join");
        otherRoom.getMember = (userId) =>
            ({
                [client.credentials.userId]: myOtherMember,
            }[userId]);

        // Mock the matrix client
        const mockRooms = [
            movingRoom,
            otherRoom,
            createRoom({ tags: { "m.favourite": { order: 0.1 } }, name: "Some other room" }),
            createRoom({ tags: { "m.favourite": { order: 0.2 } }, name: "Some other room 2" }),
            createRoom({ tags: { "m.lowpriority": {} }, name: "Some unimportant room" }),
            createRoom({ tags: { "custom.tag": {} }, name: "Some room customly tagged" }),
        ];
        client.getRooms.mockReturnValue(mockRooms);
        client.getVisibleRooms.mockReturnValue(mockRooms);

        const roomMap = {};
        client.getRooms().forEach((r) => {
            roomMap[r.roomId] = r;
        });

        client.getRoom.mockImplementation((roomId) => roomMap[roomId]);

        // Now that everything has been set up, prepare and update the store
        await (RoomListStore.instance as RoomListStoreClass).makeReady(client);

        done();
    });

    afterEach(async (done) => {
        if (parentDiv) {
            ReactDOM.unmountComponentAtNode(parentDiv);
            parentDiv.remove();
            parentDiv = null;
        }

        await RoomListLayoutStore.instance.resetLayouts();
        await (RoomListStore.instance as RoomListStoreClass).resetStore();

        done();
    });

    function expectRoomInSubList(room, subListTest) {
        const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSublist);
        const containingSubList = subLists.find(subListTest);

        let expectedRoomTile;
        try {
            const roomTiles = ReactTestUtils.scryRenderedComponentsWithType(containingSubList, RoomTile);
            console.info({ roomTiles: roomTiles.length });
            expectedRoomTile = roomTiles.find((tile) => tile.props.room === room);
        } catch (err) {
            // truncate the error message because it's spammy
            err.message =
                "Error finding RoomTile for " +
                room.roomId +
                " in " +
                subListTest +
                ": " +
                err.message.split("componentType")[0] +
                "...";
            throw err;
        }

        expect(expectedRoomTile).toBeTruthy();
        expect(expectedRoomTile.props.room).toBe(room);
    }

    function expectCorrectMove(oldTagId, newTagId) {
        const getTagSubListTest = (tagId) => {
            return (s) => s.props.tagId === tagId;
        };

        // Default to finding the destination sublist with newTag
        const destSubListTest = getTagSubListTest(newTagId);
        const srcSubListTest = getTagSubListTest(oldTagId);

        // Set up the room that will be moved such that it has the correct state for a room in
        // the section for oldTagId
        if (oldTagId === DefaultTagID.Favourite || oldTagId === DefaultTagID.LowPriority) {
            movingRoom.tags = { [oldTagId]: {} };
        } else if (oldTagId === DefaultTagID.DM) {
            // Mock inverse m.direct
            // @ts-ignore forcing private property
            DMRoomMap.shared().roomToUser = {
                [movingRoom.roomId]: "@someotheruser:domain",
            };
        }

        dis.dispatch({ action: "MatrixActions.sync", prevState: null, state: "PREPARED", matrixClient: client });

        expectRoomInSubList(movingRoom, srcSubListTest);

        dis.dispatch({
            action: "RoomListActions.tagRoom.pending",
            request: {
                oldTagId,
                newTagId,
                room: movingRoom,
            },
        });

        expectRoomInSubList(movingRoom, destSubListTest);
    }

    function itDoesCorrectOptimisticUpdatesForDraggedRoomTiles() {
        // TODO: Re-enable dragging tests when we support dragging again.
        describe.skip("does correct optimistic update when dragging from", () => {
            it("rooms to people", () => {
                expectCorrectMove(undefined, DefaultTagID.DM);
            });

            it("rooms to favourites", () => {
                expectCorrectMove(undefined, "m.favourite");
            });

            it("rooms to low priority", () => {
                expectCorrectMove(undefined, "m.lowpriority");
            });

            // XXX: Known to fail - the view does not update immediately to reflect the change.
            // Whe running the app live, it updates when some other event occurs (likely the
            // m.direct arriving) that these tests do not fire.
            xit("people to rooms", () => {
                expectCorrectMove(DefaultTagID.DM, undefined);
            });

            it("people to favourites", () => {
                expectCorrectMove(DefaultTagID.DM, "m.favourite");
            });

            it("people to lowpriority", () => {
                expectCorrectMove(DefaultTagID.DM, "m.lowpriority");
            });

            it("low priority to rooms", () => {
                expectCorrectMove("m.lowpriority", undefined);
            });

            it("low priority to people", () => {
                expectCorrectMove("m.lowpriority", DefaultTagID.DM);
            });

            it("low priority to low priority", () => {
                expectCorrectMove("m.lowpriority", "m.lowpriority");
            });

            it("favourites to rooms", () => {
                expectCorrectMove("m.favourite", undefined);
            });

            it("favourites to people", () => {
                expectCorrectMove("m.favourite", DefaultTagID.DM);
            });

            it("favourites to low priority", () => {
                expectCorrectMove("m.favourite", "m.lowpriority");
            });
        });
    }

    itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
});
