// Discord tests cover native command.think autocomplete plugin behavior.
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import {
  createEmptyPluginRegistry,
  setActivePluginRegistry,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import {
  clearSessionStoreCacheForTest,
  upsertSessionEntry,
} from "openclaw/plugin-sdk/session-store-runtime";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { ChannelType, type AutocompleteInteraction } from "../internal/discord.js";
import { createNoopThreadBindingManager } from "./thread-bindings.js";

type ConversationRuntimeModule = typeof import("openclaw/plugin-sdk/conversation-binding-runtime");
type ResolveConfiguredBindingRoute = ConversationRuntimeModule["resolveConfiguredBindingRoute"];
type ConfiguredBindingRouteResult = ReturnType<ResolveConfiguredBindingRoute>;
type EnsureConfiguredBindingRouteReady =
  ConversationRuntimeModule["ensureConfiguredBindingRouteReady"];

function createUnboundConfiguredRouteResult(): ConfiguredBindingRouteResult {
  return {
    bindingResolution: null,
    route: {
      agentId: "main",
      channel: "discord",
      accountId: "default",
      sessionKey: SESSION_KEY,
      mainSessionKey: SESSION_KEY,
      lastRoutePolicy: "main",
      matchedBy: "default",
    },
  };
}
const ensureConfiguredBindingRouteReadyMock = vi.hoisted(() =>
  vi.fn<EnsureConfiguredBindingRouteReady>(async () => ({ ok: true })),
);
const resolveConfiguredBindingRouteMock = vi.hoisted(() =>
  vi.fn<ResolveConfiguredBindingRoute>(() => createUnboundConfiguredRouteResult()),
);
const providerThinkingMocks = vi.hoisted(() => ({
  resolveProviderThinkingProfile: vi.fn(),
}));
const buildModelsProviderDataMock = vi.hoisted(() => vi.fn());

type ConfiguredBindingRoute = ConfiguredBindingRouteResult;
type ConfiguredBindingResolution = NonNullable<ConfiguredBindingRoute["bindingResolution"]>;

function createConfiguredRouteResult(
  params: Parameters<ResolveConfiguredBindingRoute>[0],
): ConfiguredBindingRoute {
  return {
    bindingResolution: {
      record: {
        bindingId: "binding-1",
        targetSessionKey: SESSION_KEY,
        targetKind: "session",
        status: "active",
        boundAt: Date.now(),
        conversation: {
          channel: "discord",
          accountId: "default",
          conversationId: "C1",
        },
      },
    } as ConfiguredBindingResolution,
    boundSessionKey: SESSION_KEY,
    route: {
      ...params.route,
      agentId: "main",
      sessionKey: SESSION_KEY,
      matchedBy: "binding.channel",
      lastRoutePolicy: "session",
    },
  };
}

vi.mock("openclaw/plugin-sdk/conversation-binding-runtime", async () => {
  const { createConfiguredBindingConversationRuntimeModuleMock } =
    await import("../test-support/configured-binding-runtime.js");
  return await createConfiguredBindingConversationRuntimeModuleMock<
    typeof import("openclaw/plugin-sdk/conversation-binding-runtime")
  >(
    {
      ensureConfiguredBindingRouteReadyMock,
      resolveConfiguredBindingRouteMock,
    },
    () =>
      vi.importActual<typeof import("openclaw/plugin-sdk/conversation-binding-runtime")>(
        "openclaw/plugin-sdk/conversation-binding-runtime",
      ),
  );
});

vi.mock("openclaw/plugin-sdk/agent-runtime", () => ({
  getPreparedModelCatalogSnapshot: vi.fn(() => ({ entries: [], routeVariants: [] })),
  loadPreparedModelCatalog: vi.fn(async () => []),
  normalizeProviderId: (value: string) => value.trim().toLowerCase(),
  resolveAgentDir: (_cfg: OpenClawConfig, agentId: string) => `/tmp/agents/${agentId}/agent`,
  resolveAgentWorkspaceDir: (_cfg: OpenClawConfig, agentId: string) => `/tmp/workspaces/${agentId}`,
  resolveDefaultModelForAgent: (params: { cfg: OpenClawConfig }) => {
    const configuredModel = params.cfg.agents?.defaults?.model;
    const primary =
      typeof configuredModel === "string"
        ? configuredModel.trim()
        : (configuredModel?.primary?.trim() ?? "");
    const slashIndex = primary.indexOf("/");
    if (slashIndex > 0 && slashIndex < primary.length - 1) {
      return {
        provider: primary.slice(0, slashIndex).trim().toLowerCase(),
        model: primary.slice(slashIndex + 1).trim(),
      };
    }
    return {
      provider: "anthropic",
      model: "claude-sonnet-4.5",
    };
  },
}));

vi.mock("openclaw/plugin-sdk/models-provider-runtime", () => ({
  buildModelsProviderData: buildModelsProviderDataMock,
}));

const STORE_PATH = path.join(
  os.tmpdir(),
  `openclaw-discord-think-autocomplete-${process.pid}.json`,
);
const SESSION_KEY = "agent:main:main";
let findCommandByNativeName: typeof import("openclaw/plugin-sdk/command-auth-native").findCommandByNativeName;
let resolveCommandArgChoices: typeof import("openclaw/plugin-sdk/command-auth-native").resolveCommandArgChoices;
let resolveDiscordNativeChoiceContext: typeof import("./native-command-model-picker-ui.js").resolveDiscordNativeChoiceContext;

async function saveSessionOverride(params: {
  providerOverride: string;
  modelOverride: string;
  agentRuntimeOverride?: string;
}): Promise<void> {
  fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
  await upsertSessionEntry({
    storePath: STORE_PATH,
    sessionKey: SESSION_KEY,
    entry: {
      sessionId: "main",
      updatedAt: Date.now(),
      providerOverride: params.providerOverride,
      modelOverride: params.modelOverride,
      ...(params.agentRuntimeOverride ? { agentRuntimeOverride: params.agentRuntimeOverride } : {}),
    },
  });
}

function installProviderThinkingRegistryForTest(): void {
  const registry = createEmptyPluginRegistry();
  registry.providers.push({
    pluginId: "discord-test",
    source: "test",
    provider: {
      id: "discord-test-thinking",
      label: "Discord Test Thinking",
      aliases: ["anthropic", "openai"],
      auth: [],
      resolveThinkingProfile: (context) =>
        providerThinkingMocks.resolveProviderThinkingProfile({
          provider: context.provider,
          context,
        }),
    },
  });
  setActivePluginRegistry(registry);
}

async function loadDiscordThinkAutocompleteModulesForTest() {
  installProviderThinkingRegistryForTest();
  const commandAuth = await import("openclaw/plugin-sdk/command-auth-native");
  const nativeCommandUi = await import("./native-command-model-picker-ui.js");
  return {
    findCommandByNativeName: commandAuth.findCommandByNativeName,
    resolveCommandArgChoices: commandAuth.resolveCommandArgChoices,
    resolveDiscordNativeChoiceContext: nativeCommandUi.resolveDiscordNativeChoiceContext,
  };
}

describe("discord native /think autocomplete", () => {
  beforeAll(async () => {
    providerThinkingMocks.resolveProviderThinkingProfile.mockImplementation(
      ({ provider, context }) =>
        provider === "openai" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId)
          ? {
              levels: [
                { id: "off" },
                { id: "low" },
                { id: "medium" },
                { id: "high" },
                { id: "xhigh" },
              ],
            }
          : undefined,
    );
    buildModelsProviderDataMock.mockResolvedValue({
      byProvider: new Map<string, Set<string>>(),
      providers: [],
      resolvedDefault: {
        provider: "anthropic",
        model: "claude-sonnet-4.5",
      },
      modelNames: new Map<string, string>(),
    });
    ({ findCommandByNativeName, resolveCommandArgChoices, resolveDiscordNativeChoiceContext } =
      await loadDiscordThinkAutocompleteModulesForTest());

    // Compile the provider-backed default choice context outside per-case timing.
    const { command, levelArg } = requireThinkLevelCommand();
    resolveCommandArgChoices({ command, arg: levelArg, cfg: createConfig(), catalog: [] });
  });

  beforeEach(async () => {
    clearSessionStoreCacheForTest();
    ensureConfiguredBindingRouteReadyMock.mockReset();
    ensureConfiguredBindingRouteReadyMock.mockResolvedValue({ ok: true });
    resolveConfiguredBindingRouteMock.mockReset();
    resolveConfiguredBindingRouteMock.mockReturnValue(createUnboundConfiguredRouteResult());
    providerThinkingMocks.resolveProviderThinkingProfile.mockReset();
    providerThinkingMocks.resolveProviderThinkingProfile.mockImplementation(
      ({ provider, context }) =>
        provider === "openai" && ["gpt-5.4", "gpt-5.4-pro"].includes(context.modelId)
          ? {
              levels: [
                { id: "off" },
                { id: "low" },
                { id: "medium" },
                { id: "high" },
                { id: "xhigh" },
              ],
            }
          : undefined,
    );
    installProviderThinkingRegistryForTest();
    await saveSessionOverride({
      providerOverride: "openai",
      modelOverride: "gpt-5.4",
    });
  });

  afterEach(() => {
    clearSessionStoreCacheForTest();
    try {
      fs.unlinkSync(STORE_PATH);
    } catch {}
  });

  function createConfig() {
    return {
      agents: {
        defaults: {
          model: {
            primary: "anthropic/claude-sonnet-4.5",
          },
        },
      },
      session: {
        store: STORE_PATH,
      },
    } as OpenClawConfig;
  }

  function requireThinkLevelCommand() {
    const command = findCommandByNativeName("think", "discord", {
      includeBundledChannelFallback: false,
    });
    if (!command) {
      throw new Error("expected Discord /think command");
    }
    const levelArg = command.args?.find((entry) => entry.name === "level");
    if (!levelArg) {
      throw new Error("expected Discord /think level arg");
    }
    return { command, levelArg };
  }

  it("uses the session override context for /think choices", async () => {
    const cfg = createConfig();
    const interaction = {
      options: {
        getFocused: () => ({ value: "xh" }),
      },
      respond: async (_choices: Array<{ name: string; value: string }>) => {},
      rawData: {},
      channel: { id: "D1", type: ChannelType.DM },
      user: { id: "U1" },
      guild: undefined,
      client: { fetchChannel: async () => ({ id: "D1", type: ChannelType.DM }) },
    } as unknown as AutocompleteInteraction & {
      respond: (choices: Array<{ name: string; value: string }>) => Promise<void>;
    };

    const { command, levelArg } = requireThinkLevelCommand();

    const context = await resolveDiscordNativeChoiceContext({
      interaction,
      cfg,
      accountId: "default",
      threadBindings: createNoopThreadBindingManager("default"),
    });
    expect(context).toEqual({
      provider: "openai",
      model: "gpt-5.4",
      agentId: "main",
      agentRuntime: "codex",
    });

    const choices = resolveCommandArgChoices({
      command,
      arg: levelArg,
      cfg,
      provider: context?.provider,
      model: context?.model,
      agentRuntime: context?.agentRuntime,
      catalog: [],
    });
    const values = choices.map((choice) => choice.value);
    expect(values).toContain("xhigh");
    expect(values).not.toContain("max");
    expect(values).not.toContain("adaptive");
  });

  it.each([
    { sessionRuntime: undefined, expectedRuntime: "codex", supportsUltra: false },
    { sessionRuntime: "openclaw", expectedRuntime: "openclaw", supportsUltra: true },
  ])(
    "uses the effective $expectedRuntime runtime for Luna choices",
    async ({ sessionRuntime, expectedRuntime, supportsUltra }) => {
      providerThinkingMocks.resolveProviderThinkingProfile.mockImplementation(
        ({ provider, context }) =>
          provider === "openai" && context.modelId === "gpt-5.6-luna"
            ? {
                levels: [
                  { id: "off" },
                  { id: "max" },
                  ...(context.agentRuntime === "openclaw" ? [{ id: "ultra" as const }] : []),
                ],
              }
            : undefined,
      );
      await saveSessionOverride({
        providerOverride: "openai",
        modelOverride: "gpt-5.6-luna",
        ...(sessionRuntime ? { agentRuntimeOverride: sessionRuntime } : {}),
      });
      const cfg = createConfig();
      const interaction = {
        options: { getFocused: () => ({ value: "" }) },
        respond: async (_choices: Array<{ name: string; value: string }>) => {},
        rawData: {},
        channel: { id: "D1", type: ChannelType.DM },
        user: { id: "U1" },
        guild: undefined,
        client: { fetchChannel: async () => ({ id: "D1", type: ChannelType.DM }) },
      } as unknown as AutocompleteInteraction;

      const context = await resolveDiscordNativeChoiceContext({
        interaction,
        cfg,
        accountId: "default",
        threadBindings: createNoopThreadBindingManager("default"),
      });
      expect(context).toEqual({
        provider: "openai",
        model: "gpt-5.6-luna",
        agentId: "main",
        agentRuntime: expectedRuntime,
      });

      const { command, levelArg } = requireThinkLevelCommand();
      const choices = resolveCommandArgChoices({
        command,
        arg: levelArg,
        cfg,
        provider: context?.provider,
        model: context?.model,
        agentRuntime: context?.agentRuntime,
        catalog: [],
      });
      expect(choices.some((choice) => choice.value === "ultra")).toBe(supportsUltra);
    },
  );

  it("includes max only for provider-advertised models", async () => {
    providerThinkingMocks.resolveProviderThinkingProfile.mockImplementation(
      ({ provider, context }) =>
        provider === "anthropic" && context.modelId === "claude-opus-4-7"
          ? { levels: [{ id: "off" }, { id: "max" }] }
          : undefined,
    );
    await saveSessionOverride({
      providerOverride: "anthropic",
      modelOverride: "claude-opus-4-7",
    });
    const cfg = createConfig();
    resolveConfiguredBindingRouteMock.mockImplementation(createConfiguredRouteResult);
    const interaction = {
      options: {
        getFocused: () => ({ value: "ma" }),
      },
      respond: async (_choices: Array<{ name: string; value: string }>) => {},
      rawData: {
        member: { roles: [] },
      },
      channel: { id: "C1", type: ChannelType.GuildText },
      user: { id: "U1" },
      guild: { id: "G1" },
      client: { fetchChannel: async () => ({ id: "C1", type: ChannelType.GuildText }) },
    } as unknown as AutocompleteInteraction & {
      respond: (choices: Array<{ name: string; value: string }>) => Promise<void>;
    };

    const context = await resolveDiscordNativeChoiceContext({
      interaction,
      cfg,
      accountId: "default",
      threadBindings: createNoopThreadBindingManager("default"),
    });
    const { command, levelArg } = requireThinkLevelCommand();

    const choices = resolveCommandArgChoices({
      command,
      arg: levelArg,
      cfg,
      provider: context?.provider,
      model: context?.model,
      agentRuntime: context?.agentRuntime,
      catalog: [],
    });
    const values = choices.map((choice) => choice.value);
    expect(values).toContain("max");
  });

  it("falls back when a configured binding is unavailable", async () => {
    const cfg = createConfig();
    resolveConfiguredBindingRouteMock.mockImplementation(createConfiguredRouteResult);
    ensureConfiguredBindingRouteReadyMock.mockResolvedValue({
      ok: false,
      error: "acpx exited",
    });
    const interaction = {
      options: {
        getFocused: () => ({ value: "xh" }),
      },
      respond: async (_choices: Array<{ name: string; value: string }>) => {},
      rawData: {
        member: { roles: [] },
      },
      channel: { id: "C1", type: ChannelType.GuildText },
      user: { id: "U1" },
      guild: { id: "G1" },
      client: { fetchChannel: async () => ({ id: "C1", type: ChannelType.GuildText }) },
    } as unknown as AutocompleteInteraction & {
      respond: (choices: Array<{ name: string; value: string }>) => Promise<void>;
    };

    const context = await resolveDiscordNativeChoiceContext({
      interaction,
      cfg,
      accountId: "default",
      threadBindings: createNoopThreadBindingManager("default"),
    });

    expect(context).toBeNull();
    expect(ensureConfiguredBindingRouteReadyMock).toHaveBeenCalledTimes(1);

    const { command, levelArg } = requireThinkLevelCommand();
    const choices = resolveCommandArgChoices({
      command,
      arg: levelArg,
      cfg,
      provider: context?.provider,
      model: context?.model,
      catalog: [],
    });
    const values = choices.map((choice) => choice.value);
    expect(values).not.toContain("xhigh");
  });
});
