/**
 * test/integration/git-ops.test.ts (w4-commit-push) — the three git-WRITE routes
 * against a real startServer:
 *   POST /projects/git/stage  — Origin guard, kill-switch, missing field, non-git,
 *                               real stage in a temp repo
 *   POST /projects/git/commit — same guards + real commit + safe 409/400 bodies
 *   POST /projects/git/push   — same guards + real push to a temp BARE remote
 *
 * Mirrors test/integration/worktree.test.ts (startServer + fetch + Origin + real
 * temp repos). No network — a local bare repo is the push remote.
 */

import net from 'node:net'
import os from 'node:os'
import path from 'node:path'
import fs from 'node:fs/promises'
import { execFileSync } from 'node:child_process'

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

import { loadConfig } from '../../src/config.js'
import { startServer } from '../../src/server.js'

const GIT_AVAILABLE = (() => {
  try {
    execFileSync('git', ['--version'], { stdio: 'ignore' })
    return true
  } catch {
    return false
  }
})()
const itGit = GIT_AVAILABLE ? it : it.skip

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('bad addr'))
        return
      }
      const port = addr.port
      srv.close(() => resolve(port))
    })
    srv.on('error', reject)
  })
}

const handles: { close(): Promise<void> }[] = []
const tmpDirs: string[] = []

async function spawnServer(
  overrides: Record<string, string | undefined> = {},
): Promise<{ port: number; origin: 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',
    ...overrides,
  })
  const handle = startServer(cfg)
  handles.push(handle)
  await new Promise<void>((r) => setTimeout(r, 80))
  return { port, origin: `http://127.0.0.1:${port}` }
}

/** Temp git repo on `main` with one committed file. Returns its absolute path. */
async function makeRealRepo(): Promise<string> {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'webterm-gitops-'))
  tmpDirs.push(dir)
  const git = (args: string[]): void => {
    execFileSync('git', args, { cwd: dir, stdio: 'ignore' })
  }
  git(['init', '-b', 'main'])
  git(['config', 'user.email', 'test@localhost'])
  git(['config', 'user.name', 'Test'])
  git(['config', 'commit.gpgsign', 'false'])
  await fs.writeFile(path.join(dir, 'file.txt'), 'line1\nline2\n', 'utf8')
  git(['add', '.'])
  git(['commit', '-m', 'init'])
  return dir
}

/** A bare repo to serve as a push `origin`. Returns its absolute path. */
async function makeBareRemote(): Promise<string> {
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'webterm-gitops-bare-'))
  tmpDirs.push(dir)
  execFileSync('git', ['init', '--bare', '-b', 'main', dir], { stdio: 'ignore' })
  return dir
}

async function postJson(
  port: number,
  route: string,
  body: unknown,
  headers: Record<string, string>,
): Promise<Response> {
  return fetch(`http://127.0.0.1:${port}${route}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', ...headers },
    body: JSON.stringify(body),
  })
}

afterEach(async () => {
  while (handles.length > 0) await handles.pop()?.close()
  for (const d of tmpDirs.splice(0)) await fs.rm(d, { recursive: true, force: true }).catch(() => undefined)
  await new Promise<void>((r) => setTimeout(r, 30))
})

// ── shared guard matrix ─────────────────────────────────────────────────────────

const ROUTES = ['/projects/git/stage', '/projects/git/commit', '/projects/git/push'] as const

describe('git-write routes — shared guards (CSRF / kill-switch)', { timeout: 30_000 }, () => {
  for (const route of ROUTES) {
    it(`${route}: rejects a foreign Origin with 403`, async () => {
      const { port } = await spawnServer()
      const res = await postJson(port, route, { path: '/tmp/x' }, { Origin: 'http://evil.example' })
      expect(res.status).toBe(403)
    })

    it(`${route}: rejects a missing Origin with 403 (default-deny)`, async () => {
      const { port } = await spawnServer()
      const res = await postJson(port, route, { path: '/tmp/x' }, {})
      expect(res.status).toBe(403)
    })

    it(`${route}: returns 403 when GIT_OPS_ENABLED=0`, async () => {
      const { port, origin } = await spawnServer({ GIT_OPS_ENABLED: '0' })
      const res = await postJson(port, route, { path: '/tmp/x' }, { Origin: origin })
      expect(res.status).toBe(403)
    })
  }
})

// ── POST /projects/git/stage ──────────────────────────────────────────────────────

describe('POST /projects/git/stage', { timeout: 30_000 }, () => {
  it('returns 400 when files are missing', async () => {
    const { port, origin } = await spawnServer()
    const res = await postJson(port, '/projects/git/stage', { path: '/tmp/x' }, { Origin: origin })
    expect(res.status).toBe(400)
  })

  it('returns 404 for a non-git directory', async () => {
    const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'webterm-nogit-'))
    tmpDirs.push(dir)
    const { port, origin } = await spawnServer()
    const res = await postJson(port, '/projects/git/stage', { path: dir, files: ['a'] }, { Origin: origin })
    expect(res.status).toBe(404)
  })

  itGit('stages a modified file in a real repo (200) and git records it', async () => {
    const repo = await makeRealRepo()
    await fs.writeFile(path.join(repo, 'file.txt'), 'line1\nCHANGED\n', 'utf8')
    const { port, origin } = await spawnServer()

    const res = await postJson(port, '/projects/git/stage', { path: repo, files: ['file.txt'] }, { Origin: origin })
    expect(res.status).toBe(200)
    const body = (await res.json()) as { ok: boolean; staged: boolean; count: number }
    expect(body).toMatchObject({ ok: true, staged: true, count: 1 })
    const cached = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: repo }).toString()
    expect(cached).toContain('file.txt')
  })
})

// ── POST /projects/git/commit ─────────────────────────────────────────────────────

describe('POST /projects/git/commit', { timeout: 30_000 }, () => {
  itGit('returns 400 for an empty message', async () => {
    const repo = await makeRealRepo()
    const { port, origin } = await spawnServer()
    const res = await postJson(port, '/projects/git/commit', { path: repo, message: '  ' }, { Origin: origin })
    expect(res.status).toBe(400)
  })

  itGit('returns a safe 409 (no "fatal:") when nothing is staged', async () => {
    const repo = await makeRealRepo()
    const { port, origin } = await spawnServer()
    const res = await postJson(port, '/projects/git/commit', { path: repo, message: 'noop' }, { Origin: origin })
    expect(res.status).toBe(409)
    const body = (await res.json()) as { ok: boolean; error: string }
    // Failure body carries ok:false + a SAFE message so the FE guard surfaces it.
    expect(body.ok).toBe(false)
    expect(body.error).not.toContain('fatal:')
    expect(body.error).toBe('Nothing staged to commit.')
  })

  itGit('commits staged changes and returns a short SHA (200)', async () => {
    const repo = await makeRealRepo()
    await fs.writeFile(path.join(repo, 'file.txt'), 'line1\nCHANGED\n', 'utf8')
    execFileSync('git', ['add', 'file.txt'], { cwd: repo })
    const { port, origin } = await spawnServer()

    const res = await postJson(port, '/projects/git/commit', { path: repo, message: 'phone commit' }, { Origin: origin })
    expect(res.status).toBe(200)
    const body = (await res.json()) as { ok: boolean; commit: string }
    expect(body.ok).toBe(true)
    const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo }).toString().trim()
    expect(head.startsWith(body.commit)).toBe(true)
  })
})

// ── POST /projects/git/push ───────────────────────────────────────────────────────

describe('POST /projects/git/push', { timeout: 30_000 }, () => {
  itGit('returns a safe 400 when the repo has no remote', async () => {
    const repo = await makeRealRepo()
    const { port, origin } = await spawnServer()
    const res = await postJson(port, '/projects/git/push', { path: repo }, { Origin: origin })
    expect(res.status).toBe(400)
    const body = (await res.json()) as { error: string }
    expect(body.error).not.toContain('fatal:')
  })

  itGit('pushes the current branch to a bare remote and sets upstream (200)', async () => {
    const repo = await makeRealRepo()
    const bare = await makeBareRemote()
    execFileSync('git', ['remote', 'add', 'origin', bare], { cwd: repo })
    const { port, origin } = await spawnServer()

    const res = await postJson(port, '/projects/git/push', { path: repo }, { Origin: origin })
    expect(res.status).toBe(200)
    const body = (await res.json()) as { ok: boolean; branch: string; remote: string }
    expect(body).toMatchObject({ ok: true, branch: 'main', remote: 'origin' })
    const remoteHead = execFileSync('git', ['--git-dir', bare, 'rev-parse', 'main']).toString().trim()
    const localHead = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo }).toString().trim()
    expect(remoteHead).toBe(localHead)
  })
})
