import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolvePluginInstallDir } from "./install.js";
import {
  removePluginFromConfig,
  resolveUninstallDirectoryTarget,
  uninstallPlugin,
} from "./uninstall.js";

describe("removePluginFromConfig", () => {
  it("removes plugin from entries", () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
          "other-plugin": { enabled: true },
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.entries).toEqual({ "other-plugin": { enabled: true } });
    expect(actions.entry).toBe(true);
  });

  it("removes plugin from installs", () => {
    const config: OpenClawConfig = {
      plugins: {
        installs: {
          "my-plugin": { source: "npm", spec: "my-plugin@1.0.0" },
          "other-plugin": { source: "npm", spec: "other-plugin@1.0.0" },
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.installs).toEqual({
      "other-plugin": { source: "npm", spec: "other-plugin@1.0.0" },
    });
    expect(actions.install).toBe(true);
  });

  it("removes plugin from allowlist", () => {
    const config: OpenClawConfig = {
      plugins: {
        allow: ["my-plugin", "other-plugin"],
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.allow).toEqual(["other-plugin"]);
    expect(actions.allowlist).toBe(true);
  });

  it("removes linked path from load.paths", () => {
    const config: OpenClawConfig = {
      plugins: {
        installs: {
          "my-plugin": {
            source: "path",
            sourcePath: "/path/to/plugin",
            installPath: "/path/to/plugin",
          },
        },
        load: {
          paths: ["/path/to/plugin", "/other/path"],
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.load?.paths).toEqual(["/other/path"]);
    expect(actions.loadPath).toBe(true);
  });

  it("cleans up load when removing the only linked path", () => {
    const config: OpenClawConfig = {
      plugins: {
        installs: {
          "my-plugin": {
            source: "path",
            sourcePath: "/path/to/plugin",
            installPath: "/path/to/plugin",
          },
        },
        load: {
          paths: ["/path/to/plugin"],
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.load).toBeUndefined();
    expect(actions.loadPath).toBe(true);
  });

  it("clears memory slot when uninstalling active memory plugin", () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "memory-plugin": { enabled: true },
        },
        slots: {
          memory: "memory-plugin",
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "memory-plugin");

    expect(result.plugins?.slots?.memory).toBe("memory-core");
    expect(actions.memorySlot).toBe(true);
  });

  it("does not modify memory slot when uninstalling non-memory plugin", () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        slots: {
          memory: "memory-core",
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.slots?.memory).toBe("memory-core");
    expect(actions.memorySlot).toBe(false);
  });

  it("removes plugins object when uninstall leaves only empty slots", () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        slots: {},
      },
    };

    const { config: result } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.slots).toBeUndefined();
  });

  it("cleans up empty slots object", () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        slots: {},
      },
    };

    const { config: result } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins).toBeUndefined();
  });

  it("handles plugin that only exists in entries", () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.entries).toBeUndefined();
    expect(actions.entry).toBe(true);
    expect(actions.install).toBe(false);
  });

  it("handles plugin that only exists in installs", () => {
    const config: OpenClawConfig = {
      plugins: {
        installs: {
          "my-plugin": { source: "npm", spec: "my-plugin@1.0.0" },
        },
      },
    };

    const { config: result, actions } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.installs).toBeUndefined();
    expect(actions.install).toBe(true);
    expect(actions.entry).toBe(false);
  });

  it("cleans up empty plugins object", () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
      },
    };

    const { config: result } = removePluginFromConfig(config, "my-plugin");

    // After removing the only entry, entries should be undefined
    expect(result.plugins?.entries).toBeUndefined();
  });

  it("preserves other config values", () => {
    const config: OpenClawConfig = {
      plugins: {
        enabled: true,
        deny: ["denied-plugin"],
        entries: {
          "my-plugin": { enabled: true },
        },
      },
    };

    const { config: result } = removePluginFromConfig(config, "my-plugin");

    expect(result.plugins?.enabled).toBe(true);
    expect(result.plugins?.deny).toEqual(["denied-plugin"]);
  });
});

describe("uninstallPlugin", () => {
  let tempDir: string;

  beforeEach(async () => {
    tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "uninstall-test-"));
  });

  afterEach(async () => {
    await fs.rm(tempDir, { recursive: true, force: true });
  });

  it("returns error when plugin not found", async () => {
    const config: OpenClawConfig = {};

    const result = await uninstallPlugin({
      config,
      pluginId: "nonexistent",
    });

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toBe("Plugin not found: nonexistent");
    }
  });

  it("removes config entries", async () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        installs: {
          "my-plugin": { source: "npm", spec: "my-plugin@1.0.0" },
        },
      },
    };

    const result = await uninstallPlugin({
      config,
      pluginId: "my-plugin",
      deleteFiles: false,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.config.plugins?.entries).toBeUndefined();
      expect(result.config.plugins?.installs).toBeUndefined();
      expect(result.actions.entry).toBe(true);
      expect(result.actions.install).toBe(true);
    }
  });

  it("deletes directory when deleteFiles is true", async () => {
    const pluginId = "my-plugin";
    const extensionsDir = path.join(tempDir, "extensions");
    const pluginDir = resolvePluginInstallDir(pluginId, extensionsDir);
    await fs.mkdir(pluginDir, { recursive: true });
    await fs.writeFile(path.join(pluginDir, "index.js"), "// plugin");

    const config: OpenClawConfig = {
      plugins: {
        entries: {
          [pluginId]: { enabled: true },
        },
        installs: {
          [pluginId]: {
            source: "npm",
            spec: `${pluginId}@1.0.0`,
            installPath: pluginDir,
          },
        },
      },
    };

    try {
      const result = await uninstallPlugin({
        config,
        pluginId,
        deleteFiles: true,
        extensionsDir,
      });

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.actions.directory).toBe(true);
        await expect(fs.access(pluginDir)).rejects.toThrow();
      }
    } finally {
      await fs.rm(pluginDir, { recursive: true, force: true });
    }
  });

  it("preserves directory for linked plugins", async () => {
    const pluginDir = path.join(tempDir, "my-plugin");
    await fs.mkdir(pluginDir, { recursive: true });
    await fs.writeFile(path.join(pluginDir, "index.js"), "// plugin");

    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        installs: {
          "my-plugin": {
            source: "path",
            sourcePath: pluginDir,
            installPath: pluginDir,
          },
        },
        load: {
          paths: [pluginDir],
        },
      },
    };

    const result = await uninstallPlugin({
      config,
      pluginId: "my-plugin",
      deleteFiles: true,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.actions.directory).toBe(false);
      expect(result.actions.loadPath).toBe(true);
      // Directory should still exist
      await expect(fs.access(pluginDir)).resolves.toBeUndefined();
    }
  });

  it("does not delete directory when deleteFiles is false", async () => {
    const pluginDir = path.join(tempDir, "my-plugin");
    await fs.mkdir(pluginDir, { recursive: true });
    await fs.writeFile(path.join(pluginDir, "index.js"), "// plugin");

    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        installs: {
          "my-plugin": {
            source: "npm",
            spec: "my-plugin@1.0.0",
            installPath: pluginDir,
          },
        },
      },
    };

    const result = await uninstallPlugin({
      config,
      pluginId: "my-plugin",
      deleteFiles: false,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.actions.directory).toBe(false);
      // Directory should still exist
      await expect(fs.access(pluginDir)).resolves.toBeUndefined();
    }
  });

  it("succeeds even if directory does not exist", async () => {
    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        installs: {
          "my-plugin": {
            source: "npm",
            spec: "my-plugin@1.0.0",
            installPath: "/nonexistent/path",
          },
        },
      },
    };

    const result = await uninstallPlugin({
      config,
      pluginId: "my-plugin",
      deleteFiles: true,
    });

    // Should succeed; directory deletion failure is not fatal
    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.actions.directory).toBe(false);
      expect(result.warnings).toEqual([]);
    }
  });

  it("returns a warning when directory deletion fails unexpectedly", async () => {
    const pluginId = "my-plugin";
    const extensionsDir = path.join(tempDir, "extensions");
    const pluginDir = resolvePluginInstallDir(pluginId, extensionsDir);
    await fs.mkdir(pluginDir, { recursive: true });
    await fs.writeFile(path.join(pluginDir, "index.js"), "// plugin");

    const config: OpenClawConfig = {
      plugins: {
        entries: {
          [pluginId]: { enabled: true },
        },
        installs: {
          [pluginId]: {
            source: "npm",
            spec: `${pluginId}@1.0.0`,
            installPath: pluginDir,
          },
        },
      },
    };

    const rmSpy = vi.spyOn(fs, "rm").mockRejectedValueOnce(new Error("permission denied"));
    try {
      const result = await uninstallPlugin({
        config,
        pluginId,
        deleteFiles: true,
        extensionsDir,
      });

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.actions.directory).toBe(false);
        expect(result.warnings).toHaveLength(1);
        expect(result.warnings[0]).toContain("Failed to remove plugin directory");
      }
    } finally {
      rmSpy.mockRestore();
    }
  });

  it("never deletes arbitrary configured install paths", async () => {
    const outsideDir = path.join(tempDir, "outside-dir");
    const extensionsDir = path.join(tempDir, "extensions");
    await fs.mkdir(outsideDir, { recursive: true });
    await fs.writeFile(path.join(outsideDir, "index.js"), "// keep me");

    const config: OpenClawConfig = {
      plugins: {
        entries: {
          "my-plugin": { enabled: true },
        },
        installs: {
          "my-plugin": {
            source: "npm",
            spec: "my-plugin@1.0.0",
            installPath: outsideDir,
          },
        },
      },
    };

    const result = await uninstallPlugin({
      config,
      pluginId: "my-plugin",
      deleteFiles: true,
      extensionsDir,
    });

    expect(result.ok).toBe(true);
    if (result.ok) {
      expect(result.actions.directory).toBe(false);
      await expect(fs.access(outsideDir)).resolves.toBeUndefined();
    }
  });
});

describe("resolveUninstallDirectoryTarget", () => {
  it("returns null for linked plugins", () => {
    expect(
      resolveUninstallDirectoryTarget({
        pluginId: "my-plugin",
        hasInstall: true,
        installRecord: {
          source: "path",
          sourcePath: "/tmp/my-plugin",
          installPath: "/tmp/my-plugin",
        },
      }),
    ).toBeNull();
  });

  it("falls back to default path when configured installPath is untrusted", () => {
    const extensionsDir = path.join(os.tmpdir(), "openclaw-uninstall-safe");
    const target = resolveUninstallDirectoryTarget({
      pluginId: "my-plugin",
      hasInstall: true,
      installRecord: {
        source: "npm",
        spec: "my-plugin@1.0.0",
        installPath: "/tmp/not-openclaw-extensions/my-plugin",
      },
      extensionsDir,
    });

    expect(target).toBe(resolvePluginInstallDir("my-plugin", extensionsDir));
  });
});
