import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
import { describe, expect, it, vi } from "vitest";
import { listMatrixReactions, removeMatrixReactions } from "./reactions.js";

function createReactionsClient(params: {
  chunk: Array<{
    event_id?: string;
    sender?: string;
    key?: string;
  }>;
  userId?: string | null;
}) {
  const doRequest = vi.fn(async (_method: string, _path: string, _query: any) => ({
    chunk: params.chunk.map((item) => ({
      event_id: item.event_id ?? "",
      sender: item.sender ?? "",
      content: item.key
        ? {
            "m.relates_to": {
              rel_type: "m.annotation",
              event_id: "$target",
              key: item.key,
            },
          }
        : {},
    })),
  }));
  const getUserId = vi.fn(async () => params.userId ?? null);
  const redactEvent = vi.fn(async () => undefined);

  return {
    client: {
      doRequest,
      getUserId,
      redactEvent,
      stop: vi.fn(),
    } as unknown as MatrixClient,
    doRequest,
    redactEvent,
  };
}

describe("matrix reaction actions", () => {
  it("aggregates reactions by key and unique sender", async () => {
    const { client, doRequest } = createReactionsClient({
      chunk: [
        { event_id: "$1", sender: "@alice:example.org", key: "👍" },
        { event_id: "$2", sender: "@bob:example.org", key: "👍" },
        { event_id: "$3", sender: "@alice:example.org", key: "👎" },
        { event_id: "$4", sender: "@bot:example.org" },
      ],
      userId: "@bot:example.org",
    });

    const result = await listMatrixReactions("!room:example.org", "$msg", { client, limit: 2.9 });

    expect(doRequest).toHaveBeenCalledWith(
      "GET",
      expect.stringContaining("/rooms/!room%3Aexample.org/relations/%24msg/"),
      expect.objectContaining({ limit: 2 }),
    );
    expect(result).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          key: "👍",
          count: 2,
          users: expect.arrayContaining(["@alice:example.org", "@bob:example.org"]),
        }),
        expect.objectContaining({
          key: "👎",
          count: 1,
          users: ["@alice:example.org"],
        }),
      ]),
    );
  });

  it("removes only current-user reactions matching emoji filter", async () => {
    const { client, redactEvent } = createReactionsClient({
      chunk: [
        { event_id: "$1", sender: "@me:example.org", key: "👍" },
        { event_id: "$2", sender: "@me:example.org", key: "👎" },
        { event_id: "$3", sender: "@other:example.org", key: "👍" },
      ],
      userId: "@me:example.org",
    });

    const result = await removeMatrixReactions("!room:example.org", "$msg", {
      client,
      emoji: "👍",
    });

    expect(result).toEqual({ removed: 1 });
    expect(redactEvent).toHaveBeenCalledTimes(1);
    expect(redactEvent).toHaveBeenCalledWith("!room:example.org", "$1");
  });

  it("returns removed=0 when current user id is unavailable", async () => {
    const { client, redactEvent } = createReactionsClient({
      chunk: [{ event_id: "$1", sender: "@me:example.org", key: "👍" }],
      userId: null,
    });

    const result = await removeMatrixReactions("!room:example.org", "$msg", { client });

    expect(result).toEqual({ removed: 0 });
    expect(redactEvent).not.toHaveBeenCalled();
  });

  it("removes all bot reactions when no emoji filter is provided", async () => {
    const { client, redactEvent } = createReactionsClient({
      chunk: [
        { event_id: "$1", sender: "@bot:example.org", key: "👀" },
        { event_id: "$2", sender: "@bot:example.org", key: "👍" },
        { event_id: "$3", sender: "@other:example.org", key: "👀" },
      ],
      userId: "@bot:example.org",
    });

    const result = await removeMatrixReactions("!room:example.org", "$msg", { client });

    expect(result).toEqual({ removed: 2 });
    expect(redactEvent).toHaveBeenCalledTimes(2);
    expect(redactEvent).toHaveBeenCalledWith("!room:example.org", "$1");
    expect(redactEvent).toHaveBeenCalledWith("!room:example.org", "$2");
  });

  it("calls onRedactError for each failed redact and counts only successes", async () => {
    const redactEvent = vi
      .fn()
      .mockResolvedValueOnce(undefined)
      .mockRejectedValueOnce(new Error("M_FORBIDDEN"));
    const getUserId = vi.fn(async () => "@bot:example.org");
    const doRequest = vi.fn(async () => ({
      chunk: [
        {
          event_id: "$1",
          sender: "@bot:example.org",
          content: { "m.relates_to": { rel_type: "m.annotation", event_id: "$target", key: "👀" } },
        },
        {
          event_id: "$2",
          sender: "@bot:example.org",
          content: { "m.relates_to": { rel_type: "m.annotation", event_id: "$target", key: "👀" } },
        },
      ],
    }));

    const client = {
      doRequest,
      getUserId,
      redactEvent,
      stop: vi.fn(),
    } as unknown as import("@vector-im/matrix-bot-sdk").MatrixClient;

    const onRedactError = vi.fn();
    const result = await removeMatrixReactions("!room:example.org", "$msg", {
      client,
      emoji: "👀",
      onRedactError,
    });

    expect(result).toEqual({ removed: 1 });
    expect(onRedactError).toHaveBeenCalledTimes(1);
    expect(onRedactError).toHaveBeenCalledWith(expect.any(Error));
  });
});
