import { ChannelType } from "discord-api-types/v10";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { NativeCommandSpec } from "../../auto-reply/commands-registry.js";
import * as dispatcherModule from "../../auto-reply/reply/provider-dispatcher.js";
import type { OpenClawConfig } from "../../config/config.js";
import type { DiscordAccountConfig } from "../../config/types.discord.js";
import * as pluginCommandsModule from "../../plugins/commands.js";
import { createDiscordNativeCommand } from "./native-command.js";
import {
  createMockCommandInteraction,
  type MockCommandInteraction,
} from "./native-command.test-helpers.js";
import { createNoopThreadBindingManager } from "./thread-bindings.js";

function createInteraction(params?: { userId?: string }): MockCommandInteraction {
  return createMockCommandInteraction({
    userId: params?.userId ?? "123456789012345678",
    username: "discord-user",
    globalName: "Discord User",
    channelType: ChannelType.GuildText,
    channelId: "234567890123456789",
    guildId: "345678901234567890",
    guildName: "Test Guild",
    interactionId: "interaction-1",
  });
}

function createConfig(): OpenClawConfig {
  return {
    commands: {
      allowFrom: {
        discord: ["user:123456789012345678"],
      },
    },
    channels: {
      discord: {
        groupPolicy: "allowlist",
        guilds: {
          "345678901234567890": {
            channels: {
              "234567890123456789": {
                allow: true,
                requireMention: false,
              },
            },
          },
        },
      },
    },
  } as OpenClawConfig;
}

function createCommand(cfg: OpenClawConfig, discordConfig?: DiscordAccountConfig) {
  const commandSpec: NativeCommandSpec = {
    name: "status",
    description: "Status",
    acceptsArgs: false,
  };
  return createDiscordNativeCommand({
    command: commandSpec,
    cfg,
    discordConfig: discordConfig ?? cfg.channels?.discord ?? {},
    accountId: "default",
    sessionPrefix: "discord:slash",
    ephemeralDefault: true,
    threadBindings: createNoopThreadBindingManager("default"),
  });
}

function createDispatchSpy() {
  return vi.spyOn(dispatcherModule, "dispatchReplyWithDispatcher").mockResolvedValue({
    counts: {
      final: 1,
      block: 0,
      tool: 0,
    },
  } as never);
}

async function runGuildSlashCommand(params?: {
  userId?: string;
  mutateConfig?: (cfg: OpenClawConfig) => void;
  runtimeDiscordConfig?: DiscordAccountConfig;
}) {
  const cfg = createConfig();
  params?.mutateConfig?.(cfg);
  const command = createCommand(cfg, params?.runtimeDiscordConfig);
  const interaction = createInteraction({ userId: params?.userId });
  vi.spyOn(pluginCommandsModule, "matchPluginCommand").mockReturnValue(null);
  const dispatchSpy = createDispatchSpy();
  await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
  return { dispatchSpy, interaction };
}

function expectNotUnauthorizedReply(interaction: MockCommandInteraction) {
  expect(interaction.reply).not.toHaveBeenCalledWith(
    expect.objectContaining({ content: "You are not authorized to use this command." }),
  );
}

function expectUnauthorizedReply(interaction: MockCommandInteraction) {
  expect(interaction.reply).toHaveBeenCalledWith(
    expect.objectContaining({
      content: "You are not authorized to use this command.",
      ephemeral: true,
    }),
  );
}

describe("Discord native slash commands with commands.allowFrom", () => {
  beforeEach(() => {
    vi.restoreAllMocks();
  });

  it("authorizes guild slash commands when commands.allowFrom.discord matches the sender", async () => {
    const { dispatchSpy, interaction } = await runGuildSlashCommand();
    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expectNotUnauthorizedReply(interaction);
  });

  it("authorizes guild slash commands from the global commands.allowFrom list when provider-specific allowFrom is missing", async () => {
    const { dispatchSpy, interaction } = await runGuildSlashCommand({
      mutateConfig: (cfg) => {
        cfg.commands = {
          allowFrom: {
            "*": ["user:123456789012345678"],
          },
        };
      },
    });
    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expectNotUnauthorizedReply(interaction);
  });

  it("authorizes guild slash commands when commands.useAccessGroups is false and commands.allowFrom.discord matches the sender", async () => {
    const { dispatchSpy, interaction } = await runGuildSlashCommand({
      mutateConfig: (cfg) => {
        cfg.commands = {
          ...cfg.commands,
          useAccessGroups: false,
        };
      },
    });
    expect(dispatchSpy).toHaveBeenCalledTimes(1);
    expectNotUnauthorizedReply(interaction);
  });

  it("rejects guild slash commands when commands.allowFrom.discord does not match the sender", async () => {
    const { dispatchSpy, interaction } = await runGuildSlashCommand({
      userId: "999999999999999999",
    });
    expect(dispatchSpy).not.toHaveBeenCalled();
    expectUnauthorizedReply(interaction);
  });

  it("rejects guild slash commands when commands.useAccessGroups is false and commands.allowFrom.discord does not match the sender", async () => {
    const { dispatchSpy, interaction } = await runGuildSlashCommand({
      userId: "999999999999999999",
      mutateConfig: (cfg) => {
        cfg.commands = {
          ...cfg.commands,
          useAccessGroups: false,
        };
      },
    });
    expect(dispatchSpy).not.toHaveBeenCalled();
    expectUnauthorizedReply(interaction);
  });

  it("uses the root discord maxLinesPerMessage when runtime discordConfig omits it", async () => {
    const longReply = Array.from({ length: 20 }, (_value, index) => `Line ${index + 1}`).join("\n");
    const { interaction } = await runGuildSlashCommand({
      mutateConfig: (cfg) => {
        cfg.channels = {
          ...cfg.channels,
          discord: {
            ...cfg.channels?.discord,
            maxLinesPerMessage: 120,
          },
        };
      },
      runtimeDiscordConfig: {
        groupPolicy: "allowlist",
        guilds: {
          "345678901234567890": {
            channels: {
              "234567890123456789": {
                allow: true,
                requireMention: false,
              },
            },
          },
        },
      },
    });

    const dispatchCall = vi.mocked(dispatcherModule.dispatchReplyWithDispatcher).mock
      .calls[0]?.[0] as
      | Parameters<typeof dispatcherModule.dispatchReplyWithDispatcher>[0]
      | undefined;
    await dispatchCall?.dispatcherOptions.deliver({ text: longReply }, { kind: "final" });

    expect(interaction.reply).toHaveBeenCalledWith(expect.objectContaining({ content: longReply }));
    expect(interaction.followUp).not.toHaveBeenCalled();
  });
});
