import { describe, expect, test } from "bun:test"
import { promises as fs } from "fs"
import os from "os"
import path from "path"
import { pathToFileURL } from "url"

const gitEnv = {
  ...process.env,
  GIT_AUTHOR_NAME: "Test",
  GIT_AUTHOR_EMAIL: "test@example.com",
  GIT_COMMITTER_NAME: "Test",
  GIT_COMMITTER_EMAIL: "test@example.com",
}

const resolveBaseScript = path.join(
  import.meta.dir,
  "..",
  "plugins",
  "compound-engineering",
  "skills",
  "ce-review",
  "references",
  "resolve-base.sh",
)

type RunResult = {
  exitCode: number
  stderr: string
  stdout: string
}

async function runCommand(
  cmd: string[],
  cwd: string,
  env?: NodeJS.ProcessEnv,
): Promise<RunResult> {
  const proc = Bun.spawn(cmd, {
    cwd,
    env: env ?? process.env,
    stderr: "pipe",
    stdout: "pipe",
  })

  const [exitCode, stdout, stderr] = await Promise.all([
    proc.exited,
    new Response(proc.stdout).text(),
    new Response(proc.stderr).text(),
  ])

  return { exitCode, stderr, stdout }
}

async function runGit(args: string[], cwd: string, env?: NodeJS.ProcessEnv): Promise<string> {
  const result = await runCommand(["git", ...args], cwd, env ?? gitEnv)
  if (result.exitCode !== 0) {
    throw new Error(
      `git ${args.join(" ")} failed (exit ${result.exitCode}).\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
    )
  }

  return result.stdout.trim()
}

async function initRepo(initialBranch = "main"): Promise<string> {
  const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), "resolve-base-repo-"))
  await runGit(["init", "-b", initialBranch], repoRoot)
  return repoRoot
}

async function commitFile(
  repoRoot: string,
  relativePath: string,
  content: string,
  message: string,
): Promise<string> {
  const filePath = path.join(repoRoot, relativePath)
  await fs.mkdir(path.dirname(filePath), { recursive: true })
  await fs.writeFile(filePath, content)
  await runGit(["add", relativePath], repoRoot)
  await runGit(["commit", "-m", message], repoRoot)
  return runGit(["rev-parse", "HEAD"], repoRoot)
}

async function writeExecutable(filePath: string, content: string): Promise<void> {
  await fs.writeFile(filePath, content)
  await fs.chmod(filePath, 0o755)
}

async function createStubBin(mode: "gh-fails" | "pr-metadata"): Promise<string> {
  const binDir = await fs.mkdtemp(path.join(os.tmpdir(), "resolve-base-bin-"))

  if (mode === "gh-fails") {
    await writeExecutable(path.join(binDir, "gh"), "#!/usr/bin/env bash\nexit 1\n")
    return binDir
  }

  await writeExecutable(
    path.join(binDir, "gh"),
    `#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -ge 2 ] && [ "$1" = "pr" ] && [ "$2" = "view" ]; then
  printf '%s' '{"baseRefName":"main","url":"https://github.com/EveryInc/compound-engineering-plugin/pull/123"}'
  exit 0
fi
exit 1
`,
  )

  await writeExecutable(
    path.join(binDir, "jq"),
    `#!/usr/bin/env bun
const args = process.argv.slice(2).filter((arg) => arg !== "-r")
const query = args[args.length - 1] ?? ""
const input = await new Response(Bun.stdin.stream()).text()
const data = input.trim() ? JSON.parse(input) : {}

let output = ""
if (query === ".baseRefName // empty") {
  output = data.baseRefName ?? ""
} else if (query === ".url // empty") {
  output = data.url ?? ""
} else if (query === ".defaultBranchRef.name") {
  output = data.defaultBranchRef?.name ?? ""
} else {
  console.error(\`unsupported jq query: \${query}\`)
  process.exit(1)
}

process.stdout.write(String(output))
`,
  )

  return binDir
}

async function runResolveBase(
  repoRoot: string,
  stubBin: string,
  extraEnv?: NodeJS.ProcessEnv,
): Promise<RunResult> {
  return runCommand(["bash", resolveBaseScript], repoRoot, {
    ...gitEnv,
    PATH: `${stubBin}:${process.env.PATH ?? ""}`,
    ...extraEnv,
  })
}

describe("resolve-base.sh", () => {
  test("prefers the PR base remote from gh metadata over origin", async () => {
    const repoRoot = await initRepo()
    const initialSha = await commitFile(repoRoot, "history.txt", "a\n", "initial")
    const upstreamMainSha = await commitFile(repoRoot, "history.txt", "b\n", "main advance")

    await runGit(["checkout", "-b", "feature"], repoRoot)
    await commitFile(repoRoot, "feature.txt", "feature\n", "feature change")

    await runGit(["checkout", "-b", "fork-main", initialSha], repoRoot)
    const forkMainSha = await commitFile(repoRoot, "fork.txt", "fork\n", "fork main diverges")
    await runGit(["checkout", "feature"], repoRoot)

    await runGit(["remote", "add", "origin", "git@github.com:someone/fork.git"], repoRoot)
    await runGit(
      ["remote", "add", "upstream", "git@github.com:EveryInc/compound-engineering-plugin.git"],
      repoRoot,
    )
    await runGit(["update-ref", "refs/remotes/origin/main", forkMainSha], repoRoot)
    await runGit(["update-ref", "refs/remotes/upstream/main", upstreamMainSha], repoRoot)

    const stubBin = await createStubBin("pr-metadata")
    const result = await runResolveBase(repoRoot, stubBin)

    expect(result.exitCode).toBe(0)
    expect(result.stdout.trim()).toBe(`BASE:${upstreamMainSha}`)
  })

  test("falls back to a local base branch when origin is absent", async () => {
    const repoRoot = await initRepo()
    await commitFile(repoRoot, "history.txt", "a\n", "initial")
    const mainSha = await commitFile(repoRoot, "history.txt", "b\n", "main advance")

    await runGit(["checkout", "-b", "feature"], repoRoot)
    await commitFile(repoRoot, "feature.txt", "feature\n", "feature change")

    const stubBin = await createStubBin("gh-fails")
    const result = await runResolveBase(repoRoot, stubBin)

    expect(result.exitCode).toBe(0)
    expect(result.stdout.trim()).toBe(`BASE:${mainSha}`)
  })

  test("resolves against origin/HEAD in a detached shallow checkout", async () => {
    const seedRepo = await initRepo()
    await commitFile(seedRepo, "history.txt", "a\n", "initial")
    const mainSha = await commitFile(seedRepo, "history.txt", "b\n", "main advance")

    await runGit(["checkout", "-b", "feature"], seedRepo)
    const featureSha = await commitFile(seedRepo, "feature.txt", "feature\n", "feature change")
    await runGit(["checkout", "main"], seedRepo)

    const bareRepo = await fs.mkdtemp(path.join(os.tmpdir(), "resolve-base-remote-"))
    await runGit(["init", "--bare", bareRepo], seedRepo)
    const bareUrl = pathToFileURL(bareRepo).toString()
    await runGit(["remote", "add", "origin", bareUrl], seedRepo)
    await runGit(["push", "origin", "main", "feature"], seedRepo)

    const checkoutRoot = await fs.mkdtemp(path.join(os.tmpdir(), "resolve-base-checkout-"))
    await runCommand(["git", "clone", "--depth", "1", bareUrl, checkoutRoot], os.tmpdir(), gitEnv)
    await runGit(["config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"], checkoutRoot)
    await runGit(
      ["fetch", "--depth", "1", "origin", "main:refs/remotes/origin/main", "feature:refs/remotes/origin/feature"],
      checkoutRoot,
    )
    await runGit(["symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"], checkoutRoot)
    await runGit(["checkout", "--detach", "origin/feature"], checkoutRoot)

    const originMain = await runGit(["rev-parse", "--verify", "origin/main"], checkoutRoot)
    expect(originMain).toBe(mainSha)

    const originFeature = await runGit(["rev-parse", "--verify", "origin/feature"], checkoutRoot)
    expect(originFeature).toBe(featureSha)

    const originHead = await runGit(
      ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
      checkoutRoot,
    )
    expect(originHead).toBe("origin/main")

    const stubBin = await createStubBin("gh-fails")
    const result = await runResolveBase(checkoutRoot, stubBin)

    expect(result.exitCode).toBe(0)
    expect(result.stdout.trim()).toBe(`BASE:${mainSha}`)
  })

  test("unshallows the PR base remote in a detached shallow checkout", async () => {
    const upstreamSeed = await initRepo()
    const initialSha = await commitFile(upstreamSeed, "history.txt", "a\n", "initial")
    const upstreamMainSha = await commitFile(upstreamSeed, "history.txt", "b\n", "upstream main")

    await runGit(["checkout", "-b", "feature"], upstreamSeed)
    const featureSha = await commitFile(upstreamSeed, "feature.txt", "feature\n", "feature change")

    const forkSeed = await initRepo()
    await commitFile(forkSeed, "history.txt", "a\n", "initial")
    const forkMainSha = await commitFile(forkSeed, "fork.txt", "fork\n", "fork main diverges")

    const remotesRoot = await fs.mkdtemp(path.join(os.tmpdir(), "resolve-base-remotes-"))
    const upstreamBare = path.join(
      remotesRoot,
      "github.com",
      "EveryInc",
      "compound-engineering-plugin.git",
    )
    await fs.mkdir(path.dirname(upstreamBare), { recursive: true })
    await runGit(["init", "--bare", upstreamBare], upstreamSeed)
    const upstreamUrl = pathToFileURL(upstreamBare).toString()
    await runGit(["remote", "add", "origin", upstreamUrl], upstreamSeed)
    await runGit(["push", "origin", "main", "feature"], upstreamSeed)

    const forkBare = path.join(remotesRoot, "github.com", "someone", "fork.git")
    await fs.mkdir(path.dirname(forkBare), { recursive: true })
    await runGit(["init", "--bare", forkBare], forkSeed)
    const forkUrl = pathToFileURL(forkBare).toString()
    await runGit(["remote", "add", "origin", forkUrl], forkSeed)
    await runGit(["push", "origin", "main"], forkSeed)

    const checkoutParent = await fs.mkdtemp(path.join(os.tmpdir(), "resolve-base-pr-shallow-"))
    const checkoutRoot = path.join(checkoutParent, "checkout")
    await runCommand(
      ["git", "clone", "--depth", "1", "--branch", "feature", upstreamUrl, checkoutRoot],
      os.tmpdir(),
      gitEnv,
    )
    await runGit(["checkout", "--detach", featureSha], checkoutRoot)
    await runGit(["remote", "rename", "origin", "upstream"], checkoutRoot)
    await runGit(["remote", "add", "origin", forkUrl], checkoutRoot)
    await runGit(["fetch", "--depth", "1", "origin", "main"], checkoutRoot)
    await runGit(["update-ref", "refs/remotes/origin/main", forkMainSha], checkoutRoot)
    await runGit(["branch", "-D", "feature"], checkoutRoot)

    const stubBin = await createStubBin("pr-metadata")
    const result = await runResolveBase(checkoutRoot, stubBin)

    expect(result.exitCode).toBe(0)
    expect(result.stdout.trim()).toBe(`BASE:${upstreamMainSha}`)
  })

  test("emits ERROR output when no base branch can be resolved", async () => {
    const repoRoot = await initRepo("scratch")
    await commitFile(repoRoot, "history.txt", "a\n", "initial")

    const stubBin = await createStubBin("gh-fails")
    const result = await runResolveBase(repoRoot, stubBin)

    expect(result.exitCode).toBe(0)
    expect(result.stdout.trim()).toBe(
      "ERROR:Unable to resolve review base branch locally. Fetch the base branch and rerun, or provide a PR number so the review scope can be determined from PR metadata.",
    )
  })
})
