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 {
  packNpmSpecToArchive,
  resolveArchiveSourcePath,
  withTempDir,
} from "./install-source-utils.js";

const runCommandWithTimeoutMock = vi.fn();

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

const tempDirs: string[] = [];

async function createTempDir(prefix: string) {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix));
  tempDirs.push(dir);
  return dir;
}

beforeEach(() => {
  runCommandWithTimeoutMock.mockReset();
});

afterEach(async () => {
  while (tempDirs.length > 0) {
    const dir = tempDirs.pop();
    if (!dir) {
      break;
    }
    await fs.rm(dir, { recursive: true, force: true });
  }
});

describe("withTempDir", () => {
  it("creates a temp dir and always removes it after callback", async () => {
    let observedDir = "";
    const markerFile = "marker.txt";

    const value = await withTempDir("openclaw-install-source-utils-", async (tmpDir) => {
      observedDir = tmpDir;
      await fs.writeFile(path.join(tmpDir, markerFile), "ok", "utf-8");
      await expect(fs.stat(path.join(tmpDir, markerFile))).resolves.toBeDefined();
      return "done";
    });

    expect(value).toBe("done");
    await expect(fs.stat(observedDir)).rejects.toThrow();
  });
});

describe("resolveArchiveSourcePath", () => {
  it("returns not found error for missing archive paths", async () => {
    const result = await resolveArchiveSourcePath("/tmp/does-not-exist-openclaw-archive.tgz");
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toContain("archive not found");
    }
  });

  it("rejects unsupported archive extensions", async () => {
    const dir = await createTempDir("openclaw-install-source-utils-");
    const filePath = path.join(dir, "plugin.txt");
    await fs.writeFile(filePath, "not-an-archive", "utf-8");

    const result = await resolveArchiveSourcePath(filePath);
    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toContain("unsupported archive");
    }
  });

  it("accepts supported archive extensions", async () => {
    const dir = await createTempDir("openclaw-install-source-utils-");
    const filePath = path.join(dir, "plugin.zip");
    await fs.writeFile(filePath, "", "utf-8");

    const result = await resolveArchiveSourcePath(filePath);
    expect(result).toEqual({ ok: true, path: filePath });
  });
});

describe("packNpmSpecToArchive", () => {
  it("packs spec and returns archive path using JSON output metadata", async () => {
    const cwd = await createTempDir("openclaw-install-source-utils-");
    runCommandWithTimeoutMock.mockResolvedValue({
      stdout: JSON.stringify([
        {
          id: "openclaw-plugin@1.2.3",
          name: "openclaw-plugin",
          version: "1.2.3",
          filename: "openclaw-plugin-1.2.3.tgz",
          integrity: "sha512-test-integrity",
          shasum: "abc123",
        },
      ]),
      stderr: "",
      code: 0,
      signal: null,
      killed: false,
    });

    const result = await packNpmSpecToArchive({
      spec: "openclaw-plugin@1.2.3",
      timeoutMs: 1000,
      cwd,
    });

    expect(result).toEqual({
      ok: true,
      archivePath: path.join(cwd, "openclaw-plugin-1.2.3.tgz"),
      metadata: {
        name: "openclaw-plugin",
        version: "1.2.3",
        resolvedSpec: "openclaw-plugin@1.2.3",
        integrity: "sha512-test-integrity",
        shasum: "abc123",
      },
    });
    expect(runCommandWithTimeoutMock).toHaveBeenCalledWith(
      ["npm", "pack", "openclaw-plugin@1.2.3", "--ignore-scripts", "--json"],
      expect.objectContaining({
        cwd,
        timeoutMs: 300_000,
      }),
    );
  });

  it("falls back to parsing final stdout line when npm json output is unavailable", async () => {
    const cwd = await createTempDir("openclaw-install-source-utils-");
    runCommandWithTimeoutMock.mockResolvedValue({
      stdout: "npm notice created package\nopenclaw-plugin-1.2.3.tgz\n",
      stderr: "",
      code: 0,
      signal: null,
      killed: false,
    });

    const result = await packNpmSpecToArchive({
      spec: "openclaw-plugin@1.2.3",
      timeoutMs: 1000,
      cwd,
    });

    expect(result).toEqual({
      ok: true,
      archivePath: path.join(cwd, "openclaw-plugin-1.2.3.tgz"),
      metadata: {},
    });
  });

  it("returns npm pack error details when command fails", async () => {
    const cwd = await createTempDir("openclaw-install-source-utils-");
    runCommandWithTimeoutMock.mockResolvedValue({
      stdout: "fallback stdout",
      stderr: "registry timeout",
      code: 1,
      signal: null,
      killed: false,
    });

    const result = await packNpmSpecToArchive({
      spec: "bad-spec",
      timeoutMs: 5000,
      cwd,
    });

    expect(result.ok).toBe(false);
    if (!result.ok) {
      expect(result.error).toContain("npm pack failed");
      expect(result.error).toContain("registry timeout");
    }
  });

  it("returns explicit error when npm pack produces no archive name", async () => {
    const cwd = await createTempDir("openclaw-install-source-utils-");
    runCommandWithTimeoutMock.mockResolvedValue({
      stdout: " \n\n",
      stderr: "",
      code: 0,
      signal: null,
      killed: false,
    });

    const result = await packNpmSpecToArchive({
      spec: "openclaw-plugin@1.2.3",
      timeoutMs: 5000,
      cwd,
    });

    expect(result).toEqual({
      ok: false,
      error: "npm pack produced no archive",
    });
  });
});
