import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
import {
  getRegistryJitiMocks,
  resetRegistryJitiMocks,
} from "./test-helpers/registry-jiti-mocks.js";

const tempDirs: string[] = [];
const mocks = getRegistryJitiMocks();

let clearPluginSetupRegistryCache: typeof import("./setup-registry.js").clearPluginSetupRegistryCache;
let setupRegistryTesting: typeof import("./setup-registry.js").__testing;
let resolvePluginSetupRegistry: typeof import("./setup-registry.js").resolvePluginSetupRegistry;
let resolvePluginSetupProvider: typeof import("./setup-registry.js").resolvePluginSetupProvider;
let resolvePluginSetupCliBackend: typeof import("./setup-registry.js").resolvePluginSetupCliBackend;
let runPluginSetupConfigMigrations: typeof import("./setup-registry.js").runPluginSetupConfigMigrations;

function makeTempDir(): string {
  return makeTrackedTempDir("openclaw-setup-registry", tempDirs);
}

async function expectNoUnhandledRejection(run: () => void | Promise<void>): Promise<void> {
  const unhandledRejections: unknown[] = [];
  const onUnhandledRejection = (reason: unknown) => {
    unhandledRejections.push(reason);
  };
  process.on("unhandledRejection", onUnhandledRejection);
  try {
    await run();
    await Promise.resolve();
    await Promise.resolve();
  } finally {
    process.off("unhandledRejection", onUnhandledRejection);
  }
  expect(unhandledRejections).toEqual([]);
}

afterEach(() => {
  cleanupTrackedTempDirs(tempDirs);
});

describe("setup-registry getJiti", () => {
  beforeEach(async () => {
    resetRegistryJitiMocks();
    vi.resetModules();
    ({
      __testing: setupRegistryTesting,
      clearPluginSetupRegistryCache,
      resolvePluginSetupRegistry,
      resolvePluginSetupProvider,
      resolvePluginSetupCliBackend,
      runPluginSetupConfigMigrations,
    } = await import("./setup-registry.js"));
    clearPluginSetupRegistryCache();
  });

  it("disables native jiti loading on Windows for setup-api modules", () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [{ id: "test-plugin", rootDir: pluginRoot }],
      diagnostics: [],
    });
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");

    try {
      resolvePluginSetupRegistry({
        workspaceDir: pluginRoot,
        env: {},
      });
    } finally {
      platformSpy.mockRestore();
    }

    expect(mocks.createJiti).toHaveBeenCalledTimes(1);
    expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "setup-api.js"));
    expect(mocks.createJiti.mock.calls[0]?.[1]).toEqual(
      expect.objectContaining({
        tryNative: false,
      }),
    );
  });

  it("skips setup-api loading when config has no relevant migration triggers", () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "amazon-bedrock",
          rootDir: pluginRoot,
          configContracts: {
            compatibilityMigrationPaths: ["models.bedrockDiscovery"],
          },
        },
      ],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation(() => {
      return () => ({
        default: {
          register(api: {
            registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
          }) {
            api.registerConfigMigration((config) => ({ config, changes: ["unexpected"] }));
          },
        },
      });
    });

    const result = runPluginSetupConfigMigrations({
      config: {
        models: {
          providers: {
            openai: { baseUrl: "https://api.openai.com/v1" },
          },
        },
      } as never,
      env: {},
    });

    expect(result.changes).toEqual([]);
    expect(mocks.createJiti).not.toHaveBeenCalled();
  });

  it("loads only plugins whose manifest migration triggers match the config", () => {
    const bedrockRoot = makeTempDir();
    const voiceCallRoot = makeTempDir();
    fs.writeFileSync(path.join(bedrockRoot, "setup-api.js"), "export default {};\n", "utf-8");
    fs.writeFileSync(path.join(voiceCallRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "amazon-bedrock",
          rootDir: bedrockRoot,
          configContracts: {
            compatibilityMigrationPaths: ["models.bedrockDiscovery"],
          },
        },
        {
          id: "voice-call",
          rootDir: voiceCallRoot,
          configContracts: {
            compatibilityMigrationPaths: ["plugins.entries.voice-call.config"],
          },
        },
      ],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation((modulePath: string) => {
      const pluginId = modulePath.includes(bedrockRoot) ? "amazon-bedrock" : "voice-call";
      return () => ({
        default: {
          register(api: {
            registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
          }) {
            api.registerConfigMigration((config) => ({
              config,
              changes: [pluginId],
            }));
          },
        },
      });
    });

    const result = runPluginSetupConfigMigrations({
      config: {
        models: {
          bedrockDiscovery: {
            enabled: true,
          },
        },
      } as never,
      env: {},
    });

    expect(result.changes).toEqual(["amazon-bedrock"]);
    expect(mocks.createJiti).toHaveBeenCalledTimes(1);
    expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(bedrockRoot, "setup-api.js"));
  });

  it("still loads explicitly configured plugin entries without manifest trigger metadata", () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [{ id: "voice-call", rootDir: pluginRoot }],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation(() => {
      return () => ({
        default: {
          register(api: {
            registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
          }) {
            api.registerConfigMigration((config) => ({ config, changes: ["voice-call"] }));
          },
        },
      });
    });

    const result = runPluginSetupConfigMigrations({
      config: {
        plugins: {
          entries: {
            "voice-call": {
              config: {
                provider: "log",
              },
            },
          },
        },
      } as never,
      env: {},
    });

    expect(result.changes).toEqual(["voice-call"]);
    expect(mocks.createJiti).toHaveBeenCalledTimes(1);
  });

  it("prefers setup provider descriptors over top-level provider ids", () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "amazon-bedrock",
          rootDir: pluginRoot,
          providers: ["legacy-bedrock"],
          setup: {
            providers: [{ id: "amazon-bedrock" }],
            requiresRuntime: true,
          },
        },
      ],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation(() => {
      return () => ({
        default: {
          register(api: {
            registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
          }) {
            api.registerProvider({
              id: "amazon-bedrock",
              label: "Amazon Bedrock",
              auth: [],
            });
          },
        },
      });
    });

    expect(resolvePluginSetupProvider({ provider: "amazon-bedrock", env: {} })).toEqual(
      expect.objectContaining({
        id: "amazon-bedrock",
        label: "Amazon Bedrock",
      }),
    );
    expect(resolvePluginSetupProvider({ provider: "legacy-bedrock", env: {} })).toBeUndefined();
    expect(mocks.createJiti).toHaveBeenCalledTimes(1);
    expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(pluginRoot, "setup-api.js"));
  });

  it("resolves setup cli backends from descriptors without loading every setup-api", () => {
    const openaiRoot = makeTempDir();
    const anthropicRoot = makeTempDir();
    fs.writeFileSync(path.join(openaiRoot, "setup-api.js"), "export default {};\n", "utf-8");
    fs.writeFileSync(path.join(anthropicRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          rootDir: openaiRoot,
          cliBackends: ["legacy-openai-cli"],
          setup: {
            cliBackends: ["codex-cli"],
            requiresRuntime: true,
          },
        },
        {
          id: "anthropic",
          rootDir: anthropicRoot,
          cliBackends: ["claude-cli"],
        },
      ],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation((modulePath: string) => {
      return () => ({
        default: {
          register(api: {
            registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
          }) {
            api.registerCliBackend(
              modulePath.includes(openaiRoot)
                ? { id: "codex-cli", config: { command: "codex" } }
                : { id: "claude-cli", config: { command: "claude" } },
            );
          },
        },
      });
    });

    const first = resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} });
    const second = resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} });

    expect(first).toEqual({
      pluginId: "openai",
      backend: {
        id: "codex-cli",
        config: {
          command: "codex",
        },
      },
    });
    expect(second).toEqual(first);
    expect(resolvePluginSetupCliBackend({ backend: "legacy-openai-cli", env: {} })).toBeUndefined();
    expect(mocks.createJiti).toHaveBeenCalledTimes(1);
    expect(mocks.createJiti.mock.calls[0]?.[0]).toBe(path.join(openaiRoot, "setup-api.js"));
  });

  it("keeps synchronously registered cli backends even when register returns a promise", () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          rootDir: pluginRoot,
          setup: {
            cliBackends: ["codex-cli"],
            requiresRuntime: true,
          },
        },
      ],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation(() => {
      return () => ({
        default: {
          register(api: {
            registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
          }) {
            api.registerCliBackend({
              id: "codex-cli",
              config: { command: "codex" },
            });
            return Promise.resolve();
          },
        },
      });
    });

    expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toEqual({
      pluginId: "openai",
      backend: {
        id: "codex-cli",
        config: {
          command: "codex",
        },
      },
    });
  });

  it("swallows rejected async setup provider registration returns", async () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          rootDir: pluginRoot,
          setup: {
            providers: [{ id: "openai" }],
          },
        },
      ],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation(() => {
      return () => ({
        default: {
          register(api: {
            registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
          }) {
            api.registerProvider({
              id: "openai",
              label: "OpenAI",
              auth: [],
            });
            return Promise.reject(new Error("async provider register failed"));
          },
        },
      });
    });

    await expectNoUnhandledRejection(() => {
      expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toEqual(
        expect.objectContaining({
          id: "openai",
          label: "OpenAI",
        }),
      );
    });
  });

  it("swallows rejected async setup cli backend registration returns", async () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          rootDir: pluginRoot,
          setup: {
            cliBackends: ["codex-cli"],
          },
        },
      ],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation(() => {
      return () => ({
        default: {
          register(api: {
            registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
          }) {
            api.registerCliBackend({
              id: "codex-cli",
              config: { command: "codex" },
            });
            return Promise.reject(new Error("async cli backend register failed"));
          },
        },
      });
    });

    await expectNoUnhandledRejection(() => {
      expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toEqual({
        pluginId: "openai",
        backend: {
          id: "codex-cli",
          config: {
            command: "codex",
          },
        },
      });
    });
  });

  it("swallows rejected async setup registry registration returns", async () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [{ id: "voice-call", rootDir: pluginRoot }],
      diagnostics: [],
    });
    mocks.createJiti.mockImplementation(() => {
      return () => ({
        default: {
          register(api: {
            registerConfigMigration: (migrate: (config: unknown) => unknown) => void;
          }) {
            api.registerConfigMigration((config) => ({ config, changes: ["voice-call"] }));
            return Promise.reject(new Error("async setup registry register failed"));
          },
        },
      });
    });

    await expectNoUnhandledRejection(() => {
      expect(resolvePluginSetupRegistry({ env: {} }).configMigrations).toHaveLength(1);
    });
  });

  it("fails closed when multiple plugins claim the same setup provider id", () => {
    const bundledRoot = makeTempDir();
    const workspaceRoot = makeTempDir();
    fs.writeFileSync(path.join(bundledRoot, "setup-api.js"), "export default {};\n", "utf-8");
    fs.writeFileSync(path.join(workspaceRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          origin: "bundled",
          rootDir: bundledRoot,
          setup: {
            providers: [{ id: "openai" }],
          },
        },
        {
          id: "workspace-shadow",
          origin: "workspace",
          rootDir: workspaceRoot,
          setup: {
            providers: [{ id: "OpenAI" }],
          },
        },
      ],
      diagnostics: [],
    });

    expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toBeUndefined();
    expect(mocks.createJiti).not.toHaveBeenCalled();
  });

  it("fails closed when duplicate plugin ids shadow the same setup provider id", () => {
    const bundledRoot = makeTempDir();
    const workspaceRoot = makeTempDir();
    fs.writeFileSync(path.join(bundledRoot, "setup-api.js"), "export default {};\n", "utf-8");
    fs.writeFileSync(path.join(workspaceRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          origin: "bundled",
          rootDir: bundledRoot,
          setup: {
            providers: [{ id: "openai" }],
          },
        },
        {
          id: "openai",
          origin: "workspace",
          rootDir: workspaceRoot,
          setup: {
            providers: [{ id: "OpenAI" }],
          },
        },
      ],
      diagnostics: [],
    });

    expect(resolvePluginSetupProvider({ provider: "openai", env: {} })).toBeUndefined();
    expect(mocks.createJiti).not.toHaveBeenCalled();
  });

  it("fails closed when multiple plugins claim the same setup cli backend id", () => {
    const bundledRoot = makeTempDir();
    const workspaceRoot = makeTempDir();
    fs.writeFileSync(path.join(bundledRoot, "setup-api.js"), "export default {};\n", "utf-8");
    fs.writeFileSync(path.join(workspaceRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          origin: "bundled",
          rootDir: bundledRoot,
          setup: {
            cliBackends: ["codex-cli"],
          },
        },
        {
          id: "workspace-shadow",
          origin: "workspace",
          rootDir: workspaceRoot,
          setup: {
            cliBackends: ["CODEX-CLI"],
          },
        },
      ],
      diagnostics: [],
    });

    expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toBeUndefined();
    expect(mocks.createJiti).not.toHaveBeenCalled();
  });

  it("fails closed when duplicate plugin ids shadow the same setup cli backend id", () => {
    const bundledRoot = makeTempDir();
    const workspaceRoot = makeTempDir();
    fs.writeFileSync(path.join(bundledRoot, "setup-api.js"), "export default {};\n", "utf-8");
    fs.writeFileSync(path.join(workspaceRoot, "setup-api.js"), "export default {};\n", "utf-8");
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          origin: "bundled",
          rootDir: bundledRoot,
          setup: {
            cliBackends: ["codex-cli"],
          },
        },
        {
          id: "openai",
          origin: "workspace",
          rootDir: workspaceRoot,
          setup: {
            cliBackends: ["CODEX-CLI"],
          },
        },
      ],
      diagnostics: [],
    });

    expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })).toBeUndefined();
    expect(mocks.createJiti).not.toHaveBeenCalled();
  });

  it("bounds setup lookup caches with least-recently-used eviction", () => {
    const pluginRoot = makeTempDir();
    fs.writeFileSync(path.join(pluginRoot, "setup-api.js"), "export default {};\n", "utf-8");
    setupRegistryTesting.setMaxSetupLookupCacheEntriesForTest(1);
    mocks.loadPluginManifestRegistry.mockReturnValue({
      plugins: [
        {
          id: "openai",
          rootDir: pluginRoot,
          setup: {
            providers: [{ id: "openai" }, { id: "anthropic" }],
            cliBackends: ["codex-cli", "claude-cli"],
            requiresRuntime: true,
          },
        },
      ],
      diagnostics: [],
    });
    const loadSetupModule = vi.fn(() => ({
      default: {
        register(api: {
          registerProvider: (provider: { id: string; label: string; auth: [] }) => void;
          registerCliBackend: (backend: { id: string; config: { command: string } }) => void;
        }) {
          api.registerProvider({ id: "openai", label: "OpenAI", auth: [] });
          api.registerProvider({ id: "anthropic", label: "Anthropic", auth: [] });
          api.registerCliBackend({ id: "codex-cli", config: { command: "codex" } });
          api.registerCliBackend({ id: "claude-cli", config: { command: "claude" } });
        },
      },
    }));
    mocks.createJiti.mockImplementation(() => loadSetupModule);

    expect(resolvePluginSetupProvider({ provider: "openai", env: {} })?.id).toBe("openai");
    expect(resolvePluginSetupProvider({ provider: "anthropic", env: {} })?.id).toBe("anthropic");
    expect(setupRegistryTesting.getCacheSizes().setupProvider).toBe(1);
    expect(resolvePluginSetupProvider({ provider: "openai", env: {} })?.id).toBe("openai");

    expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })?.backend.id).toBe(
      "codex-cli",
    );
    expect(resolvePluginSetupCliBackend({ backend: "claude-cli", env: {} })?.backend.id).toBe(
      "claude-cli",
    );
    expect(setupRegistryTesting.getCacheSizes().setupCliBackend).toBe(1);
    expect(resolvePluginSetupCliBackend({ backend: "codex-cli", env: {} })?.backend.id).toBe(
      "codex-cli",
    );

    resolvePluginSetupRegistry({
      env: {},
      pluginIds: ["openai"],
    });
    resolvePluginSetupRegistry({
      env: {},
      pluginIds: ["anthropic"],
    });
    expect(setupRegistryTesting.getCacheSizes().setupRegistry).toBe(1);
    expect(loadSetupModule).toHaveBeenCalledTimes(7);
  });
});
