import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { mkdtemp, rm, mkdir, writeFile, chmod } from "fs/promises";
import { tmpdir } from "os";
import * as path from "path";
import { execFileSync } from "child_process";

const ORIGINAL_ENV = { ...process.env };

async function freshGitClient() {
  vi.resetModules();
  return import("./gitClient");
}

describe("gitClient timeout", () => {
  let repoPath: string;

  beforeEach(async () => {
    process.env = { ...ORIGINAL_ENV };
    repoPath = await mkdtemp(path.join(tmpdir(), "gitclient-test-"));
    execFileSync("git", ["init", "-q"], { cwd: repoPath });
    execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoPath });
    execFileSync("git", ["config", "user.name", "test"], { cwd: repoPath });
  });

  afterEach(async () => {
    process.env = { ...ORIGINAL_ENV };
    await rm(repoPath, { recursive: true, force: true });
  });

  it("a normal, fast git command still succeeds with the default timeout", async () => {
    const { createBranch } = await freshGitClient();
    expect(() => createBranch(repoPath, "some-branch")).not.toThrow();
  });

  it("kills a git command that hangs instead of blocking forever", async () => {
    // A pre-commit hook that sleeps far longer than the configured timeout
    // stands in for a real-world hang (e.g. a stalled network mid-push) --
    // git actually runs this hook synchronously as part of `git commit`,
    // so it exercises the real execFileSync timeout mechanism end to end,
    // not just a mocked stand-in for it.
    const hooksDir = path.join(repoPath, ".git", "hooks");
    await mkdir(hooksDir, { recursive: true });
    const hookPath = path.join(hooksDir, "pre-commit");
    await writeFile(hookPath, "#!/bin/sh\nsleep 5\n");
    await chmod(hookPath, 0o755);
    await writeFile(path.join(repoPath, "file.txt"), "content");
    execFileSync("git", ["add", "."], { cwd: repoPath });

    process.env.GIT_TIMEOUT_MS = "300";
    const { commitAll } = await freshGitClient();

    const start = Date.now();
    expect(() => commitAll(repoPath, "should time out")).toThrow();
    const elapsed = Date.now() - start;
    // Proves it was actually killed by the timeout rather than the hook's
    // full 5-second sleep running to completion.
    expect(elapsed).toBeLessThan(3000);
  });
});
