import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { withEnvAsync } from "../test-utils/env.js";
import { withTempDir } from "../test-utils/temp-dir.js";
import {
  cleanupTrackedTempDirsAsync,
  makeTrackedTempDirAsync,
} from "./test-helpers/fs-fixtures.js";

const installPluginFromPathMock = vi.fn();
const fetchWithSsrFGuardMock = vi.hoisted(() =>
  vi.fn(async (params: { url: string; init?: RequestInit }) => {
    // Keep unit tests focused on guarded call sites, not AbortSignal timer behavior.
    const { signal: _signal, ...init } = params.init ?? {};
    const response = await fetch(params.url, init);
    return {
      response,
      finalUrl: params.url,
      release: async () => {
        await response.body?.cancel().catch(() => undefined);
      },
    };
  }),
);
const runCommandWithTimeoutMock = vi.hoisted(() => vi.fn());
let installPluginFromMarketplace: typeof import("./marketplace.js").installPluginFromMarketplace;
let listMarketplacePlugins: typeof import("./marketplace.js").listMarketplacePlugins;
let resolveMarketplaceInstallShortcut: typeof import("./marketplace.js").resolveMarketplaceInstallShortcut;
const tempOutsideDirs: string[] = [];

vi.mock("./install.js", () => ({
  installPluginFromPath: (...args: unknown[]) => installPluginFromPathMock(...args),
}));

vi.mock("../infra/net/fetch-guard.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../infra/net/fetch-guard.js")>();
  return {
    ...actual,
    fetchWithSsrFGuard: (params: { url: string; init?: RequestInit }) =>
      fetchWithSsrFGuardMock(params),
  };
});

vi.mock("../process/exec.js", () => ({
  runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
}));

beforeAll(async () => {
  ({ installPluginFromMarketplace, listMarketplacePlugins, resolveMarketplaceInstallShortcut } =
    await import("./marketplace.js"));
});

async function listMarketplaceDownloadTempDirs(): Promise<string[]> {
  const entries = await fs.readdir(os.tmpdir(), { withFileTypes: true });
  return entries
    .filter(
      (entry) => entry.isDirectory() && entry.name.startsWith("openclaw-marketplace-download-"),
    )
    .map((entry) => entry.name)
    .toSorted();
}

async function writeMarketplaceManifest(rootDir: string, manifest: unknown): Promise<string> {
  const manifestPath = path.join(rootDir, ".claude-plugin", "marketplace.json");
  await fs.mkdir(path.dirname(manifestPath), { recursive: true });
  await fs.writeFile(manifestPath, JSON.stringify(manifest));
  return manifestPath;
}

async function writeRemoteMarketplaceFixture(params: {
  repoDir: string;
  manifest: unknown;
  pluginDir?: string;
  pluginFile?: string;
}) {
  await fs.mkdir(path.join(params.repoDir, ".claude-plugin"), { recursive: true });
  if (params.pluginDir) {
    await fs.mkdir(path.join(params.repoDir, params.pluginDir), { recursive: true });
  }
  if (params.pluginFile) {
    const pluginFilePath = path.join(params.repoDir, params.pluginFile);
    await fs.mkdir(path.dirname(pluginFilePath), { recursive: true });
    await fs.writeFile(pluginFilePath, "plugin fixture");
  }
  await fs.writeFile(
    path.join(params.repoDir, ".claude-plugin", "marketplace.json"),
    JSON.stringify(params.manifest),
  );
}

async function writeLocalMarketplaceFixture(params: {
  rootDir: string;
  manifest: unknown;
  pluginDir?: string;
}) {
  if (params.pluginDir) {
    await fs.mkdir(params.pluginDir, { recursive: true });
  }
  return writeMarketplaceManifest(params.rootDir, params.manifest);
}

function mockRemoteMarketplaceClone(params: {
  manifest: unknown;
  pluginDir?: string;
  pluginFile?: string;
}) {
  runCommandWithTimeoutMock.mockImplementationOnce(async (argv: string[]) => {
    const repoDir = argv.at(-1);
    expect(typeof repoDir).toBe("string");
    await writeRemoteMarketplaceFixture({
      repoDir: repoDir as string,
      manifest: params.manifest,
      ...(params.pluginDir ? { pluginDir: params.pluginDir } : {}),
      ...(params.pluginFile ? { pluginFile: params.pluginFile } : {}),
    });
    return { code: 0, stdout: "", stderr: "", killed: false };
  });
}

function mockRemoteMarketplaceCloneWithOutsideSymlink(params: {
  manifest: unknown;
  symlinkPath: string;
}) {
  runCommandWithTimeoutMock.mockImplementationOnce(async (argv: string[]) => {
    const repoDir = argv.at(-1);
    expect(typeof repoDir).toBe("string");
    await writeRemoteMarketplaceFixture({
      repoDir: repoDir as string,
      manifest: params.manifest,
    });
    const outsideDir = await makeTrackedTempDirAsync(
      "openclaw-marketplace-outside",
      tempOutsideDirs,
    );
    await fs.mkdir(path.dirname(path.join(repoDir as string, params.symlinkPath)), {
      recursive: true,
    });
    await fs.symlink(outsideDir, path.join(repoDir as string, params.symlinkPath));
    return { code: 0, stdout: "", stderr: "", killed: false };
  });
}

async function expectRemoteMarketplaceError(params: { manifest: unknown; expectedError: string }) {
  mockRemoteMarketplaceClone({ manifest: params.manifest });

  const result = await listMarketplacePlugins({ marketplace: "owner/repo" });

  expect(result).toEqual({
    ok: false,
    error: params.expectedError,
  });
  expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
}

function expectRemoteMarketplaceInstallResult(result: unknown) {
  expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
  expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(
    ["git", "clone", "--depth", "1", "https://github.com/owner/repo.git", expect.any(String)],
    { timeoutMs: 120_000 },
  );
  expect(installPluginFromPathMock).toHaveBeenCalledWith(
    expect.objectContaining({
      path: expect.stringMatching(/[\\/]repo[\\/]plugins[\\/]frontend-design$/),
    }),
  );
  expect(result).toMatchObject({
    ok: true,
    pluginId: "frontend-design",
    marketplacePlugin: "frontend-design",
    marketplaceSource: "owner/repo",
  });
}

function expectMarketplaceManifestListing(
  result: Awaited<ReturnType<typeof import("./marketplace.js").listMarketplacePlugins>>,
) {
  expect(result.ok).toBe(true);
  if (!result.ok) {
    throw new Error("expected marketplace listing to succeed");
  }
  expect(result.sourceLabel.replaceAll("\\", "/")).toContain(".claude-plugin/marketplace.json");
  expect(result.manifest).toEqual({
    name: "Example Marketplace",
    version: "1.0.0",
    plugins: [
      {
        name: "frontend-design",
        version: "0.1.0",
        description: "Design system bundle",
        source: { kind: "path", path: "./plugins/frontend-design" },
      },
    ],
  });
}

function expectLocalMarketplaceInstallResult(params: {
  result: unknown;
  pluginDir: string;
  marketplaceSource: string;
}) {
  expect(installPluginFromPathMock).toHaveBeenCalledWith(
    expect.objectContaining({
      path: params.pluginDir,
    }),
  );
  expect(params.result).toMatchObject({
    ok: true,
    pluginId: "frontend-design",
    marketplacePlugin: "frontend-design",
    marketplaceSource: params.marketplaceSource,
  });
}

describe("marketplace plugins", () => {
  afterEach(async () => {
    fetchWithSsrFGuardMock.mockClear();
    installPluginFromPathMock.mockReset();
    runCommandWithTimeoutMock.mockReset();
    vi.unstubAllGlobals();
    await cleanupTrackedTempDirsAsync(tempOutsideDirs);
  });

  it("lists plugins from a local marketplace root", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      await writeMarketplaceManifest(rootDir, {
        name: "Example Marketplace",
        version: "1.0.0",
        plugins: [
          {
            name: "frontend-design",
            version: "0.1.0",
            description: "Design system bundle",
            source: "./plugins/frontend-design",
          },
        ],
      });

      expectMarketplaceManifestListing(await listMarketplacePlugins({ marketplace: rootDir }));
    });
  });

  it("resolves relative plugin paths against the marketplace root", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const pluginDir = path.join(rootDir, "plugins", "frontend-design");
      const manifestPath = await writeLocalMarketplaceFixture({
        rootDir,
        pluginDir,
        manifest: {
          plugins: [
            {
              name: "frontend-design",
              source: "./plugins/frontend-design",
            },
          ],
        },
      });
      installPluginFromPathMock.mockResolvedValue({
        ok: true,
        pluginId: "frontend-design",
        targetDir: "/tmp/frontend-design",
        version: "0.1.0",
        extensions: ["index.ts"],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expectLocalMarketplaceInstallResult({
        result,
        pluginDir,
        marketplaceSource: path.join(rootDir, ".claude-plugin", "marketplace.json"),
      });
    });
  });

  it("preserves the logical local install path instead of canonicalizing it", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const canonicalRootDir = await fs.realpath(rootDir);
      const pluginDir = path.join(rootDir, "plugins", "frontend-design");
      const canonicalPluginDir = path.join(canonicalRootDir, "plugins", "frontend-design");
      const manifestPath = await writeLocalMarketplaceFixture({
        rootDir,
        pluginDir,
        manifest: {
          plugins: [
            {
              name: "frontend-design",
              source: "./plugins/frontend-design",
            },
          ],
        },
      });
      installPluginFromPathMock.mockResolvedValue({
        ok: true,
        pluginId: "frontend-design",
        targetDir: "/tmp/frontend-design",
        version: "0.1.0",
        extensions: ["index.ts"],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expectLocalMarketplaceInstallResult({
        result,
        pluginDir,
        marketplaceSource: manifestPath,
      });
      if (canonicalPluginDir !== pluginDir) {
        expect(installPluginFromPathMock).not.toHaveBeenCalledWith(
          expect.objectContaining({
            path: canonicalPluginDir,
          }),
        );
      }
    });
  });

  it("passes dangerous force unsafe install through to marketplace path installs", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const pluginDir = path.join(rootDir, "plugins", "frontend-design");
      const manifestPath = await writeLocalMarketplaceFixture({
        rootDir,
        pluginDir,
        manifest: {
          plugins: [
            {
              name: "frontend-design",
              source: "./plugins/frontend-design",
            },
          ],
        },
      });
      installPluginFromPathMock.mockResolvedValue({
        ok: true,
        pluginId: "frontend-design",
        targetDir: "/tmp/frontend-design",
        version: "0.1.0",
        extensions: ["index.ts"],
      });

      await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
        dangerouslyForceUnsafeInstall: true,
      });

      expect(installPluginFromPathMock).toHaveBeenCalledWith(
        expect.objectContaining({
          path: pluginDir,
          dangerouslyForceUnsafeInstall: true,
        }),
      );
    });
  });

  it("resolves Claude-style plugin@marketplace shortcuts from known_marketplaces.json", async () => {
    await withTempDir("openclaw-marketplace-test-", async (homeDir) => {
      const openClawHome = path.join(homeDir, "openclaw-home");
      await fs.mkdir(path.join(homeDir, ".claude", "plugins"), { recursive: true });
      await fs.mkdir(openClawHome, { recursive: true });
      await fs.writeFile(
        path.join(homeDir, ".claude", "plugins", "known_marketplaces.json"),
        JSON.stringify({
          "claude-plugins-official": {
            source: {
              source: "github",
              repo: "anthropics/claude-plugins-official",
            },
            installLocation: path.join(homeDir, ".claude", "plugins", "marketplaces", "official"),
          },
        }),
      );

      const shortcut = await withEnvAsync(
        { HOME: homeDir, OPENCLAW_HOME: openClawHome },
        async () => await resolveMarketplaceInstallShortcut("superpowers@claude-plugins-official"),
      );

      expect(shortcut).toEqual({
        ok: true,
        plugin: "superpowers",
        marketplaceName: "claude-plugins-official",
        marketplaceSource: "claude-plugins-official",
      });
    });
  });

  it("installs remote marketplace plugins from relative paths inside the cloned repo", async () => {
    mockRemoteMarketplaceClone({
      pluginDir: path.join("plugins", "frontend-design"),
      manifest: {
        plugins: [
          {
            name: "frontend-design",
            source: "./plugins/frontend-design",
          },
        ],
      },
    });
    installPluginFromPathMock.mockResolvedValue({
      ok: true,
      pluginId: "frontend-design",
      targetDir: "/tmp/frontend-design",
      version: "0.1.0",
      extensions: ["index.ts"],
    });

    const result = await installPluginFromMarketplace({
      marketplace: "owner/repo",
      plugin: "frontend-design",
    });

    expectRemoteMarketplaceInstallResult(result);
  });

  it("preserves remote marketplace file path sources inside the cloned repo", async () => {
    mockRemoteMarketplaceClone({
      pluginFile: path.join("plugins", "frontend-design.tgz"),
      manifest: {
        plugins: [
          {
            name: "frontend-design",
            source: "./plugins/frontend-design.tgz",
          },
        ],
      },
    });
    installPluginFromPathMock.mockResolvedValue({
      ok: true,
      pluginId: "frontend-design",
      targetDir: "/tmp/frontend-design",
      version: "0.1.0",
      extensions: ["index.ts"],
    });

    const result = await installPluginFromMarketplace({
      marketplace: "owner/repo",
      plugin: "frontend-design",
    });

    expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
    expect(installPluginFromPathMock).toHaveBeenCalledWith(
      expect.objectContaining({
        path: expect.stringMatching(/[\\/]repo[\\/]plugins[\\/]frontend-design\.tgz$/),
      }),
    );
    expect(result).toMatchObject({
      ok: true,
      pluginId: "frontend-design",
      marketplacePlugin: "frontend-design",
      marketplaceSource: "owner/repo",
    });
  });

  it("lists remote marketplace file path sources inside the cloned repo", async () => {
    mockRemoteMarketplaceClone({
      pluginFile: path.join("plugins", "frontend-design.tgz"),
      manifest: {
        plugins: [
          {
            name: "frontend-design",
            source: "./plugins/frontend-design.tgz",
          },
        ],
      },
    });

    const result = await listMarketplacePlugins({ marketplace: "owner/repo" });

    expect(result).toEqual({
      ok: true,
      manifest: {
        name: undefined,
        version: undefined,
        plugins: [
          {
            name: "frontend-design",
            description: undefined,
            version: undefined,
            source: {
              kind: "path",
              path: "./plugins/frontend-design.tgz",
            },
          },
        ],
      },
      sourceLabel: "owner/repo",
    });
  });

  it.runIf(process.platform !== "win32")(
    "rejects remote marketplace plugin paths that resolve through symlinks outside the cloned repo",
    async () => {
      mockRemoteMarketplaceCloneWithOutsideSymlink({
        symlinkPath: "plugins/evil-link",
        manifest: {
          plugins: [
            {
              name: "frontend-design",
              source: "./plugins/evil-link",
            },
          ],
        },
      });

      const result = await installPluginFromMarketplace({
        marketplace: "owner/repo",
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error:
          'invalid marketplace entry "frontend-design" in owner/repo: ' +
          "plugin source escapes marketplace root: ./plugins/evil-link",
      });
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
    },
  );

  it("returns a structured error for archive downloads with an empty response body", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const release = vi.fn(async () => undefined);
      fetchWithSsrFGuardMock.mockResolvedValueOnce({
        response: new Response(null, { status: 200 }),
        finalUrl: "https://example.com/frontend-design.tgz",
        release,
      });
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error: "failed to download https://example.com/frontend-design.tgz: empty response body",
      });
      expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
        expect.objectContaining({
          url: "https://example.com/frontend-design.tgz",
          timeoutMs: 120_000,
          auditContext: "marketplace-plugin-download",
        }),
      );
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
      expect(release).toHaveBeenCalledTimes(1);
    });
  });

  it("returns a structured error for invalid archive URLs", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://%/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error: "failed to download https://%/frontend-design.tgz: Invalid URL",
      });
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
      expect(fetchWithSsrFGuardMock).not.toHaveBeenCalled();
    });
  });

  it("rejects Windows drive-relative archive filenames from redirects", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      fetchWithSsrFGuardMock.mockResolvedValueOnce({
        response: new Response(new Blob([Buffer.from("tgz-bytes")]), {
          status: 200,
        }),
        finalUrl: "https://cdn.example.com/C:plugin.tgz",
        release: vi.fn(async () => undefined),
      });
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error:
          "failed to download https://example.com/frontend-design.tgz: invalid download filename",
      });
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
    });
  });

  it("falls back to the default archive timeout when the caller passes NaN", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      fetchWithSsrFGuardMock.mockResolvedValueOnce({
        response: new Response(new Blob([Buffer.from("tgz-bytes")]), {
          status: 200,
        }),
        finalUrl: "https://cdn.example.com/releases/12345",
        release: vi.fn(async () => undefined),
      });
      installPluginFromPathMock.mockResolvedValue({
        ok: true,
        pluginId: "frontend-design",
        targetDir: "/tmp/frontend-design",
        version: "0.1.0",
        extensions: ["index.ts"],
      });
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
        timeoutMs: Number.NaN,
      });

      expect(result).toMatchObject({
        ok: true,
        pluginId: "frontend-design",
      });
      expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
        expect.objectContaining({
          url: "https://example.com/frontend-design.tgz",
          timeoutMs: 120_000,
          auditContext: "marketplace-plugin-download",
        }),
      );
    });
  });

  it("downloads archive plugin sources through the SSRF guard", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const release = vi.fn(async () => {
        throw new Error("dispatcher close failed");
      });
      fetchWithSsrFGuardMock.mockResolvedValueOnce({
        response: new Response(new Blob([Buffer.from("tgz-bytes")]), {
          status: 200,
        }),
        finalUrl: "https://cdn.example.com/releases/12345",
        release,
      });
      installPluginFromPathMock.mockResolvedValue({
        ok: true,
        pluginId: "frontend-design",
        targetDir: "/tmp/frontend-design",
        version: "0.1.0",
        extensions: ["index.ts"],
      });
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toMatchObject({
        ok: true,
        pluginId: "frontend-design",
        marketplacePlugin: "frontend-design",
        marketplaceSource: manifestPath,
      });
      expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
        expect.objectContaining({
          url: "https://example.com/frontend-design.tgz",
          timeoutMs: 120_000,
          auditContext: "marketplace-plugin-download",
        }),
      );
      expect(installPluginFromPathMock).toHaveBeenCalledWith(
        expect.objectContaining({
          path: expect.stringMatching(/[\\/]frontend-design\.tgz$/),
        }),
      );
      expect(release).toHaveBeenCalledTimes(1);
    });
  });

  it("rejects non-streaming archive responses before buffering them", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const arrayBuffer = vi.fn(async () => new Uint8Array([1, 2, 3]).buffer);
      fetchWithSsrFGuardMock.mockResolvedValueOnce({
        response: {
          ok: true,
          status: 200,
          body: {} as Response["body"],
          headers: new Headers(),
          arrayBuffer,
        } as unknown as Response,
        finalUrl: "https://cdn.example.com/releases/frontend-design.tgz",
        release: vi.fn(async () => undefined),
      });
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error:
          "failed to download https://example.com/frontend-design.tgz: " +
          "streaming response body unavailable",
      });
      expect(arrayBuffer).not.toHaveBeenCalled();
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
    });
  });

  it("rejects oversized streamed archive responses without falling back to arrayBuffer", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const arrayBuffer = vi.fn(async () => new Uint8Array([1, 2, 3]).buffer);
      const reader = {
        read: vi
          .fn()
          .mockResolvedValueOnce({
            done: false,
            value: {
              length: 256 * 1024 * 1024 + 1,
            } as Uint8Array,
          })
          .mockResolvedValueOnce({ done: true, value: undefined }),
        cancel: vi.fn(async () => undefined),
        releaseLock: vi.fn(),
      };
      fetchWithSsrFGuardMock.mockResolvedValueOnce({
        response: {
          ok: true,
          status: 200,
          body: {
            getReader: () => reader,
          } as unknown as Response["body"],
          headers: new Headers(),
          arrayBuffer,
        } as unknown as Response,
        finalUrl: "https://cdn.example.com/releases/frontend-design.tgz",
        release: vi.fn(async () => undefined),
      });
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error:
          "failed to download https://example.com/frontend-design.tgz: " +
          "download too large: 268435457 bytes (limit: 268435456 bytes)",
      });
      expect(arrayBuffer).not.toHaveBeenCalled();
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
    });
  });

  it("cleans up a partial download temp dir when streaming the archive fails", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      const beforeTempDirs = await listMarketplaceDownloadTempDirs();
      fetchWithSsrFGuardMock.mockResolvedValueOnce({
        response: new Response("x".repeat(1024), {
          status: 200,
          headers: {
            "content-length": String(300 * 1024 * 1024),
          },
        }),
        finalUrl: "https://cdn.example.com/releases/frontend-design.tgz",
        release: vi.fn(async () => undefined),
      });
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error:
          "failed to download https://example.com/frontend-design.tgz: " +
          "download too large: 314572800 bytes (limit: 268435456 bytes)",
      });
      expect(await listMarketplaceDownloadTempDirs()).toEqual(beforeTempDirs);
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
    });
  });

  it("sanitizes archive download errors before returning them", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      fetchWithSsrFGuardMock.mockRejectedValueOnce(
        new Error(
          "blocked\n\u001b[31mAuthorization: Bearer sk-1234567890abcdefghijklmnop\u001b[0m",
        ),
      );
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://user:pass@example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result.ok).toBe(false);
      if (result.ok) {
        return;
      }
      expect(result.error).toContain(
        "failed to download https://***:***@example.com/frontend-design.tgz:",
      );
      expect(result.error).toContain("Authorization: Bearer sk-123…mnop");
      expect(result.error).not.toContain("user:pass@");
      let hasControlChars = false;
      for (const char of result.error) {
        const codePoint = char.codePointAt(0);
        if (codePoint != null && (codePoint < 0x20 || codePoint === 0x7f)) {
          hasControlChars = true;
          break;
        }
      }
      expect(hasControlChars).toBe(false);
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
    });
  });

  it("returns a structured error when the SSRF guard rejects an archive URL", async () => {
    await withTempDir("openclaw-marketplace-test-", async (rootDir) => {
      fetchWithSsrFGuardMock.mockRejectedValueOnce(
        new Error("Blocked hostname (not in allowlist): 169.254.169.254"),
      );
      const manifestPath = await writeMarketplaceManifest(rootDir, {
        plugins: [
          {
            name: "frontend-design",
            source: "https://example.com/frontend-design.tgz",
          },
        ],
      });

      const result = await installPluginFromMarketplace({
        marketplace: manifestPath,
        plugin: "frontend-design",
      });

      expect(result).toEqual({
        ok: false,
        error:
          "failed to download https://example.com/frontend-design.tgz: " +
          "Blocked hostname (not in allowlist): 169.254.169.254",
      });
      expect(installPluginFromPathMock).not.toHaveBeenCalled();
    });
  });

  it.each([
    {
      name: "rejects remote marketplace git plugin sources before cloning nested remotes",
      manifest: {
        plugins: [
          {
            name: "frontend-design",
            source: {
              type: "git",
              url: "https://evil.example/repo.git",
            },
          },
        ],
      },
      expectedError:
        'invalid marketplace entry "frontend-design" in owner/repo: ' +
        "remote marketplaces may not use git plugin sources",
    },
    {
      name: "rejects remote marketplace absolute plugin paths",
      manifest: {
        plugins: [
          {
            name: "frontend-design",
            source: {
              type: "path",
              path: "/tmp/frontend-design",
            },
          },
        ],
      },
      expectedError:
        'invalid marketplace entry "frontend-design" in owner/repo: ' +
        "remote marketplaces may only use relative plugin paths",
    },
    {
      name: "rejects remote marketplace HTTP plugin paths",
      manifest: {
        plugins: [
          {
            name: "frontend-design",
            source: {
              type: "path",
              path: "https://evil.example/plugin.tgz",
            },
          },
        ],
      },
      expectedError:
        'invalid marketplace entry "frontend-design" in owner/repo: ' +
        "remote marketplaces may not use HTTP(S) plugin paths",
    },
  ] as const)("$name", async ({ manifest, expectedError }) => {
    await expectRemoteMarketplaceError({ manifest, expectedError });
  });

  it.runIf(process.platform !== "win32")(
    "rejects remote marketplace symlink plugin paths during manifest validation",
    async () => {
      mockRemoteMarketplaceCloneWithOutsideSymlink({
        symlinkPath: "evil-link",
        manifest: {
          plugins: [
            {
              name: "frontend-design",
              source: {
                type: "path",
                path: "evil-link",
              },
            },
          ],
        },
      });

      const result = await listMarketplacePlugins({ marketplace: "owner/repo" });

      expect(result).toEqual({
        ok: false,
        error:
          'invalid marketplace entry "frontend-design" in owner/repo: ' +
          "plugin source escapes marketplace root: evil-link",
      });
    },
  );

  it("reports missing remote marketplace paths as not found instead of escapes", async () => {
    mockRemoteMarketplaceClone({
      manifest: {
        plugins: [
          {
            name: "frontend-design",
            source: {
              type: "path",
              path: "plugins/missing-plugin",
            },
          },
        ],
      },
    });

    const result = await listMarketplacePlugins({ marketplace: "owner/repo" });

    expect(result).toEqual({
      ok: false,
      error:
        'invalid marketplace entry "frontend-design" in owner/repo: ' +
        "plugin source not found in marketplace root: plugins/missing-plugin",
    });
  });
});
