/**
 * Registro de providers da camada agnóstica. ÚNICO lugar (junto do resto de
 * edge/llm/) onde SDK de vendor é importado. Instância POR CHAMADA com a chave
 * BYOK da org: sem pool global de chave, sem fallback silencioso.
 */
import { createAnthropic } from '@ai-sdk/anthropic';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
import { createOpenAI } from '@ai-sdk/openai';
import type { LanguageModel } from 'ai';
import { MockLanguageModelV3 } from 'ai/test';

import { PROVEDOR_POR_ASSINATURA } from '@/lib/ai/pontos/provedores';
import { fetchParaDestinoDaOrganizacao } from '@/lib/automation/destinos-internos-autorizados';

import { allowlistedFetch, buildAllowlist } from '../egress';
import { comCacheDaAnthropicNoOpenRouter, type CacheDaChamada } from './cache-da-openrouter';

/**
 * provider name → (chave BYOK da org, id do modelo, endpoint opcional, cache
 * opcional) → modelo pronto para generateText.
 *
 * O terceiro parâmetro é o endpoint escolhido no painel de provedores
 * (`ai_purpose_bindings.base_url`). Existe por causa dos dois casos que o
 * registry precisa atender e que não têm endpoint fixo: um gateway
 * OpenAI-compatível na frente da OpenRouter e, no roteiro do produto, um modelo
 * rodando na máquina do próprio cliente. É opcional — os providers canônicos
 * ignoram e continuam indo ao endpoint intrínseco de terem sido escolhidos.
 *
 * O quarto também é opcional: quem não o passa (mídia, transcrição, testes)
 * recebe a fábrica de sempre.
 */
export type ProviderRegistry = Record<
  string,
  (apiKey: string, modelId: string, baseUrl?: string, cache?: CacheDaChamada) => LanguageModel
>;

/**
 * Endpoint canônico do provider Anthropic (baseURL default do @ai-sdk/anthropic). NÃO é
 * um knob de política (a allowlist de política é a do egress.ts) — é o destino INTRÍNSECO
 * de ter escolhido o provider anthropic. Se uma org precisar de proxy/baseURL custom, é aqui
 * que ele entra (junto do `fetch` contido), nunca espalhado.
 */
const ANTHROPIC_ENDPOINT = 'https://api.anthropic.com';
const OPENAI_ENDPOINT = 'https://api.openai.com';
const GOOGLE_ENDPOINT = 'https://generativelanguage.googleapis.com';
/**
 * A OpenRouter fala a API da OpenAI, então o provider `@ai-sdk/openai` conversa
 * com ela sem dependência nova — e os ids dela já vêm no formato
 * `familia/modelo`, o mesmo dos nossos, sem tradução no meio.
 */
export const OPENROUTER_ENDPOINT = process.env.OPENROUTER_BASE_URL?.trim() || 'https://openrouter.ai/api/v1';

/**
 * O endereço da chamada é a OpenRouter OFICIAL? Resolvido igual à fábrica
 * (`baseUrl ?? OPENROUTER_ENDPOINT`), porque OPENROUTER_BASE_URL e o painel
 * podem apontar um gateway do operador. Hostname exato: `openrouter.ai.exemplo`
 * não passa. É o critério ÚNICO de quem manda campo próprio da OpenRouter no
 * corpo (cache, sessão) ou pede o corpo da resposta (custo): um gateway estrito
 * recusaria o campo desconhecido, e o turno inteiro cairia.
 */
export function ehOpenRouterOficial(baseUrl?: string): boolean {
  try {
    return new URL(baseUrl ?? OPENROUTER_ENDPOINT).hostname === 'openrouter.ai';
  } catch {
    // Endereço que nem é URL não é a OpenRouter oficial — a fábrica erra sozinha depois.
    return false;
  }
}

/**
 * A DeepSeek também fala a API da OpenAI — mesma fábrica, mesmo formato de
 * payload, sem SDK novo. O endpoint é a raiz que o provedor documenta (ele
 * também aceita `/v1`); o `@ai-sdk/openai` acrescenta `/chat/completions`.
 */
export const DEEPSEEK_ENDPOINT = 'https://api.deepseek.com';

/**
 * A Requesty é um roteador OpenAI-compatível como a OpenRouter: uma chave dá
 * acesso a modelos de vários fabricantes, e os ids vêm no formato
 * `fabricante/modelo` (`openai/gpt-4o-mini`). Mesma fábrica, sem SDK novo.
 * Quem precisa ficar na Europa aponta o endpoint próprio do painel para
 * `https://router.eu.requesty.ai/v1`.
 */
export const REQUESTY_ENDPOINT = 'https://router.requesty.ai/v1';

/**
 * O endpoint do CODEX — o mesmo login do ChatGPT, falando a API de resposta.
 *
 * A assinatura (`openai-assinatura`) não tem chave: o `access_token` SIWC
 * viaja como Bearer para a API pública de Responses da OpenAI. A chamada é
 * transmitida (`stream:true`) por `runModelCall`, sem opções fora do contrato
 * SIWC, e nunca usa endpoints internos do ChatGPT.
 *
 * A allowlist de egress do provider deriva daqui (`contain(...)` abaixo) — mas
 * o CATRACA de host do `branding.test.ts` é régua à parte: ele exige a linha
 * declarada em `HOSTS_DECLARADOS`, que está lá, com categoria e motivo.
 *
 * A listagem de modelos da assinatura também usa a API pública oficial `/v1/models`,
 * documentada para Sign in with ChatGPT. Não chamamos endpoints internos do ChatGPT.
 */
export const OPENAI_CODEX_ENDPOINT = 'https://api.openai.com/v1';

/** Endpoint oficial de catálogo para tokens de Sign in with ChatGPT. */
export const OPENAI_MODELS_ENDPOINT = 'https://api.openai.com/v1/models';

/**
 * Cabeçalhos OPCIONAIS de atribuição da OpenRouter.
 *
 * A doc deles chama `HTTP-Referer` e `X-Title` de "optional headers to identify
 * your app and make it discoverable to users on our site" — servem para
 * atribuição e para o ranking público do site deles, NÃO para a chamada
 * funcionar. Chamada sem eles é atendida normalmente.
 *
 * Por isso eles saem da INSTALAÇÃO e nunca do código: uma URL literal aqui
 * viajaria dentro da imagem que todo self-hoster roda, creditando o consumo de
 * OpenRouter de cada cliente a um site que não é dele. E um título literal com
 * o nome do produto é a marca vazando por fora do resolvedor — a catraca de
 * `tests/unit/branding.test.ts` reprova, e está certa.
 *
 * Sem valor, nenhum header vai: falha aberta na informação, porque a ausência
 * de atribuição não quebra ninguém.
 */
export function cabecalhosDeAtribuicaoOpenRouter(): Record<string, string> | undefined {
  const url = process.env.OPENROUTER_APP_URL?.trim();
  const titulo = process.env.OPENROUTER_APP_TITLE?.trim();
  const headers: Record<string, string> = {};
  if (url) headers['HTTP-Referer'] = url;
  if (titulo) headers['X-Title'] = titulo;
  return Object.keys(headers).length > 0 ? headers : undefined;
}

/**
 * Toggle do raciocínio (thinking) da DeepSeek — knob DEEPSEEK_THINKING.
 * `'provider'` preserva o default do provedor (raciocínio LIGADO); `'disabled'`
 * injeta o desligamento no corpo das chamadas. Só a DeepSeek o lê.
 */
export type RaciocinioDeepseek = 'provider' | 'disabled';

/**
 * Desliga o raciocínio da DeepSeek no CORPO do request — sem tocar em prompt,
 * tools, temperatura ou qualquer outro parâmetro da chamada.
 *
 * POR QUE UM CAMPO, E POR QUE DOIS NOMES: o raciocínio da DeepSeek nasce LIGADO
 * e o token de raciocínio é cobrado como SAÍDA (medido em produção: ~8× a saída
 * do OpenAI por turno e +22 s de latência — o desconto de preço foi anulado
 * pelo volume). São DUAS superfícies e cada uma lê um nome diferente (doc
 * oficial, "Thinking Mode › Toggle and Effort Control"):
 *
 *   - Responses API (`/responses`)   → `reasoning.effort = 'none'` desliga;
 *   - Chat Completions               → `thinking.type = 'disabled'`.
 *
 * O SDK instalado (`createOpenAI(...)(modelId)`) fala a Responses API — o teste
 * `deepseek-sem-raciocinio.test.ts` prende essa rota. Por isso vão os DOIS
 * campos: o provedor ignora em SILÊNCIO o que a rota não conhece (não erra),
 * então o desligamento vale em qualquer uma das duas — e o caminho real do SDK
 * é o `/responses`, que só entende `reasoning`.
 *
 * O corpo é lido por cima do `init` que o SDK montou, então nada mais muda; um
 * `reasoning` já presente é preservado (só o `effort` é forçado a 'none').
 */
function comRaciocinioDesligado(inner: typeof fetch): typeof fetch {
  return (input, init) => {
    const corpo = init?.body;
    if (typeof corpo === 'string') {
      try {
        const json = JSON.parse(corpo) as Record<string, unknown>;
        const reasoningBruto = json['reasoning'];
        const reasoning =
          reasoningBruto !== null && typeof reasoningBruto === 'object'
            ? (reasoningBruto as Record<string, unknown>)
            : {};
        return inner(input, {
          ...init,
          body: JSON.stringify({
            ...json,
            thinking: { type: 'disabled' },
            reasoning: { ...reasoning, effort: 'none' },
          }),
        });
      } catch {
        // Corpo não-JSON: repassa intacto. Um ajuste de tuning nunca pode
        // derrubar a chamada que ele veio otimizar.
      }
    }
    return inner(input, init);
  };
}

/**
 * Esforço de raciocínio das chamadas DIRETAS à OpenAI — knob
 * OPENAI_REASONING_EFFORT (opcional; ausente = nada é injetado e vale o padrão
 * do modelo).
 *
 * Por que existe: nos modelos de raciocínio da OpenAI (famílias gpt-5.x e
 * gpt-6) o padrão é PENSAR antes de responder, e o agente de WhatsApp paga isso
 * em latência. Medido em 2026-09-24 com `gpt-6-luna`, prompt de produção e a
 * tool `send_message`, 2 repetições por configuração:
 *
 *   padrão ........ 4,9–5,5 s, 248–282 tokens de saída, send_message 0 de 2
 *   effort=low .... 5,9–6,3 s, 258–284 tokens de saída, send_message 1 de 2
 *   effort=none ... 1,8–2,0 s,       49 tokens de saída, send_message 2 de 2
 *
 * No turno real (`agent_preview`) o padrão levou 9–27 s, com ~700 tokens de
 * saída para um rascunho de ~40 — e, quando o modelo não chama a tool, o loop
 * roda outra etapa. É o mesmo campo que a DeepSeek já recebe
 * (`comRaciocinioDesligado`), só que com o valor escolhido pelo operador.
 *
 * ⚠️ Só vale para o provider `openai` (endpoint oficial, Responses API) e só
 * em modelo que raciocina (`modeloOpenAIRaciocina`): modelo SEM raciocínio
 * recusa o campo — medido: `gpt-4.1-nano` + `reasoning.effort` → 400
 * "Unsupported parameter". A grafia é validada no boot do worker
 * (`lib/agent-engine/env.ts`, pela mesma função abaixo): errada, o worker não
 * sobe e diz qual variável corrigir, em vez de falhar a cada turno.
 */
export type EsforcoDeRaciocinioOpenAI = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
const ESFORCOS_OPENAI: readonly EsforcoDeRaciocinioOpenAI[] = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'];

export function esforcoDeRaciocinioOpenAI(
  valor: string | undefined = process.env.OPENAI_REASONING_EFFORT,
): EsforcoDeRaciocinioOpenAI | null {
  const v = valor?.trim().toLowerCase();
  if (!v) return null;
  if (!(ESFORCOS_OPENAI as readonly string[]).includes(v)) {
    throw new Error(`OPENAI_REASONING_EFFORT inválido — use ${ESFORCOS_OPENAI.join(', ')} (ou deixe vazio)`);
  }
  return v as EsforcoDeRaciocinioOpenAI;
}

/**
 * Famílias de raciocínio da OpenAI: `o1`/`o3`/`o4-…`, `gpt-5…` e `gpt-6…` — menos
 * as variantes `-chat`, que não raciocinam. Fora daqui o knob não injeta nada.
 */
export function modeloOpenAIRaciocina(modelId: string): boolean {
  const id = modelId.toLowerCase();
  return /^(o\d|gpt-5|gpt-6)/.test(id) && !id.includes('chat');
}

/**
 * Da linha 6 em diante nenhum modelo lista `minimal`, e só `gpt-6-luna`, `gpt-6-sol` e
 * `gpt-6.1-terra` listam `none` (doc oficial de cada id em 11/10/2026: o gpt-6.1-sol diz
 * "`none` and `minimal` reasoning efforts are not supported", o gpt-6-astra lista
 * low…max, o gpt-6-luna lista none, low…max; o @ai-sdk/openai declara as mesmas listas
 * em `supportedReasoningEfforts`. O gpt-6.1-terra, em 27/10/2026, lista none, low…max).
 * Mandar o knob como está seria 400 em toda resposta.
 *
 * `none`/`minimal` é pedido de velocidade, então vira `none` onde ele existe e `low`
 * onde não existe. `minimal` NÃO vira `low` no Luna: medido acima, lá `low` é mais
 * lento até que o padrão, e `none` é o único que entrega o que o operador pediu.
 */
function esforcoAceitoPeloModelo(
  modelId: string,
  esforco: EsforcoDeRaciocinioOpenAI,
): EsforcoDeRaciocinioOpenAI {
  if (esforco !== 'none' && esforco !== 'minimal') return esforco;
  const id = modelId.toLowerCase();
  const major = /^gpt-(\d+)/.exec(id)?.[1];
  if (major === undefined || Number(major) < 6) return esforco;
  const aceitaNone = id === 'gpt-6-luna' || id === 'gpt-6-sol' || id === 'gpt-6.1-terra';
  return aceitaNone ? 'none' : 'low';
}

/**
 * Injeta `reasoning.effort` no corpo, preservando o resto de `reasoning` e da chamada.
 *
 * O knob é o padrão da INSTALAÇÃO, não uma ordem por cima de quem pediu: a
 * chamada que já traz `reasoning.effort` (a opção `reasoning` de `runModelCall`,
 * que o SDK só escreve quando alguém pede) fica com o seu. Sem isso, um
 * `OPENAI_REASONING_EFFORT=high` faria as classificações, que pedem `none`,
 * voltarem a pensar antes de devolver uma palavra.
 */
function comEsforcoDeRaciocinio(inner: typeof fetch, esforco: EsforcoDeRaciocinioOpenAI): typeof fetch {
  return (input, init) => {
    const corpo = init?.body;
    if (typeof corpo === 'string') {
      try {
        const json = JSON.parse(corpo) as Record<string, unknown>;
        const bruto = json['reasoning'];
        const reasoning = bruto !== null && typeof bruto === 'object' ? (bruto as Record<string, unknown>) : {};
        if (typeof reasoning['effort'] === 'string') return inner(input, init);
        return inner(input, { ...init, body: JSON.stringify({ ...json, reasoning: { ...reasoning, effort: esforco } }) });
      } catch {
        // Corpo não-JSON: repassa intacto — um ajuste de tuning nunca derruba a chamada.
      }
    }
    return inner(input, init);
  };
}

/** Mesmo transporte contido para chat e serviços do provedor, inclusive redirects. */
export function fetchDoProvedor(endpoint: string, allowedHosts: readonly string[] = []): typeof fetch {
  const allow = buildAllowlist([endpoint, ...allowedHosts]);
  return (input, init) => {
    const url = typeof input === 'string' || input instanceof URL ? input : input.url;
    return allowlistedFetch(url, init, { allowlist: allow });
  };
}

/**
 * Providers reais do lançamento. Sonnet (Anthropic) é o default RECOMENDADO —
 * recomendação vive em .env.example/docs; o id do modelo é sempre config da org.
 *
 * O `fetch` INTERNO do provider (generateText) também roteia pela allowlist
 * (`allowlistedFetch`) — sem isso o egress do SDK escapava da contenção. A
 * allowlist do provider = seu endpoint canônico + hosts extra de config
 * (`allowedHosts`, ex.: proxy corporativo). Testes usam o registry fake
 * (createFakeRegistry, sem fetch real); este caminho só é exercitado pelo smoke
 * (rede real → endpoint canônico do provider allowlistado).
 */
export function createDefaultRegistry(opts?: {
  allowedHosts?: string[];
  /**
   * Knob DEEPSEEK_THINKING aplicado à fábrica da DeepSeek (e SÓ a ela — a
   * fábrica é dela). Ausente = 'provider' = nada é injetado.
   */
  deepseekThinking?: RaciocinioDeepseek;
  /** Knob OPENAI_REASONING_EFFORT; ausente = lido do ambiente. `null` = não injeta. */
  openaiReasoningEffort?: EsforcoDeRaciocinioOpenAI | null;
  /**
   * Knob OPENROUTER_PROMPT_CACHE; ausente = lido do ambiente. Lido de
   * `process.env`, como OPENAI_REASONING_EFFORT, e não por `llmEdgeConfigFromEnv`:
   * o env do app descarta chave que não declara, e as rotas do app também chamam
   * o seam. Ligado por padrão desde a prova paga em cada provedor de origem
   * (scripts/prova-cache-openrouter.ts): só `off` desliga — vazio e ausente ligam,
   * porque o `.env.example` sempre trouxe a linha vazia e quem instalou a tem assim.
   */
  openrouterPromptCache?: boolean;
}): ProviderRegistry {
  const extra = opts?.allowedHosts ?? [];
  const esforcoOpenAI =
    opts?.openaiReasoningEffort !== undefined ? opts.openaiReasoningEffort : esforcoDeRaciocinioOpenAI();
  const cacheNaOpenRouter =
    opts?.openrouterPromptCache ?? process.env.OPENROUTER_PROMPT_CACHE?.trim().toLowerCase() !== 'off';
  const contain = (endpoint: string): typeof fetch => fetchDoProvedor(endpoint, extra);
  return {
    anthropic: (apiKey, modelId) =>
      createAnthropic({ apiKey, fetch: contain(ANTHROPIC_ENDPOINT) })(modelId),
    openai: (apiKey, modelId) => {
      const contido = contain(OPENAI_ENDPOINT);
      const fetchFinal =
        esforcoOpenAI && modeloOpenAIRaciocina(modelId)
          ? comEsforcoDeRaciocinio(contido, esforcoAceitoPeloModelo(modelId, esforcoOpenAI))
          : contido;
      return createOpenAI({ apiKey, fetch: fetchFinal })(modelId);
    },
    /**
     * A ASSINATURA (#1639) — mesma fábrica da OpenAI, outro destino e outro
     * segredo: o `apiKey` aqui é o `access_token` do login por PKCE, nunca uma
     * chave de API (quem o monta é `resolveOrgLlmConfig`, e só ele).
     *
     * Sem `baseUrl`: este provedor não aceita endpoint próprio (`aceitaEndpointProprio:
     * false` na lista), e honrar um endereço escolhido num provider que a tela
     * diz não poder ser apontado seria a tela e o runtime discordando.
     *
     * Sem injeção de `reasoning.effort` também: o knob é da fábrica `openai`,
     * e o campo existe onde a OpenAI o documenta — aqui ele só correria o risco
     * de o backend recusar um parâmetro que não pediu.
     *
     * A listagem da conta usa GET /v1/models com o mesmo token SIWC, conforme o
     * contrato oficial Sign in with ChatGPT; este runtime continua em /v1/responses.
     */
    [PROVEDOR_POR_ASSINATURA]: (apiKey, modelId) =>
      createOpenAI({
        apiKey,
        baseURL: OPENAI_CODEX_ENDPOINT,
        fetch: contain(OPENAI_CODEX_ENDPOINT),
      })(modelId),
    google: (apiKey, modelId) =>
      createGoogleGenerativeAI({ apiKey, fetch: contain(GOOGLE_ENDPOINT) })(modelId),
    /**
     * O `baseUrl` do painel é honrado aqui, e a allowlist do egress passa a ser
     * a DELE — não a da OpenRouter mais um furo. Apontar para um gateway
     * próprio é escolha legítima do operador; deixar a allowlist fixa no
     * endpoint canônico faria o egress bloquear a própria configuração que a
     * tela ofereceu, com erro de rede que ninguém liga ao painel.
     */
    openrouter: (apiKey, modelId, baseUrl, cache) => {
      const endpoint = baseUrl ?? OPENROUTER_ENDPOINT;
      // O embrulho fica POR FORA da contenção: o pedido modificado continua
      // passando pela allowlist. Gateway do operador nunca recebe campo novo.
      const contido = contain(endpoint);
      const provider = createOpenAI({
        apiKey,
        baseURL: endpoint,
        headers: cabecalhosDeAtribuicaoOpenRouter(),
        fetch:
          cache && cacheNaOpenRouter && ehOpenRouterOficial(baseUrl)
            ? comCacheDaAnthropicNoOpenRouter(contido, cache)
            : contido,
      });
      // Chat Completions, NÃO Responses: a OpenRouter fala a API da OpenAI
      // (chat/completions). O `createOpenAI()(modelId)` desta versão do SDK usa
      // o endpoint /responses por padrão, e a OpenRouter NÃO o implementa para
      // todo modelo: medido em 2026-09-19, `google/gemini-2.5-flash-lite`
      // devolvia "Invalid JSON response" (o SDK tentava
      // /responses e recebia a página do chat), enquanto gpt-4o/4.1 passavam
      // por sorte do roteamento. `.chat()` fixa o formato que a OpenRouter
      // realmente serve, para qualquer família de modelo.
      return provider.chat(modelId);
    },
    /**
     * DeepSeek é OpenAI-compatível e aceita um `base_url` próprio pela mesma
     * razão da OpenRouter: o painel de provedores oferece apontar para um
     * gateway, e a allowlist do egress precisa ser a DELE — fixá-la no endpoint
     * canônico bloquearia a configuração que a própria tela permitiu.
     */
    deepseek: (apiKey, modelId, baseUrl) => {
      const endpoint = baseUrl ?? DEEPSEEK_ENDPOINT;
      const contido = contain(endpoint);
      const fetchFinal =
        opts?.deepseekThinking === 'disabled' ? comRaciocinioDesligado(contido) : contido;
      return createOpenAI({ apiKey, baseURL: endpoint, fetch: fetchFinal })(modelId);
    },
    /**
     * Requesty: roteador OpenAI-compatível, com `base_url` próprio pela mesma
     * razão da OpenRouter (a allowlist do egress segue o endpoint escolhido).
     * `.chat()` pela mesma razão também: Chat Completions é o formato que o
     * roteador serve para qualquer família de modelo.
     */
    requesty: (apiKey, modelId, baseUrl) => {
      const endpoint = baseUrl ?? REQUESTY_ENDPOINT;
      return createOpenAI({ apiKey, baseURL: endpoint, fetch: contain(endpoint) }).chat(modelId);
    },
    /**
     * Provedor personalizado (#1642): o endpoint É DO OPERADOR e vem na
     * credencial (`ai_provider_credentials.base_url`), através de
     * `decisao.baseUrl ?? config.baseUrl`. Não existe endpoint canônico aqui de
     * propósito: sem endereço a chamada é RECUSADA, porque cair no endpoint da
     * OpenAI seria mandar a chave de um gateway privado para a OpenAI — e
     * silenciosamente, que é a forma pior de errar. Mesma fábrica e mesmo
     * `.chat()` da OpenRouter: quem fala a API da OpenAI fala Chat Completions.
     * A allowlist do egress é a do endpoint escolhido, como nos roteadores, e
     * cada requisição passa ANTES pela régua de destino de organização: o
     * endereço foi escolhido por uma empresa, então não aponta para a rede
     * interna do servidor (decisão 22-d).
     */
    custom: (apiKey, modelId, baseUrl) => {
      if (!baseUrl) {
        throw new Error(
          "custom_provider_sem_base_url: cadastre o endereço (base URL) na credencial do provedor personalizado",
        );
      }
      return createOpenAI({
        apiKey,
        baseURL: baseUrl,
        fetch: fetchParaDestinoDaOrganizacao(contain(baseUrl)),
      }).chat(modelId);
    },
  };
}

/**
 * Registry FAKE para testes: provider 'anthropic' (e alias 'fake') respondendo
 * com o MockLanguageModelV3 do SDK v6 instalado — zero rede, zero chave real.
 * O doGenerate default devolve `text` com usage fixo; injete o seu para cenários
 * de tool-call/erro.
 */
type MockDoGenerate = NonNullable<ConstructorParameters<typeof MockLanguageModelV3>[0]>['doGenerate'];

export function createFakeRegistry(
  doGenerate?: MockDoGenerate,
  opts?: { text?: string },
): ProviderRegistry {
  const factory = (_apiKey: string, modelId: string): LanguageModel =>
    new MockLanguageModelV3({
      modelId,
      doGenerate:
        doGenerate ??
        {
          content: [{ type: 'text', text: opts?.text ?? 'ok' }],
          finishReason: { unified: 'stop' as const, raw: undefined },
          usage: {
            inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 },
            outputTokens: { total: 1, text: 1, reasoning: 0 },
          },
          warnings: [],
        },
    });
  return { anthropic: factory, fake: factory };
}
