/*
Copyright 2024 New Vector Ltd.
Copyright 2021 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 { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";

import WidgetStore, { type IApp } from "../../../src/stores/WidgetStore";
import { WidgetLayoutStore } from "../../../src/stores/widgets/WidgetLayoutStore";
import { stubClient } from "../../test-utils";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../src/settings/SettingsStore";
import { Action } from "../../../src/dispatcher/actions.ts";

// setup test env values
const roomId = "!room:server";

describe("WidgetLayoutStore", () => {
    let client: MatrixClient;
    let store: WidgetLayoutStore;
    let roomUpdateListener: (event: string) => void;
    let mockApps: IApp[];
    let mockRoom: Room;
    let layoutEventContent: Record<string, any> | null;

    beforeEach(() => {
        layoutEventContent = null;
        mockRoom = <Room>{
            roomId: roomId,
            currentState: {
                getStateEvents: (_l, _x) => {
                    return {
                        getId: () => "$layoutEventId",
                        getContent: () => layoutEventContent,
                    };
                },
            },
        };

        mockApps = [
            <IApp>{ roomId: roomId, id: "1" },
            <IApp>{ roomId: roomId, id: "2" },
            <IApp>{ roomId: roomId, id: "3" },
            <IApp>{ roomId: roomId, id: "4" },
        ];

        // fake the WidgetStore.instance to just return an object with `getApps`
        jest.spyOn(WidgetStore, "instance", "get").mockReturnValue({
            on: jest.fn(),
            off: jest.fn(),
            getApps: () => mockApps,
        } as unknown as WidgetStore);

        SettingsStore.reset();
    });

    beforeAll(() => {
        // we need to init a client so it does not error, when asking for DeviceStorage handlers (SettingsStore.setValue("Widgets.layout"))
        client = stubClient();

        roomUpdateListener = jest.fn();
        // @ts-ignore bypass private ctor for tests
        store = new WidgetLayoutStore();
        store.addListener(`update_${roomId}`, roomUpdateListener);
    });

    afterAll(() => {
        store.removeListener(`update_${roomId}`, roomUpdateListener);
    });

    it("all widgets should be in the right container by default", () => {
        store.recalculateRoom(mockRoom);
        expect(store.getContainerWidgets(mockRoom, "right").length).toStrictEqual(mockApps.length);
    });

    it("add widget to top container", async () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0]]);
        expect(store.getContainerHeight(mockRoom, "top")).toBeNull();
    });

    it("ordering of top container widgets should be consistent even if no index specified", async () => {
        layoutEventContent = {
            widgets: {
                "1": {
                    container: "top",
                },
                "2": {
                    container: "top",
                },
            },
        };

        store.recalculateRoom(mockRoom);
        expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0], mockApps[1]]);
    });

    it("add three widgets to top container", async () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        store.moveToContainer(mockRoom, mockApps[2], "top");
        expect(new Set(store.getContainerWidgets(mockRoom, "top"))).toEqual(
            new Set([mockApps[0], mockApps[1], mockApps[2]]),
        );
    });

    it("cannot add more than three widgets to top container", async () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        store.moveToContainer(mockRoom, mockApps[2], "top");
        expect(store.canAddToContainer(mockRoom, "top")).toEqual(false);
    });

    it("remove pins when maximising (other widget)", async () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        store.moveToContainer(mockRoom, mockApps[2], "top");
        store.moveToContainer(mockRoom, mockApps[3], "center");
        expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
        expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
            new Set([mockApps[0], mockApps[1], mockApps[2]]),
        );
        expect(store.getContainerWidgets(mockRoom, "center")).toEqual([mockApps[3]]);
    });

    it("remove pins when maximising (one of the pinned widgets)", async () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        store.moveToContainer(mockRoom, mockApps[2], "top");
        store.moveToContainer(mockRoom, mockApps[0], "center");
        expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
        expect(store.getContainerWidgets(mockRoom, "center")).toEqual([mockApps[0]]);
        expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
            new Set([mockApps[1], mockApps[2], mockApps[3]]),
        );
    });

    it("remove maximised when pinning (other widget)", async () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "center");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[1]]);
        expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
        expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
            new Set([mockApps[2], mockApps[3], mockApps[0]]),
        );
    });

    it("remove maximised when pinning (same widget)", async () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "center");
        store.moveToContainer(mockRoom, mockApps[0], "top");
        expect(store.getContainerWidgets(mockRoom, "top")).toEqual([mockApps[0]]);
        expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
        expect(new Set(store.getContainerWidgets(mockRoom, "right"))).toEqual(
            new Set([mockApps[2], mockApps[3], mockApps[1]]),
        );
    });

    it("should recalculate all rooms when the client is ready", async () => {
        mocked(client.getVisibleRooms).mockReturnValue([mockRoom]);
        await store.start();

        expect(roomUpdateListener).toHaveBeenCalled();
        expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
        expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
        expect(store.getContainerWidgets(mockRoom, "right")).toEqual([
            mockApps[0],
            mockApps[1],
            mockApps[2],
            mockApps[3],
        ]);
    });

    it("should clear the layout and emit an update if there are no longer apps in the room", () => {
        store.recalculateRoom(mockRoom);
        mocked(roomUpdateListener).mockClear();

        jest.spyOn(WidgetStore, "instance", "get").mockReturnValue(<WidgetStore>(
            ({ getApps: (): IApp[] => [] } as unknown as WidgetStore)
        ));
        store.recalculateRoom(mockRoom);
        expect(roomUpdateListener).toHaveBeenCalled();
        expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
        expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
        expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
    });

    it("should clear the layout if the client is not viable", () => {
        store.recalculateRoom(mockRoom);
        defaultDispatcher.dispatch({ action: Action.ClientNotViable }, true);

        expect(store.getContainerWidgets(mockRoom, "top")).toEqual([]);
        expect(store.getContainerWidgets(mockRoom, "center")).toEqual([]);
        expect(store.getContainerWidgets(mockRoom, "right")).toEqual([]);
    });

    it("should return the expected resizer distributions", () => {
        // this only works for top widgets
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        expect(store.getResizerDistributions(mockRoom, "top")).toEqual(["50.0%"]);
    });

    it("should set and return container height", () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        store.setContainerHeight(mockRoom, "top", 23);
        expect(store.getContainerHeight(mockRoom, "top")).toBe(23);
    });

    it("should move a widget within a container", () => {
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.moveToContainer(mockRoom, mockApps[1], "top");
        store.moveToContainer(mockRoom, mockApps[2], "top");
        expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[0], mockApps[1], mockApps[2]]);
        store.moveWithinContainer(mockRoom, "top", mockApps[0], 1);
        expect(store.getContainerWidgets(mockRoom, "top")).toStrictEqual([mockApps[1], mockApps[0], mockApps[2]]);
    });

    it("should copy the layout to the room", async () => {
        await store.start();
        store.recalculateRoom(mockRoom);
        store.moveToContainer(mockRoom, mockApps[0], "top");
        store.copyLayoutToRoom(mockRoom);

        expect(mocked(client.sendStateEvent).mock.calls).toMatchInlineSnapshot(`
            [
              [
                "!room:server",
                "io.element.widgets.layout",
                {
                  "widgets": {
                    "1": {
                      "container": "top",
                      "height": undefined,
                      "index": 0,
                      "width": 100,
                    },
                    "2": {
                      "container": "right",
                    },
                    "3": {
                      "container": "right",
                    },
                    "4": {
                      "container": "right",
                    },
                  },
                },
                "",
              ],
            ]
        `);
    });

    it("Can call onNotReady before onReady has been called", () => {
        // Just to quieten SonarCloud :-(

        // @ts-ignore bypass private ctor for tests
        const store = new WidgetLayoutStore();
        // @ts-ignore calling private method
        store.onNotReady();
    });

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

        it("passes the flag in to getVisibleRooms", async () => {
            mocked(client.getVisibleRooms).mockRestore();
            mocked(client.getVisibleRooms).mockReturnValue([]);
            // @ts-ignore bypass private ctor for tests
            const store = new WidgetLayoutStore();
            await store.start();
            expect(client.getVisibleRooms).toHaveBeenCalledWith(false);
        });
    });

    describe("when feature_dynamic_room_predecessors is enabled", () => {
        beforeAll(() => {
            jest.spyOn(SettingsStore, "getValue").mockImplementation(
                (settingName) => settingName === "feature_dynamic_room_predecessors",
            );
        });

        it("passes the flag in to getVisibleRooms", async () => {
            mocked(client.getVisibleRooms).mockRestore();
            mocked(client.getVisibleRooms).mockReturnValue([]);
            // @ts-ignore bypass private ctor for tests
            const store = new WidgetLayoutStore();
            await store.start();
            expect(client.getVisibleRooms).toHaveBeenCalledWith(true);
        });
    });
});
