import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
  ACPX_LOCAL_INSTALL_COMMAND,
  ACPX_PINNED_VERSION,
  buildAcpxLocalInstallCommand,
} from "./config.js";

const { resolveSpawnFailureMock, spawnAndCollectMock } = vi.hoisted(() => ({
  resolveSpawnFailureMock: vi.fn<
    (error: unknown, cwd: string) => "missing-command" | "missing-cwd" | null
  >(() => null),
  spawnAndCollectMock: vi.fn(),
}));

vi.mock("./runtime-internals/process.js", () => ({
  resolveSpawnFailure: resolveSpawnFailureMock,
  spawnAndCollect: spawnAndCollectMock,
}));

import { checkAcpxVersion, ensureAcpx } from "./ensure.js";

describe("acpx ensure", () => {
  const tempDirs: string[] = [];

  beforeEach(() => {
    resolveSpawnFailureMock.mockReset();
    resolveSpawnFailureMock.mockReturnValue(null);
    spawnAndCollectMock.mockReset();
  });

  function makeTempAcpxInstall(version: string): string {
    const root = fs.mkdtempSync(path.join(os.tmpdir(), "acpx-ensure-test-"));
    tempDirs.push(root);
    const packageRoot = path.join(root, "node_modules", "acpx");
    fs.mkdirSync(path.join(packageRoot, "dist"), { recursive: true });
    fs.mkdirSync(path.join(root, "node_modules", ".bin"), { recursive: true });
    fs.writeFileSync(
      path.join(packageRoot, "package.json"),
      JSON.stringify({ name: "acpx", version }, null, 2),
      "utf8",
    );
    fs.writeFileSync(path.join(packageRoot, "dist", "cli.js"), "#!/usr/bin/env node\n", "utf8");
    const binPath = path.join(root, "node_modules", ".bin", "acpx");
    fs.symlinkSync(path.join(packageRoot, "dist", "cli.js"), binPath);
    return binPath;
  }

  afterEach(() => {
    for (const dir of tempDirs.splice(0)) {
      fs.rmSync(dir, { recursive: true, force: true });
    }
  });

  function mockEnsureInstallFlow() {
    spawnAndCollectMock
      .mockResolvedValueOnce({
        stdout: "acpx 0.0.9\n",
        stderr: "",
        code: 0,
        error: null,
      })
      .mockResolvedValueOnce({
        stdout: "added 1 package\n",
        stderr: "",
        code: 0,
        error: null,
      })
      .mockResolvedValueOnce({
        stdout: `acpx ${ACPX_PINNED_VERSION}\n`,
        stderr: "",
        code: 0,
        error: null,
      });
  }

  function expectEnsureInstallCalls(stripProviderAuthEnvVars?: boolean) {
    expect(spawnAndCollectMock.mock.calls[0]?.[0]).toMatchObject({
      command: "/plugin/node_modules/.bin/acpx",
      args: ["--version"],
      cwd: "/plugin",
      stripProviderAuthEnvVars,
    });
    expect(spawnAndCollectMock.mock.calls[1]?.[0]).toMatchObject({
      command: "npm",
      args: ["install", "--omit=dev", "--no-save", `acpx@${ACPX_PINNED_VERSION}`],
      cwd: "/plugin",
      stripProviderAuthEnvVars,
    });
    expect(spawnAndCollectMock.mock.calls[2]?.[0]).toMatchObject({
      command: "/plugin/node_modules/.bin/acpx",
      args: ["--version"],
      cwd: "/plugin",
      stripProviderAuthEnvVars,
    });
  }

  it("accepts the pinned acpx version", async () => {
    spawnAndCollectMock.mockResolvedValueOnce({
      stdout: `acpx ${ACPX_PINNED_VERSION}\n`,
      stderr: "",
      code: 0,
      error: null,
    });

    const result = await checkAcpxVersion({
      command: "/plugin/node_modules/.bin/acpx",
      cwd: "/plugin",
      expectedVersion: ACPX_PINNED_VERSION,
    });

    expect(result).toEqual({
      ok: true,
      version: ACPX_PINNED_VERSION,
      expectedVersion: ACPX_PINNED_VERSION,
    });
    expect(spawnAndCollectMock).toHaveBeenCalledWith({
      command: "/plugin/node_modules/.bin/acpx",
      args: ["--version"],
      cwd: "/plugin",
      stripProviderAuthEnvVars: undefined,
    });
  });

  it("reports version mismatch", async () => {
    spawnAndCollectMock.mockResolvedValueOnce({
      stdout: "acpx 0.0.9\n",
      stderr: "",
      code: 0,
      error: null,
    });

    const result = await checkAcpxVersion({
      command: "/plugin/node_modules/.bin/acpx",
      cwd: "/plugin",
      expectedVersion: ACPX_PINNED_VERSION,
    });

    expect(result).toMatchObject({
      ok: false,
      reason: "version-mismatch",
      expectedVersion: ACPX_PINNED_VERSION,
      installedVersion: "0.0.9",
      installCommand: ACPX_LOCAL_INSTALL_COMMAND,
    });
  });

  it("falls back to package.json version when --version is unsupported", async () => {
    const command = makeTempAcpxInstall(ACPX_PINNED_VERSION);
    spawnAndCollectMock.mockResolvedValueOnce({
      stdout: "",
      stderr: "error: unknown option '--version'",
      code: 2,
      error: null,
    });

    const result = await checkAcpxVersion({
      command,
      cwd: path.dirname(path.dirname(command)),
      expectedVersion: ACPX_PINNED_VERSION,
    });

    expect(result).toEqual({
      ok: true,
      version: ACPX_PINNED_VERSION,
      expectedVersion: ACPX_PINNED_VERSION,
    });
  });

  it("accepts command availability when expectedVersion is unset", async () => {
    spawnAndCollectMock.mockResolvedValueOnce({
      stdout: "Usage: acpx [options]\n",
      stderr: "",
      code: 0,
      error: null,
    });

    const result = await checkAcpxVersion({
      command: "/custom/acpx",
      cwd: "/custom",
      expectedVersion: undefined,
    });

    expect(result).toEqual({
      ok: true,
      version: "unknown",
      expectedVersion: undefined,
    });
    expect(spawnAndCollectMock).toHaveBeenCalledWith({
      command: "/custom/acpx",
      args: ["--help"],
      cwd: "/custom",
      stripProviderAuthEnvVars: undefined,
    });
  });

  it("forwards stripProviderAuthEnvVars to version checks", async () => {
    spawnAndCollectMock.mockResolvedValueOnce({
      stdout: "Usage: acpx [options]\n",
      stderr: "",
      code: 0,
      error: null,
    });

    await checkAcpxVersion({
      command: "/plugin/node_modules/.bin/acpx",
      cwd: "/plugin",
      expectedVersion: undefined,
      stripProviderAuthEnvVars: true,
    });

    expect(spawnAndCollectMock).toHaveBeenCalledWith({
      command: "/plugin/node_modules/.bin/acpx",
      args: ["--help"],
      cwd: "/plugin",
      stripProviderAuthEnvVars: true,
    });
  });

  it("installs and verifies pinned acpx when precheck fails", async () => {
    mockEnsureInstallFlow();

    await ensureAcpx({
      command: "/plugin/node_modules/.bin/acpx",
      pluginRoot: "/plugin",
      expectedVersion: ACPX_PINNED_VERSION,
    });

    expect(spawnAndCollectMock).toHaveBeenCalledTimes(3);
    expectEnsureInstallCalls();
  });

  it("threads stripProviderAuthEnvVars through version probes and install", async () => {
    mockEnsureInstallFlow();

    await ensureAcpx({
      command: "/plugin/node_modules/.bin/acpx",
      pluginRoot: "/plugin",
      expectedVersion: ACPX_PINNED_VERSION,
      stripProviderAuthEnvVars: true,
    });

    expectEnsureInstallCalls(true);
  });

  it("fails with actionable error when npm install fails", async () => {
    spawnAndCollectMock
      .mockResolvedValueOnce({
        stdout: "acpx 0.0.9\n",
        stderr: "",
        code: 0,
        error: null,
      })
      .mockResolvedValueOnce({
        stdout: "",
        stderr: "network down",
        code: 1,
        error: null,
      });

    await expect(
      ensureAcpx({
        command: "/plugin/node_modules/.bin/acpx",
        pluginRoot: "/plugin",
        expectedVersion: ACPX_PINNED_VERSION,
      }),
    ).rejects.toThrow("failed to install plugin-local acpx");
  });

  it("skips install path when allowInstall=false", async () => {
    spawnAndCollectMock.mockResolvedValueOnce({
      stdout: "",
      stderr: "",
      code: 0,
      error: new Error("not found"),
    });
    resolveSpawnFailureMock.mockReturnValue("missing-command");

    await expect(
      ensureAcpx({
        command: "/custom/acpx",
        pluginRoot: "/plugin",
        expectedVersion: undefined,
        allowInstall: false,
      }),
    ).rejects.toThrow("acpx command not found at /custom/acpx");

    expect(spawnAndCollectMock).toHaveBeenCalledTimes(1);
  });

  it("uses expectedVersion for install command metadata", async () => {
    spawnAndCollectMock.mockResolvedValueOnce({
      stdout: "acpx 0.0.9\n",
      stderr: "",
      code: 0,
      error: null,
    });

    const result = await checkAcpxVersion({
      command: "/plugin/node_modules/.bin/acpx",
      cwd: "/plugin",
      expectedVersion: "0.2.0",
    });

    expect(result).toMatchObject({
      ok: false,
      installCommand: buildAcpxLocalInstallCommand("0.2.0"),
    });
  });
});
