/**
 * Integration test for GET /projects/pr (W3 PR + CI status chip).
 *
 * Starts a real HTTP server against a temp git repo and stubs `gh` with a PATH
 * shim: an executable script named `gh` in a temp bin/ dir prepended to PATH, so
 * the server's execFile('gh', …) resolves OUR script (which echoes canned JSON or
 * exits 1 with a canned stderr). Asserts:
 *   - missing ?path        → 400
 *   - non-git ?path        → 404 (isValidGitDir three-prong)
 *   - gh emits valid PR    → 200 {availability:'ok', checks:…}
 *   - gh exits 1 (no PR)   → 200 {availability:'no-pr'}
 *   - GH_ENABLED=0         → 200 {availability:'disabled'} and gh is NEVER spawned
 *
 * Determinism: PROJECT_SCAN_TTL=0 disables the gh module cache; _clearPrCache()
 * runs in afterEach; each test writes the exact gh shim it needs.
 */

import fs from 'node:fs/promises'
import net from 'node:net'
import os from 'node:os'
import path from 'node:path'

import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'

import { loadConfig } from '../../src/config.js'
import { startServer } from '../../src/server.js'
import { _clearPrCache } from '../../src/http/gh.js'
import type { PrStatus } from '../../src/types.js'

// ── helpers ───────────────────────────────────────────────────────────────────

function getFreePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const srv = net.createServer()
    srv.listen(0, '127.0.0.1', () => {
      const addr = srv.address()
      if (addr === null || typeof addr === 'string') {
        srv.close()
        reject(new Error('unexpected address type'))
        return
      }
      const port = addr.port
      srv.close(() => resolve(port))
    })
    srv.on('error', reject)
  })
}

async function makeFakeGitRepo(parentDir: string, repoName: string): Promise<string> {
  const repoPath = path.join(parentDir, repoName)
  await fs.mkdir(path.join(repoPath, '.git'), { recursive: true })
  await fs.writeFile(path.join(repoPath, '.git', 'HEAD'), 'ref: refs/heads/main\n', 'utf8')
  return repoPath
}

/** Write an executable `gh` shim into binDir with the given /bin/sh body. */
async function writeGhShim(binDir: string, body: string): Promise<void> {
  const file = path.join(binDir, 'gh')
  await fs.writeFile(file, `#!/bin/sh\n${body}\n`, 'utf8')
  await fs.chmod(file, 0o755)
}

const okJson = JSON.stringify({
  number: 7,
  state: 'OPEN',
  title: 'A pull request',
  url: 'https://github.com/o/r/pull/7',
  isDraft: false,
  mergeable: 'MERGEABLE',
  headRefName: 'main',
  baseRefName: 'main',
  statusCheckRollup: [
    { __typename: 'CheckRun', status: 'COMPLETED', conclusion: 'SUCCESS' },
    { __typename: 'CheckRun', status: 'COMPLETED', conclusion: 'SUCCESS' },
    { __typename: 'CheckRun', status: 'IN_PROGRESS', conclusion: null },
  ],
})

// ── suite ─────────────────────────────────────────────────────────────────────

describe('GET /projects/pr — integration', () => {
  let tmpRoot: string
  let repoPath: string
  let binDir: string
  let originalPath: string
  let serverHandle: { close(): Promise<void> } | null = null

  beforeAll(async () => {
    tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'webterm-pr-test-'))
    repoPath = await makeFakeGitRepo(tmpRoot, 'fake-repo')
    binDir = path.join(tmpRoot, 'bin')
    await fs.mkdir(binDir, { recursive: true })
    // Prepend our shim dir so execFile('gh', …) resolves OUR gh, not the host's.
    originalPath = process.env['PATH'] ?? ''
    process.env['PATH'] = `${binDir}${path.delimiter}${originalPath}`
  })

  afterAll(async () => {
    process.env['PATH'] = originalPath
    await fs.rm(tmpRoot, { recursive: true, force: true })
  })

  afterEach(async () => {
    if (serverHandle !== null) {
      await serverHandle.close()
      serverHandle = null
    }
    _clearPrCache()
  })

  /** Start a server with the given env overrides; returns its base URL. */
  async function start(overrides: Record<string, string> = {}): Promise<string> {
    const port = await getFreePort()
    const cfg = loadConfig({
      PORT: String(port),
      BIND_HOST: '127.0.0.1',
      SHELL_PATH: process.env['SHELL'] ?? '/bin/zsh',
      ALLOWED_ORIGINS: `http://127.0.0.1:${port}`,
      USE_TMUX: '0',
      IDLE_TTL: '86400',
      PROJECT_SCAN_TTL: '0', // no gh cache between requests
      ...overrides,
    })
    serverHandle = startServer(cfg)
    await new Promise<void>((r) => setTimeout(r, 100))
    return `http://127.0.0.1:${port}`
  }

  it('returns 400 when ?path is missing', async () => {
    const base = await start()
    const res = await fetch(`${base}/projects/pr`)
    expect(res.status).toBe(400)
  })

  it('returns 404 for a non-git directory path', async () => {
    const base = await start()
    const res = await fetch(`${base}/projects/pr?path=${encodeURIComponent(tmpRoot)}`)
    expect(res.status).toBe(404)
  })

  it('returns 200 {availability:ok} with a checks summary when gh emits PR JSON', async () => {
    await writeGhShim(binDir, `cat <<'JSON'\n${okJson}\nJSON`)
    const base = await start()
    const res = await fetch(`${base}/projects/pr?path=${encodeURIComponent(repoPath)}`)
    expect(res.status).toBe(200)
    const body = (await res.json()) as PrStatus
    expect(body.availability).toBe('ok')
    expect(body.number).toBe(7)
    expect(body.state).toBe('open')
    expect(body.checks).toEqual({ total: 3, passing: 2, failing: 0, pending: 1 })
  })

  it('returns 200 {availability:no-pr} when gh exits 1 with a no-PR stderr', async () => {
    await writeGhShim(binDir, 'echo "no pull requests found for branch \\"main\\"" 1>&2\nexit 1')
    const base = await start()
    const res = await fetch(`${base}/projects/pr?path=${encodeURIComponent(repoPath)}`)
    expect(res.status).toBe(200)
    const body = (await res.json()) as PrStatus
    expect(body.availability).toBe('no-pr')
  })

  it('returns 200 {availability:disabled} and never spawns gh when GH_ENABLED=0', async () => {
    const marker = path.join(tmpRoot, 'gh-was-spawned')
    await fs.rm(marker, { force: true })
    // This shim would create a marker if ever invoked.
    await writeGhShim(binDir, `touch "${marker}"\necho '{}'`)
    const base = await start({ GH_ENABLED: '0' })
    const res = await fetch(`${base}/projects/pr?path=${encodeURIComponent(repoPath)}`)
    expect(res.status).toBe(200)
    const body = (await res.json()) as PrStatus
    expect(body).toEqual({ availability: 'disabled' })
    // gh must never have run.
    await expect(fs.stat(marker)).rejects.toBeDefined()
  })
})
