import type { Client } from "@buape/carbon";
import { ChannelType, MessageType } from "@buape/carbon";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
  dispatchMock,
  readAllowFromStoreMock,
  sendMock,
  updateLastRouteMock,
  upsertPairingRequestMock,
} from "./monitor.tool-result.test-harness.js";

type Config = ReturnType<typeof import("../config/config.js").loadConfig>;

beforeEach(() => {
  vi.resetModules();
  sendMock.mockReset().mockResolvedValue(undefined);
  updateLastRouteMock.mockReset();
  dispatchMock.mockReset().mockImplementation(async ({ dispatcher }) => {
    dispatcher.sendFinalReply({ text: "hi" });
    return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } };
  });
  readAllowFromStoreMock.mockReset().mockResolvedValue([]);
  upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true });
});

const BASE_CFG: Config = {
  agents: {
    defaults: {
      model: { primary: "anthropic/claude-opus-4-5" },
      workspace: "/tmp/openclaw",
    },
  },
  session: { store: "/tmp/openclaw-sessions.json" },
};

const CATEGORY_GUILD_CFG = {
  ...BASE_CFG,
  channels: {
    discord: {
      dm: { enabled: true, policy: "open" },
      guilds: {
        "*": {
          requireMention: false,
          channels: { c1: { allow: true } },
        },
      },
    },
  },
} satisfies Config;

async function createDmHandler(opts: { cfg: Config; runtimeError?: (err: unknown) => void }) {
  const { createDiscordMessageHandler } = await import("./monitor.js");
  return createDiscordMessageHandler({
    cfg: opts.cfg,
    discordConfig: opts.cfg.channels?.discord,
    accountId: "default",
    token: "token",
    runtime: {
      log: vi.fn(),
      error: opts.runtimeError ?? vi.fn(),
      exit: (code: number): never => {
        throw new Error(`exit ${code}`);
      },
    },
    botUserId: "bot-id",
    guildHistories: new Map(),
    historyLimit: 0,
    mediaMaxBytes: 10_000,
    textLimit: 2000,
    replyToMode: "off",
    dmEnabled: true,
    groupDmEnabled: false,
  });
}

function createDmClient(fetchChannel?: ReturnType<typeof vi.fn>) {
  const resolvedFetchChannel =
    fetchChannel ??
    vi.fn().mockResolvedValue({
      type: ChannelType.DM,
      name: "dm",
    });

  return { fetchChannel: resolvedFetchChannel } as unknown as Client;
}

async function createCategoryGuildHandler() {
  const { createDiscordMessageHandler } = await import("./monitor.js");
  return createDiscordMessageHandler({
    cfg: CATEGORY_GUILD_CFG,
    discordConfig: CATEGORY_GUILD_CFG.channels?.discord,
    accountId: "default",
    token: "token",
    runtime: {
      log: vi.fn(),
      error: vi.fn(),
      exit: (code: number): never => {
        throw new Error(`exit ${code}`);
      },
    },
    botUserId: "bot-id",
    guildHistories: new Map(),
    historyLimit: 0,
    mediaMaxBytes: 10_000,
    textLimit: 2000,
    replyToMode: "off",
    dmEnabled: true,
    groupDmEnabled: false,
    guildEntries: {
      "*": { requireMention: false, channels: { c1: { allow: true } } },
    },
  });
}

function createCategoryGuildClient() {
  return {
    fetchChannel: vi.fn().mockResolvedValue({
      type: ChannelType.GuildText,
      name: "general",
      parentId: "category-1",
    }),
    rest: { get: vi.fn() },
  } as unknown as Client;
}

describe("discord tool result dispatch", () => {
  it("sends status replies with responsePrefix", async () => {
    const cfg = {
      ...BASE_CFG,
      messages: { responsePrefix: "PFX" },
      channels: { discord: { dmPolicy: "open", allowFrom: ["*"], dm: { enabled: true } } },
    } as ReturnType<typeof import("../config/config.js").loadConfig>;

    const runtimeError = vi.fn();
    const handler = await createDmHandler({ cfg, runtimeError });
    const client = createDmClient();

    await handler(
      {
        message: {
          id: "m1",
          content: "/status",
          channelId: "c1",
          timestamp: new Date().toISOString(),
          type: MessageType.Default,
          attachments: [],
          embeds: [],
          mentionedEveryone: false,
          mentionedUsers: [],
          mentionedRoles: [],
          author: { id: "u1", bot: false, username: "Ada" },
        },
        author: { id: "u1", bot: false, username: "Ada" },
        guild_id: null,
      },
      client,
    );

    expect(runtimeError).not.toHaveBeenCalled();
    expect(sendMock).toHaveBeenCalledTimes(1);
    expect(sendMock.mock.calls[0]?.[1]).toMatch(/^PFX /);
  }, 30_000);

  it("caches channel info lookups between messages", async () => {
    const cfg = {
      ...BASE_CFG,
      channels: { discord: { dm: { enabled: true, policy: "open" } } },
    } as ReturnType<typeof import("../config/config.js").loadConfig>;

    const handler = await createDmHandler({ cfg });
    const fetchChannel = vi.fn().mockResolvedValue({
      type: ChannelType.DM,
      name: "dm",
    });
    const client = createDmClient(fetchChannel);
    const baseMessage = {
      content: "hello",
      channelId: "cache-channel-1",
      timestamp: new Date().toISOString(),
      type: MessageType.Default,
      attachments: [],
      embeds: [],
      mentionedEveryone: false,
      mentionedUsers: [],
      mentionedRoles: [],
      author: { id: "u-cache", bot: false, username: "Ada" },
    };

    await handler(
      {
        message: { ...baseMessage, id: "m-cache-1" },
        author: baseMessage.author,
        guild_id: null,
      },
      client,
    );
    await handler(
      {
        message: { ...baseMessage, id: "m-cache-2" },
        author: baseMessage.author,
        guild_id: null,
      },
      client,
    );

    expect(fetchChannel).toHaveBeenCalledTimes(1);
  });

  it("includes forwarded message snapshots in body", async () => {
    let capturedBody = "";
    dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
      capturedBody = ctx.Body ?? "";
      dispatcher.sendFinalReply({ text: "ok" });
      return { queuedFinal: true, counts: { final: 1 } };
    });

    const cfg = {
      ...BASE_CFG,
      channels: { discord: { dm: { enabled: true, policy: "open" } } },
    } as ReturnType<typeof import("../config/config.js").loadConfig>;

    const handler = await createDmHandler({ cfg });
    const client = createDmClient();

    await handler(
      {
        message: {
          id: "m-forward-1",
          content: "",
          channelId: "c-forward-1",
          timestamp: new Date().toISOString(),
          type: MessageType.Default,
          attachments: [],
          embeds: [],
          mentionedEveryone: false,
          mentionedUsers: [],
          mentionedRoles: [],
          author: { id: "u1", bot: false, username: "Ada" },
          rawData: {
            message_snapshots: [
              {
                message: {
                  content: "forwarded hello",
                  embeds: [],
                  attachments: [],
                  author: {
                    id: "u2",
                    username: "Bob",
                    discriminator: "0",
                  },
                },
              },
            ],
          },
        },
        author: { id: "u1", bot: false, username: "Ada" },
        guild_id: null,
      },
      client,
    );

    expect(capturedBody).toContain("[Forwarded message from @Bob]");
    expect(capturedBody).toContain("forwarded hello");
  });

  it("uses channel id allowlists for non-thread channels with categories", async () => {
    let capturedCtx: { SessionKey?: string } | undefined;
    dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
      capturedCtx = ctx;
      dispatcher.sendFinalReply({ text: "hi" });
      return { queuedFinal: true, counts: { final: 1 } };
    });

    const handler = await createCategoryGuildHandler();
    const client = createCategoryGuildClient();

    await handler(
      {
        message: {
          id: "m-category",
          content: "hello",
          channelId: "c1",
          timestamp: new Date().toISOString(),
          type: MessageType.Default,
          attachments: [],
          embeds: [],
          mentionedEveryone: false,
          mentionedUsers: [],
          mentionedRoles: [],
          author: { id: "u1", bot: false, username: "Ada", tag: "Ada#1" },
        },
        author: { id: "u1", bot: false, username: "Ada", tag: "Ada#1" },
        member: { displayName: "Ada" },
        guild: { id: "g1", name: "Guild" },
        guild_id: "g1",
      },
      client,
    );

    expect(capturedCtx?.SessionKey).toBe("agent:main:discord:channel:c1");
  });

  it("prefixes group bodies with sender label", async () => {
    let capturedBody = "";
    dispatchMock.mockImplementationOnce(async ({ ctx, dispatcher }) => {
      capturedBody = ctx.Body ?? "";
      dispatcher.sendFinalReply({ text: "ok" });
      return { queuedFinal: true, counts: { final: 1 } };
    });

    const handler = await createCategoryGuildHandler();
    const client = createCategoryGuildClient();

    await handler(
      {
        message: {
          id: "m-prefix",
          content: "hello",
          channelId: "c1",
          timestamp: new Date("2026-01-17T00:00:00Z").toISOString(),
          type: MessageType.Default,
          attachments: [],
          embeds: [],
          mentionedEveryone: false,
          mentionedUsers: [],
          mentionedRoles: [],
          author: { id: "u1", bot: false, username: "Ada", discriminator: "1234" },
        },
        author: { id: "u1", bot: false, username: "Ada", discriminator: "1234" },
        member: { displayName: "Ada" },
        guild: { id: "g1", name: "Guild" },
        guild_id: "g1",
      },
      client,
    );

    expect(capturedBody).toContain("Ada (Ada#1234): hello");
  });

  it("replies with pairing code and sender id when dmPolicy is pairing", async () => {
    const cfg = {
      ...BASE_CFG,
      channels: {
        discord: { dm: { enabled: true, policy: "pairing", allowFrom: [] } },
      },
    } as Config;

    const handler = await createDmHandler({ cfg });
    const client = createDmClient();

    await handler(
      {
        message: {
          id: "m1",
          content: "hello",
          channelId: "c1",
          timestamp: new Date().toISOString(),
          type: MessageType.Default,
          attachments: [],
          embeds: [],
          mentionedEveryone: false,
          mentionedUsers: [],
          mentionedRoles: [],
          author: { id: "u2", bot: false, username: "Ada" },
        },
        author: { id: "u2", bot: false, username: "Ada" },
        guild_id: null,
      },
      client,
    );

    expect(dispatchMock).not.toHaveBeenCalled();
    expect(upsertPairingRequestMock).toHaveBeenCalled();
    expect(sendMock).toHaveBeenCalledTimes(1);
    expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain("Your Discord user id: u2");
    expect(String(sendMock.mock.calls[0]?.[1] ?? "")).toContain("Pairing code: PAIRCODE");
  }, 10000);
});
