/*
 * Copyright 2026 Element Creations 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 EventEmitter from "events";
import { waitFor } from "@testing-library/dom";
import { mocked } from "jest-mock";
import {
    EventStatus,
    EventTimeline,
    EventType,
    M_BEACON_INFO,
    MatrixEvent,
    MatrixEventEvent,
    MsgType,
    RelationType,
    RoomStateEvent,
} from "matrix-js-sdk/src/matrix";
import { ActionBarAction } from "@element-hq/web-shared-components";

import {
    EventTileActionBarViewModel,
    type EventTileActionBarViewModelProps,
} from "../../../src/viewmodels/room/EventTileActionBarViewModel";
import { TimelineRenderingType } from "../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import Resend from "../../../src/Resend";
import PinningUtils from "../../../src/utils/PinningUtils";
import PosthogTrackers from "../../../src/PosthogTrackers";
import Modal from "../../../src/Modal";
import ErrorDialog from "../../../src/components/views/dialogs/ErrorDialog";
import SettingsStore from "../../../src/settings/SettingsStore";
import { ModuleApi } from "../../../src/modules/Api";
import { canCancel, canEditContent, editEvent, isContentActionable } from "../../../src/utils/EventUtils";
import { shouldDisplayReply } from "../../../src/utils/Reply";
import { MediaEventHelper } from "../../../src/utils/MediaEventHelper";
import { getMediaVisibility, setMediaVisibility } from "../../../src/utils/media/mediaVisibility";
import { createTestClient } from "../../test-utils";

jest.mock("../../../src/dispatcher/dispatcher", () => ({
    __esModule: true,
    default: {
        dispatch: jest.fn(),
        register: jest.fn().mockReturnValue("dispatcher-ref"),
        unregister: jest.fn(),
    },
}));

jest.mock("../../../src/Resend", () => ({
    __esModule: true,
    default: {
        resend: jest.fn(),
        removeFromQueue: jest.fn(),
    },
}));

jest.mock("../../../src/PosthogTrackers", () => ({
    __esModule: true,
    default: {
        trackPinUnpinMessage: jest.fn(),
    },
}));

jest.mock("../../../src/Modal", () => ({
    __esModule: true,
    default: {
        createDialog: jest.fn(),
    },
}));

jest.mock("../../../src/languageHandler", () => ({
    _t: (key: string) => {
        switch (key) {
            case "timeline|download_failed":
                return "Download failed";
            case "timeline|download_failed_description":
                return "Failed to download file";
            case "common|image":
                return "Image";
            default:
                return key;
        }
    },
    _td: (key: string) => key,
}));

jest.mock("../../../src/utils/EventUtils", () => ({
    canCancel: jest.fn(),
    canEditContent: jest.fn(),
    editEvent: jest.fn(),
    isContentActionable: jest.fn(),
}));

jest.mock("../../../src/utils/PinningUtils", () => ({
    __esModule: true,
    default: {
        canPin: jest.fn(),
        canUnpin: jest.fn(),
        isPinned: jest.fn(),
        pinOrUnpinEvent: jest.fn(),
    },
}));

jest.mock("../../../src/utils/Reply", () => ({
    shouldDisplayReply: jest.fn(),
}));

jest.mock("../../../src/utils/media/mediaVisibility", () => ({
    getMediaVisibility: jest.fn(),
    setMediaVisibility: jest.fn(),
}));

const mockDownload = jest.fn();
jest.mock("../../../src/utils/FileDownloader", () => ({
    FileDownloader: jest.fn().mockImplementation(() => ({
        download: mockDownload,
    })),
}));

describe("EventTileActionBarViewModel", () => {
    const userId = "@alice:example.org";
    const roomId = "!room:example.org";
    const rootEvent = new MatrixEvent({
        type: EventType.RoomMessage,
        room_id: roomId,
        sender: "@root:example.org",
        event_id: "$root",
        content: { msgtype: MsgType.Text, body: "Root" },
    });

    let client: ReturnType<typeof createTestClient>;
    let roomState: EventEmitter;
    let room: {
        getLiveTimeline: jest.Mock;
    };
    let getHintsForMessageSpy: jest.SpyInstance;

    const createMessageEvent = (overrides: Partial<ConstructorParameters<typeof MatrixEvent>[0]> = {}): MatrixEvent =>
        new MatrixEvent({
            type: EventType.RoomMessage,
            room_id: roomId,
            sender: userId,
            event_id: "$event",
            content: { msgtype: MsgType.Text, body: "Hello" },
            ...overrides,
        });

    const createVm = (props: Partial<EventTileActionBarViewModelProps> = {}): EventTileActionBarViewModel => {
        const mxEvent = props.mxEvent ?? createMessageEvent();
        return new EventTileActionBarViewModel({
            mxEvent,
            timelineRenderingType: TimelineRenderingType.Room,
            canSendMessages: true,
            canReact: true,
            ...props,
        });
    };

    const createPendingPromise = <T>(): {
        promise: Promise<T>;
        resolve: (value: T) => void;
        reject: (reason?: unknown) => void;
    } => {
        let resolve!: (value: T) => void;
        let reject!: (reason?: unknown) => void;
        const promise = new Promise<T>((res, rej) => {
            resolve = res;
            reject = rej;
        });
        return { promise, resolve, reject };
    };

    beforeEach(() => {
        jest.clearAllMocks();
        client = createTestClient();
        roomState = new EventEmitter();
        room = {
            getLiveTimeline: jest.fn().mockReturnValue({
                getState: jest
                    .fn()
                    .mockImplementation((dir) => (dir === EventTimeline.FORWARDS ? roomState : undefined)),
            }),
        };

        jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(client);
        jest.spyOn(client, "getRoom").mockReturnValue(room as never);
        jest.spyOn(client, "decryptEventIfNeeded");

        jest.spyOn(SettingsStore, "watchSetting").mockImplementation((name, scope) => `${name}:${scope ?? "global"}`);
        jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(() => {});

        mocked(canCancel).mockImplementation((status) => status === EventStatus.QUEUED);
        mocked(canEditContent).mockReturnValue(true);
        mocked(isContentActionable).mockReturnValue(true);
        mocked(shouldDisplayReply).mockReturnValue(true);
        mocked(getMediaVisibility).mockReturnValue(true);
        mocked(setMediaVisibility).mockResolvedValue(undefined);
        mocked(PinningUtils.canPin).mockReturnValue(false);
        mocked(PinningUtils.canUnpin).mockReturnValue(false);
        mocked(PinningUtils.isPinned).mockReturnValue(false);
        mocked(PinningUtils.pinOrUnpinEvent).mockResolvedValue(undefined);
        jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(false);
        jest.spyOn(MediaEventHelper, "canHide").mockReturnValue(false);
        mockDownload.mockResolvedValue(undefined);

        getHintsForMessageSpy = jest.spyOn(ModuleApi.instance.customComponents, "getHintsForMessage");
        getHintsForMessageSpy.mockReturnValue(null);
    });

    afterEach(() => {
        getHintsForMessageSpy.mockRestore();
        jest.restoreAllMocks();
    });

    it("builds the snapshot for an actionable message", async () => {
        const vm = createVm({ isQuoteExpanded: true });

        await waitFor(() =>
            expect(vm.getSnapshot()).toMatchObject({
                actions: [
                    ActionBarAction.React,
                    ActionBarAction.Reply,
                    ActionBarAction.ReplyInThread,
                    ActionBarAction.Edit,
                    ActionBarAction.Expand,
                    ActionBarAction.Options,
                ],
                presentation: "icon",
                isDownloadEncrypted: false,
                isDownloadLoading: false,
                isPinned: false,
                isQuoteExpanded: true,
                isThreadReplyAllowed: true,
            }),
        );
    });

    it("reacts to media download permission hints and room state updates", async () => {
        jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(true);
        jest.spyOn(MediaEventHelper, "canHide").mockReturnValue(true);
        getHintsForMessageSpy.mockReturnValue({
            allowDownloadingMedia: jest.fn().mockResolvedValue(true),
        } as never);

        const vm = createVm({
            mxEvent: createMessageEvent({
                content: { msgtype: MsgType.Image, body: "Image", url: "mxc://example.org/file" },
            }),
        });

        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);
        expect(vm.getSnapshot().actions).toContain(ActionBarAction.Hide);

        await waitFor(() => expect(vm.getSnapshot().actions).toContain(ActionBarAction.Download));

        mocked(PinningUtils.isPinned).mockReturnValue(true);
        roomState.emit(
            RoomStateEvent.Events,
            new MatrixEvent({
                type: EventType.RoomPinnedEvents,
                room_id: roomId,
                sender: userId,
                content: { pinned: ["$event"] },
            }),
        );

        expect(vm.getSnapshot().isPinned).toBe(true);

        mocked(getMediaVisibility).mockReturnValue(false);
        roomState.emit(
            RoomStateEvent.Events,
            new MatrixEvent({
                type: EventType.RoomJoinRules,
                room_id: roomId,
                sender: userId,
                content: { join_rule: "public" },
            }),
        );

        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Hide);
    });

    it("ignores stale download permission results after setProps changes the event", async () => {
        jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(true);
        const permissionA = createPendingPromise<boolean>();
        const permissionB = createPendingPromise<boolean>();
        const eventA = createMessageEvent({
            event_id: "$eventA",
            content: { msgtype: MsgType.Image, body: "Image A", url: "mxc://example.org/a" },
        });
        const eventB = createMessageEvent({
            event_id: "$eventB",
            content: { msgtype: MsgType.Image, body: "Image B", url: "mxc://example.org/b" },
        });

        getHintsForMessageSpy.mockImplementation((event) => {
            if (event === eventA) {
                return {
                    allowDownloadingMedia: jest.fn().mockReturnValue(permissionA.promise),
                } as never;
            }

            if (event === eventB) {
                return {
                    allowDownloadingMedia: jest.fn().mockReturnValue(permissionB.promise),
                } as never;
            }

            return null;
        });

        const vm = createVm({ mxEvent: eventA });
        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);

        vm.setProps({ mxEvent: eventB });
        permissionA.resolve(true);
        await Promise.resolve();

        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);

        permissionB.resolve(false);
        await Promise.resolve();

        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);
    });

    it("refreshes on event status changes and removes listeners on dispose", () => {
        const mxEvent = createMessageEvent();
        const offSpy = jest.spyOn(mxEvent, "off");
        const roomStateOffSpy = jest.spyOn(roomState, "off");
        const vm = createVm({ mxEvent });

        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Cancel);

        mxEvent.setStatus(EventStatus.QUEUED);

        expect(vm.getSnapshot().actions).toContain(ActionBarAction.Cancel);
        expect(client.decryptEventIfNeeded).toHaveBeenCalledWith(mxEvent);

        vm.dispose();

        expect(offSpy).toHaveBeenCalledWith(MatrixEventEvent.Status, expect.any(Function));
        expect(offSpy).toHaveBeenCalledWith(MatrixEventEvent.Decrypted, expect.any(Function));
        expect(offSpy).toHaveBeenCalledWith(MatrixEventEvent.BeforeRedaction, expect.any(Function));
        expect(roomStateOffSpy).toHaveBeenCalledWith(RoomStateEvent.Events, expect.any(Function));
        expect(SettingsStore.unwatchSetting).toHaveBeenCalledWith("mediaPreviewConfig:!room:example.org");
        expect(SettingsStore.unwatchSetting).toHaveBeenCalledWith("showMediaEventIds:global");
    });

    it("routes resend and cancel actions to the actionable failed event variant", () => {
        const mxEvent = createMessageEvent();
        const localRedactionEvent = createMessageEvent({ event_id: "$redaction" });
        const replacingEvent = createMessageEvent({ event_id: "$replacement" });

        localRedactionEvent.setStatus(EventStatus.SENT);
        replacingEvent.setStatus(EventStatus.QUEUED);

        jest.spyOn(mxEvent, "localRedactionEvent").mockReturnValue(localRedactionEvent);
        jest.spyOn(mxEvent, "replacingEvent").mockReturnValue(replacingEvent);

        const vm = createVm({ mxEvent });

        vm.onResendClick(null);
        vm.onCancelClick(null);

        expect(Resend.resend).toHaveBeenCalledWith(client, localRedactionEvent);
        expect(Resend.removeFromQueue).toHaveBeenCalledWith(client, replacingEvent);
    });

    it("downloads a cached blob and shows an error dialog on failure", async () => {
        const blob = new Blob(["downloaded"]);
        jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(true);

        const vm = createVm({
            mxEvent: createMessageEvent({
                content: { msgtype: MsgType.Image, body: "Image", url: "mxc://example.org/file" },
            }),
        });
        (vm as unknown as { downloadedBlob: Blob }).downloadedBlob = blob;

        await vm.onDownloadClick(null);
        await vm.onDownloadClick(null);

        expect(mockDownload).toHaveBeenNthCalledWith(1, { blob, name: "Image" });
        expect(mockDownload).toHaveBeenNthCalledWith(2, { blob, name: "Image" });

        mockDownload.mockRejectedValueOnce(new Error("boom"));

        await vm.onDownloadClick(null);

        expect(Modal.createDialog).toHaveBeenCalledWith(
            ErrorDialog,
            expect.objectContaining({
                title: "Download failed",
                description: expect.stringContaining("boom"),
            }),
        );
        expect(vm.getSnapshot().isDownloadLoading).toBe(false);
    });

    it("ignores stale download completion after setProps changes the event", async () => {
        jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(true);
        const firstDownload = createPendingPromise<void>();
        const eventA = createMessageEvent({
            event_id: "$eventA",
            content: { msgtype: MsgType.Image, body: "Image A", url: "mxc://example.org/a" },
        });
        const eventB = createMessageEvent({
            event_id: "$eventB",
            content: { msgtype: MsgType.Image, body: "Image B", url: "mxc://example.org/b" },
        });

        const vm = createVm({ mxEvent: eventA });
        (vm as unknown as { downloadedBlob: Blob }).downloadedBlob = new Blob(["a"]);
        mockDownload.mockReturnValueOnce(firstDownload.promise);

        const firstDownloadCall = vm.onDownloadClick(null);

        expect(vm.getSnapshot().isDownloadLoading).toBe(true);

        vm.setProps({ mxEvent: eventB });
        (vm as unknown as { downloadedBlob: Blob }).downloadedBlob = new Blob(["b"]);

        expect(vm.getSnapshot().isDownloadLoading).toBe(false);

        const secondDownload = vm.onDownloadClick(null);
        await secondDownload;

        firstDownload.resolve();
        await firstDownloadCall;

        expect(mockDownload).toHaveBeenCalledTimes(2);
        expect(mockDownload).toHaveBeenNthCalledWith(2, {
            blob: expect.any(Blob),
            name: "Image B",
        });
        expect(vm.getSnapshot().isDownloadLoading).toBe(false);
    });

    it("ignores stale download permission results after dispose", async () => {
        jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(true);
        const permission = createPendingPromise<boolean>();
        const event = createMessageEvent({
            event_id: "$eventA",
            content: { msgtype: MsgType.Image, body: "Image A", url: "mxc://example.org/a" },
        });

        getHintsForMessageSpy.mockReturnValue({
            allowDownloadingMedia: jest.fn().mockReturnValue(permission.promise),
        } as never);

        const vm = createVm({ mxEvent: event });
        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);

        vm.dispose();
        permission.resolve(true);
        await Promise.resolve();

        expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);
    });

    it("dispatches reply and thread actions and forwards callbacks", async () => {
        const onOptionsClick = jest.fn();
        const onReactionsClick = jest.fn();
        const onToggleThreadExpanded = jest.fn();
        const threadReply = createMessageEvent({
            sender: "@bob:example.org",
            event_id: "$reply",
            content: {
                "msgtype": MsgType.Text,
                "body": "Reply",
                "m.relates_to": {
                    rel_type: RelationType.Thread,
                    event_id: rootEvent.getId(),
                },
            },
        });

        Object.defineProperty(threadReply, "isThreadRoot", { value: false });
        jest.spyOn(threadReply, "getThread").mockReturnValue({ rootEvent } as never);

        const vm = createVm({
            mxEvent: threadReply,
            isCard: true,
            onOptionsClick,
            onReactionsClick,
            onToggleThreadExpanded,
        });
        mocked(PinningUtils.isPinned).mockReturnValue(false);

        vm.onReplyClick(null);
        vm.onReplyInThreadClick(null);
        vm.onEditClick(null);
        await vm.onPinClick(null);
        vm.onHideClick(null);
        vm.onOptionsClick(null);
        vm.onReactionsClick(null);
        vm.onToggleThreadExpanded(null);

        expect(defaultDispatcher.dispatch).toHaveBeenNthCalledWith(1, {
            action: "reply_to_event",
            event: threadReply,
            context: TimelineRenderingType.Room,
        });
        expect(defaultDispatcher.dispatch).toHaveBeenNthCalledWith(2, {
            action: Action.ShowThread,
            rootEvent,
            initialEvent: threadReply,
            scroll_into_view: true,
            highlighted: true,
            push: true,
        });
        expect(editEvent).toHaveBeenCalledWith(client, threadReply, TimelineRenderingType.Room, undefined);
        expect(PinningUtils.pinOrUnpinEvent).toHaveBeenCalledWith(client, threadReply);
        expect(PosthogTrackers.trackPinUnpinMessage).toHaveBeenCalledWith(expect.any(String), "Timeline");
        expect(setMediaVisibility).toHaveBeenCalledWith(threadReply, false);
        expect(onOptionsClick).toHaveBeenCalledWith(null);
        expect(onReactionsClick).toHaveBeenCalledWith(null);
        expect(onToggleThreadExpanded).toHaveBeenCalledWith(null);
    });

    describe("business logic parity", () => {
        it.each([
            {
                name: "hides reply and react for non-actionable events",
                actionable: false,
                props: {},
                expectedActions: [],
                unexpectedActions: [ActionBarAction.Reply, ActionBarAction.React],
            },
            {
                name: "hides reply when sending messages is disabled",
                actionable: true,
                props: { canSendMessages: false },
                expectedActions: [ActionBarAction.React],
                unexpectedActions: [ActionBarAction.Reply],
            },
            {
                name: "hides react when reactions are disabled",
                actionable: true,
                props: { canReact: false },
                expectedActions: [ActionBarAction.Reply],
                unexpectedActions: [ActionBarAction.React],
            },
            {
                name: "hides react in search results",
                actionable: true,
                props: { isSearch: true },
                expectedActions: [ActionBarAction.Reply],
                unexpectedActions: [ActionBarAction.React],
            },
        ])("$name", ({ actionable, props, expectedActions, unexpectedActions }) => {
            mocked(isContentActionable).mockReturnValue(actionable);

            const vm = createVm(props);

            expectedActions.forEach((action) => expect(vm.getSnapshot().actions).toContain(action));
            unexpectedActions.forEach((action) => expect(vm.getSnapshot().actions).not.toContain(action));
        });

        it.each([
            {
                name: "shows expand collapse only when quote state is provided and reply should display",
                quoteExpanded: true,
                displayReply: true,
                expected: true,
            },
            {
                name: "hides expand collapse when quote state is missing",
                quoteExpanded: undefined,
                displayReply: true,
                expected: false,
            },
            {
                name: "hides expand collapse when reply should not display",
                quoteExpanded: false,
                displayReply: false,
                expected: false,
            },
        ])("$name", ({ quoteExpanded, displayReply, expected }) => {
            mocked(shouldDisplayReply).mockReturnValue(displayReply);

            const vm = createVm({ isQuoteExpanded: quoteExpanded });

            expect(vm.getSnapshot().actions.includes(ActionBarAction.Expand)).toBe(expected);
        });

        it.each([
            {
                name: "allows reply in thread for normal room messages in room timeline",
                timelineRenderingType: TimelineRenderingType.Room,
                content: { msgtype: MsgType.Text, body: "Hello" },
                relation: undefined,
                type: EventType.RoomMessage,
                expectedReplyInThread: true,
                expectedAllowed: true,
            },
            {
                name: "blocks reply in thread in thread timeline",
                timelineRenderingType: TimelineRenderingType.Thread,
                content: { msgtype: MsgType.Text, body: "Hello" },
                relation: undefined,
                type: EventType.RoomMessage,
                expectedReplyInThread: false,
                expectedAllowed: true,
            },
            {
                name: "blocks reply in thread for verification requests",
                timelineRenderingType: TimelineRenderingType.Room,
                content: { msgtype: MsgType.KeyVerificationRequest, body: "verify" },
                relation: undefined,
                type: EventType.RoomMessage,
                expectedReplyInThread: false,
                expectedAllowed: true,
            },
            {
                name: "blocks reply in thread for beacon info events",
                timelineRenderingType: TimelineRenderingType.Room,
                content: {},
                relation: undefined,
                type: M_BEACON_INFO.name,
                expectedReplyInThread: false,
                expectedAllowed: true,
            },
            {
                name: "marks non-thread relations as not thread reply allowed",
                timelineRenderingType: TimelineRenderingType.Room,
                content: { msgtype: MsgType.Text, body: "Hello" },
                relation: { rel_type: RelationType.Annotation },
                type: EventType.RoomMessage,
                expectedReplyInThread: true,
                expectedAllowed: false,
            },
        ])("$name", ({ timelineRenderingType, content, relation, type, expectedReplyInThread, expectedAllowed }) => {
            const mxEvent = new MatrixEvent({
                type,
                room_id: roomId,
                sender: userId,
                event_id: "$scenario",
                content,
            });
            jest.spyOn(mxEvent, "getRelation").mockReturnValue(relation as never);

            const vm = createVm({ mxEvent, timelineRenderingType });

            expect(vm.getSnapshot().actions.includes(ActionBarAction.ReplyInThread)).toBe(expectedReplyInThread);
            expect(vm.getSnapshot().isThreadReplyAllowed).toBe(expectedAllowed);
        });

        it("shows thread action for deleted messages with a thread in the room timeline", () => {
            const mxEvent = createMessageEvent();
            mocked(isContentActionable).mockReturnValue(false);
            jest.spyOn(mxEvent, "getThread").mockReturnValue({ rootEvent } as never);

            const vm = createVm({ mxEvent, timelineRenderingType: TimelineRenderingType.Room });

            expect(vm.getSnapshot().actions).toContain(ActionBarAction.ReplyInThread);
            expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Reply);
        });

        it("matches media visibility rules for hide and download actions", async () => {
            jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(true);
            jest.spyOn(MediaEventHelper, "canHide").mockReturnValue(true);
            getHintsForMessageSpy.mockReturnValue({
                allowDownloadingMedia: jest.fn().mockResolvedValue(false),
            } as never);

            const mxEvent = createMessageEvent({
                content: { msgtype: MsgType.Image, body: "Image", file: { url: "mxc://example.org/file" } },
            });
            const vm = createVm({ mxEvent });

            expect(vm.getSnapshot()).toMatchObject({
                isDownloadEncrypted: true,
            });
            expect(vm.getSnapshot().actions).toContain(ActionBarAction.Hide);
            expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);

            await waitFor(() => expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download));
        });

        it("recomputes parity-relevant flags and resets download state when the event changes", () => {
            jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(true);

            const vm = createVm({
                mxEvent: createMessageEvent({
                    event_id: "$image",
                    content: { msgtype: MsgType.Image, body: "Image", url: "mxc://example.org/file" },
                }),
            });
            (vm as unknown as { downloadedBlob?: Blob; isDownloadLoading: boolean }).downloadedBlob = new Blob(["x"]);
            (vm as unknown as { downloadedBlob?: Blob; isDownloadLoading: boolean }).isDownloadLoading = true;

            mocked(isContentActionable).mockReturnValue(false);
            jest.spyOn(MediaEventHelper, "isEligible").mockReturnValue(false);

            vm.setProps({
                mxEvent: createMessageEvent({
                    event_id: "$text",
                    content: { msgtype: MsgType.Text, body: "Text" },
                }),
            });

            expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Download);
            expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Hide);
            expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.Reply);
            expect(vm.getSnapshot().actions).not.toContain(ActionBarAction.React);
            expect(vm.getSnapshot().isDownloadLoading).toBe(false);
        });
    });
});
