import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { test } from 'node:test'

import { safeMerge } from './safe-merge.mjs'

const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')

function read(relativePath) {
  return readFileSync(join(root, relativePath), 'utf8')
}

test('pr gate runs the full gate on every PR before merge', () => {
  const workflow = read('.github/workflows/pr-gate.yml')

  assert.match(workflow, /^name: PR Gate$/m)
  assert.match(workflow, /pull_request:\n\s+branches: \[main\]/)
  assert.match(workflow, /runs-on: \[self-hosted, linux, x64, platform-gate\]/)
  assert.match(workflow, /pnpm --filter @app\/consumer exec playwright install chromium/)
  assert.match(workflow, /run: pnpm gate/)
  // same-box runner shares the machine-wide turbo cache; no actions/cache tar/untar
  assert.match(workflow, /TURBO_CACHE_DIR: \/var\/cache\/platform-turbo/)
  assert.doesNotMatch(workflow, /uses: actions\/cache@/)
  assert.match(workflow, /BUILD_CPU_RESERVE: "2"/)
  // timeouts-under-starvation guard: CI must not run under SCHED_IDLE
  assert.match(workflow, /BUILD_SCHED_IDLE: "0"/)
})

test('main gate runs the full gate on the isolated runner and no longer auto-reverts', () => {
  const workflow = read('.github/workflows/main-gate.yml')

  assert.match(workflow, /^name: Main Gate$/m)
  assert.match(workflow, /push:\n\s+branches: \[main\]/)
  assert.match(workflow, /runs-on: \[self-hosted, linux, x64, platform-gate\]/)
  assert.match(workflow, /pnpm --filter @app\/consumer exec playwright install chromium/)
  assert.match(workflow, /run: pnpm gate/)
  assert.match(workflow, /TURBO_CACHE_DIR: \/var\/cache\/platform-turbo/)
  assert.doesNotMatch(workflow, /uses: actions\/cache@/)
  assert.match(workflow, /BUILD_CPU_RESERVE: "2"/)
  // timeouts-under-starvation guard: CI must not run under SCHED_IDLE
  assert.match(workflow, /BUILD_SCHED_IDLE: "0"/)
  // The auto-revert bot is gone — main is kept green by the pre-merge PR gate.
  assert.doesNotMatch(workflow, /recover:/)
  assert.doesNotMatch(workflow, /revert-main-push/)
})

test('the auto-revert automation script is deleted', () => {
  assert.throws(() => read('scripts/revert-main-push.mjs'), /ENOENT/)
})

test('delivery uses only the exact successful Main Gate SHA', () => {
  const workflow = read('.github/workflows/delivery.yml')

  assert.match(workflow, /^name: Delivery$/m)
  assert.match(workflow, /workflows: \["Main Gate"\]/)
  assert.match(workflow, /github\.event\.workflow_run\.conclusion == 'success'/)
  assert.match(workflow, /ref: \$\{\{ github\.event\.workflow_run\.head_sha \}\}/)
  assert.match(workflow, /TESTED_SHA: \$\{\{ github\.event\.workflow_run\.head_sha \}\}/)
  assert.doesNotMatch(workflow, /pull_request:/)
})

test('delivery owns package publication and production Worker deployment', () => {
  const workflow = read('.github/workflows/delivery.yml')
  const rootPackage = JSON.parse(read('package.json'))
  const cmsPackage = JSON.parse(read('apps/mod-cms/package.json'))

  assert.match(workflow, /publish: pnpm exec changeset publish/)
  assert.match(workflow, /registry-url: https:\/\/npm\.pkg\.github\.com/)
  assert.doesNotMatch(workflow, />> ~\/\.npmrc/)
  assert.match(workflow, /pnpm --filter @app\/mod-cms exec wrangler deploy/)
  assert.match(workflow, /environment: production/)
  assert.match(workflow, /CLOUDFLARE_API_TOKEN: \$\{\{ secrets\.CLOUDFLARE_API_TOKEN \}\}/)
  assert.equal(rootPackage.scripts['changeset:publish'], undefined)
  assert.equal(cmsPackage.scripts.deploy, undefined)
})

function fakeExecutor(overrides = {}) {
  const calls = []
  const exec = async (command, args, options = {}) => {
    calls.push({ command, args, input: options.input })
    const key = `${command} ${args.join(' ')}`
    if (overrides[key] instanceof Error) throw overrides[key]
    if (key in overrides) return overrides[key]
    return { status: 0, stdout: '' }
  }
  return { calls, exec }
}

const viewArgs = pr => `gh pr view ${pr} --json statusCheckRollup,state,headRefName`

test('safe-merge merges only when the PR Gate is green', async () => {
  const rollup = { state: 'OPEN', statusCheckRollup: [{ workflowName: 'PR Gate', name: 'Full gate', conclusion: 'SUCCESS' }] }
  const fake = fakeExecutor({ [viewArgs('7')]: { status: 0, stdout: JSON.stringify(rollup) } })

  const result = await safeMerge({ pr: '7' }, { exec: fake.exec })

  assert.deepEqual(result, { merged: true, pr: 7, method: 'squash' })
  assert(fake.calls.some(c => c.command === 'gh' && c.args.join(' ') === 'pr merge 7 --squash --delete-branch'))
})

test('safe-merge refuses when the PR Gate has not run', async () => {
  const rollup = { state: 'OPEN', statusCheckRollup: [{ workflowName: 'Some Other', name: 'x', conclusion: 'SUCCESS' }] }
  const fake = fakeExecutor({ [viewArgs('7')]: { status: 0, stdout: JSON.stringify(rollup) } })

  await assert.rejects(safeMerge({ pr: '7' }, { exec: fake.exec }), /no "PR Gate" check has run/)
  assert.equal(fake.calls.some(c => c.args[0] === 'pr' && c.args[1] === 'merge'), false)
})

test('safe-merge refuses when the PR Gate is failing or pending', async () => {
  const rollup = { state: 'OPEN', statusCheckRollup: [{ workflowName: 'PR Gate', name: 'Full gate', conclusion: 'FAILURE' }] }
  const fake = fakeExecutor({ [viewArgs('7')]: { status: 0, stdout: JSON.stringify(rollup) } })

  await assert.rejects(safeMerge({ pr: '7' }, { exec: fake.exec }), /not green/)
  assert.equal(fake.calls.some(c => c.args[0] === 'pr' && c.args[1] === 'merge'), false)
})

test('safe-merge refuses a PR that is not open', async () => {
  const rollup = { state: 'MERGED', statusCheckRollup: [{ workflowName: 'PR Gate', name: 'Full gate', conclusion: 'SUCCESS' }] }
  const fake = fakeExecutor({ [viewArgs('7')]: { status: 0, stdout: JSON.stringify(rollup) } })

  await assert.rejects(safeMerge({ pr: '7' }, { exec: fake.exec }), /state is MERGED/)
})

test('safe-merge rejects a non-numeric PR or bad merge method', async () => {
  const fake = fakeExecutor()

  await assert.rejects(safeMerge({ pr: 'abc' }, { exec: fake.exec }), /PR number is required/)
  await assert.rejects(safeMerge({ pr: '7', method: 'ff' }, { exec: fake.exec }), /invalid merge method/)
  assert.equal(fake.calls.length, 0)
})

test('safe-merge treats a server-side merge as success when gh local cleanup fails', async () => {
  // gh merged the PR but its post-merge local `git checkout main` failed because another
  // worktree holds main. The merge landed — safe-merge must not report a false failure.
  const rollup = { state: 'OPEN', headRefName: 'fix/x', statusCheckRollup: [{ workflowName: 'PR Gate', name: 'Full gate', conclusion: 'SUCCESS' }] }
  const fake = fakeExecutor({
    [viewArgs('7')]: { status: 0, stdout: JSON.stringify(rollup) },
    'gh pr merge 7 --squash --delete-branch': { status: 1, stderr: "failed to run git: fatal: 'main' is already used by worktree" },
    'gh pr view 7 --json state': { status: 0, stdout: JSON.stringify({ state: 'MERGED' }) },
  })

  const result = await safeMerge({ pr: '7' }, { exec: fake.exec })

  assert.deepEqual(result, { merged: true, pr: 7, method: 'squash' })
  // it deletes the remote branch that gh's failed cleanup left behind
  assert(fake.calls.some(c => c.args.join(' ') === 'api -X DELETE repos/{owner}/{repo}/git/refs/heads/fix/x'))
})

test('safe-merge still fails when gh errors and the merge did not land', async () => {
  const rollup = { state: 'OPEN', headRefName: 'fix/x', statusCheckRollup: [{ workflowName: 'PR Gate', name: 'Full gate', conclusion: 'SUCCESS' }] }
  const fake = fakeExecutor({
    [viewArgs('7')]: { status: 0, stdout: JSON.stringify(rollup) },
    'gh pr merge 7 --squash --delete-branch': { status: 1, stderr: 'merge conflict between base and head' },
    'gh pr view 7 --json state': { status: 0, stdout: JSON.stringify({ state: 'OPEN' }) },
  })

  await assert.rejects(safeMerge({ pr: '7' }, { exec: fake.exec }), /gh pr merge failed/)
  assert.equal(fake.calls.some(c => c.args[0] === 'api'), false)
})
