import type { execFile as execFileType } from "node:child_process";
import { EventEmitter } from "node:events";
import fs from "node:fs";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";

const spawnMock = vi.hoisted(() => vi.fn());
const execFileMock = vi.hoisted(() =>
  Object.assign(vi.fn(), {
    __promisify__: vi.fn(),
  }),
);

vi.mock("node:child_process", async () => {
  const { mockNodeBuiltinModule } = await import("../../test/helpers/node-builtin-mocks.js");
  return mockNodeBuiltinModule(
    () => vi.importActual<typeof import("node:child_process")>("node:child_process"),
    {
      spawn: spawnMock,
      execFile: execFileMock as unknown as typeof execFileType,
    },
  );
});

let runCommandWithTimeout: typeof import("./exec.js").runCommandWithTimeout;
let runExec: typeof import("./exec.js").runExec;

type MockChild = EventEmitter & {
  exitCode?: number | null;
  signalCode?: NodeJS.Signals | null;
  stdout: EventEmitter;
  stderr: EventEmitter;
  stdin: { write: ReturnType<typeof vi.fn>; end: ReturnType<typeof vi.fn> };
  kill: ReturnType<typeof vi.fn>;
  pid?: number;
  killed?: boolean;
};

function createMockChild(params?: {
  closeCode?: number | null;
  closeSignal?: NodeJS.Signals | null;
  exitCode?: number | null;
  exitCodeAfterClose?: number | null;
  exitCodeAfterCloseDelayMs?: number;
  signal?: NodeJS.Signals | null;
}): MockChild {
  const child = new EventEmitter() as MockChild;
  child.stdout = new EventEmitter();
  child.stderr = new EventEmitter();
  child.exitCode = params?.exitCode ?? params?.closeCode ?? 0;
  child.signalCode = params?.signal ?? null;
  child.stdin = {
    write: vi.fn(),
    end: vi.fn(),
  };
  child.kill = vi.fn(() => true);
  child.pid = 1234;
  child.killed = false;
  queueMicrotask(() => {
    child.emit("close", params?.closeCode ?? 0, params?.closeSignal ?? params?.signal ?? null);
    if (params?.exitCodeAfterClose !== undefined) {
      setTimeout(() => {
        child.exitCode = params.exitCodeAfterClose ?? null;
      }, params.exitCodeAfterCloseDelayMs ?? 0);
    }
  });
  return child;
}

type SpawnCall = [string, string[], Record<string, unknown>];

type ExecCall = [
  string,
  string[],
  Record<string, unknown>,
  (err: Error | null, stdout: string, stderr: string) => void,
];

function expectCmdWrappedInvocation(params: {
  captured: SpawnCall | ExecCall | undefined;
  expectedComSpec: string;
}) {
  if (!params.captured) {
    throw new Error("expected command wrapper to be called");
  }
  expect(params.captured[0]).toBe(params.expectedComSpec);
  expect(params.captured[1].slice(0, 3)).toEqual(["/d", "/s", "/c"]);
  expect(params.captured[1][3]).toContain("pnpm.cmd --version");
  expect(params.captured[2].windowsHide).toBe(true);
  expect(params.captured[2].windowsVerbatimArguments).toBe(true);
}

async function expectShimmedWindowsCommandWithoutExitCodeSucceeds(params?: { killed?: boolean }) {
  const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
  const child = createMockChild({
    closeCode: null,
    exitCode: null,
  });
  child.killed = params?.killed ?? false;

  spawnMock.mockImplementation(() => child);

  try {
    const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 });
    expect(result.code).toBe(0);
    expect(result.signal).toBeNull();
    expect(result.termination).toBe("exit");
  } finally {
    platformSpy.mockRestore();
  }
}

describe("windows command wrapper behavior", () => {
  beforeAll(async () => {
    ({ runCommandWithTimeout, runExec } = await import("./exec.js"));
  });

  beforeEach(() => {
    spawnMock.mockReset();
    execFileMock.mockReset();
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  it("wraps .cmd commands via cmd.exe in runCommandWithTimeout", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    const expectedComSpec = process.env.ComSpec ?? "cmd.exe";

    spawnMock.mockImplementation(
      (_command: string, _args: string[], _options: Record<string, unknown>) => createMockChild(),
    );

    try {
      const result = await runCommandWithTimeout(["pnpm", "--version"], { timeoutMs: 1000 });
      expect(result.code).toBe(0);
      const captured = spawnMock.mock.calls[0] as SpawnCall | undefined;
      expectCmdWrappedInvocation({ captured, expectedComSpec });
    } finally {
      platformSpy.mockRestore();
    }
  });

  it("wraps corepack.cmd via cmd.exe in runCommandWithTimeout", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    const expectedComSpec = process.env.ComSpec ?? "cmd.exe";

    spawnMock.mockImplementation(
      (_command: string, _args: string[], _options: Record<string, unknown>) => createMockChild(),
    );

    try {
      const result = await runCommandWithTimeout(["corepack", "--version"], { timeoutMs: 1000 });
      expect(result.code).toBe(0);
      const captured = spawnMock.mock.calls[0] as SpawnCall | undefined;
      if (!captured) {
        throw new Error("expected corepack shim spawn");
      }
      expect(captured[0]).toBe(expectedComSpec);
      expect(captured[1].slice(0, 3)).toEqual(["/d", "/s", "/c"]);
      expect(captured[1][3]).toContain("corepack.cmd --version");
      expect(captured[2].windowsHide).toBe(true);
      expect(captured[2].windowsVerbatimArguments).toBe(true);
    } finally {
      platformSpy.mockRestore();
    }
  });

  it("keeps child exitCode when close reports null on Windows npm shims", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    const child = createMockChild({ closeCode: null, exitCode: 0 });

    spawnMock.mockImplementation(() => child);

    try {
      const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 });
      expect(result.code).toBe(0);
    } finally {
      platformSpy.mockRestore();
    }
  });

  it("spawns node + npm-cli.js for npm argv to avoid direct .cmd execution", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(true);
    const child = createMockChild({ closeCode: 0, exitCode: 0 });

    spawnMock.mockImplementation(() => child);

    try {
      const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 });
      expect(result.code).toBe(0);
      const captured = spawnMock.mock.calls[0] as SpawnCall | undefined;
      if (!captured) {
        throw new Error("expected npm shim spawn");
      }
      expect(captured[0]).toBe(process.execPath);
      expect(captured[1][0]).toBe(
        path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
      );
      expect(captured[1][1]).toBe("--version");
      expect(captured[2].windowsHide).toBe(true);
      expect(captured[2].windowsVerbatimArguments).toBeUndefined();
      expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]);
    } finally {
      existsSpy.mockRestore();
      platformSpy.mockRestore();
    }
  });

  it("falls back to npm.cmd when npm-cli.js is unavailable", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    const existsSpy = vi.spyOn(fs, "existsSync").mockReturnValue(false);
    const expectedComSpec = process.env.ComSpec ?? "cmd.exe";

    spawnMock.mockImplementation(
      (_command: string, _args: string[], _options: Record<string, unknown>) => createMockChild(),
    );

    try {
      const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 });
      expect(result.code).toBe(0);
      const captured = spawnMock.mock.calls[0] as SpawnCall | undefined;
      if (!captured) {
        throw new Error("expected npm.cmd fallback spawn");
      }
      expect(captured[0]).toBe(expectedComSpec);
      expect(captured[1].slice(0, 3)).toEqual(["/d", "/s", "/c"]);
      expect(captured[1][3]).toContain("npm.cmd --version");
      expect(captured[2].windowsHide).toBe(true);
      expect(captured[2].windowsVerbatimArguments).toBe(true);
      expect(captured[2].stdio).toEqual(["inherit", "pipe", "pipe"]);
    } finally {
      existsSpy.mockRestore();
      platformSpy.mockRestore();
    }
  });

  it("waits for Windows exitCode settlement after close reports null", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    const child = createMockChild({
      closeCode: null,
      exitCode: null,
      exitCodeAfterClose: 0,
      exitCodeAfterCloseDelayMs: 50,
    });

    spawnMock.mockImplementation(() => child);

    try {
      const result = await runCommandWithTimeout(["npm", "--version"], { timeoutMs: 1000 });
      expect(result.code).toBe(0);
    } finally {
      platformSpy.mockRestore();
    }
  });

  it("treats shimmed Windows commands without a reported exit code as success when they close cleanly", async () => {
    await expectShimmedWindowsCommandWithoutExitCodeSucceeds();
  });

  it("treats shimmed Windows commands without a reported exit code as success even when child.killed is true", async () => {
    await expectShimmedWindowsCommandWithoutExitCodeSucceeds({ killed: true });
  });

  it("uses cmd.exe wrapper with windowsVerbatimArguments in runExec for .cmd shims", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
    const expectedComSpec = process.env.ComSpec ?? "cmd.exe";

    execFileMock.mockImplementation(
      (
        _command: string,
        _args: string[],
        _options: Record<string, unknown>,
        cb: (err: Error | null, stdout: string, stderr: string) => void,
      ) => {
        cb(null, "ok", "");
      },
    );

    try {
      await runExec("pnpm", ["--version"], 1000);
      const captured = execFileMock.mock.calls[0] as ExecCall | undefined;
      expectCmdWrappedInvocation({ captured, expectedComSpec });
    } finally {
      platformSpy.mockRestore();
    }
  });

  it("sets windowsHide on direct runExec invocations too", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");

    execFileMock.mockImplementation(
      (
        _command: string,
        _args: string[],
        _options: Record<string, unknown>,
        cb: (err: Error | null, stdout: string, stderr: string) => void,
      ) => {
        cb(null, "ok", "");
      },
    );

    try {
      await runExec("node", ["--version"], 1000);
      const captured = execFileMock.mock.calls[0] as ExecCall | undefined;
      if (!captured) {
        throw new Error("expected direct execFile invocation");
      }
      expect(captured[0]).toBe("node");
      expect(captured[1]).toEqual(["--version"]);
      expect(captured[2].windowsHide).toBe(true);
    } finally {
      platformSpy.mockRestore();
    }
  });

  it("sets windowsHide on direct runCommandWithTimeout invocations too", async () => {
    const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");

    spawnMock.mockImplementation(
      (_command: string, _args: string[], _options: Record<string, unknown>) => createMockChild(),
    );

    try {
      const result = await runCommandWithTimeout(["node", "--version"], { timeoutMs: 1000 });
      expect(result.code).toBe(0);
      const captured = spawnMock.mock.calls[0] as SpawnCall | undefined;
      if (!captured) {
        throw new Error("expected direct spawn invocation");
      }
      expect(captured[0]).toBe("node");
      expect(captured[1]).toEqual(["--version"]);
      expect(captured[2].windowsHide).toBe(true);
      expect(captured[2].windowsVerbatimArguments).toBeUndefined();
    } finally {
      platformSpy.mockRestore();
    }
  });
});
