import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

type RegistryModule = typeof import("./registry.js");
type RuntimeModule = typeof import("./runtime.js");
type WebSearchProvidersRuntimeModule = typeof import("./web-search-providers.runtime.js");
type PluginAutoEnableModule = typeof import("../config/plugin-auto-enable.js");
type WebSearchProvidersSharedModule = typeof import("./web-search-providers.shared.js");
type PluginManifestRegistry = import("./manifest-registry.js").PluginManifestRegistry;
type LoadPluginManifestRegistryForPluginRegistry =
  typeof import("./plugin-registry.js").loadPluginManifestRegistryForPluginRegistry;
type LoadPluginManifestRegistryForInstalledIndex =
  typeof import("./manifest-registry-installed.js").loadPluginManifestRegistryForInstalledIndex;

const BUNDLED_WEB_SEARCH_PROVIDERS = [
  { pluginId: "brave", id: "brave", order: 10 },
  { pluginId: "google", id: "gemini", order: 20 },
  { pluginId: "xai", id: "grok", order: 30 },
  { pluginId: "moonshot", id: "kimi", order: 40 },
  { pluginId: "perplexity", id: "perplexity", order: 50 },
  { pluginId: "firecrawl", id: "firecrawl", order: 60 },
  { pluginId: "exa", id: "exa", order: 65 },
  { pluginId: "tavily", id: "tavily", order: 70 },
  { pluginId: "duckduckgo", id: "duckduckgo", order: 100 },
] as const;

let createEmptyPluginRegistry: RegistryModule["createEmptyPluginRegistry"];
let loadPluginManifestRegistryMock: ReturnType<
  typeof vi.fn<LoadPluginManifestRegistryForPluginRegistry>
>;
let loadInstalledPluginManifestRegistryMock: ReturnType<
  typeof vi.fn<LoadPluginManifestRegistryForInstalledIndex>
>;
let setActivePluginRegistry: RuntimeModule["setActivePluginRegistry"];
let resolvePluginWebSearchProviders: WebSearchProvidersRuntimeModule["resolvePluginWebSearchProviders"];
let resolveRuntimeWebSearchProviders: WebSearchProvidersRuntimeModule["resolveRuntimeWebSearchProviders"];
let loadOpenClawPluginsMock: ReturnType<typeof vi.fn>;
let loaderModule: typeof import("./loader.js");
let pluginAutoEnableModule: PluginAutoEnableModule;
let applyPluginAutoEnableSpy: ReturnType<typeof vi.fn>;
let webSearchProvidersSharedModule: WebSearchProvidersSharedModule;
let resetPluginRuntimeStateForTest: RuntimeModule["resetPluginRuntimeStateForTest"];
let clearLoadPluginMetadataSnapshotMemo: typeof import("./plugin-metadata-snapshot.js").clearLoadPluginMetadataSnapshotMemo;

const DEFAULT_WEB_SEARCH_WORKSPACE = "/tmp/workspace";
const EXPECTED_BUNDLED_RUNTIME_WEB_SEARCH_PROVIDER_KEYS = [
  "brave:brave",
  "duckduckgo:duckduckgo",
  "exa:exa",
  "firecrawl:firecrawl",
  "google:gemini",
  "xai:grok",
  "moonshot:kimi",
  "perplexity:perplexity",
  "tavily:tavily",
] as const;

function buildMockedWebSearchProviders(params?: {
  config?: { plugins?: Record<string, unknown> };
}) {
  const plugins = params?.config?.plugins as
    | {
        enabled?: boolean;
        allow?: string[];
        entries?: Record<string, { enabled?: boolean }>;
      }
    | undefined;
  if (plugins?.enabled === false) {
    return [];
  }
  const allow = Array.isArray(plugins?.allow) && plugins.allow.length > 0 ? plugins.allow : null;
  const entries = plugins?.entries ?? {};
  const webSearchProviders = BUNDLED_WEB_SEARCH_PROVIDERS.filter((provider) => {
    if (allow && !allow.includes(provider.pluginId)) {
      return false;
    }
    if (entries[provider.pluginId]?.enabled === false) {
      return false;
    }
    return true;
  }).map((provider) => ({
    pluginId: provider.pluginId,
    pluginName: provider.pluginId,
    source: "test" as const,
    provider: {
      id: provider.id,
      label: provider.id,
      hint: `${provider.id} provider`,
      envVars: [`${provider.id.toUpperCase()}_API_KEY`],
      placeholder: `${provider.id}-...`,
      signupUrl: `https://example.com/${provider.id}`,
      autoDetectOrder: provider.order,
      credentialPath: `plugins.entries.${provider.pluginId}.config.webSearch.apiKey`,
      getCredentialValue: () => "configured",
      setCredentialValue: () => {},
      createTool: () => ({
        description: provider.id,
        parameters: {},
        execute: async () => ({}),
      }),
    },
  }));
  return webSearchProviders;
}

function createBraveAllowConfig() {
  return {
    plugins: {
      allow: ["brave"],
    },
  };
}

function createWebSearchEnv(overrides?: Partial<NodeJS.ProcessEnv>) {
  return {
    OPENCLAW_HOME: "/tmp/openclaw-home",
    ...overrides,
  } as NodeJS.ProcessEnv;
}

function createSnapshotParams(params?: {
  config?: { plugins?: Record<string, unknown> };
  env?: NodeJS.ProcessEnv;
  bundledAllowlistCompat?: boolean;
  workspaceDir?: string;
}) {
  return {
    config: params?.config ?? createBraveAllowConfig(),
    env: params?.env ?? createWebSearchEnv(),
    bundledAllowlistCompat: params?.bundledAllowlistCompat ?? true,
    workspaceDir: params?.workspaceDir ?? DEFAULT_WEB_SEARCH_WORKSPACE,
  };
}

function toRuntimeProviderKeys(
  providers: ReturnType<WebSearchProvidersRuntimeModule["resolvePluginWebSearchProviders"]>,
) {
  return providers.map((provider) => `${provider.pluginId}:${provider.id}`);
}

function expectBundledRuntimeProviderKeys(
  providers: ReturnType<WebSearchProvidersRuntimeModule["resolvePluginWebSearchProviders"]>,
) {
  expect(toRuntimeProviderKeys(providers)).toEqual(
    EXPECTED_BUNDLED_RUNTIME_WEB_SEARCH_PROVIDER_KEYS,
  );
}

function createManifestRegistryFixture(): PluginManifestRegistry {
  return {
    plugins: [
      {
        id: "brave",
        origin: "bundled",
        rootDir: "/tmp/brave",
        source: "/tmp/brave/index.js",
        manifestPath: "/tmp/brave/openclaw.plugin.json",
        channels: [],
        providers: [],
        cliBackends: [],
        syntheticAuthRefs: [],
        nonSecretAuthMarkers: [],
        skills: [],
        hooks: [],
        configUiHints: { "webSearch.apiKey": { label: "key" } },
      },
      {
        id: "noise",
        origin: "bundled",
        rootDir: "/tmp/noise",
        source: "/tmp/noise/index.js",
        manifestPath: "/tmp/noise/openclaw.plugin.json",
        channels: [],
        providers: [],
        cliBackends: [],
        syntheticAuthRefs: [],
        nonSecretAuthMarkers: [],
        skills: [],
        hooks: [],
        configUiHints: { unrelated: { label: "nope" } },
      },
    ],
    diagnostics: [],
  };
}

function createWebSearchManifestRecord(params: {
  id: string;
  providerId: string;
}): PluginManifestRegistry["plugins"][number] {
  return {
    id: params.id,
    origin: "bundled",
    rootDir: `/tmp/${params.id}`,
    source: `/tmp/${params.id}/index.js`,
    manifestPath: `/tmp/${params.id}/openclaw.plugin.json`,
    channels: [],
    providers: [],
    cliBackends: [],
    syntheticAuthRefs: [],
    nonSecretAuthMarkers: [],
    skills: [],
    hooks: [],
    contracts: { webSearchProviders: [params.providerId] },
  };
}

function expectLoaderCallCount(count: number) {
  expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(count);
}

function requireRecord(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    throw new Error("Expected a non-array record");
  }
  return value as Record<string, unknown>;
}

function requireLastCallFirstArg(
  mock: { mock: { calls: readonly (readonly unknown[])[] } },
  label: string,
): Record<string, unknown> {
  const calls = mock.mock.calls;
  const call = calls[calls.length - 1];
  if (!call) {
    throw new Error(`${label} should have been called`);
  }
  return requireRecord(call[0]);
}

function requirePluginsConfig(params: Record<string, unknown>): Record<string, unknown> {
  const config = requireRecord(params.config);
  return requireRecord(config.plugins);
}

function expectScopedWebSearchCandidates(pluginIds: readonly string[]) {
  expect(loadInstalledPluginManifestRegistryMock).toHaveBeenCalled();
  expect(
    requireLastCallFirstArg(loadOpenClawPluginsMock, "loadOpenClawPlugins").onlyPluginIds,
  ).toEqual([...pluginIds]);
}

function expectAutoEnabledWebSearchLoad(params: {
  rawConfig: { plugins?: Record<string, unknown> };
  expectedAllow: readonly string[];
}) {
  expect(applyPluginAutoEnableSpy).toHaveBeenCalledWith({
    config: params.rawConfig,
    env: createWebSearchEnv(),
  });
  const loaderParams = requireLastCallFirstArg(loadOpenClawPluginsMock, "loadOpenClawPlugins");
  const plugins = requirePluginsConfig(loaderParams);
  expect(plugins.allow).toEqual([...params.expectedAllow]);
}

function expectSnapshotLoaderCalls(params: {
  config: { plugins?: Record<string, unknown> };
  env: NodeJS.ProcessEnv;
  mutate: () => void;
  expectedLoaderCalls: number;
}) {
  resolvePluginWebSearchProviders(
    createSnapshotParams({
      config: params.config,
      env: params.env,
    }),
  );
  params.mutate();
  resolvePluginWebSearchProviders(
    createSnapshotParams({
      config: params.config,
      env: params.env,
    }),
  );
  expectLoaderCallCount(params.expectedLoaderCalls);
}

function createRuntimeWebSearchProvider(params: {
  pluginId: string;
  pluginName: string;
  id: string;
  label: string;
  hint: string;
  envVar: string;
  signupUrl: string;
  credentialPath: string;
}) {
  return {
    pluginId: params.pluginId,
    pluginName: params.pluginName,
    provider: {
      id: params.id,
      label: params.label,
      hint: params.hint,
      envVars: [params.envVar],
      placeholder: `${params.id}-...`,
      signupUrl: params.signupUrl,
      autoDetectOrder: 1,
      credentialPath: params.credentialPath,
      getCredentialValue: () => "configured",
      setCredentialValue: () => {},
      createTool: () => ({
        description: params.id,
        parameters: {},
        execute: async () => ({}),
      }),
    },
    source: "test" as const,
  };
}

function createBraveRuntimeWebSearchProvider() {
  return createRuntimeWebSearchProvider({
    pluginId: "brave",
    pluginName: "Brave",
    id: "brave",
    label: "Brave Search",
    hint: "Brave runtime provider",
    envVar: "BRAVE_API_KEY",
    signupUrl: "https://example.com/brave",
    credentialPath: "plugins.entries.brave.config.webSearch.apiKey",
  });
}

function createActiveBraveRegistryFixture(params?: {
  includeResolutionWorkspaceDir?: boolean;
  activeWorkspaceDir?: string;
}) {
  const env = createWebSearchEnv();
  const rawConfig = createBraveAllowConfig();
  const { config, activationSourceConfig, autoEnabledReasons } =
    webSearchProvidersSharedModule.resolveBundledWebSearchResolutionConfig({
      config: rawConfig,
      bundledAllowlistCompat: true,
      ...(params?.includeResolutionWorkspaceDir
        ? { workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE }
        : {}),
      env,
    });
  const { cacheKey } = loaderModule.testing.resolvePluginLoadCacheContext({
    config,
    activationSourceConfig,
    autoEnabledReasons,
    workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
    env,
    onlyPluginIds: ["brave"],
    cache: true,
    activate: false,
  });
  const registry = createEmptyPluginRegistry();
  registry.plugins.push({ id: "brave", status: "loaded" } as never);
  registry.webSearchProviders.push(createBraveRuntimeWebSearchProvider());
  setActivePluginRegistry(registry, cacheKey, "default", params?.activeWorkspaceDir);

  return { env, rawConfig };
}

function expectRuntimeProviderResolution(
  providers: ReturnType<WebSearchProvidersRuntimeModule["resolveRuntimeWebSearchProviders"]>,
  expected: readonly string[],
) {
  expect(toRuntimeProviderKeys(providers)).toEqual([...expected]);
  expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
}

describe("resolvePluginWebSearchProviders", () => {
  beforeAll(async () => {
    loadPluginManifestRegistryMock = vi.fn<LoadPluginManifestRegistryForPluginRegistry>();
    loadInstalledPluginManifestRegistryMock = vi.fn<LoadPluginManifestRegistryForInstalledIndex>();
    vi.doMock("./manifest-registry.js", async () => {
      const actual =
        await vi.importActual<typeof import("./manifest-registry.js")>("./manifest-registry.js");
      return {
        ...actual,
        loadPluginManifestRegistry: (
          ...args: Parameters<LoadPluginManifestRegistryForPluginRegistry>
        ) => loadPluginManifestRegistryMock(...args),
      };
    });
    vi.doMock("./plugin-registry.js", async () => {
      const actual =
        await vi.importActual<typeof import("./plugin-registry.js")>("./plugin-registry.js");
      return {
        ...actual,
        loadPluginRegistrySnapshotWithMetadata: () => ({
          source: "derived",
          snapshot: {
            plugins: [
              {
                pluginId: "__test_manifest_registry_fixture__",
                origin: "bundled",
                enabled: true,
              },
            ],
          },
          diagnostics: [],
        }),
        loadPluginManifestRegistryForPluginRegistry: (
          ...args: Parameters<LoadPluginManifestRegistryForPluginRegistry>
        ) => loadPluginManifestRegistryMock(...args),
      };
    });
    vi.doMock("./manifest-registry-installed.js", async () => {
      const actual = await vi.importActual<typeof import("./manifest-registry-installed.js")>(
        "./manifest-registry-installed.js",
      );
      return {
        ...actual,
        loadPluginManifestRegistryForInstalledIndex: (
          ...args: Parameters<LoadPluginManifestRegistryForInstalledIndex>
        ) => loadInstalledPluginManifestRegistryMock(...args),
      };
    });

    ({ createEmptyPluginRegistry } = await import("./registry-empty.js"));
    loaderModule = await import("./loader.js");
    pluginAutoEnableModule = await import("../config/plugin-auto-enable.js");
    webSearchProvidersSharedModule = await import("./web-search-providers.shared.js");
    ({ resetPluginRuntimeStateForTest, setActivePluginRegistry } = await import("./runtime.js"));
    ({ clearLoadPluginMetadataSnapshotMemo } = await import("./plugin-metadata-snapshot.js"));
    ({ resolvePluginWebSearchProviders, resolveRuntimeWebSearchProviders } =
      await import("./web-search-providers.runtime.js"));
  });

  beforeEach(() => {
    clearLoadPluginMetadataSnapshotMemo();
    applyPluginAutoEnableSpy?.mockRestore();
    applyPluginAutoEnableSpy = vi
      .spyOn(pluginAutoEnableModule, "applyPluginAutoEnable")
      .mockImplementation(
        (params) =>
          ({
            config: params.config ?? {},
            changes: [],
            autoEnabledReasons: {},
          }) as ReturnType<PluginAutoEnableModule["applyPluginAutoEnable"]>,
      );
    loadPluginManifestRegistryMock.mockReset();
    loadPluginManifestRegistryMock.mockReturnValue(createManifestRegistryFixture());
    loadInstalledPluginManifestRegistryMock.mockReset();
    loadInstalledPluginManifestRegistryMock.mockReturnValue(createManifestRegistryFixture());
    loadOpenClawPluginsMock = vi
      .spyOn(loaderModule, "loadOpenClawPlugins")
      .mockImplementation((params) => {
        const registry = createEmptyPluginRegistry();
        registry.webSearchProviders = buildMockedWebSearchProviders(params);
        return registry;
      });
    resetPluginRuntimeStateForTest();
    vi.useRealTimers();
  });

  afterEach(() => {
    resetPluginRuntimeStateForTest();
    clearLoadPluginMetadataSnapshotMemo();
    vi.restoreAllMocks();
  });

  it("loads bundled providers through the plugin loader in alphabetical order", () => {
    const providers = resolvePluginWebSearchProviders({});

    expectBundledRuntimeProviderKeys(providers);
    expectLoaderCallCount(1);
  });

  it("loads manifest-declared web-search providers in setup mode", () => {
    const providers = resolvePluginWebSearchProviders({
      config: {
        plugins: {
          allow: ["brave"],
        },
      },
      mode: "setup",
    });

    expect(toRuntimeProviderKeys(providers)).toEqual(["brave:brave"]);
    expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
  });

  it("loads plugin web-search providers from the auto-enabled config snapshot", () => {
    const rawConfig = createBraveAllowConfig();
    const autoEnabledConfig = {
      plugins: {
        allow: ["brave", "perplexity"],
      },
    };
    applyPluginAutoEnableSpy.mockReturnValue({
      config: autoEnabledConfig,
      changes: [],
      autoEnabledReasons: {},
    });

    resolvePluginWebSearchProviders(createSnapshotParams({ config: rawConfig }));

    expectAutoEnabledWebSearchLoad({
      rawConfig,
      expectedAllow: ["brave", "perplexity"],
    });
  });

  it("scopes plugin loading to manifest-declared web-search candidates", () => {
    resolvePluginWebSearchProviders({});

    expectScopedWebSearchCandidates(["brave"]);
  });

  it("keeps allowlist web-search provider discovery scoped to the configured allowlist", () => {
    loadInstalledPluginManifestRegistryMock.mockReturnValueOnce({
      plugins: [
        createWebSearchManifestRecord({ id: "brave", providerId: "brave" }),
        createWebSearchManifestRecord({ id: "google", providerId: "gemini" }),
      ],
      diagnostics: [],
    });

    const providers = resolvePluginWebSearchProviders({
      config: {
        plugins: {
          allow: ["brave"],
          bundledDiscovery: "allowlist",
        },
      },
      bundledAllowlistCompat: true,
      env: createWebSearchEnv(),
      workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
    });

    expect(toRuntimeProviderKeys(providers)).toEqual(["brave:brave"]);
    expectScopedWebSearchCandidates(["brave"]);
    const loaderParams = requireLastCallFirstArg(loadOpenClawPluginsMock, "loadOpenClawPlugins");
    expect(requirePluginsConfig(loaderParams)).toEqual({
      allow: ["brave"],
      bundledDiscovery: "allowlist",
      entries: { brave: { enabled: true } },
    });
  });

  it("uses the active registry workspace for candidate discovery and snapshot loads when workspaceDir is omitted", () => {
    const env = createWebSearchEnv();
    const rawConfig = createBraveAllowConfig();

    setActivePluginRegistry(
      createEmptyPluginRegistry(),
      undefined,
      "default",
      "/tmp/runtime-workspace",
    );

    resolvePluginWebSearchProviders({
      config: rawConfig,
      bundledAllowlistCompat: true,
      env,
    });

    const manifestParams = requireLastCallFirstArg(
      loadInstalledPluginManifestRegistryMock,
      "loadPluginManifestRegistryForInstalledIndex",
    );
    expect(manifestParams.workspaceDir).toBe("/tmp/runtime-workspace");
    const loaderParams = requireLastCallFirstArg(loadOpenClawPluginsMock, "loadOpenClawPlugins");
    expect(loaderParams.workspaceDir).toBe("/tmp/runtime-workspace");
    expect(loaderParams.onlyPluginIds).toEqual(["brave"]);
  });
  it("reuses a compatible active registry for snapshot resolution when config is provided", () => {
    const { env, rawConfig } = createActiveBraveRegistryFixture();

    const providers = resolvePluginWebSearchProviders({
      config: rawConfig,
      bundledAllowlistCompat: true,
      workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
      env,
    });

    expectRuntimeProviderResolution(providers, ["brave:brave"]);
    expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
  });

  it("inherits workspaceDir from the active registry for compatible web-search snapshot reuse", () => {
    const { env, rawConfig } = createActiveBraveRegistryFixture({
      includeResolutionWorkspaceDir: true,
      activeWorkspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
    });

    const providers = resolvePluginWebSearchProviders({
      config: rawConfig,
      bundledAllowlistCompat: true,
      env,
    });

    expectRuntimeProviderResolution(providers, ["brave:brave"]);
    expect(loadOpenClawPluginsMock).not.toHaveBeenCalled();
  });

  it("uses the inherited active workspace for each web-search resolution", () => {
    const env = createWebSearchEnv();
    const rawConfig = createBraveAllowConfig();

    setActivePluginRegistry(createEmptyPluginRegistry(), undefined, "default", "/tmp/workspace-a");
    resolvePluginWebSearchProviders({
      config: rawConfig,
      bundledAllowlistCompat: true,
      env,
    });

    setActivePluginRegistry(createEmptyPluginRegistry(), undefined, "default", "/tmp/workspace-b");
    resolvePluginWebSearchProviders({
      config: rawConfig,
      bundledAllowlistCompat: true,
      env,
    });

    expectLoaderCallCount(2);
  });

  it("resolves current config contents when config changes in place", () => {
    const config = createBraveAllowConfig();
    const env = createWebSearchEnv({ OPENCLAW_HOME: "/tmp/openclaw-home-a" });

    expectSnapshotLoaderCalls({
      config,
      env,
      mutate: () => {
        config.plugins = { allow: ["perplexity"] };
      },
      expectedLoaderCalls: 2,
    });
  });

  it("resolves current env contents when env changes in place", () => {
    const config = createBraveAllowConfig();
    const env = createWebSearchEnv({ OPENCLAW_HOME: "/tmp/openclaw-home-a" });

    expectSnapshotLoaderCalls({
      config,
      env,
      mutate: () => {
        env.OPENCLAW_HOME = "/tmp/openclaw-home-b";
      },
      expectedLoaderCalls: 2,
    });
  });

  it("does not reuse snapshot provider loads across host Vitest env changes", () => {
    const originalVitest = process.env.VITEST;
    const config = {};
    const env = createWebSearchEnv();

    try {
      delete process.env.VITEST;
      resolvePluginWebSearchProviders(createSnapshotParams({ config, env }));

      process.env.VITEST = "1";
      resolvePluginWebSearchProviders(createSnapshotParams({ config, env }));
    } finally {
      if (originalVitest === undefined) {
        delete process.env.VITEST;
      } else {
        process.env.VITEST = originalVitest;
      }
    }

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

  it.each([
    {
      name: "prefers the active plugin registry for runtime resolution",
      setupRegistry: () => {
        const registry = createEmptyPluginRegistry();
        registry.webSearchProviders.push(
          createRuntimeWebSearchProvider({
            pluginId: "custom-search",
            pluginName: "Custom Search",
            id: "custom",
            label: "Custom Search",
            hint: "Custom runtime provider",
            envVar: "CUSTOM_SEARCH_API_KEY",
            signupUrl: "https://example.com/signup",
            credentialPath: "tools.web.search.custom.apiKey",
          }),
        );
        setActivePluginRegistry(registry);
      },
      params: {},
      expected: ["custom-search:custom"],
    },
    {
      name: "reuses a compatible active registry for runtime resolution when config is provided",
      setupRegistry: () => {
        const { env, rawConfig } = createActiveBraveRegistryFixture();
        return {
          config: rawConfig,
          bundledAllowlistCompat: true,
          workspaceDir: DEFAULT_WEB_SEARCH_WORKSPACE,
          env,
        };
      },
      expected: ["brave:brave"],
    },
  ] as const)("$name", ({ setupRegistry, params, expected }) => {
    const runtimeParams = setupRegistry() ?? params ?? {};
    const providers = resolveRuntimeWebSearchProviders(runtimeParams);

    expectRuntimeProviderResolution(providers, expected);
  });
});
