// Google tests cover transport stream plugin behavior.
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { gzipSync } from "node:zlib";
import type { Model } from "openclaw/plugin-sdk/llm";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

const {
  buildGuardedModelFetchMock,
  guardedFetchMock,
  googleAuthGetAccessTokenMock,
  googleAuthMock,
} = vi.hoisted(() => {
  const googleAuthGetAccessTokenMockLocal = vi.fn();
  return {
    buildGuardedModelFetchMock: vi.fn(),
    guardedFetchMock: vi.fn(),
    googleAuthGetAccessTokenMock: googleAuthGetAccessTokenMockLocal,
    googleAuthMock: vi.fn(function GoogleAuthMock() {
      return {
        getAccessToken: googleAuthGetAccessTokenMockLocal,
      };
    }),
  };
});

vi.mock("openclaw/plugin-sdk/provider-transport-runtime", async (importOriginal) => ({
  ...(await importOriginal()),
  buildGuardedModelFetch: buildGuardedModelFetchMock,
}));

vi.mock("google-auth-library", () => ({
  GoogleAuth: googleAuthMock,
}));

let buildGoogleGenerativeAiParams: typeof import("./transport-stream.js").buildGoogleGenerativeAiParams;
let buildGoogleGemini3FirstResponseRetryParams: typeof import("./transport-stream.js").buildGoogleGemini3FirstResponseRetryParams;
let createGoogleGenerativeAiTransportStreamFn: typeof import("./transport-stream.js").createGoogleGenerativeAiTransportStreamFn;
let createGoogleVertexTransportStreamFn: typeof import("./transport-stream.js").createGoogleVertexTransportStreamFn;
let resolveGoogleGemini3FirstResponseRetryMs: typeof import("./transport-stream.js").resolveGoogleGemini3FirstResponseRetryMs;
let hasGoogleVertexAuthorizedUserAdcSync: typeof import("./vertex-adc.js").hasGoogleVertexAuthorizedUserAdcSync;
let resolveGoogleVertexAuthorizedUserHeaders: typeof import("./vertex-adc.js").resolveGoogleVertexAuthorizedUserHeaders;
let resetGoogleVertexAuthorizedUserTokenCacheForTest: typeof import("./vertex-adc.js").resetGoogleVertexAuthorizedUserTokenCacheForTest;

const MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL = Symbol.for(
  "openclaw.modelProviderRequestTransport",
);

function attachModelProviderRequestTransport<TModel extends object>(
  model: TModel,
  request: unknown,
): TModel {
  return {
    ...model,
    [MODEL_PROVIDER_REQUEST_TRANSPORT_SYMBOL]: request,
  };
}

function buildGeminiModel(
  overrides: Partial<Model<"google-generative-ai">> = {},
): Model<"google-generative-ai"> {
  return {
    id: "gemini-2.5-pro",
    name: "Gemini 2.5 Pro",
    api: "google-generative-ai",
    provider: "google",
    baseUrl: "https://generativelanguage.googleapis.com/v1beta",
    reasoning: true,
    input: ["text"],
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    contextWindow: 128000,
    maxTokens: 8192,
    ...overrides,
  };
}

function buildGoogleVertexModel(
  overrides: Partial<Model<"google-vertex">> = {},
): Model<"google-vertex"> {
  return {
    id: "gemini-3.1-pro-preview",
    name: "Gemini 3.1 Pro Preview",
    api: "google-vertex",
    provider: "google-vertex",
    baseUrl: "https://{location}-aiplatform.googleapis.com",
    reasoning: true,
    input: ["text"],
    cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
    contextWindow: 128000,
    maxTokens: 8192,
    ...overrides,
  };
}

function buildSseResponse(events: unknown[]): Response {
  const sse = `${events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join("")}data: [DONE]\n\n`;
  return buildRawSseResponse(sse);
}

function buildRawSseResponse(sse: string): Response {
  const encoder = new TextEncoder();
  const body = new ReadableStream<Uint8Array>({
    start(controller) {
      controller.enqueue(encoder.encode(sse));
      controller.close();
    },
  });
  return new Response(body, {
    status: 200,
    headers: { "content-type": "text/event-stream" },
  });
}

function buildOpenRawSseResponse(params: { sse: string; onCancel: () => void }): Response {
  const encoder = new TextEncoder();
  const body = new ReadableStream<Uint8Array>({
    start(controller) {
      controller.enqueue(encoder.encode(params.sse));
    },
    cancel() {
      params.onCancel();
    },
  });
  return new Response(body, {
    status: 200,
    headers: { "content-type": "text/event-stream" },
  });
}

function buildDelayedSecondSseResponse(params: {
  first: unknown;
  second: unknown;
  delayMs: number;
}): Response {
  const encoder = new TextEncoder();
  const first = `data: ${JSON.stringify(params.first)}\n\n`;
  const second = `data: ${JSON.stringify(params.second)}\n\ndata: [DONE]\n\n`;
  let timeout: ReturnType<typeof setTimeout> | undefined;
  const body = new ReadableStream<Uint8Array>({
    start(controller) {
      controller.enqueue(encoder.encode(first));
      timeout = setTimeout(() => {
        controller.enqueue(encoder.encode(second));
        controller.close();
      }, params.delayMs);
    },
    cancel() {
      if (timeout) {
        clearTimeout(timeout);
      }
    },
  });
  return new Response(body, {
    status: 200,
    headers: { "content-type": "text/event-stream" },
  });
}

function requireMockCall<TArgs extends unknown[]>(
  mock: { mock: { calls: TArgs[] } },
  index: number,
  label: string,
): TArgs {
  const call = mock.mock.calls[index];
  if (!call) {
    throw new Error(`Expected ${label} mock call ${index}`);
  }
  return call;
}

function requireRequestInit(call: unknown[], label: string): RequestInit {
  const init = call[1];
  if (!init || typeof init !== "object") {
    throw new Error(`Expected ${label} request init`);
  }
  return init as RequestInit;
}

function expectHeaders(init: RequestInit, expected: Record<string, string>): void {
  const headers = new Headers(init.headers);
  for (const [key, value] of Object.entries(expected)) {
    expect(headers.get(key)).toBe(value);
  }
}

function parseRequestJsonBody(init: RequestInit): Record<string, unknown> {
  const requestBody = init.body;
  if (typeof requestBody !== "string") {
    throw new Error("Expected request body to be serialized JSON");
  }
  return JSON.parse(requestBody) as Record<string, unknown>;
}

function requireGenerationConfig(params: { generationConfig?: unknown }): Record<string, unknown> {
  const config = params.generationConfig;
  if (!config || typeof config !== "object") {
    throw new Error("Expected generationConfig");
  }
  return config as Record<string, unknown>;
}

function requireThinkingConfig(config: Record<string, unknown>): Record<string, unknown> {
  const thinkingConfig = config.thinkingConfig;
  if (!thinkingConfig || typeof thinkingConfig !== "object") {
    throw new Error("Expected thinkingConfig");
  }
  return thinkingConfig as Record<string, unknown>;
}

type GoogleTestContentTurn = Record<string, unknown> & {
  parts: Array<Record<string, unknown>>;
};

function isModelTurnWithParts(content: Record<string, unknown>): content is GoogleTestContentTurn {
  return content.role === "model" && Array.isArray(content.parts);
}

function getFirstModelTurn(contents: Array<Record<string, unknown>>): GoogleTestContentTurn {
  const turn = contents.find(isModelTurnWithParts);
  if (!turn) {
    throw new Error("Expected at least one Google model turn");
  }
  return turn;
}

function getLastModelTurn(contents: Array<Record<string, unknown>>): GoogleTestContentTurn {
  const turn = contents.toReversed().find(isModelTurnWithParts);
  if (!turn) {
    throw new Error("Expected at least one Google model turn");
  }
  return turn;
}

function googleToolCallAssistantTurn({
  timestamp = 0,
  provider = "google",
  api = "google-generative-ai",
  model = "gemini-3.1-pro-preview",
  id = "call_1",
  name = "lookup",
  args = { q: "hello" },
  thoughtSignature,
}: {
  timestamp?: number;
  provider?: string;
  api?: string;
  model?: string;
  id?: string;
  name?: string;
  args?: Record<string, unknown>;
  thoughtSignature?: string;
} = {}): Record<string, unknown> {
  return {
    role: "assistant",
    provider,
    api,
    model,
    stopReason: "toolUse",
    timestamp,
    content: [
      {
        type: "toolCall",
        id,
        name,
        arguments: args,
        ...(thoughtSignature ? { thoughtSignature } : {}),
      },
    ],
  };
}

function toolResultTurn(toolCallId = "call_1", timestamp = 1): Record<string, unknown> {
  return {
    role: "toolResult",
    timestamp,
    content: [
      {
        type: "toolResult",
        toolCallId,
        content: [{ type: "text", text: "ok" }],
      },
    ],
  };
}

describe("google transport stream", () => {
  beforeAll(async () => {
    ({
      buildGoogleGenerativeAiParams,
      buildGoogleGemini3FirstResponseRetryParams,
      createGoogleGenerativeAiTransportStreamFn,
      createGoogleVertexTransportStreamFn,
      resolveGoogleGemini3FirstResponseRetryMs,
    } = await import("./transport-stream.js"));
    ({
      hasGoogleVertexAuthorizedUserAdcSync,
      resolveGoogleVertexAuthorizedUserHeaders,
      resetGoogleVertexAuthorizedUserTokenCacheForTest,
    } = await import("./vertex-adc.js"));
  });

  beforeEach(() => {
    buildGuardedModelFetchMock.mockReset();
    guardedFetchMock.mockReset();
    googleAuthGetAccessTokenMock.mockReset();
    googleAuthMock.mockClear();
    buildGuardedModelFetchMock.mockReturnValue(guardedFetchMock);
    resetGoogleVertexAuthorizedUserTokenCacheForTest();
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.unstubAllEnvs();
  });

  afterAll(() => {
    vi.doUnmock("openclaw/plugin-sdk/provider-transport-runtime");
    vi.doUnmock("google-auth-library");
    vi.resetModules();
  });

  it("uses the guarded fetch transport and parses Gemini SSE output", async () => {
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          responseId: "resp_1",
          candidates: [
            {
              content: {
                parts: [
                  { thought: true, text: "draft", thoughtSignature: "c2lnXzE=" },
                  { text: "answer" },
                  {
                    thoughtSignature: "Y2FsbF9zaWdfMQ==",
                    functionCall: { name: "lookup", args: { q: "hello" } },
                  },
                ],
              },
              finishReason: "STOP",
            },
          ],
          usageMetadata: {
            promptTokenCount: 10,
            cachedContentTokenCount: 2,
            candidatesTokenCount: 5,
            thoughtsTokenCount: 3,
            totalTokenCount: 18,
          },
        },
      ]),
    );

    const model = attachModelProviderRequestTransport(
      {
        id: "gemini-3.1-pro-preview",
        name: "Gemini 3.1 Pro Preview",
        api: "google-generative-ai",
        provider: "google",
        baseUrl: "https://generativelanguage.googleapis.com",
        reasoning: true,
        input: ["text"],
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 128000,
        maxTokens: 8192,
        headers: { "X-Provider": "google" },
      } satisfies Model<"google-generative-ai">,
      {
        proxy: {
          mode: "explicit-proxy",
          url: "http://proxy.internal:8443",
        },
      },
    );

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          systemPrompt: "Follow policy.",
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
          tools: [
            {
              name: "lookup",
              description: "Look up a value",
              parameters: {
                type: "object",
                properties: { q: { type: "string" } },
                required: ["q"],
              },
            },
          ],
        } as unknown as Parameters<typeof streamFn>[1],
        {
          apiKey: "gemini-api-key",
          cachedContent: "cachedContents/request-cache",
          reasoning: "medium",
          toolChoice: "auto",
        } as Parameters<typeof streamFn>[2],
      ),
    );
    const result = await stream.result();

    expect(buildGuardedModelFetchMock).toHaveBeenCalledWith(model);
    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    expect(guardedCall[0]).toBe(
      "https://generativelanguage.googleapis.com/v1beta/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse",
    );
    const init = requireRequestInit(guardedCall, "guarded fetch");
    expect(init.method).toBe("POST");
    expectHeaders(init, {
      accept: "text/event-stream",
      "Content-Type": "application/json",
      "x-goog-api-key": "gemini-api-key",
      "X-Provider": "google",
    });

    const payload = parseRequestJsonBody(init);
    expect(payload.cachedContent).toBe("cachedContents/request-cache");
    expect(payload.systemInstruction).toBeUndefined();
    expect(payload.tools).toBeUndefined();
    expect(payload.toolConfig).toBeUndefined();
    expect((payload.generationConfig as { thinkingConfig?: unknown }).thinkingConfig).toEqual({
      includeThoughts: true,
      thinkingLevel: "HIGH",
    });
    expect(result.api).toBe("google-generative-ai");
    expect(result.provider).toBe("google");
    expect(result.responseId).toBe("resp_1");
    expect(result.stopReason).toBe("toolUse");
    expect(result.usage.input).toBe(8);
    expect(result.usage.output).toBe(8);
    expect(result.usage.cacheRead).toBe(2);
    expect(result.usage.totalTokens).toBe(18);
    expect(result.content).toHaveLength(3);
    expect(result.content[0]).toEqual({
      type: "thinking",
      thinking: "draft",
      thinkingSignature: "c2lnXzE=",
    });
    expect(result.content[1]?.type).toBe("text");
    expect(result.content[1]).toHaveProperty("text", "answer");
    expect(result.content[2]?.type).toBe("toolCall");
    expect(result.content[2]).toHaveProperty("name", "lookup");
    expect(result.content[2]).toHaveProperty("arguments", { q: "hello" });
    expect(result.content[2]).toHaveProperty("thoughtSignature", "Y2FsbF9zaWdfMQ==");
  });

  it("strips redundant google provider prefixes from Gemini API model paths", async () => {
    guardedFetchMock.mockResolvedValueOnce(buildSseResponse([]));

    const model = buildGeminiModel({
      id: "google/gemini-3-flash-preview",
      name: "Gemini 3 Flash Preview",
    });
    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        { apiKey: "gemini-api-key" } as Parameters<typeof streamFn>[2],
      ),
    );
    await stream.result();

    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    expect(guardedCall[0]).toBe(
      "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:streamGenerateContent?alt=sse",
    );
  });

  it("merges tool-call thought signatures from sibling SSE parts", async () => {
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [
            {
              content: {
                parts: [
                  {
                    functionCall: { id: "call_1", name: "lookup", args: { q: "hello" } },
                  },
                  { thoughtSignature: "Y2FsbF9zaWdfbWVyZ2VkXzE=" },
                ],
              },
              finishReason: "STOP",
            },
          ],
        },
      ]),
    );

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGeminiModel({
          id: "gemini-3.1-pro-preview",
          name: "Gemini 3.1 Pro Preview",
        }),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as never,
      ),
    );
    const result = await stream.result();

    expect(result.content).toEqual([
      {
        type: "toolCall",
        id: "call_1",
        name: "lookup",
        arguments: { q: "hello" },
        thoughtSignature: "Y2FsbF9zaWdfbWVyZ2VkXzE=",
      },
    ]);
  });

  it("keeps explicit thinking signatures after tool-call SSE parts", async () => {
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [
            {
              content: {
                parts: [
                  {
                    functionCall: { id: "call_1", name: "lookup", args: { q: "hello" } },
                  },
                  { thought: true, thoughtSignature: "dGhvdWdodF9zaWdfYWZ0ZXJfY2FsbA==" },
                  { thought: true, text: "draft" },
                  { text: "answer" },
                ],
              },
              finishReason: "STOP",
            },
          ],
        },
      ]),
    );

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGeminiModel({
          id: "gemini-3.1-pro-preview",
          name: "Gemini 3.1 Pro Preview",
        }),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as never,
      ),
    );
    const result = await stream.result();

    expect(result.content[0]).toMatchObject({
      type: "toolCall",
      id: "call_1",
      name: "lookup",
      arguments: { q: "hello" },
    });
    expect(result.content[1]).toEqual({
      type: "thinking",
      thinking: "draft",
      thinkingSignature: "dGhvdWdodF9zaWdfYWZ0ZXJfY2FsbA==",
    });
    expect(result.content[2]).toEqual({ type: "text", text: "answer" });
  });

  it("builds a lean Gemini 3 first-response retry payload", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });
    const retryPayload = buildGoogleGemini3FirstResponseRetryParams({
      model,
      request: {
        contents: [{ role: "user", parts: [{ text: "hello" }] }],
        generationConfig: {
          thinkingConfig: {
            includeThoughts: true,
            thinkingLevel: "HIGH",
          },
        },
      },
    });

    expect(retryPayload?.generationConfig).toEqual({
      thinkingConfig: {
        thinkingLevel: "LOW",
      },
    });
  });

  it("rejects non-integer Gemini 3 first-response retry env values", () => {
    const envName = "OPENCLAW_GOOGLE_GEMINI_FIRST_RESPONSE_RETRY_MS";

    expect(resolveGoogleGemini3FirstResponseRetryMs({ [envName]: "1200" })).toBe(1200);
    expect(resolveGoogleGemini3FirstResponseRetryMs({ [envName]: "0" })).toBe(0);
    expect(resolveGoogleGemini3FirstResponseRetryMs({ [envName]: "0x10" })).toBe(45_000);
    expect(resolveGoogleGemini3FirstResponseRetryMs({ [envName]: "100.5" })).toBe(45_000);
    expect(resolveGoogleGemini3FirstResponseRetryMs({ [envName]: "1e3" })).toBe(45_000);
  });

  it("wraps malformed Gemini SSE JSON", async () => {
    guardedFetchMock.mockResolvedValueOnce(buildRawSseResponse("data: {not json\n\n"));

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGeminiModel(),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as unknown as Parameters<typeof streamFn>[1],
        {
          apiKey: "gemini-api-key",
        } as Parameters<typeof streamFn>[2],
      ),
    );

    const result = await stream.result();

    expect(result.stopReason).toBe("error");
    expect(result.errorMessage).toBe("Google SSE stream returned malformed JSON");
  });

  it("cancels open Gemini SSE bodies when parsing fails", async () => {
    let cancelCalled = false;
    guardedFetchMock.mockResolvedValueOnce(
      buildOpenRawSseResponse({
        sse: "data: {not json\n\n",
        onCancel: () => {
          cancelCalled = true;
        },
      }),
    );

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGeminiModel(),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as unknown as Parameters<typeof streamFn>[1],
        {
          apiKey: "gemini-api-key",
        } as Parameters<typeof streamFn>[2],
      ),
    );

    const result = await stream.result();

    expect(result.stopReason).toBe("error");
    expect(result.errorMessage).toBe("Google SSE stream returned malformed JSON");
    expect(cancelCalled).toBe(true);
  });

  it("retries Gemini 3 requests with lean thinking when the first attempt has no first response", async () => {
    vi.stubEnv("OPENCLAW_GOOGLE_GEMINI_FIRST_RESPONSE_RETRY_MS", "10");
    guardedFetchMock
      .mockImplementationOnce(
        (_url: string, init?: RequestInit) =>
          new Promise<Response>((_resolve, reject) => {
            init?.signal?.addEventListener("abort", () => {
              reject(
                toLintErrorObject(
                  init.signal?.reason ?? new Error("aborted"),
                  "Non-Error rejection",
                ),
              );
            });
          }),
      )
      .mockResolvedValueOnce(
        buildSseResponse([
          {
            candidates: [{ content: { parts: [{ text: "recovered" }] }, finishReason: "STOP" }],
          },
        ]),
      );

    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });
    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
          tools: [
            {
              name: "lookup",
              description: "Look up a value",
              parameters: {
                type: "object",
                properties: { q: { type: "string" } },
              },
            },
          ],
        } as never,
        { reasoning: "high" } as never,
      ),
    );
    const result = await stream.result();

    expect(result.content).toEqual([{ type: "text", text: "recovered" }]);
    expect(guardedFetchMock).toHaveBeenCalledTimes(2);
    const firstBody = parseRequestJsonBody(
      requireRequestInit(requireMockCall(guardedFetchMock, 0, "guarded fetch"), "guarded fetch"),
    );
    const retryBody = parseRequestJsonBody(
      requireRequestInit(requireMockCall(guardedFetchMock, 1, "guarded fetch"), "guarded fetch"),
    );
    const firstGenerationConfig = requireGenerationConfig(firstBody);
    const retryGenerationConfig = requireGenerationConfig(retryBody);
    expect(firstGenerationConfig.thinkingConfig).toEqual({
      includeThoughts: true,
      thinkingLevel: "HIGH",
    });
    expect(retryGenerationConfig.thinkingConfig).toEqual({
      thinkingLevel: "LOW",
    });
    expect(retryBody.tools).toEqual(firstBody.tools);
  });

  it("keeps streaming after the first Gemini 3 chunk arrives before the retry deadline", async () => {
    vi.stubEnv("OPENCLAW_GOOGLE_GEMINI_FIRST_RESPONSE_RETRY_MS", "10");
    guardedFetchMock.mockResolvedValueOnce(
      buildDelayedSecondSseResponse({
        first: {
          candidates: [{ content: { parts: [{ text: "first " }] } }],
        },
        second: {
          candidates: [{ content: { parts: [{ text: "second" }] }, finishReason: "STOP" }],
        },
        delayMs: 25,
      }),
    );

    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });
    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as never,
        { reasoning: "high" } as never,
      ),
    );
    const result = await stream.result();

    expect(result.content).toEqual([{ type: "text", text: "first second" }]);
    expect(guardedFetchMock).toHaveBeenCalledTimes(1);
  });

  it("uses bearer auth when the Google api key is an OAuth JSON payload", async () => {
    guardedFetchMock.mockResolvedValueOnce(buildSseResponse([]));

    const model = attachModelProviderRequestTransport(
      {
        id: "gemini-3-flash-preview",
        name: "Gemini 3 Flash Preview",
        api: "google-generative-ai",
        provider: "custom-google",
        baseUrl: "https://generativelanguage.googleapis.com/v1beta",
        reasoning: false,
        input: ["text"],
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 128000,
        maxTokens: 8192,
      } satisfies Model<"google-generative-ai">,
      {
        tls: {
          ca: "ca-pem",
        },
      },
    );

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        {
          apiKey: JSON.stringify({ token: "oauth-token", projectId: "demo" }),
        } as Parameters<typeof streamFn>[2],
      ),
    );
    await stream.result();

    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    expect(typeof guardedCall[0]).toBe("string");
    const init = requireRequestInit(guardedCall, "guarded fetch");
    expectHeaders(init, {
      Authorization: "Bearer oauth-token",
      "Content-Type": "application/json",
    });
  });

  it("detects supported Vertex ADC sources synchronously", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-detect-"));
    for (const type of ["authorized_user", "external_account", "service_account"]) {
      const credentialsPath = path.join(tempDir, `${type}.json`);
      await writeFile(credentialsPath, JSON.stringify({ type }), "utf8");

      expect(
        hasGoogleVertexAuthorizedUserAdcSync({
          GOOGLE_APPLICATION_CREDENTIALS: credentialsPath,
        }),
      ).toBe(true);
    }

    expect(
      hasGoogleVertexAuthorizedUserAdcSync({
        HOME: path.join(tempDir, "empty-home"),
        KUBERNETES_SERVICE_HOST: "10.0.0.1",
      }),
    ).toBe(false);
  });

  it("resolves non-file Vertex ADC through google-auth-library without OAuth refresh fetch", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-authlib-"));
    vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");
    vi.stubEnv("HOME", path.join(tempDir, "home"));
    vi.stubEnv("APPDATA", "");
    googleAuthGetAccessTokenMock.mockResolvedValueOnce("ya29.google-auth-token");
    const tokenFetchMock = vi.fn();

    await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
      Authorization: "Bearer ya29.google-auth-token",
    });

    expect(googleAuthMock).toHaveBeenCalledWith({
      scopes: ["https://www.googleapis.com/auth/cloud-platform"],
    });
    expect(googleAuthGetAccessTokenMock).toHaveBeenCalledTimes(1);
    expect(tokenFetchMock).not.toHaveBeenCalled();
  });

  it("does not cache google-auth ADC tokens when fallback expiry would exceed Date range", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-authlib-expiry-"));
    vi.useFakeTimers();
    vi.setSystemTime(new Date(8_640_000_000_000_000));
    vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");
    vi.stubEnv("HOME", path.join(tempDir, "home"));
    vi.stubEnv("APPDATA", "");
    googleAuthGetAccessTokenMock
      .mockResolvedValueOnce("ya29.first-token")
      .mockResolvedValueOnce("ya29.second-token");
    const tokenFetchMock = vi.fn();

    await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
      Authorization: "Bearer ya29.first-token",
    });
    await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
      Authorization: "Bearer ya29.second-token",
    });

    expect(googleAuthGetAccessTokenMock).toHaveBeenCalledTimes(2);
    expect(tokenFetchMock).not.toHaveBeenCalled();
  });

  it("uses google-auth-library bearer auth for Google Vertex credential marker requests", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-authlib-stream-"));
    vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");
    vi.stubEnv("HOME", path.join(tempDir, "home"));
    vi.stubEnv("APPDATA", "");
    vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
    vi.stubEnv("GOOGLE_CLOUD_LOCATION", "us-central1");
    googleAuthGetAccessTokenMock.mockResolvedValueOnce("ya29.transport-token");
    const tokenFetchMock = vi.fn();
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
        },
      ]),
    );

    const streamFn = createGoogleVertexTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGoogleVertexModel(),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        {
          apiKey: "gcp-vertex-credentials",
          fetch: tokenFetchMock,
        } as Parameters<typeof streamFn>[2],
      ),
    );
    await stream.result();

    expect(tokenFetchMock).not.toHaveBeenCalled();
    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    const guardedInit = requireRequestInit(guardedCall, "guarded fetch");
    expectHeaders(guardedInit, {
      Authorization: "Bearer ya29.transport-token",
      "Content-Type": "application/json",
      accept: "text/event-stream",
    });
    expect(new Headers(guardedInit.headers).has("x-goog-api-key")).toBe(false);
  });

  it("strips redundant google provider prefixes from Google Vertex model paths", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-prefix-"));
    vi.stubEnv("HOME", path.join(tempDir, "home"));
    vi.stubEnv("APPDATA", "");
    vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
    vi.stubEnv("GOOGLE_CLOUD_LOCATION", "us-central1");
    googleAuthGetAccessTokenMock.mockResolvedValueOnce("ya29.transport-token");
    const tokenFetchMock = vi.fn();
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
        },
      ]),
    );

    const streamFn = createGoogleVertexTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGoogleVertexModel({ id: "google/gemini-3.1-pro-preview" }),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        {
          apiKey: "gcp-vertex-credentials",
          fetch: tokenFetchMock,
        } as Parameters<typeof streamFn>[2],
      ),
    );
    await stream.result();

    // The provider prefix must be stripped from the Vertex model path, matching
    // resolveGoogleModelPath; otherwise the id becomes models/google%2F... (404).
    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    expect(guardedCall[0]).toContain(
      "/publishers/google/models/gemini-3.1-pro-preview:streamGenerateContent",
    );
    expect(guardedCall[0]).not.toContain("google%2F");
  });

  it("refreshes authorized_user ADC before Google Vertex requests", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-"));
    const credentialsPath = path.join(tempDir, "application_default_credentials.json");
    await writeFile(
      credentialsPath,
      JSON.stringify({
        type: "authorized_user",
        client_id: "client-id",
        client_secret: "client-secret",
        refresh_token: "refresh-token",
      }),
      "utf8",
    );
    vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", credentialsPath);
    vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
    vi.stubEnv("GOOGLE_CLOUD_LOCATION", "global");
    const tokenFetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ access_token: "ya29.vertex-token", expires_in: 3600 }), {
        status: 200,
        headers: { "content-type": "application/json" },
      }),
    );
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
        },
      ]),
    );

    expect(hasGoogleVertexAuthorizedUserAdcSync()).toBe(true);

    const model = buildGoogleVertexModel();

    const streamFn = createGoogleVertexTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        {
          apiKey: "gcp-vertex-credentials",
          fetch: tokenFetchMock,
        } as Parameters<typeof streamFn>[2],
      ),
    );
    const result = await stream.result();

    const tokenCall = requireMockCall(tokenFetchMock, 0, "token fetch");
    expect(tokenCall[0]).toBe("https://oauth2.googleapis.com/token");
    expect(requireRequestInit(tokenCall, "token fetch").method).toBe("POST");

    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    expect(guardedCall[0]).toBe(
      "https://aiplatform.googleapis.com/v1/projects/vertex-project/locations/global/publishers/google/models/gemini-3.1-pro-preview:streamGenerateContent?alt=sse",
    );
    const guardedInit = requireRequestInit(guardedCall, "guarded fetch");
    expect(guardedInit.method).toBe("POST");
    expectHeaders(guardedInit, {
      Authorization: "Bearer ya29.vertex-token",
      "Content-Type": "application/json",
      accept: "text/event-stream",
    });
    expect(result.api).toBe("google-vertex");
    expect(result.provider).toBe("google-vertex");
    expect(result.stopReason).toBe("stop");
    expect(result.content).toEqual([{ type: "text", text: "ok" }]);
  });

  it("refreshes authorized_user ADC from a compressed token response", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-adc-gzip-"));
    const credentialsPath = path.join(tempDir, "application_default_credentials.json");
    await writeFile(
      credentialsPath,
      JSON.stringify({
        type: "authorized_user",
        client_id: "client-id",
        client_secret: "client-secret",
        refresh_token: "gzip-refresh-token",
      }),
      "utf8",
    );
    vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", credentialsPath);
    vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
    vi.stubEnv("GOOGLE_CLOUD_LOCATION", "global");
    const tokenFetchMock = vi.fn().mockResolvedValue(
      new Response(
        gzipSync(JSON.stringify({ access_token: "ya29.gzip-token", expires_in: 3600 })),
        {
          status: 200,
          headers: {
            "content-encoding": "gzip",
            "content-type": "application/json",
          },
        },
      ),
    );
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
        },
      ]),
    );

    const streamFn = createGoogleVertexTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGoogleVertexModel(),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        {
          apiKey: "gcp-vertex-credentials",
          fetch: tokenFetchMock,
        } as Parameters<typeof streamFn>[2],
      ),
    );
    await stream.result();

    expect(tokenFetchMock).toHaveBeenCalledTimes(1);
    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    expectHeaders(requireRequestInit(guardedCall, "guarded fetch"), {
      Authorization: "Bearer ya29.gzip-token",
    });
  });

  it("does not reuse authorized_user ADC tokens with unsafe expiry lifetimes", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-unsafe-adc-"));
    const credentialsPath = path.join(tempDir, "application_default_credentials.json");
    await writeFile(
      credentialsPath,
      JSON.stringify({
        type: "authorized_user",
        client_id: "client-id",
        client_secret: "client-secret",
        refresh_token: "refresh-token",
      }),
      "utf8",
    );
    vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", credentialsPath);
    const tokenFetchMock = vi
      .fn()
      .mockResolvedValueOnce(
        new Response(
          JSON.stringify({
            access_token: "ya29.unsafe-token",
            expires_in: Number.MAX_SAFE_INTEGER,
          }),
          { status: 200, headers: { "content-type": "application/json" } },
        ),
      )
      .mockResolvedValueOnce(
        new Response(JSON.stringify({ access_token: "ya29.fresh-token", expires_in: 3600 }), {
          status: 200,
          headers: { "content-type": "application/json" },
        }),
      );

    await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
      Authorization: "Bearer ya29.unsafe-token",
    });
    await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
      Authorization: "Bearer ya29.fresh-token",
    });

    expect(tokenFetchMock).toHaveBeenCalledTimes(2);
  });

  it("refreshes authorized_user ADC from the Windows APPDATA fallback for Google Vertex requests", async () => {
    const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-appdata-adc-"));
    const homeDir = path.join(tempDir, "home");
    const appDataDir = path.join(tempDir, "AppData", "Roaming");
    const fallbackDir = path.join(appDataDir, "gcloud");
    const credentialsPath = path.join(fallbackDir, "application_default_credentials.json");
    await mkdir(fallbackDir, { recursive: true });
    await writeFile(
      credentialsPath,
      JSON.stringify({
        type: "authorized_user",
        client_id: "client-id",
        client_secret: "client-secret",
        refresh_token: "appdata-refresh-token",
      }),
      "utf8",
    );
    vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");
    vi.stubEnv("HOME", homeDir);
    vi.stubEnv("APPDATA", appDataDir);
    vi.stubEnv("GOOGLE_CLOUD_PROJECT", "vertex-project");
    vi.stubEnv("GOOGLE_CLOUD_LOCATION", "global");
    const tokenFetchMock = vi.fn().mockResolvedValue(
      new Response(JSON.stringify({ access_token: "ya29.appdata-token", expires_in: 3600 }), {
        status: 200,
        headers: { "content-type": "application/json" },
      }),
    );
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [{ content: { parts: [{ text: "ok" }] }, finishReason: "STOP" }],
        },
      ]),
    );

    expect(hasGoogleVertexAuthorizedUserAdcSync()).toBe(true);

    const streamFn = createGoogleVertexTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        buildGoogleVertexModel(),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        {
          apiKey: "gcp-vertex-credentials",
          fetch: tokenFetchMock,
        } as Parameters<typeof streamFn>[2],
      ),
    );
    await stream.result();

    const tokenCall = requireMockCall(tokenFetchMock, 0, "token fetch");
    expect(tokenCall[0]).toBe("https://oauth2.googleapis.com/token");
    const tokenInit = requireRequestInit(tokenCall, "token fetch");
    expect(tokenInit.method).toBe("POST");
    expect(tokenInit.body).toBeInstanceOf(URLSearchParams);
    const requestBody = tokenInit.body as URLSearchParams;
    expect(requestBody?.get("refresh_token")).toBe("appdata-refresh-token");
    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    expect(typeof guardedCall[0]).toBe("string");
    expectHeaders(requireRequestInit(guardedCall, "guarded fetch"), {
      Authorization: "Bearer ya29.appdata-token",
    });
  });

  it("coerces replayed malformed tool-call args to an object for Google payloads", () => {
    const params = buildGoogleGenerativeAiParams(buildGeminiModel(), {
      messages: [
        {
          role: "assistant",
          provider: "openai",
          api: "openai-responses",
          model: "gpt-5.4",
          stopReason: "toolUse",
          timestamp: 0,
          content: [
            {
              type: "toolCall",
              id: "call_1",
              name: "lookup",
              arguments: "{not valid json",
            },
          ],
        },
      ],
    } as never);

    expect(params.contents[0]).toEqual({
      role: "model",
      parts: [{ functionCall: { name: "lookup", args: {} } }],
    });
  });

  it("replays Gemini tool call thought signatures for same-model history", () => {
    const model = buildGeminiModel({
      id: "gemini-3-flash-preview",
      name: "Gemini 3 Flash Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        {
          role: "assistant",
          provider: "google",
          api: "google-generative-ai",
          model: "gemini-3-flash-preview",
          stopReason: "toolUse",
          timestamp: 0,
          content: [
            {
              type: "toolCall",
              id: "call_1",
              name: "lookup",
              arguments: { q: "hello" },
              thoughtSignature: "Y2FsbF9zaWdfMQ==",
            },
          ],
        },
      ],
    } as never);

    expect(params.contents[0]).toEqual({
      role: "model",
      parts: [
        {
          thoughtSignature: "Y2FsbF9zaWdfMQ==",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
  });

  it("re-attaches replayed Gemini thought signatures when a later tool call is missing one", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({ thoughtSignature: "Y2FsbF9zaWdfcmVwbGF5XzE=" }),
        toolResultTurn(),
        googleToolCallAssistantTurn({ timestamp: 2 }),
      ],
    } as never);

    // Find the last model-role content; should carry the replayed signature
    // even though the second stored toolCall block had none.
    expect(getLastModelTurn(params.contents)).toMatchObject({
      role: "model",
      parts: [
        {
          thoughtSignature: "Y2FsbF9zaWdfcmVwbGF5XzE=",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
  });

  it("treats the Google transport alias as the same route for signature replay", () => {
    const model = {
      ...buildGeminiModel({
        id: "gemini-3.1-pro-preview",
        name: "Gemini 3.1 Pro Preview",
      }),
      api: "openclaw-google-generative-ai-transport",
    } as Model<"openclaw-google-generative-ai-transport">;

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({ thoughtSignature: "Y2FsbF9zaWdfYWxpYXNfMQ==" }),
        toolResultTurn(),
        googleToolCallAssistantTurn({ timestamp: 2 }),
      ],
    } as never);

    expect(getLastModelTurn(params.contents)).toMatchObject({
      role: "model",
      parts: [
        {
          thoughtSignature: "Y2FsbF9zaWdfYWxpYXNfMQ==",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
  });

  it("keeps text and thinking signatures when the request uses the Google transport alias", () => {
    const model = {
      ...buildGeminiModel({
        id: "gemini-3.1-pro-preview",
        name: "Gemini 3.1 Pro Preview",
      }),
      api: "openclaw-google-generative-ai-transport",
    } as Model<"openclaw-google-generative-ai-transport">;

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        {
          role: "assistant",
          provider: "google",
          api: "google-generative-ai",
          model: "gemini-3.1-pro-preview",
          stopReason: "stop",
          timestamp: 0,
          content: [
            {
              type: "thinking",
              thinking: "plan",
              thinkingSignature: "dGhpbmtfc2lnX2FsaWFzXzE=",
            },
            {
              type: "text",
              text: "answer",
              textSignature: "dGV4dF9zaWdfYWxpYXNfMQ==",
            },
          ],
        },
      ],
    } as never);

    expect(params.contents[0]).toEqual({
      role: "model",
      parts: [
        {
          thought: true,
          text: "plan",
          thoughtSignature: "dGhpbmtfc2lnX2FsaWFzXzE=",
        },
        {
          text: "answer",
          thoughtSignature: "dGV4dF9zaWdfYWxpYXNfMQ==",
        },
      ],
    });
  });

  it("preserves opaque same-route Gemini thought signatures during replay", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({ thoughtSignature: "b3BhcXVlLnNpZy11cmxfc2FmZX4x" }),
        toolResultTurn(),
        googleToolCallAssistantTurn({ timestamp: 2 }),
      ],
    } as never);

    expect(getLastModelTurn(params.contents)).toMatchObject({
      role: "model",
      parts: [
        {
          thoughtSignature: "b3BhcXVlLnNpZy11cmxfc2FmZX4x",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
  });

  it("keeps a tool call's own Gemini thought signature before replay fallback", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({ thoughtSignature: "Y2FsbF9zaWdfZmlyc3RfMQ==" }),
        toolResultTurn(),
        googleToolCallAssistantTurn({
          timestamp: 2,
          thoughtSignature: "Y2FsbF9zaWdfc2Vjb25kXzE=",
        }),
      ],
    } as never);

    const modelTurns = params.contents.filter(isModelTurnWithParts);
    expect(modelTurns).toHaveLength(2);
    expect(modelTurns[0]).toMatchObject({
      parts: [
        {
          thoughtSignature: "Y2FsbF9zaWdfZmlyc3RfMQ==",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
    expect(modelTurns[1]).toMatchObject({
      parts: [
        {
          thoughtSignature: "Y2FsbF9zaWdfc2Vjb25kXzE=",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
  });

  it("does not replay Gemini thought signatures from later turns", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn(),
        toolResultTurn(),
        googleToolCallAssistantTurn({
          timestamp: 2,
          thoughtSignature: "Y2FsbF9zaWdfZnV0dXJlXzE=",
        }),
      ],
    } as never);

    const modelTurns = params.contents.filter(isModelTurnWithParts);
    expect(modelTurns).toHaveLength(2);
    expect(modelTurns[0]).toMatchObject({
      parts: [
        {
          thoughtSignature: "skip_thought_signature_validator",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
    expect(modelTurns[1]).toMatchObject({
      parts: [
        {
          thoughtSignature: "Y2FsbF9zaWdfZnV0dXJlXzE=",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
  });

  it("does not re-attach replayed Gemini thought signatures to a different tool-call part", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({ thoughtSignature: "Y2FsbF9zaWdfcmVwbGF5XzE=" }),
        toolResultTurn(),
        googleToolCallAssistantTurn({ timestamp: 2, args: { q: "hello-again" } }),
      ],
    } as never);

    expect(getLastModelTurn(params.contents)).toMatchObject({
      role: "model",
      parts: [
        {
          thoughtSignature: "skip_thought_signature_validator",
          functionCall: { name: "lookup", args: { q: "hello-again" } },
        },
      ],
    });
  });

  it("does not replay tool-call thought signatures from a different provider route", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    // Prior turn came from an Anthropic route — its signature looks valid base64
    // but must NOT be replayed into a Gemini request.
    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({
          provider: "anthropic",
          api: "anthropic",
          model: "claude-sonnet-4",
          id: "call_foreign",
          // Plausible-looking base64 from a non-Gemini provider.
          thoughtSignature: "bXNnXzAxWEZEVURZSmdBQUNjblNNMlRUZ1FzQQ==",
        }),
        toolResultTurn("call_foreign"),
        {
          role: "user",
          content: [{ type: "text", text: "Continue." }],
        },
      ],
    } as never);

    // The foreign signature should not be replayed into the Gemini payload.
    // Gemini 3 still needs the documented skip fallback for unsigned function
    // calls that came from another route.
    const firstModelTurn = getFirstModelTurn(params.contents);
    expect(firstModelTurn).toMatchObject({
      role: "model",
      parts: [
        {
          thoughtSignature: "skip_thought_signature_validator",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
    expect(firstModelTurn.parts[0].thoughtSignature).not.toBe(
      "bXNnXzAxWEZEVURZSmdBQUNjblNNMlRUZ1FzQQ==",
    );
  });

  it("does not replay prior Gemini thought signatures onto a later foreign route", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({ thoughtSignature: "Y2FsbF9zaWdfZ29vZ2xlXzE=" }),
        toolResultTurn(),
        googleToolCallAssistantTurn({
          provider: "anthropic",
          api: "anthropic",
          model: "claude-sonnet-4",
          timestamp: 2,
        }),
      ],
    } as never);

    const modelTurns = params.contents.filter(isModelTurnWithParts);
    expect(modelTurns).toHaveLength(2);
    expect(modelTurns[1]).toMatchObject({
      parts: [
        {
          thoughtSignature: "skip_thought_signature_validator",
          functionCall: { name: "lookup", args: { q: "hello" } },
        },
      ],
    });
    expect(modelTurns[1]?.parts[0].thoughtSignature).not.toBe("Y2FsbF9zaWdfZ29vZ2xlXzE=");
  });

  it("replaces invalid Gemini tool-call sentinel signatures with the skip fallback", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [googleToolCallAssistantTurn({ thoughtSignature: "reasoning" })],
    } as never);

    const part = (params.contents[0] as { parts: Array<Record<string, unknown>> }).parts[0];
    expect(part).toMatchObject({
      thoughtSignature: "skip_thought_signature_validator",
      functionCall: { name: "lookup", args: { q: "hello" } },
    });
  });

  it("preserves the skip-validator fallback for unsigned Gemini tool-call replay", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        googleToolCallAssistantTurn({ thoughtSignature: "skip_thought_signature_validator" }),
      ],
    } as never);

    const part = (params.contents[0] as { parts: Array<Record<string, unknown>> }).parts[0];
    expect(part).toMatchObject({
      thoughtSignature: "skip_thought_signature_validator",
      functionCall: { name: "lookup", args: { q: "hello" } },
    });
  });

  it("adds skip-validator fallback to unsigned sibling Gemini 3 tool calls", () => {
    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        {
          role: "assistant",
          provider: "google",
          api: "google-generative-ai",
          model: "gemini-3.1-pro-preview",
          stopReason: "toolUse",
          timestamp: 0,
          content: [
            {
              type: "toolCall",
              id: "call_math",
              name: "math_eval",
              arguments: { expression: "17*23" },
              thoughtSignature: "cmVhbF9zaWdfMQ==",
            },
            {
              type: "toolCall",
              id: "call_lookup",
              name: "lookup_fact",
              arguments: { key: "beta" },
            },
            {
              type: "toolCall",
              id: "call_transform",
              name: "string_transform",
              arguments: { text: "claw", mode: "reverse" },
            },
          ],
        },
      ],
    } as never);

    const parts = (params.contents[0] as { parts: Array<Record<string, unknown>> }).parts;
    expect(parts).toHaveLength(3);
    expect(parts[0]).toMatchObject({
      thoughtSignature: "cmVhbF9zaWdfMQ==",
      functionCall: { name: "math_eval", args: { expression: "17*23" } },
    });
    expect(parts[1]).toMatchObject({
      thoughtSignature: "skip_thought_signature_validator",
      functionCall: { name: "lookup_fact", args: { key: "beta" } },
    });
    expect(parts[2]).toMatchObject({
      thoughtSignature: "skip_thought_signature_validator",
      functionCall: { name: "string_transform", args: { text: "claw", mode: "reverse" } },
    });
  });

  it("does not trust cross-provider tool-call thought signatures for non-Gemini-3 models", () => {
    const model = buildGeminiModel({
      id: "gemini-2.5-pro",
      name: "Gemini 2.5 Pro",
    });

    const params = buildGoogleGenerativeAiParams(model, {
      messages: [
        {
          role: "assistant",
          provider: "anthropic",
          api: "anthropic-messages",
          model: "claude-opus-4-7",
          stopReason: "toolUse",
          timestamp: 0,
          content: [
            {
              type: "toolCall",
              id: "call_1",
              name: "lookup",
              arguments: { q: "hello" },
              thoughtSignature: "Zm9yZWlnbl9zaWc=",
            },
          ],
        },
      ],
    } as never);

    expect(params.contents[0]).toEqual({
      role: "model",
      parts: [{ functionCall: { name: "lookup", args: { q: "hello" } } }],
    });
    expect(JSON.stringify(params.contents)).not.toContain("Zm9yZWlnbl9zaWc=");
    expect(JSON.stringify(params.contents)).not.toContain("skip_thought_signature_validator");
  });

  it("builds direct Gemini payloads without negative fallback thinking budgets", () => {
    const model = {
      id: "custom-gemini-model",
      name: "Custom Gemini",
      api: "google-generative-ai",
      provider: "custom-google",
      baseUrl: "https://proxy.example.com/gemini/v1beta",
      reasoning: true,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 128000,
      maxTokens: 8192,
    } satisfies Model<"google-generative-ai">;

    const params = buildGoogleGenerativeAiParams(
      model,
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        reasoning: "medium",
      },
    );

    const generationConfig = requireGenerationConfig(params);
    const thinkingConfig = requireThinkingConfig(generationConfig);
    expect(thinkingConfig.includeThoughts).toBe(true);
    expect(thinkingConfig).not.toHaveProperty("thinkingBudget");
  });

  it("does not send thinkingConfig when the resolved Google model disables reasoning", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel({
        id: "gemma-4-26b-a4b-it",
        reasoning: false,
      }),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        reasoning: "medium",
      },
    );

    expect(params.generationConfig ?? {}).not.toHaveProperty("thinkingConfig");
  });

  it("omits disabled thinkingBudget=0 for Gemini 2.5 Pro direct payloads", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel(),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        maxTokens: 128,
      } as never,
    );

    const generationConfig = requireGenerationConfig(params);
    expect(generationConfig.maxOutputTokens).toBe(128);
    expect(generationConfig).not.toHaveProperty("thinkingConfig");
  });

  it("forwards configured stop sequences to the Gemini generationConfig", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel(),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        stop: ["</tool>", "\n\nObservation:"],
      } as never,
    );

    const generationConfig = requireGenerationConfig(params);
    expect(generationConfig.stopSequences).toEqual(["</tool>", "\n\nObservation:"]);
  });

  it("omits stopSequences when the stop list is empty", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel(),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        stop: [],
      } as never,
    );

    expect(params.generationConfig ?? {}).not.toHaveProperty("stopSequences");
  });

  it("sends stopSequences in the serialized Gemini request body via the guarded fetch transport", async () => {
    guardedFetchMock.mockResolvedValueOnce(buildSseResponse([]));

    const model = attachModelProviderRequestTransport(
      {
        id: "gemini-3.1-pro-preview",
        name: "Gemini 3.1 Pro Preview",
        api: "google-generative-ai",
        provider: "google",
        baseUrl: "https://generativelanguage.googleapis.com",
        reasoning: true,
        input: ["text"],
        cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
        contextWindow: 128000,
        maxTokens: 8192,
      } satisfies Model<"google-generative-ai">,
      {},
    );

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as Parameters<typeof streamFn>[1],
        {
          apiKey: "gemini-api-key",
          stop: ["</tool>", "\n\nObservation:"],
        } as Parameters<typeof streamFn>[2],
      ),
    );
    await stream.result();

    const guardedCall = requireMockCall(guardedFetchMock, 0, "guarded fetch");
    const init = requireRequestInit(guardedCall, "guarded fetch");
    const payload = parseRequestJsonBody(init);
    const generationConfig = requireGenerationConfig(payload);
    expect(generationConfig.stopSequences).toEqual(["</tool>", "\n\nObservation:"]);
  });

  it("strips explicit thinkingBudget=0 but preserves includeThoughts for Gemini 2.5 Pro", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel(),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        thinking: {
          enabled: true,
          budgetTokens: 0,
        },
      } as never,
    );

    const generationConfig = requireGenerationConfig(params);
    const thinkingConfig = requireThinkingConfig(generationConfig);
    expect(thinkingConfig.includeThoughts).toBe(true);
    expect(thinkingConfig).not.toHaveProperty("thinkingBudget");
  });

  it.each([
    ["gemini-pro-latest", "LOW"],
    ["gemini-flash-latest", "MINIMAL"],
    ["gemini-flash-lite-latest", "MINIMAL"],
  ] as const)(
    "uses thinkingLevel instead of disabled thinkingBudget for %s defaults",
    (id, level) => {
      const params = buildGoogleGenerativeAiParams(
        buildGeminiModel({ id }),
        {
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as never,
        {
          maxTokens: 128,
        } as never,
      );

      const generationConfig = requireGenerationConfig(params);
      const thinkingConfig = requireThinkingConfig(generationConfig);
      expect(generationConfig.maxOutputTokens).toBe(128);
      expect(thinkingConfig.thinkingLevel).toBe(level);
      expect(thinkingConfig).not.toHaveProperty("thinkingBudget");
    },
  );

  it("maps explicit Gemini 3 thinking budgets to thinkingLevel", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel({ id: "gemini-3-flash-preview" }),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        thinking: {
          enabled: true,
          budgetTokens: 8192,
        },
      } as never,
    );

    const generationConfig = requireGenerationConfig(params);
    const thinkingConfig = requireThinkingConfig(generationConfig);
    expect(thinkingConfig).toEqual({
      includeThoughts: true,
      thinkingLevel: "MEDIUM",
    });
    expect(thinkingConfig).not.toHaveProperty("thinkingBudget");
  });

  it("keeps adaptive Gemini 3 thinking on provider dynamic defaults", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel({ id: "gemini-3-flash-preview" }),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        reasoning: "adaptive",
      } as never,
    );

    const generationConfig = requireGenerationConfig(params);
    const thinkingConfig = requireThinkingConfig(generationConfig);
    expect(thinkingConfig.includeThoughts).toBe(true);
    expect(thinkingConfig).not.toHaveProperty("thinkingLevel");
    expect(thinkingConfig).not.toHaveProperty("thinkingBudget");
  });

  it("maps adaptive Gemini 2.5 thinking to dynamic thinkingBudget", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel({ id: "gemini-2.5-flash" }),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        reasoning: "adaptive",
      } as never,
    );

    const generationConfig = requireGenerationConfig(params);
    expect(requireThinkingConfig(generationConfig)).toEqual({
      includeThoughts: true,
      thinkingBudget: -1,
    });
  });

  it("normalizes explicit Gemini 3 Pro thinking levels", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel({ id: "gemini-3.1-pro-preview" }),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        thinking: {
          enabled: true,
          level: "MINIMAL",
        },
      } as never,
    );

    const generationConfig = requireGenerationConfig(params);
    expect(requireThinkingConfig(generationConfig)).toEqual({
      includeThoughts: true,
      thinkingLevel: "LOW",
    });
  });

  it("includes cachedContent in direct Gemini payloads when requested", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel(),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      {
        cachedContent: "cachedContents/prebuilt-context",
      },
    );

    expect(params.cachedContent).toBe("cachedContents/prebuilt-context");
  });

  it("omits per-request system and tool settings when using cachedContent", () => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel(),
      {
        systemPrompt: "Follow policy.",
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
        tools: [
          {
            name: "lookup",
            description: "Look up a value",
            parameters: {
              type: "object",
              properties: { q: { type: "string" } },
              required: ["q"],
            },
          },
        ],
      } as never,
      {
        cachedContent: " cachedContents/prebuilt-context ",
        toolChoice: "auto",
      },
    );

    expect(params.cachedContent).toBe("cachedContents/prebuilt-context");
    expect(params.systemInstruction).toBeUndefined();
    expect(params.tools).toBeUndefined();
    expect(params.toolConfig).toBeUndefined();
  });

  it("uses a non-empty text placeholder for empty user text", () => {
    const params = buildGoogleGenerativeAiParams(buildGeminiModel(), {
      messages: [
        { role: "user", content: "", timestamp: 0 },
        {
          role: "user",
          content: [{ type: "text", text: "" }],
          timestamp: 1,
        },
      ],
    } as never);

    expect(params.contents).toEqual([
      { role: "user", parts: [{ text: " " }] },
      { role: "user", parts: [{ text: " " }] },
    ]);
  });

  it("uses a text placeholder when user parts are filtered out for text-only models", () => {
    const params = buildGoogleGenerativeAiParams(buildGeminiModel({ input: ["text"] }), {
      messages: [
        {
          role: "user",
          content: [{ type: "image", mimeType: "image/png", data: "png-bytes" }],
          timestamp: 0,
        },
      ],
    } as never);

    expect(params.contents).toEqual([{ role: "user", parts: [{ text: " " }] }]);
  });

  it("uses a user placeholder when converted Gemini contents would otherwise be empty", () => {
    const params = buildGoogleGenerativeAiParams(buildGeminiModel(), {
      messages: [
        {
          role: "assistant",
          provider: "google",
          api: "google-generative-ai",
          model: "gemini-2.5-pro",
          stopReason: "stop",
          timestamp: 0,
          content: [{ type: "text", text: "   " }],
        },
      ],
    } as never);

    expect(params.contents).toEqual([{ role: "user", parts: [{ text: " " }] }]);
  });

  it.each([
    ["gemini-2.5-flash-lite", "minimal", 512],
    ["gemini-2.5-flash-lite", "low", 2048],
    ["gemini-2.5-flash", "minimal", 128],
    ["gemini-2.5-flash", "low", 2048],
    ["gemini-2.5-pro", "minimal", 128],
    ["gemini-2.5-pro", "low", 2048],
    ["gemini-2.5-flash", "medium", 8192],
    ["gemini-2.5-pro", "medium", 8192],
  ] as const)("%s with reasoning=%s uses thinkingBudget %i", (id, reasoning, expectedBudget) => {
    const params = buildGoogleGenerativeAiParams(
      buildGeminiModel({ id }),
      {
        messages: [{ role: "user", content: "hello", timestamp: 0 }],
      } as never,
      { reasoning },
    );

    const generationConfig = requireGenerationConfig(params);
    expect(requireThinkingConfig(generationConfig)).toEqual({
      includeThoughts: true,
      thinkingBudget: expectedBudget,
    });
  });

  it("emits thinking activity for thoughtSignature-only parts to keep the stream active", async () => {
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [
            {
              content: {
                parts: [
                  { thought: true, text: "draft", thoughtSignature: "c2lnXzE=" },
                  { thoughtSignature: "c2lnXzI=" },
                  { text: "answer" },
                ],
              },
              finishReason: "STOP",
            },
          ],
          usageMetadata: {
            promptTokenCount: 10,
            candidatesTokenCount: 5,
            thoughtsTokenCount: 3,
            totalTokenCount: 18,
          },
        },
      ]),
    );

    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          systemPrompt: "You are a helpful assistant.",
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as never,
        { reasoning: "high" },
      ),
    );
    const events = [];
    for await (const event of stream) {
      events.push(event);
    }
    const result = await stream.result();

    expect(result.content).toEqual([
      { type: "thinking", thinking: "draft", thinkingSignature: "c2lnXzI=" },
      { type: "text", text: "answer" },
    ]);
    expect(events.map((event) => event.type)).toEqual([
      "start",
      "thinking_start",
      "thinking_delta",
      "thinking_delta",
      "thinking_end",
      "text_start",
      "text_delta",
      "text_end",
      "done",
    ]);
    expect(events[3]?.type).toBe("thinking_delta");
    expect(events[3]).toHaveProperty("delta", "");
  });

  it("starts a thinking block for thoughtSignature-only parts that arrive before any text", async () => {
    guardedFetchMock.mockResolvedValueOnce(
      buildSseResponse([
        {
          candidates: [
            {
              content: {
                parts: [
                  { thoughtSignature: "c2lnXzE=" },
                  { thought: true, text: "draft" },
                  { text: "answer" },
                ],
              },
              finishReason: "STOP",
            },
          ],
          usageMetadata: {
            promptTokenCount: 10,
            candidatesTokenCount: 5,
            thoughtsTokenCount: 3,
            totalTokenCount: 18,
          },
        },
      ]),
    );

    const model = buildGeminiModel({
      id: "gemini-3.1-pro-preview",
      name: "Gemini 3.1 Pro Preview",
    });

    const streamFn = createGoogleGenerativeAiTransportStreamFn();
    const stream = await Promise.resolve(
      streamFn(
        model,
        {
          systemPrompt: "You are a helpful assistant.",
          messages: [{ role: "user", content: "hello", timestamp: 0 }],
        } as never,
        { reasoning: "high" },
      ),
    );
    const result = await stream.result();

    expect(result.content).toEqual([
      { type: "thinking", thinking: "draft", thinkingSignature: "c2lnXzE=" },
      { type: "text", text: "answer" },
    ]);
  });
});

function toLintErrorObject(value: unknown, fallbackMessage: string): Error {
  if (value instanceof Error) {
    return value;
  }
  if (typeof value === "string") {
    return new Error(value);
  }
  const error = new Error(fallbackMessage, { cause: value });
  if ((typeof value === "object" && value !== null) || typeof value === "function") {
    Object.assign(error, value);
  }
  return error;
}
