import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { withTempHome } from "./test-helpers.js";

async function writePluginFixture(params: {
  dir: string;
  id: string;
  schema: Record<string, unknown>;
  channels?: string[];
}) {
  await fs.mkdir(params.dir, { recursive: true });
  await fs.writeFile(
    path.join(params.dir, "index.js"),
    `export default { id: "${params.id}", register() {} };`,
    "utf-8",
  );
  const manifest: Record<string, unknown> = {
    id: params.id,
    configSchema: params.schema,
  };
  if (params.channels) {
    manifest.channels = params.channels;
  }
  await fs.writeFile(
    path.join(params.dir, "openclaw.plugin.json"),
    JSON.stringify(manifest, null, 2),
    "utf-8",
  );
}

describe("config plugin validation", () => {
  it("rejects missing plugin load paths", async () => {
    await withTempHome(async (home) => {
      process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
      vi.resetModules();
      const { validateConfigObjectWithPlugins } = await import("./config.js");
      const missingPath = path.join(home, "missing-plugin");
      const res = validateConfigObjectWithPlugins({
        agents: { list: [{ id: "pi" }] },
        plugins: { enabled: false, load: { paths: [missingPath] } },
      });
      expect(res.ok).toBe(false);
      if (!res.ok) {
        const hasIssue = res.issues.some(
          (issue) =>
            issue.path === "plugins.load.paths" && issue.message.includes("plugin path not found"),
        );
        expect(hasIssue).toBe(true);
      }
    });
  });

  it("rejects missing plugin ids in entries", async () => {
    await withTempHome(async (home) => {
      process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
      vi.resetModules();
      const { validateConfigObjectWithPlugins } = await import("./config.js");
      const res = validateConfigObjectWithPlugins({
        agents: { list: [{ id: "pi" }] },
        plugins: { enabled: false, entries: { "missing-plugin": { enabled: true } } },
      });
      expect(res.ok).toBe(false);
      if (!res.ok) {
        expect(res.issues).toContainEqual({
          path: "plugins.entries.missing-plugin",
          message: "plugin not found: missing-plugin",
        });
      }
    });
  });

  it("rejects missing plugin ids in allow/deny/slots", async () => {
    await withTempHome(async (home) => {
      process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
      vi.resetModules();
      const { validateConfigObjectWithPlugins } = await import("./config.js");
      const res = validateConfigObjectWithPlugins({
        agents: { list: [{ id: "pi" }] },
        plugins: {
          enabled: false,
          allow: ["missing-allow"],
          deny: ["missing-deny"],
          slots: { memory: "missing-slot" },
        },
      });
      expect(res.ok).toBe(false);
      if (!res.ok) {
        expect(res.issues).toEqual(
          expect.arrayContaining([
            { path: "plugins.allow", message: "plugin not found: missing-allow" },
            { path: "plugins.deny", message: "plugin not found: missing-deny" },
            { path: "plugins.slots.memory", message: "plugin not found: missing-slot" },
          ]),
        );
      }
    });
  });

  it("surfaces plugin config diagnostics", async () => {
    await withTempHome(async (home) => {
      process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
      const pluginDir = path.join(home, "bad-plugin");
      await writePluginFixture({
        dir: pluginDir,
        id: "bad-plugin",
        schema: {
          type: "object",
          additionalProperties: false,
          properties: {
            value: { type: "boolean" },
          },
          required: ["value"],
        },
      });

      vi.resetModules();
      const { validateConfigObjectWithPlugins } = await import("./config.js");
      const res = validateConfigObjectWithPlugins({
        agents: { list: [{ id: "pi" }] },
        plugins: {
          enabled: true,
          load: { paths: [pluginDir] },
          entries: { "bad-plugin": { config: { value: "nope" } } },
        },
      });
      expect(res.ok).toBe(false);
      if (!res.ok) {
        const hasIssue = res.issues.some(
          (issue) =>
            issue.path === "plugins.entries.bad-plugin.config" &&
            issue.message.includes("invalid config"),
        );
        expect(hasIssue).toBe(true);
      }
    });
  });

  it("accepts known plugin ids", async () => {
    await withTempHome(async (home) => {
      process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
      vi.resetModules();
      const { validateConfigObjectWithPlugins } = await import("./config.js");
      const res = validateConfigObjectWithPlugins({
        agents: { list: [{ id: "pi" }] },
        plugins: { enabled: false, entries: { discord: { enabled: true } } },
      });
      expect(res.ok).toBe(true);
    });
  });

  it("accepts plugin heartbeat targets", async () => {
    await withTempHome(async (home) => {
      process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
      const pluginDir = path.join(home, "bluebubbles-plugin");
      await writePluginFixture({
        dir: pluginDir,
        id: "bluebubbles-plugin",
        channels: ["bluebubbles"],
        schema: { type: "object" },
      });

      vi.resetModules();
      const { validateConfigObjectWithPlugins } = await import("./config.js");
      const res = validateConfigObjectWithPlugins({
        agents: { defaults: { heartbeat: { target: "bluebubbles" } }, list: [{ id: "pi" }] },
        plugins: { enabled: false, load: { paths: [pluginDir] } },
      });
      expect(res.ok).toBe(true);
    });
  });

  it("rejects unknown heartbeat targets", async () => {
    await withTempHome(async (home) => {
      process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw");
      vi.resetModules();
      const { validateConfigObjectWithPlugins } = await import("./config.js");
      const res = validateConfigObjectWithPlugins({
        agents: { defaults: { heartbeat: { target: "not-a-channel" } }, list: [{ id: "pi" }] },
      });
      expect(res.ok).toBe(false);
      if (!res.ok) {
        expect(res.issues).toContainEqual({
          path: "agents.defaults.heartbeat.target",
          message: "unknown heartbeat target: not-a-channel",
        });
      }
    });
  });
});
