import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import type { CliBackendPlugin } from "openclaw/plugin-sdk/cli-backend";
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
import { withTempDir } from "openclaw/plugin-sdk/test-env";
import { describe, expect, it, vi } from "vitest";
import { buildGoogleGeminiCliBackend } from "./cli-backend.js";
import setupEntry from "./setup-api.js";

type GeminiPrepareContext = Parameters<
  NonNullable<ReturnType<typeof buildGoogleGeminiCliBackend>["prepareExecution"]>
>[0] & {
  env?: Record<string, string>;
  authCredential?: {
    type: "api_key" | "oauth" | "token";
    provider: string;
    access?: string;
    refresh?: string;
    expires?: number;
    idToken?: string;
    projectId?: string;
    key?: string;
    email?: string;
  };
};
type GeminiPreparedExecution = Awaited<
  ReturnType<NonNullable<ReturnType<typeof buildGoogleGeminiCliBackend>["prepareExecution"]>>
>;

async function stageGeminiPreparedExecution(
  prepared: GeminiPreparedExecution | null | undefined,
): Promise<void> {
  await prepared?.beforeExecution?.();
}

function buildGeminiOAuthPrepareContext(workspaceDir: string): GeminiPrepareContext {
  const agentDir = path.join(workspaceDir, "agent");
  return {
    workspaceDir,
    agentDir,
    provider: "google-gemini-cli",
    modelId: "gemini-3.1-pro-preview",
    authProfileId: "google-gemini-cli:user@example.test",
    // Private bundled-runtime bridge, not public Plugin SDK surface.
    authCredential: {
      type: "oauth",
      provider: "google-gemini-cli",
      access: "access-token",
      refresh: "refresh-token",
      expires: 1_800_000_000_000,
      idToken: "id-token",
      projectId: "profile-project",
      email: "user@example.test",
    },
  };
}

function buildGeminiApiKeyPrepareContext(workspaceDir: string): GeminiPrepareContext {
  const agentDir = path.join(workspaceDir, "agent");
  return {
    workspaceDir,
    agentDir,
    provider: "google-gemini-cli",
    modelId: "gemini-3.1-flash-lite",
    authProfileId: "google:api-key",
    // Private bundled-runtime bridge, not public Plugin SDK surface.
    authCredential: {
      type: "api_key",
      provider: "google",
      key: "gemini-api-key",
      email: "user@example.test",
    },
  };
}

function restoreEnv(name: string, value: string | undefined): void {
  if (value === undefined) {
    delete process.env[name];
    return;
  }
  process.env[name] = value;
}

describe("google setup entry", () => {
  it("registers setup runtime providers declared by the manifest", () => {
    const providerIds: string[] = [];
    const cliBackendIds: string[] = [];

    setupEntry.register({
      registerProvider(provider: ProviderPlugin) {
        providerIds.push(provider.id);
      },
      registerCliBackend(backend: CliBackendPlugin) {
        cliBackendIds.push(backend.id);
      },
    } as never);

    expect(providerIds).toEqual(["google-vertex"]);
    expect(cliBackendIds).toEqual(["google-gemini-cli"]);
  });
});

describe("google gemini cli backend config", () => {
  it("keeps legacy json output overrides on the json parser", () => {
    const backend = buildGoogleGeminiCliBackend();
    const normalized = backend.normalizeConfig?.({
      ...backend.config,
      args: ["--skip-trust", "--output-format", "json", "--prompt", "{prompt}"],
      resumeArgs: [
        "--skip-trust",
        "--resume",
        "{sessionId}",
        "--output-format=json",
        "--prompt",
        "{prompt}",
      ],
    });

    expect(normalized?.output).toBe("json");
    expect(normalized?.resumeOutput).toBe("json");
    expect(normalized?.jsonlDialect).toBeUndefined();
  });

  it("keeps short stream-json output overrides on the jsonl parser", () => {
    const backend = buildGoogleGeminiCliBackend();
    const normalized = backend.normalizeConfig?.({
      ...backend.config,
      args: ["--skip-trust", "-o", "stream-json", "--prompt", "{prompt}"],
      resumeArgs: [
        "--skip-trust",
        "--resume",
        "{sessionId}",
        "-o=stream-json",
        "--prompt",
        "{prompt}",
      ],
    });

    expect(normalized?.output).toBe("jsonl");
    expect(normalized?.resumeOutput).toBe("jsonl");
    expect(normalized?.jsonlDialect).toBe("gemini-stream-json");
  });
});

describe("google gemini cli backend auth bridge", () => {
  it("materializes selected OpenClaw OAuth credentials into a persistent profile-scoped Gemini CLI home", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
    let home: string | undefined;
    const cleanups: Array<() => Promise<void>> = [];

    try {
      const context = buildGeminiOAuthPrepareContext(workspaceDir);
      const inheritedSettingsPath = path.join(workspaceDir, "generated-mcp-settings.json");
      await fs.writeFile(
        inheritedSettingsPath,
        `${JSON.stringify({
          security: {
            auth: {
              selectedType: "vertex-ai",
              enforcedType: "oauth-personal",
              useExternal: true,
            },
          },
          mcp: { allowed: ["openclaw"] },
          mcpServers: { openclaw: { url: "http://127.0.0.1:23119/mcp" } },
        })}\n`,
        "utf8",
      );
      context.env = { GEMINI_CLI_SYSTEM_SETTINGS_PATH: inheritedSettingsPath };
      const prepared = await backend.prepareExecution?.(context);
      if (prepared?.cleanup) {
        cleanups.push(prepared.cleanup);
      }
      await stageGeminiPreparedExecution(prepared);

      home = prepared?.env?.GEMINI_CLI_HOME;
      const systemSettingsPath = prepared?.env?.GEMINI_CLI_SYSTEM_SETTINGS_PATH;
      expect(home).toBeTruthy();
      expect(systemSettingsPath).toBeTruthy();
      expect(systemSettingsPath).not.toBe(inheritedSettingsPath);
      expect(path.dirname(systemSettingsPath ?? "")).not.toBe(home);
      expect(
        path.relative(resolvePreferredOpenClawTmpDir(), path.dirname(systemSettingsPath ?? "")),
      ).toMatch(/^openclaw-gemini-cli-/);
      expect(prepared?.env?.GEMINI_FORCE_FILE_STORAGE).toBe("true");
      expect(prepared?.env?.GOOGLE_CLOUD_PROJECT).toBe("profile-project");
      expect(prepared?.env?.GOOGLE_CLOUD_PROJECT_ID).toBe("profile-project");
      expect(prepared?.env?.GOOGLE_CLOUD_QUOTA_PROJECT).toBe("profile-project");
      if (!context.agentDir) {
        throw new Error("expected Gemini test context to include an agent directory");
      }
      expect(home).toContain(path.join(context.agentDir, "google-gemini-cli-home"));
      expect(home).not.toContain("user@example.test");

      const raw = await fs.readFile(path.join(home ?? "", ".gemini", "oauth_creds.json"), "utf8");
      expect(JSON.parse(raw)).toEqual({
        access_token: "access-token",
        refresh_token: "refresh-token",
        id_token: "id-token",
        expiry_date: 1_800_000_000_000,
        token_type: "Bearer",
      });
      const nestedSettingsRaw = await fs.readFile(
        path.join(home ?? "", ".gemini", "settings.json"),
        "utf8",
      );
      const rootSettingsRaw = await fs.readFile(path.join(home ?? "", "settings.json"), "utf8");
      expect(JSON.parse(nestedSettingsRaw)).toEqual({
        security: { auth: { selectedType: "oauth-personal" } },
      });
      expect(JSON.parse(rootSettingsRaw)).toEqual(JSON.parse(nestedSettingsRaw));
      const systemSettingsRaw = await fs.readFile(systemSettingsPath ?? "", "utf8");
      expect(JSON.parse(systemSettingsRaw)).toEqual({
        security: {
          auth: {
            selectedType: "oauth-personal",
            enforcedType: "oauth-personal",
            useExternal: true,
          },
        },
        mcp: { allowed: ["openclaw"] },
        mcpServers: { openclaw: { url: "http://127.0.0.1:23119/mcp" } },
      });

      const sessionMarker = path.join(home ?? "", ".gemini", "session-state.json");
      await fs.writeFile(sessionMarker, '{"keep":true}\n', "utf8");
      const cachedCredentialsPath = path.join(home ?? "", ".gemini", "gemini-credentials.json");
      await fs.writeFile(cachedCredentialsPath, "stale-cache", "utf8");

      const preparedAgain = await backend.prepareExecution?.(context);
      if (preparedAgain?.cleanup) {
        cleanups.push(preparedAgain.cleanup);
      }
      await stageGeminiPreparedExecution(preparedAgain);
      expect(preparedAgain?.env?.GEMINI_CLI_HOME).toBe(home);
      await expect(fs.access(sessionMarker)).resolves.toBeUndefined();
      await expect(fs.access(cachedCredentialsPath)).rejects.toThrow();
    } finally {
      for (const cleanup of cleanups.toReversed()) {
        await cleanup();
      }
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("stages Gemini CLI JSON through same-directory atomic renames", async () => {
    await withTempDir("openclaw-test-workspace-", async (workspaceDir) => {
      const backend = buildGoogleGeminiCliBackend();
      const realRename = fs.rename.bind(fs);
      const renameCalls: Array<{ from: string; to: string }> = [];
      const renameSpy = vi
        .spyOn(fs, "rename")
        .mockImplementation(async (...args: Parameters<typeof fs.rename>) => {
          renameCalls.push({ from: String(args[0]), to: String(args[1]) });
          await realRename(...args);
        });
      let prepared: GeminiPreparedExecution | null | undefined;

      try {
        prepared = await backend.prepareExecution?.(buildGeminiOAuthPrepareContext(workspaceDir));
        await stageGeminiPreparedExecution(prepared);

        const home = prepared?.env?.GEMINI_CLI_HOME;
        const systemSettingsPath = prepared?.env?.GEMINI_CLI_SYSTEM_SETTINGS_PATH;
        if (!home || !systemSettingsPath) {
          throw new Error("expected Gemini CLI staging paths");
        }
        const expectedTargets = [
          path.join(home, ".gemini", "settings.json"),
          path.join(home, "settings.json"),
          systemSettingsPath,
          path.join(home, ".gemini", "oauth_creds.json"),
        ];
        expect(renameCalls.map((call) => call.to).toSorted()).toEqual(expectedTargets.toSorted());
        for (const call of renameCalls) {
          expect(path.dirname(call.from)).toBe(path.dirname(call.to));
          expect(path.basename(call.from).startsWith(`.${path.basename(call.to)}.`)).toBe(true);
          expect(path.basename(call.from).endsWith(".tmp")).toBe(true);
        }
        const oauthStat = await fs.stat(path.join(home, ".gemini", "oauth_creds.json"));
        expect(oauthStat.mode & 0o777).toBe(0o600);
      } finally {
        renameSpy.mockRestore();
        await prepared?.cleanup?.();
      }
    });
  });

  it("prepares selected canonical Google API-key credentials and removes stale OAuth state for that profile home", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
    let home: string | undefined;
    const cleanups: Array<() => Promise<void>> = [];

    try {
      const context = buildGeminiApiKeyPrepareContext(workspaceDir);
      const firstPrepared = await backend.prepareExecution?.(context);
      if (firstPrepared?.cleanup) {
        cleanups.push(firstPrepared.cleanup);
      }
      await stageGeminiPreparedExecution(firstPrepared);
      home = firstPrepared?.env?.GEMINI_CLI_HOME;
      expect(home).toBeTruthy();
      await fs.writeFile(path.join(home ?? "", ".gemini", "oauth_creds.json"), "{}\n", "utf8");
      await fs.writeFile(
        path.join(home ?? "", ".gemini", "gemini-credentials.json"),
        "stale-cache",
        "utf8",
      );

      const prepared = await backend.prepareExecution?.(context);
      if (prepared?.cleanup) {
        cleanups.push(prepared.cleanup);
      }
      await stageGeminiPreparedExecution(prepared);

      home = prepared?.env?.GEMINI_CLI_HOME;
      expect(home).toBeTruthy();
      expect(prepared?.env?.GEMINI_API_KEY).toBe("gemini-api-key");
      expect(prepared?.env?.GEMINI_FORCE_FILE_STORAGE).toBe("true");
      expect(prepared?.clearEnv).toContain("GEMINI_API_KEY");
      expect(prepared?.clearEnv).toContain("GOOGLE_GENAI_USE_GCA");
      expect(prepared?.clearEnv).toContain("GOOGLE_GENAI_USE_VERTEXAI");
      expect(prepared?.clearEnv).toContain("GOOGLE_GEMINI_BASE_URL");

      const settingsRaw = await fs.readFile(
        path.join(home ?? "", ".gemini", "settings.json"),
        "utf8",
      );
      expect(JSON.parse(settingsRaw)).toEqual({
        security: { auth: { selectedType: "gemini-api-key" } },
      });
      await expect(
        fs.access(path.join(home ?? "", ".gemini", "oauth_creds.json")),
      ).rejects.toThrow();
      await expect(
        fs.access(path.join(home ?? "", ".gemini", "gemini-credentials.json")),
      ).rejects.toThrow();
    } finally {
      for (const cleanup of cleanups.toReversed()) {
        await cleanup();
      }
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("rejects inherited Gemini system settings that enforce a different auth type", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));

    try {
      const inheritedSettingsPath = path.join(workspaceDir, "generated-mcp-settings.json");
      await fs.writeFile(
        inheritedSettingsPath,
        `${JSON.stringify({
          security: { auth: { enforcedType: "gemini-api-key" } },
        })}\n`,
        "utf8",
      );
      const context = buildGeminiOAuthPrepareContext(workspaceDir);
      context.env = { GEMINI_CLI_SYSTEM_SETTINGS_PATH: inheritedSettingsPath };

      await expect(backend.prepareExecution?.(context)).rejects.toThrow(/enforce gemini-api-key/);
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("inherits process Gemini system settings when no generated settings path is present", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
    const originalSystemSettingsPath = process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH;
    let prepared:
      | Awaited<ReturnType<NonNullable<typeof backend.prepareExecution>>>
      | null
      | undefined;

    try {
      const inheritedSettingsPath = path.join(workspaceDir, "ambient-system-settings.json");
      await fs.writeFile(
        inheritedSettingsPath,
        `${JSON.stringify({
          security: {
            auth: {
              selectedType: "oauth-code-assist",
              enforcedType: "oauth-personal",
            },
            folderTrust: { enabled: true },
          },
        })}\n`,
        "utf8",
      );
      process.env.GEMINI_CLI_SYSTEM_SETTINGS_PATH = inheritedSettingsPath;

      prepared = await backend.prepareExecution?.(buildGeminiOAuthPrepareContext(workspaceDir));
      await stageGeminiPreparedExecution(prepared);

      const systemSettingsRaw = await fs.readFile(
        prepared?.env?.GEMINI_CLI_SYSTEM_SETTINGS_PATH ?? "",
        "utf8",
      );
      expect(JSON.parse(systemSettingsRaw)).toEqual({
        security: {
          auth: {
            selectedType: "oauth-personal",
            enforcedType: "oauth-personal",
          },
          folderTrust: { enabled: true },
        },
      });
    } finally {
      restoreEnv("GEMINI_CLI_SYSTEM_SETTINGS_PATH", originalSystemSettingsPath);
      await prepared?.cleanup?.();
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("rejects Vercel AI Gateway profiles for the Gemini CLI backend", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));

    try {
      await expect(
        backend.prepareExecution?.({
          workspaceDir,
          agentDir: path.join(workspaceDir, "agent"),
          provider: "google-gemini-cli",
          modelId: "gemini-3.1-flash-lite",
          authProfileId: "vercel-ai-gateway:default",
          authCredential: {
            type: "api_key",
            provider: "vercel-ai-gateway",
            key: "vercel-key",
          },
        } as never),
      ).rejects.toThrow(/vercel-ai-gateway auth profile/);
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("rejects selected Gemini token profiles before the CLI can use ambient auth", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));

    try {
      await expect(
        backend.prepareExecution?.({
          workspaceDir,
          agentDir: path.join(workspaceDir, "agent"),
          provider: "google-gemini-cli",
          modelId: "gemini-3.1-flash-lite",
          authProfileId: "google-gemini-cli:token",
          authCredential: {
            type: "token",
            provider: "google-gemini-cli",
            token: "bearer-token",
          },
        } as never),
      ).rejects.toThrow(/OAuth or API-key auth profiles/);
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("rejects selected Gemini profiles with no material before the CLI can use ambient auth", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));

    try {
      await expect(
        backend.prepareExecution?.({
          workspaceDir,
          agentDir: path.join(workspaceDir, "agent"),
          provider: "google-gemini-cli",
          modelId: "gemini-3.1-flash-lite",
          authProfileId: "google-gemini-cli:missing",
        } as never),
      ).rejects.toThrow(/no credential material/);
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("clears inherited Gemini auth credentials when staging selected OAuth credentials", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));
    const originalUseGca = process.env.GOOGLE_GENAI_USE_GCA;
    const originalCloudAccessToken = process.env.GOOGLE_CLOUD_ACCESS_TOKEN;
    const originalGoogleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
    const originalForceEncryptedFileStorage = process.env.GEMINI_FORCE_ENCRYPTED_FILE_STORAGE;
    const originalGeminiApiKey = process.env.GEMINI_API_KEY;
    const originalGoogleApiKey = process.env.GOOGLE_API_KEY;
    const originalQuotaProject = process.env.GOOGLE_CLOUD_QUOTA_PROJECT;
    let prepared:
      | Awaited<ReturnType<NonNullable<typeof backend.prepareExecution>>>
      | null
      | undefined;

    process.env.GOOGLE_GENAI_USE_GCA = "true";
    process.env.GOOGLE_CLOUD_ACCESS_TOKEN = "ambient-cloud-token";
    process.env.GOOGLE_APPLICATION_CREDENTIALS = "/tmp/ambient-google-adc.json";
    process.env.GEMINI_FORCE_ENCRYPTED_FILE_STORAGE = "true";
    process.env.GEMINI_API_KEY = "ambient-gemini-key";
    process.env.GOOGLE_API_KEY = "ambient-google-key";
    process.env.GOOGLE_CLOUD_QUOTA_PROJECT = "ambient-project";

    try {
      prepared = await backend.prepareExecution?.(buildGeminiOAuthPrepareContext(workspaceDir));

      expect(prepared?.env?.GEMINI_CLI_HOME).toBeTruthy();
      expect(prepared?.clearEnv).toEqual([
        "GOOGLE_GENAI_USE_GCA",
        "GOOGLE_CLOUD_ACCESS_TOKEN",
        "GOOGLE_APPLICATION_CREDENTIALS",
        "GEMINI_FORCE_ENCRYPTED_FILE_STORAGE",
        "GEMINI_FORCE_FILE_STORAGE",
        "GOOGLE_GENAI_USE_VERTEXAI",
        "GOOGLE_API_KEY",
        "GOOGLE_CLOUD_PROJECT",
        "GOOGLE_CLOUD_PROJECT_ID",
        "GOOGLE_CLOUD_QUOTA_PROJECT",
        "GOOGLE_CLOUD_LOCATION",
        "GOOGLE_GEMINI_BASE_URL",
        "GEMINI_CLI_CUSTOM_HEADERS",
        "GEMINI_API_KEY_AUTH_MECHANISM",
        "GEMINI_API_KEY",
        "GEMINI_CLI_SYSTEM_SETTINGS_PATH",
      ]);
    } finally {
      restoreEnv("GOOGLE_GENAI_USE_GCA", originalUseGca);
      restoreEnv("GOOGLE_CLOUD_ACCESS_TOKEN", originalCloudAccessToken);
      restoreEnv("GOOGLE_APPLICATION_CREDENTIALS", originalGoogleApplicationCredentials);
      restoreEnv("GEMINI_FORCE_ENCRYPTED_FILE_STORAGE", originalForceEncryptedFileStorage);
      restoreEnv("GEMINI_API_KEY", originalGeminiApiKey);
      restoreEnv("GOOGLE_API_KEY", originalGoogleApiKey);
      restoreEnv("GOOGLE_CLOUD_QUOTA_PROJECT", originalQuotaProject);
      await prepared?.cleanup?.();
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("requires an agent directory for profile-owned Gemini CLI state", async () => {
    const backend = buildGoogleGeminiCliBackend();
    const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-test-workspace-"));

    try {
      const { agentDir: _agentDir, ...context } = buildGeminiOAuthPrepareContext(workspaceDir);
      await expect(backend.prepareExecution?.(context)).rejects.toThrow(/agent directory/);
    } finally {
      await fs.rm(workspaceDir, { recursive: true, force: true });
    }
  });

  it("uses profile-only auth epochs for the private Gemini CLI bridge", () => {
    const backend = buildGoogleGeminiCliBackend();

    expect(backend.authEpochMode).toBe("profile-only");
    expect(backend.prepareExecution).toBeTypeOf("function");
  });
});
