import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { WhatsAppSendResult } from "../../inbound/send-result.js";
import type { WebInboundMessage } from "../../inbound/types.js";
import { maybeSendAckReaction } from "./ack-reaction.js";

const hoisted = vi.hoisted(() => ({
  sendReactionWhatsApp: vi.fn(async () => undefined),
}));

vi.mock("../../send.js", () => ({
  sendReactionWhatsApp: hoisted.sendReactionWhatsApp,
}));

function acceptedSendResult(kind: "media" | "text", id: string): WhatsAppSendResult {
  return {
    kind,
    messageId: id,
    messageIds: [id],
    keys: [{ id }],
    providerAccepted: true,
  };
}

function createMessage(overrides: Partial<WebInboundMessage> = {}): WebInboundMessage {
  return {
    id: "msg-1",
    from: "15551234567",
    conversationId: "15551234567",
    to: "15559876543",
    accountId: "default",
    body: "hello",
    chatType: "direct",
    chatId: "15551234567@s.whatsapp.net",
    sendComposing: async () => {},
    reply: async () => acceptedSendResult("text", "r1"),
    sendMedia: async () => acceptedSendResult("media", "m1"),
    ...overrides,
  };
}

function createConfig(
  reactionLevel: "off" | "ack" | "minimal" | "extensive",
  extras?: Partial<NonNullable<OpenClawConfig["channels"]>["whatsapp"]>,
): OpenClawConfig {
  return {
    channels: {
      whatsapp: {
        reactionLevel,
        ackReaction: {
          emoji: "👀",
          direct: true,
          group: "mentions",
        },
        ...extras,
      },
    },
  } as OpenClawConfig;
}

type AckReactionParams = Parameters<typeof maybeSendAckReaction>[0];

const runAckReaction = (overrides: Partial<AckReactionParams> = {}) =>
  maybeSendAckReaction({
    cfg: createConfig("ack"),
    msg: createMessage(),
    agentId: "agent",
    sessionKey: "whatsapp:default:15551234567",
    conversationId: "15551234567",
    verbose: false,
    accountId: "default",
    info: vi.fn(),
    warn: vi.fn(),
    ...overrides,
  });

const expectAckReactionSent = (accountId: string) => {
  expect(hoisted.sendReactionWhatsApp).toHaveBeenCalledWith(
    "15551234567@s.whatsapp.net",
    "msg-1",
    "👀",
    expect.objectContaining({
      verbose: false,
      fromMe: false,
      accountId,
    }),
  );
};

describe("maybeSendAckReaction", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it.each(["ack", "minimal", "extensive"] as const)(
    "sends ack reactions when reactionLevel is %s",
    async (reactionLevel) => {
      const ackReaction = await runAckReaction({
        cfg: createConfig(reactionLevel),
      });

      expect(ackReaction?.ackReactionValue).toBe("👀");
      await expect(ackReaction?.ackReactionPromise).resolves.toBe(true);
      expectAckReactionSent("default");
    },
  );

  it("suppresses ack reactions when reactionLevel is off", async () => {
    const ackReaction = await runAckReaction({
      cfg: createConfig("off"),
    });

    expect(ackReaction).toBeNull();
    expect(hoisted.sendReactionWhatsApp).not.toHaveBeenCalled();
  });

  it("uses the active account reactionLevel override for ack gating", async () => {
    const ackReaction = await runAckReaction({
      cfg: createConfig("off", {
        accounts: {
          work: {
            reactionLevel: "ack",
          },
        },
      }),
      msg: createMessage({
        accountId: "work",
      }),
      sessionKey: "whatsapp:work:15551234567",
      accountId: "work",
    });

    expect(ackReaction?.ackReactionValue).toBe("👀");
    expectAckReactionSent("work");
  });

  it("returns a handle that removes the ack with an empty reaction", async () => {
    const ackReaction = await runAckReaction();

    await ackReaction?.remove();

    expect(hoisted.sendReactionWhatsApp).toHaveBeenLastCalledWith(
      "15551234567@s.whatsapp.net",
      "msg-1",
      "",
      expect.objectContaining({
        verbose: false,
        fromMe: false,
        accountId: "default",
      }),
    );
  });

  it("records ack send failures on the handle", async () => {
    const warn = vi.fn();
    hoisted.sendReactionWhatsApp.mockRejectedValueOnce(new Error("session down"));

    const ackReaction = await runAckReaction({ warn });

    await expect(ackReaction?.ackReactionPromise).resolves.toBe(false);
    expect(warn).toHaveBeenCalledWith(
      expect.objectContaining({
        error: "session down",
        chatId: "15551234567@s.whatsapp.net",
        messageId: "msg-1",
      }),
      "failed to send ack reaction",
    );
  });
});
