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

// The module inspects `process.argv[1]` + `realpathSync`. We stub both so
// each test describes a hypothetical install layout without touching the
// filesystem.

type InstallerInfo =
  (typeof import("./installerDetection.js"))["detectInstaller"] extends () => infer R ? R : never;

async function detectWith(realPath: string | null): Promise<InstallerInfo> {
  vi.resetModules();
  const origArgv1 = process.argv[1];
  if (realPath === null) {
    process.argv[1] = "";
  } else {
    // argv[1] doesn't matter — realpathSync is what gets checked after the
    // resolver runs. Set it to the unresolved form and stub fs.realpathSync
    // to return the scenario's resolved path.
    process.argv[1] = "/not/used/at/runtime";
  }
  vi.doMock("node:fs", async () => {
    const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
    return {
      ...actual,
      realpathSync:
        realPath === null
          ? () => {
              throw new Error("simulated unresolved path");
            }
          : () => realPath,
    };
  });

  const mod = await import("./installerDetection.js");
  const info = mod.detectInstaller();
  process.argv[1] = origArgv1 ?? "";
  return info;
}

describe("detectInstaller", () => {
  afterEach(() => {
    vi.doUnmock("node:fs");
    vi.resetModules();
  });

  it("classifies workspace link as skip (monorepo dev)", async () => {
    const info = await detectWith("/Users/dev/hyperframes-oss/packages/cli/dist/cli.js");
    expect(info.kind).toBe("skip");
    expect(info.reason).toContain("workspace");
    expect(info.installCommand("0.4.4")).toBeNull();
  });

  it("classifies npx _npx cache path as skip", async () => {
    const info = await detectWith(
      "/Users/me/.npm/_npx/abc123/node_modules/hyperframes/dist/cli.js",
    );
    expect(info.kind).toBe("skip");
    expect(info.reason.toLowerCase()).toContain("ephemeral");
  });

  it("classifies bunx temp dir as skip", async () => {
    const info = await detectWith("/var/folders/tmp/bunx-501-hyperframes/entry.js");
    expect(info.kind).toBe("skip");
    expect(info.reason.toLowerCase()).toContain("ephemeral");
  });

  it("detects Homebrew install", async () => {
    const info = await detectWith("/opt/homebrew/Cellar/hyperframes/0.4.3/bin/hyperframes");
    expect(info.kind).toBe("brew");
    expect(info.installCommand("0.4.4")).toBe("brew upgrade hyperframes");
  });

  it("detects bun global install", async () => {
    const info = await detectWith(
      "/Users/me/.bun/install/global/node_modules/hyperframes/dist/cli.js",
    );
    expect(info.kind).toBe("bun");
    expect(info.installCommand("0.4.4")).toBe("bun add -g hyperframes@0.4.4");
  });

  it("detects pnpm global install (Library/pnpm path)", async () => {
    const info = await detectWith(
      "/Users/me/Library/pnpm/global/5/node_modules/hyperframes/dist/cli.js",
    );
    expect(info.kind).toBe("pnpm");
    expect(info.installCommand("0.4.4")).toBe("pnpm add -g hyperframes@0.4.4");
  });

  it("treats pnpm project-local installs as unknown layouts", async () => {
    const info = await detectWith(
      "/path/to/project/node_modules/.pnpm/hyperframes@0.4.3/node_modules/hyperframes/dist/cli.js",
    );
    expect(info.kind).toBe("skip");
    expect(info.installCommand("0.4.4")).toBeNull();
  });

  it("detects npm global install", async () => {
    const info = await detectWith("/usr/local/lib/node_modules/hyperframes/dist/cli.js");
    expect(info.kind).toBe("npm");
    expect(info.installCommand("0.4.4")).toBe("npm install -g hyperframes@0.4.4");
  });

  it("detects npm global install on Windows", async () => {
    const info = await detectWith(
      "C:\\Users\\me\\AppData\\Roaming\\npm\\node_modules\\hyperframes\\dist\\cli.js",
    );
    expect(info.kind).toBe("npm");
    expect(info.installCommand("0.4.4")).toBe("npm install -g hyperframes@0.4.4");
  });

  it("returns skip when the entry cannot be resolved", async () => {
    const info = await detectWith(null);
    // realpathSync throws → reason is "could not resolve" OR the path itself
    // (the fallback returns the unresolved argv[1]); either way the kind is
    // skip-or-unknown which we treat as skip downstream.
    expect(info.kind).toBe("skip");
    expect(info.installCommand("0.4.4")).toBeNull();
  });

  it("returns skip for an unknown install layout", async () => {
    const info = await detectWith("/some/random/path/hyperframes");
    expect(info.kind).toBe("skip");
    expect(info.reason).toMatch(/Unknown install layout/);
  });
});

import { installInvocation } from "./installerDetection.js";

describe("installInvocation", () => {
  it("returns the npm global argv for kind npm", () => {
    expect(installInvocation("npm", "1.2.3")).toEqual({
      bin: "npm",
      args: ["install", "-g", "hyperframes@1.2.3"],
    });
  });

  it("returns bun/pnpm add -g argv for those managers", () => {
    expect(installInvocation("bun", "1.2.3")).toEqual({
      bin: "bun",
      args: ["add", "-g", "hyperframes@1.2.3"],
    });
    expect(installInvocation("pnpm", "1.2.3")).toEqual({
      bin: "pnpm",
      args: ["add", "-g", "hyperframes@1.2.3"],
    });
  });

  it("returns a version-less brew upgrade for kind brew", () => {
    expect(installInvocation("brew", "1.2.3")).toEqual({
      bin: "brew",
      args: ["upgrade", "hyperframes"],
    });
  });

  it("returns null for kind skip (ephemeral / project-local / unknown)", () => {
    expect(installInvocation("skip", "1.2.3")).toBeNull();
  });
});
