import { afterEach, beforeEach, describe, it, expect, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.js";
import { resetLogger, setLoggerOverride } from "../logging/logger.js";
import { createWarnLogCapture } from "../logging/test-helpers/warn-log-capture.js";
import { __testing as setupRegistryRuntimeTesting } from "../plugins/setup-registry.runtime.js";
import {
  buildAllowedModelSet,
  inferUniqueProviderFromConfiguredModels,
  isCliProvider,
  parseModelRef,
  buildModelAliasIndex,
  normalizeModelSelection,
  normalizeProviderId,
  normalizeProviderIdForAuth,
  modelKey,
  resolvePersistedOverrideModelRef,
  resolvePersistedModelRef,
  resolvePersistedSelectedModelRef,
  resolveAllowedModelRef,
  resolveConfiguredModelRef,
  resolveSubagentConfiguredModelSelection,
  resolveThinkingDefault,
  resolveModelRefFromString,
} from "./model-selection.js";

const EXPLICIT_ALLOWLIST_CONFIG = {
  agents: {
    defaults: {
      model: { primary: "openai/gpt-5.4" },
      models: {
        "anthropic/claude-sonnet-4-6": { alias: "sonnet" },
      },
    },
  },
} as OpenClawConfig;

const BUNDLED_ALLOWLIST_CATALOG = [
  { provider: "anthropic", id: "claude-sonnet-4-6", name: "Claude Sonnet 4.5" },
  { provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
];

const ANTHROPIC_OPUS_CATALOG = [
  {
    provider: "anthropic",
    id: "claude-opus-4-6",
    name: "Claude Opus 4.6",
    reasoning: true,
  },
];

const ANTHROPIC_OPUS_47_CATALOG = [
  {
    provider: "anthropic",
    id: "claude-opus-4-7",
    name: "Claude Opus 4.7",
    reasoning: true,
  },
];

function resolveAnthropicOpusThinking(cfg: OpenClawConfig) {
  return resolveThinkingDefault({
    cfg,
    provider: "anthropic",
    model: "claude-opus-4-6",
    catalog: ANTHROPIC_OPUS_CATALOG,
  });
}

function resolveAnthropicOpus47Thinking(cfg: OpenClawConfig) {
  return resolveThinkingDefault({
    cfg,
    provider: "anthropic",
    model: "claude-opus-4-7",
    catalog: ANTHROPIC_OPUS_47_CATALOG,
  });
}

function createAgentFallbackConfig(params: {
  primary?: string;
  fallbacks?: string[];
  agentFallbacks?: string[];
}) {
  return {
    agents: {
      defaults: {
        models: {
          "openai/gpt-4o": {},
        },
        model: {
          primary: params.primary ?? "openai/gpt-4o",
          fallbacks: params.fallbacks ?? [],
        },
      },
      ...(params.agentFallbacks
        ? {
            list: [
              {
                id: "coder",
                model: {
                  primary: params.primary ?? "openai/gpt-4o",
                  fallbacks: params.agentFallbacks,
                },
              },
            ],
          }
        : {}),
    },
  } as OpenClawConfig;
}

function createProviderWithModelsConfig(provider: string, models: Array<Record<string, unknown>>) {
  return {
    models: {
      providers: {
        [provider]: {
          baseUrl: `https://${provider}.example.com`,
          models,
        },
      },
    },
  } as Partial<OpenClawConfig>;
}

function resolveConfiguredRefForTest(cfg: Partial<OpenClawConfig>) {
  return resolveConfiguredModelRef({
    cfg: cfg as OpenClawConfig,
    defaultProvider: "openai",
    defaultModel: "gpt-5.4",
  });
}

describe("model-selection", () => {
  describe("normalizeProviderId", () => {
    it("should normalize provider names", () => {
      expect(normalizeProviderId("Anthropic")).toBe("anthropic");
      expect(normalizeProviderId("Z.ai")).toBe("zai");
      expect(normalizeProviderId("z-ai")).toBe("zai");
      expect(normalizeProviderId("OpenCode-Zen")).toBe("opencode");
      expect(normalizeProviderId("qwen")).toBe("qwen");
      expect(normalizeProviderId("kimi-code")).toBe("kimi");
      expect(normalizeProviderId("kimi-coding")).toBe("kimi");
      expect(normalizeProviderId("bedrock")).toBe("amazon-bedrock");
      expect(normalizeProviderId("aws-bedrock")).toBe("amazon-bedrock");
      expect(normalizeProviderId("amazon-bedrock")).toBe("amazon-bedrock");
    });
  });

  describe("normalizeProviderIdForAuth", () => {
    it("only applies generic provider-id normalization before auth alias lookup", () => {
      expect(normalizeProviderIdForAuth("qwencloud")).toBe("qwen");
      expect(normalizeProviderIdForAuth("openai-codex")).toBe("openai-codex");
      expect(normalizeProviderIdForAuth("openai")).toBe("openai");
    });
  });

  describe("isCliProvider", () => {
    beforeEach(() => {
      setupRegistryRuntimeTesting.resetRuntimeState();
      setupRegistryRuntimeTesting.setRuntimeModuleForTest({
        resolvePluginSetupCliBackend: ({ backend }) =>
          backend === "claude-cli"
            ? {
                pluginId: "anthropic",
                backend: { id: "claude-cli", config: { command: "claude" } },
              }
            : undefined,
      });
    });

    afterEach(() => {
      setupRegistryRuntimeTesting.resetRuntimeState();
    });

    it("returns true for setup-registered cli backends", () => {
      expect(isCliProvider("claude-cli", {} as OpenClawConfig)).toBe(true);
    });

    it("returns false for provider ids", () => {
      expect(isCliProvider("example-cli", {} as OpenClawConfig)).toBe(false);
    });
  });

  describe("modelKey", () => {
    it("keeps canonical OpenRouter native ids without duplicating the provider", () => {
      expect(modelKey("openrouter", "openrouter/hunter-alpha")).toBe("openrouter/hunter-alpha");
    });
  });

  describe("parseModelRef", () => {
    const expectParsedModelVariants = (
      variants: string[],
      defaultProvider: string,
      expected: { provider: string; model: string },
    ) => {
      for (const raw of variants) {
        expect(parseModelRef(raw, defaultProvider), raw).toEqual(expected);
      }
    };

    it.each([
      {
        name: "parses explicit provider/model refs",
        variants: ["anthropic/claude-3-5-sonnet"],
        defaultProvider: "openai",
        expected: { provider: "anthropic", model: "claude-3-5-sonnet" },
      },
      {
        name: "uses the default provider when omitted",
        variants: ["claude-3-5-sonnet"],
        defaultProvider: "anthropic",
        expected: { provider: "anthropic", model: "claude-3-5-sonnet" },
      },
      {
        name: "preserves nested model ids after the provider prefix",
        variants: ["nvidia/moonshotai/kimi-k2.5"],
        defaultProvider: "anthropic",
        expected: { provider: "nvidia", model: "moonshotai/kimi-k2.5" },
      },
      {
        name: "normalizes anthropic shorthand aliases",
        variants: ["anthropic/opus-4.6", "opus-4.6", " anthropic / opus-4.6 "],
        defaultProvider: "anthropic",
        expected: { provider: "anthropic", model: "claude-opus-4-6" },
      },
      {
        name: "normalizes anthropic sonnet aliases",
        variants: ["anthropic/sonnet-4.6", "sonnet-4.6"],
        defaultProvider: "anthropic",
        expected: { provider: "anthropic", model: "claude-sonnet-4-6" },
      },
      {
        name: "keeps dated anthropic model ids unchanged",
        variants: ["anthropic/claude-sonnet-4-20250514", "claude-sonnet-4-20250514"],
        defaultProvider: "anthropic",
        expected: { provider: "anthropic", model: "claude-sonnet-4-20250514" },
      },
      {
        name: "normalizes deprecated google flash preview ids",
        variants: ["google/gemini-3.1-flash-preview", "gemini-3.1-flash-preview"],
        defaultProvider: "google",
        expected: { provider: "google", model: "gemini-3-flash-preview" },
      },
      {
        name: "normalizes gemini 3.1 flash-lite ids",
        variants: ["google/gemini-3.1-flash-lite", "gemini-3.1-flash-lite"],
        defaultProvider: "google",
        expected: { provider: "google", model: "gemini-3.1-flash-lite-preview" },
      },
      {
        name: "normalizes deprecated xai grok 4.20 beta ids",
        variants: [
          "xai/grok-4.20-experimental-beta-0304-reasoning",
          "grok-4.20-experimental-beta-0304-reasoning",
        ],
        defaultProvider: "xai",
        expected: { provider: "xai", model: "grok-4.20-beta-latest-reasoning" },
      },
      {
        name: "keeps OpenAI codex refs on the openai provider",
        variants: ["openai/gpt-5.4", "gpt-5.4"],
        defaultProvider: "openai",
        expected: { provider: "openai", model: "gpt-5.4" },
      },
      {
        name: "preserves openrouter native model prefixes",
        variants: ["openrouter/aurora-alpha"],
        defaultProvider: "openai",
        expected: { provider: "openrouter", model: "openrouter/aurora-alpha" },
      },
      {
        name: "passes through openrouter upstream provider ids",
        variants: ["openrouter/anthropic/claude-sonnet-4-6"],
        defaultProvider: "openai",
        expected: { provider: "openrouter", model: "anthropic/claude-sonnet-4-6" },
      },
      {
        name: "strips duplicate Hugging Face provider prefixes",
        variants: ["huggingface/deepseek-ai/DeepSeek-R1"],
        defaultProvider: "huggingface",
        expected: { provider: "huggingface", model: "deepseek-ai/DeepSeek-R1" },
      },
      {
        name: "normalizes Vercel Claude shorthand to anthropic-prefixed model ids",
        variants: ["vercel-ai-gateway/claude-opus-4.6"],
        defaultProvider: "openai",
        expected: { provider: "vercel-ai-gateway", model: "anthropic/claude-opus-4.6" },
      },
      {
        name: "normalizes Vercel Anthropic aliases without double-prefixing",
        variants: ["vercel-ai-gateway/opus-4.6"],
        defaultProvider: "openai",
        expected: { provider: "vercel-ai-gateway", model: "anthropic/claude-opus-4-6" },
      },
      {
        name: "keeps already-prefixed Vercel Anthropic models unchanged",
        variants: ["vercel-ai-gateway/anthropic/claude-opus-4.6"],
        defaultProvider: "openai",
        expected: { provider: "vercel-ai-gateway", model: "anthropic/claude-opus-4.6" },
      },
      {
        name: "passes through non-Claude Vercel model ids unchanged",
        variants: ["vercel-ai-gateway/openai/gpt-5.4"],
        defaultProvider: "openai",
        expected: { provider: "vercel-ai-gateway", model: "openai/gpt-5.4" },
      },
      {
        name: "keeps already-suffixed codex variants unchanged",
        variants: ["openai/gpt-5.4-codex-codex"],
        defaultProvider: "anthropic",
        expected: { provider: "openai", model: "gpt-5.4-codex-codex" },
      },
      {
        name: "normalizes gemini 3.1 flash-lite ids for google-vertex",
        variants: ["google-vertex/gemini-3.1-flash-lite", "gemini-3.1-flash-lite"],
        defaultProvider: "google-vertex",
        expected: { provider: "google-vertex", model: "gemini-3.1-flash-lite-preview" },
      },
    ])("$name", ({ variants, defaultProvider, expected }) => {
      expectParsedModelVariants(variants, defaultProvider, expected);
    });

    it("round-trips normalized refs through modelKey", () => {
      const parsed = parseModelRef(" opus-4.6 ", "anthropic");
      expect(parsed).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
      expect(modelKey(parsed?.provider ?? "", parsed?.model ?? "")).toBe(
        "anthropic/claude-opus-4-6",
      );
    });
    it.each(["", "  ", "/", "anthropic/", "/model"])("returns null for invalid ref %j", (raw) => {
      expect(parseModelRef(raw, "anthropic")).toBeNull();
    });
  });

  describe("resolvePersistedModelRef", () => {
    it("splits legacy combined refs when provider is not stored separately", () => {
      expect(
        resolvePersistedModelRef({
          defaultProvider: "anthropic",
          overrideModel: "ollama-beelink2/qwen2.5-coder:7b",
        }),
      ).toEqual({
        provider: "ollama-beelink2",
        model: "qwen2.5-coder:7b",
      });
    });

    it("preserves explicit runtime provider for vendor-prefixed model ids", () => {
      expect(
        resolvePersistedModelRef({
          defaultProvider: "anthropic",
          runtimeProvider: "openrouter",
          runtimeModel: "anthropic/claude-haiku-4.5",
        }),
      ).toEqual({
        provider: "openrouter",
        model: "anthropic/claude-haiku-4.5",
      });
    });

    it("normalizes explicit override providers without reparsing runtime semantics", () => {
      expect(
        resolvePersistedModelRef({
          defaultProvider: "anthropic",
          overrideProvider: "kimi-coding",
          overrideModel: "kimi-code",
        }),
      ).toEqual({
        provider: "kimi",
        model: "kimi-code",
      });
    });
  });

  describe("resolvePersistedOverrideModelRef", () => {
    it("splits legacy combined override refs when provider is not stored separately", () => {
      expect(
        resolvePersistedOverrideModelRef({
          defaultProvider: "anthropic",
          overrideModel: "ollama-beelink2/qwen2.5-coder:7b",
        }),
      ).toEqual({
        provider: "ollama-beelink2",
        model: "qwen2.5-coder:7b",
      });
    });

    it("normalizes explicit override providers without reparsing away wrapper semantics", () => {
      expect(
        resolvePersistedOverrideModelRef({
          defaultProvider: "anthropic",
          overrideProvider: "kimi-coding",
          overrideModel: "kimi-code",
        }),
      ).toEqual({
        provider: "kimi",
        model: "kimi-code",
      });
    });
  });

  describe("resolvePersistedSelectedModelRef", () => {
    it("prefers explicit overrides ahead of runtime model fields", () => {
      expect(
        resolvePersistedSelectedModelRef({
          defaultProvider: "anthropic",
          runtimeProvider: "openai-codex",
          runtimeModel: "gpt-5.4",
          overrideProvider: "anthropic",
          overrideModel: "claude-opus-4-6",
        }),
      ).toEqual({
        provider: "anthropic",
        model: "claude-opus-4-6",
      });
    });

    it("preserves explicit wrapper providers for vendor-prefixed override models", () => {
      expect(
        resolvePersistedSelectedModelRef({
          defaultProvider: "anthropic",
          runtimeProvider: "openrouter",
          runtimeModel: "openrouter/free",
          overrideProvider: "openrouter",
          overrideModel: "anthropic/claude-haiku-4.5",
        }),
      ).toEqual({
        provider: "openrouter",
        model: "anthropic/claude-haiku-4.5",
      });
    });
  });

  describe("inferUniqueProviderFromConfiguredModels", () => {
    it("infers provider when configured model match is unique", () => {
      const cfg = {
        agents: {
          defaults: {
            models: {
              "anthropic/claude-sonnet-4-6": {},
            },
          },
        },
      } as unknown as OpenClawConfig;

      expect(
        inferUniqueProviderFromConfiguredModels({
          cfg,
          model: "claude-sonnet-4-6",
        }),
      ).toBe("anthropic");
    });

    it("returns undefined when configured matches are ambiguous", () => {
      const cfg = {
        agents: {
          defaults: {
            models: {
              "anthropic/claude-sonnet-4-6": {},
              "minimax/claude-sonnet-4-6": {},
            },
          },
        },
      } as unknown as OpenClawConfig;

      expect(
        inferUniqueProviderFromConfiguredModels({
          cfg,
          model: "claude-sonnet-4-6",
        }),
      ).toBeUndefined();
    });

    it("returns undefined for provider-prefixed model ids", () => {
      const cfg = {
        agents: {
          defaults: {
            models: {
              "anthropic/claude-sonnet-4-6": {},
            },
          },
        },
      } as unknown as OpenClawConfig;

      expect(
        inferUniqueProviderFromConfiguredModels({
          cfg,
          model: "anthropic/claude-sonnet-4-6",
        }),
      ).toBeUndefined();
    });

    it("infers provider for slash-containing model id when allowlist match is unique", () => {
      const cfg = {
        agents: {
          defaults: {
            models: {
              "vercel-ai-gateway/anthropic/claude-sonnet-4-6": {},
            },
          },
        },
      } as unknown as OpenClawConfig;

      expect(
        inferUniqueProviderFromConfiguredModels({
          cfg,
          model: "anthropic/claude-sonnet-4-6",
        }),
      ).toBe("vercel-ai-gateway");
    });

    it("infers provider from configured provider catalogs when allowlist is absent", () => {
      const cfg = {
        models: {
          providers: {
            "qwen-dashscope": {
              models: [{ id: "qwen-max" }],
            },
          },
        },
      } as unknown as OpenClawConfig;

      expect(
        inferUniqueProviderFromConfiguredModels({
          cfg,
          model: "qwen-max",
        }),
      ).toBe("qwen-dashscope");
    });

    it("returns undefined when provider catalog matches are ambiguous", () => {
      const cfg = {
        models: {
          providers: {
            "qwen-dashscope": {
              models: [{ id: "qwen-max" }],
            },
            qwen: {
              models: [{ id: "qwen-max" }],
            },
          },
        },
      } as unknown as OpenClawConfig;

      expect(
        inferUniqueProviderFromConfiguredModels({
          cfg,
          model: "qwen-max",
        }),
      ).toBeUndefined();
    });
  });

  describe("buildModelAliasIndex", () => {
    it("should build alias index from config", () => {
      const cfg: Partial<OpenClawConfig> = {
        agents: {
          defaults: {
            models: {
              "anthropic/claude-3-5-sonnet": { alias: "fast" },
              "openai/gpt-4o": { alias: "smart" },
            },
          },
        },
      };

      const index = buildModelAliasIndex({
        cfg: cfg as OpenClawConfig,
        defaultProvider: "anthropic",
      });

      expect(index.byAlias.get("fast")?.ref).toEqual({
        provider: "anthropic",
        model: "claude-3-5-sonnet",
      });
      expect(index.byAlias.get("smart")?.ref).toEqual({ provider: "openai", model: "gpt-4o" });
      expect(index.byKey.get(modelKey("anthropic", "claude-3-5-sonnet"))).toEqual(["fast"]);
    });
  });

  describe("buildAllowedModelSet", () => {
    it("keeps explicitly allowlisted models even when missing from bundled catalog", () => {
      const result = buildAllowedModelSet({
        cfg: EXPLICIT_ALLOWLIST_CONFIG,
        catalog: BUNDLED_ALLOWLIST_CATALOG,
        defaultProvider: "anthropic",
      });

      expect(result.allowAny).toBe(false);
      expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
      expect(result.allowedCatalog).toEqual([
        {
          provider: "anthropic",
          id: "claude-sonnet-4-6",
          name: "Claude Sonnet 4.5",
          alias: "sonnet",
        },
      ]);
    });

    it("overlays configured provider metadata and alias onto matching catalog entries", () => {
      const cfg: OpenClawConfig = {
        agents: {
          defaults: {
            model: { primary: "openai/gpt-test-z" },
            models: {
              "openai/gpt-test-z": { alias: "GPT Test Z Alias" },
            },
          },
        },
        models: {
          providers: {
            openai: {
              baseUrl: "https://openai.example.com",
              models: [
                {
                  id: "gpt-test-z",
                  name: "Configured GPT Test Z",
                  contextWindow: 64_000,
                },
              ],
            },
          },
        },
      } as unknown as OpenClawConfig;

      const result = buildAllowedModelSet({
        cfg,
        catalog: [{ provider: "openai", id: "gpt-test-z", name: "gpt-test-z" }],
        defaultProvider: "anthropic",
      });

      expect(result.allowAny).toBe(false);
      expect(result.allowedCatalog).toEqual([
        {
          provider: "openai",
          id: "gpt-test-z",
          name: "Configured GPT Test Z",
          alias: "GPT Test Z Alias",
          contextWindow: 64_000,
        },
      ]);
    });

    it("applies configured provider metadata and alias to synthetic allowlist entries", () => {
      const cfg: OpenClawConfig = {
        agents: {
          defaults: {
            model: { primary: "nvidia/moonshotai/kimi-k2.5" },
            models: {
              "nvidia/moonshotai/kimi-k2.5": { alias: "Kimi K2.5 (NVIDIA)" },
            },
          },
        },
        models: {
          providers: {
            nvidia: {
              baseUrl: "https://nvidia.example.com",
              models: [
                {
                  id: "moonshotai/kimi-k2.5",
                  name: "Kimi K2.5 (Configured)",
                  contextWindow: 32_000,
                  reasoning: true,
                },
              ],
            },
          },
        },
      } as unknown as OpenClawConfig;

      const result = buildAllowedModelSet({
        cfg,
        catalog: [],
        defaultProvider: "anthropic",
      });

      expect(result.allowAny).toBe(false);
      expect(result.allowedCatalog).toEqual([
        {
          provider: "nvidia",
          id: "moonshotai/kimi-k2.5",
          name: "Kimi K2.5 (Configured)",
          alias: "Kimi K2.5 (NVIDIA)",
          contextWindow: 32_000,
          reasoning: true,
        },
      ]);
    });

    it("includes fallback models in allowed set", () => {
      const cfg = createAgentFallbackConfig({
        fallbacks: ["anthropic/claude-sonnet-4-6", "google/gemini-3-pro"],
      });

      const result = buildAllowedModelSet({
        cfg,
        catalog: [],
        defaultProvider: "openai",
        defaultModel: "gpt-4o",
      });

      expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
      expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
      expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(true);
      expect(result.allowAny).toBe(false);
    });

    it("handles empty fallbacks gracefully", () => {
      const cfg = createAgentFallbackConfig({});

      const result = buildAllowedModelSet({
        cfg,
        catalog: [],
        defaultProvider: "openai",
        defaultModel: "gpt-4o",
      });

      expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
      expect(result.allowAny).toBe(false);
    });

    it("prefers per-agent fallback overrides when agentId is provided", () => {
      const cfg = createAgentFallbackConfig({
        fallbacks: ["google/gemini-3-pro"],
        agentFallbacks: ["anthropic/claude-sonnet-4-6"],
      });

      const result = buildAllowedModelSet({
        cfg,
        catalog: [],
        defaultProvider: "openai",
        defaultModel: "gpt-4o",
        agentId: "coder",
      });

      expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
      expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
      expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(false);
      expect(result.allowAny).toBe(false);
    });
  });

  describe("resolveAllowedModelRef", () => {
    it("accepts explicit allowlist refs absent from bundled catalog", () => {
      const result = resolveAllowedModelRef({
        cfg: EXPLICIT_ALLOWLIST_CONFIG,
        catalog: BUNDLED_ALLOWLIST_CATALOG,
        raw: "anthropic/claude-sonnet-4-6",
        defaultProvider: "openai",
        defaultModel: "gpt-5.4",
      });

      expect(result).toEqual({
        key: "anthropic/claude-sonnet-4-6",
        ref: { provider: "anthropic", model: "claude-sonnet-4-6" },
      });
    });

    it("strips trailing auth profile suffix before allowlist matching", () => {
      const cfg: OpenClawConfig = {
        agents: {
          defaults: {
            models: {
              "openai/@cf/openai/gpt-oss-20b": {},
            },
          },
        },
      } as OpenClawConfig;

      const result = resolveAllowedModelRef({
        cfg,
        catalog: [],
        raw: "openai/@cf/openai/gpt-oss-20b@cf:default",
        defaultProvider: "anthropic",
      });

      expect(result).toEqual({
        key: "openai/@cf/openai/gpt-oss-20b",
        ref: { provider: "openai", model: "@cf/openai/gpt-oss-20b" },
      });
    });

    it("infers provider from allowlist for bare model ids to prevent prefix drift (#48369)", () => {
      const cfg = {
        agents: {
          defaults: {
            models: {
              "openai-codex/gpt-5.4": {},
              "opencode-go/kimi-k2.5": {},
              "opencode-go/glm-5": {},
            },
          },
        },
      } as OpenClawConfig;

      // When session default is openai-codex, switching to a bare "kimi-k2.5"
      // should resolve to opencode-go/kimi-k2.5, not openai-codex/kimi-k2.5
      const result = resolveAllowedModelRef({
        cfg,
        catalog: [],
        raw: "kimi-k2.5",
        defaultProvider: "openai-codex", // session's current provider
      });

      expect(result).toEqual({
        key: "opencode-go/kimi-k2.5",
        ref: { provider: "opencode-go", model: "kimi-k2.5" },
      });
    });
  });

  describe("resolveModelRefFromString", () => {
    it("should resolve from string with alias", () => {
      const index = {
        byAlias: new Map([
          ["fast", { alias: "fast", ref: { provider: "anthropic", model: "sonnet" } }],
        ]),
        byKey: new Map(),
      };

      const resolved = resolveModelRefFromString({
        raw: "fast",
        defaultProvider: "openai",
        aliasIndex: index,
      });

      expect(resolved?.ref).toEqual({ provider: "anthropic", model: "sonnet" });
      expect(resolved?.alias).toBe("fast");
    });

    it("should resolve direct ref if no alias match", () => {
      const resolved = resolveModelRefFromString({
        raw: "openai/gpt-4",
        defaultProvider: "anthropic",
      });
      expect(resolved?.ref).toEqual({ provider: "openai", model: "gpt-4" });
    });

    it("strips trailing profile suffix for simple model refs", () => {
      const resolved = resolveModelRefFromString({
        raw: "gpt-5@myprofile",
        defaultProvider: "openai",
      });
      expect(resolved?.ref).toEqual({ provider: "openai", model: "gpt-5" });
    });

    it("strips trailing profile suffix for provider/model refs", () => {
      const resolved = resolveModelRefFromString({
        raw: "google/gemini-flash-latest@google:bevfresh",
        defaultProvider: "anthropic",
      });
      expect(resolved?.ref).toEqual({
        provider: "google",
        model: "gemini-flash-latest",
      });
    });

    it("preserves Cloudflare @cf model segments", () => {
      const resolved = resolveModelRefFromString({
        raw: "openai/@cf/openai/gpt-oss-20b",
        defaultProvider: "anthropic",
      });
      expect(resolved?.ref).toEqual({
        provider: "openai",
        model: "@cf/openai/gpt-oss-20b",
      });
    });

    it("preserves OpenRouter @preset model segments", () => {
      const resolved = resolveModelRefFromString({
        raw: "openrouter/@preset/kimi-2-5",
        defaultProvider: "anthropic",
      });
      expect(resolved?.ref).toEqual({
        provider: "openrouter",
        model: "@preset/kimi-2-5",
      });
    });

    it("splits trailing profile suffix after OpenRouter preset paths", () => {
      const resolved = resolveModelRefFromString({
        raw: "openrouter/@preset/kimi-2-5@work",
        defaultProvider: "anthropic",
      });
      expect(resolved?.ref).toEqual({
        provider: "openrouter",
        model: "@preset/kimi-2-5",
      });
    });

    it("strips profile suffix before alias resolution", () => {
      const index = {
        byAlias: new Map([
          ["kimi", { alias: "kimi", ref: { provider: "nvidia", model: "moonshotai/kimi-k2.5" } }],
        ]),
        byKey: new Map(),
      };

      const resolved = resolveModelRefFromString({
        raw: "kimi@nvidia:default",
        defaultProvider: "openai",
        aliasIndex: index,
      });
      expect(resolved?.ref).toEqual({
        provider: "nvidia",
        model: "moonshotai/kimi-k2.5",
      });
      expect(resolved?.alias).toBe("kimi");
    });
  });

  describe("resolveConfiguredModelRef", () => {
    it("should infer the unique provider from configured models for bare defaults", () => {
      const cfg = {
        agents: {
          defaults: {
            model: { primary: "claude-opus-4-6" },
            models: {
              "anthropic/claude-opus-4-6": {},
            },
          },
        },
      } as OpenClawConfig;

      const result = resolveConfiguredModelRef({
        cfg,
        defaultProvider: "openai",
        defaultModel: "gpt-5.4",
      });

      expect(result).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
    });

    it("should fall back to the configured default provider and warn if provider is missing for non-alias", () => {
      setLoggerOverride({ level: "silent", consoleLevel: "warn" });
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
      try {
        const cfg: Partial<OpenClawConfig> = {
          agents: {
            defaults: {
              model: { primary: "claude-3-5-sonnet" },
            },
          },
        };

        const result = resolveConfiguredModelRef({
          cfg: cfg as OpenClawConfig,
          defaultProvider: "google",
          defaultModel: "gemini-pro",
        });

        expect(result).toEqual({ provider: "google", model: "claude-3-5-sonnet" });
        expect(warnSpy).toHaveBeenCalledWith(
          expect.stringContaining('Falling back to "google/claude-3-5-sonnet"'),
        );
      } finally {
        warnSpy.mockRestore();
        setLoggerOverride(null);
        resetLogger();
      }
    });

    it("sanitizes control characters in providerless-model warnings", () => {
      const warnLogs = createWarnLogCapture("openclaw-model-selection-test");
      try {
        const cfg: Partial<OpenClawConfig> = {
          agents: {
            defaults: {
              model: { primary: "\u001B[31mclaude-3-5-sonnet\nspoof" },
            },
          },
        };

        const result = resolveConfiguredModelRef({
          cfg: cfg as OpenClawConfig,
          defaultProvider: "google",
          defaultModel: "gemini-pro",
        });

        expect(result).toEqual({
          provider: "google",
          model: "\u001B[31mclaude-3-5-sonnet\nspoof",
        });
        const warning = warnLogs.findText('Falling back to "google/claude-3-5-sonnet"');
        expect(warning).toContain('Falling back to "google/claude-3-5-sonnet"');
        expect(warning).not.toContain("\u001B");
        expect(warning).not.toContain("\n");
      } finally {
        warnLogs.cleanup();
      }
    });

    it("infers a unique configured provider for bare default model strings", () => {
      setLoggerOverride({ level: "silent", consoleLevel: "warn" });
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
      try {
        const cfg = {
          agents: {
            defaults: {
              model: { primary: "claude-opus-4-6" },
              models: {
                "anthropic/claude-opus-4-6": {},
              },
            },
          },
        } as OpenClawConfig;

        const result = resolveConfiguredModelRef({
          cfg,
          defaultProvider: "openai",
          defaultModel: "gpt-5.4",
        });

        expect(result).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
        expect(warnSpy).not.toHaveBeenCalled();
      } finally {
        warnSpy.mockRestore();
        setLoggerOverride(null);
        resetLogger();
      }
    });

    it("should use default provider/model if config is empty", () => {
      const cfg: Partial<OpenClawConfig> = {};
      const result = resolveConfiguredModelRef({
        cfg: cfg as OpenClawConfig,
        defaultProvider: "openai",
        defaultModel: "gpt-4",
      });
      expect(result).toEqual({ provider: "openai", model: "gpt-4" });
    });

    it("should prefer configured custom provider when default provider is not in models.providers", () => {
      const cfg = createProviderWithModelsConfig("n1n", [
        {
          id: "gpt-5.4",
          name: "GPT 5.4",
          reasoning: false,
          input: ["text"],
          cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
          contextWindow: 128000,
          maxTokens: 4096,
        },
      ]);
      const result = resolveConfiguredRefForTest(cfg);
      expect(result).toEqual({ provider: "n1n", model: "gpt-5.4" });
    });

    it("should keep default provider when it is in models.providers", () => {
      const cfg = createProviderWithModelsConfig("anthropic", [
        {
          id: "claude-opus-4-6",
          name: "Claude Opus 4.6",
          reasoning: true,
          input: ["text", "image"],
          cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
          contextWindow: 200000,
          maxTokens: 4096,
        },
      ]);
      const result = resolveConfiguredRefForTest(cfg);
      expect(result).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
    });

    it("can skip plugin-backed model normalization for display-only callers", () => {
      const cfg = {
        agents: {
          defaults: {
            model: { primary: "google-vertex/gemini-3.1-flash-lite" },
          },
        },
      } as OpenClawConfig;

      const result = resolveConfiguredModelRef({
        cfg,
        defaultProvider: "anthropic",
        defaultModel: "claude-opus-4-6",
        allowPluginNormalization: false,
      });

      expect(result).toEqual({
        provider: "google-vertex",
        model: "gemini-3.1-flash-lite-preview",
      });
    });

    it("should fall back to hardcoded default when no custom providers have models", () => {
      const cfg = createProviderWithModelsConfig("empty-provider", []);
      const result = resolveConfiguredRefForTest(cfg);
      expect(result).toEqual({ provider: "openai", model: "gpt-5.4" });
    });

    it("should warn when specified model cannot be resolved and falls back to default", () => {
      setLoggerOverride({ level: "silent", consoleLevel: "warn" });
      const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
      try {
        const cfg: Partial<OpenClawConfig> = {
          agents: {
            defaults: {
              model: { primary: "openai/" },
            },
          },
        };

        const result = resolveConfiguredModelRef({
          cfg: cfg as OpenClawConfig,
          defaultProvider: "openai",
          defaultModel: "gpt-5.4",
        });

        expect(result).toEqual({ provider: "openai", model: "gpt-5.4" });
        expect(warnSpy).toHaveBeenCalledWith(
          expect.stringContaining('Falling back to default "openai/gpt-5.4"'),
        );
      } finally {
        warnSpy.mockRestore();
        setLoggerOverride(null);
        resetLogger();
      }
    });
  });

  describe("resolveThinkingDefault", () => {
    it("prefers per-model params.thinking over global thinkingDefault", () => {
      const cfg = {
        agents: {
          defaults: {
            thinkingDefault: "low",
            models: {
              "anthropic/claude-opus-4-6": {
                params: { thinking: "high" },
              },
            },
          },
        },
      } as OpenClawConfig;

      expect(resolveAnthropicOpusThinking(cfg)).toBe("high");
    });

    it("accepts legacy duplicated OpenRouter keys for per-model thinking", () => {
      const cfg = {
        agents: {
          defaults: {
            models: {
              "openrouter/openrouter/hunter-alpha": {
                params: { thinking: "high" },
              },
            },
          },
        },
      } as OpenClawConfig;

      expect(
        resolveThinkingDefault({
          cfg,
          provider: "openrouter",
          model: "openrouter/hunter-alpha",
        }),
      ).toBe("high");
    });

    it("accepts per-model params.thinking=adaptive", () => {
      const cfg = {
        agents: {
          defaults: {
            models: {
              "anthropic/claude-opus-4-6": {
                params: { thinking: "adaptive" },
              },
            },
          },
        },
      } as OpenClawConfig;

      expect(resolveAnthropicOpusThinking(cfg)).toBe("adaptive");
    });

    it("keeps thinking off by default for explicitly configured Anthropic Opus 4.7", () => {
      const cfg = {
        agents: {
          defaults: {
            model: { primary: "anthropic/claude-opus-4-7" },
          },
        },
      } as OpenClawConfig;

      expect(resolveAnthropicOpus47Thinking(cfg)).toBe("off");
    });

    it("falls back to low when no provider thinking hook is active", () => {
      const cfg = {} as OpenClawConfig;

      expect(resolveAnthropicOpusThinking(cfg)).toBe("low");

      expect(
        resolveThinkingDefault({
          cfg,
          provider: "amazon-bedrock",
          model: "us.anthropic.claude-sonnet-4-6-v1:0",
          catalog: [
            {
              provider: "amazon-bedrock",
              id: "us.anthropic.claude-sonnet-4-6-v1:0",
              name: "Claude Sonnet 4.6",
              reasoning: true,
            },
          ],
        }),
      ).toBe("low");
    });
  });
});

describe("normalizeModelSelection", () => {
  it("returns trimmed string for string input", () => {
    expect(normalizeModelSelection("ollama/llama3.2:3b")).toBe("ollama/llama3.2:3b");
  });

  it("returns undefined for empty/whitespace string", () => {
    expect(normalizeModelSelection("")).toBeUndefined();
    expect(normalizeModelSelection("   ")).toBeUndefined();
  });

  it("extracts primary from object", () => {
    expect(normalizeModelSelection({ primary: "google/gemini-2.5-flash" })).toBe(
      "google/gemini-2.5-flash",
    );
  });

  it("returns undefined for object without primary", () => {
    expect(normalizeModelSelection({ fallbacks: ["a"] })).toBeUndefined();
    expect(normalizeModelSelection({})).toBeUndefined();
  });

  it("returns undefined for null/undefined/number", () => {
    expect(normalizeModelSelection(undefined)).toBeUndefined();
    expect(normalizeModelSelection(null)).toBeUndefined();
    expect(normalizeModelSelection(42)).toBeUndefined();
  });
});

describe("resolveSubagentConfiguredModelSelection", () => {
  it("prefers the agent primary model over agents.defaults.subagents.model", () => {
    const cfg = {
      agents: {
        defaults: {
          model: { primary: "anthropic/claude-sonnet-4-6" },
          subagents: { model: "openai/gpt-5.4" },
        },
        list: [
          {
            id: "research",
            model: { primary: "anthropic/claude-opus-4-6" },
          },
        ],
      },
    } as OpenClawConfig;

    expect(resolveSubagentConfiguredModelSelection({ cfg, agentId: "research" })).toBe(
      "anthropic/claude-opus-4-6",
    );
  });

  it("still prefers agent subagents.model over the agent primary model", () => {
    const cfg = {
      agents: {
        defaults: {
          model: { primary: "anthropic/claude-sonnet-4-6" },
          subagents: { model: "openai/gpt-5.4" },
        },
        list: [
          {
            id: "research",
            model: { primary: "anthropic/claude-opus-4-6" },
            subagents: { model: "google/gemini-2.5-pro" },
          },
        ],
      },
    } as OpenClawConfig;

    expect(resolveSubagentConfiguredModelSelection({ cfg, agentId: "research" })).toBe(
      "google/gemini-2.5-pro",
    );
  });
});
