import { describe, it, expect } from 'vitest'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'

// 复刻 publish.sh 渲染段（generic placeholder substitution）的最小脚本，锁定占位符替换契约。
// 实现若改 publish.sh 的扫描循环，本片段须同步。
const RENDER_SNIPPET = `
set -euo pipefail
TMPL="$1"; OUT="$2"
sed_args=()
for ph in $(grep -oE '__[A-Z0-9_]+__' "$TMPL" | sort -u); do
  var="\${ph#__}"; var="\${var%__}"
  if ! declare -p "$var" >/dev/null 2>&1; then
    echo "MISSING:$ph" >&2; exit 1
  fi
  val="\${!var}"
  val="\${val//\\\\/\\\\\\\\}"
  val="\${val//|/\\\\|}"
  val="\${val//&/\\\\&}"
  sed_args+=(-e "s|$ph|$val|g")
done
sed "\${sed_args[@]}" "$TMPL" > "$OUT"
`

function render(tmpl: string, env: Record<string, string>): { ok: boolean; out?: string; err?: string } {
  const dir = mkdtempSync(join(tmpdir(), 'pub-'))
  const tmplPath = join(dir, 's.yaml.tmpl')
  const outPath = join(dir, 's.yaml')
  writeFileSync(tmplPath, tmpl)
  try {
    execFileSync('bash', ['-c', RENDER_SNIPPET, 'render', tmplPath, outPath], {
      env: { ...process.env, ...env },
    })
    return { ok: true, out: readFileSync(outPath, 'utf8') }
  } catch (e) {
    return { ok: false, err: (e as { stderr?: Buffer }).stderr?.toString() ?? '' }
  } finally {
    rmSync(dir, { recursive: true, force: true })
  }
}

describe('publish.sh 占位符替换', () => {
  it('定义量正常替换', () => {
    const r = render('url: __FOO__\nkey: __BAR__\n', { FOO: 'http://x', BAR: 'abc' })
    expect(r.ok).toBe(true)
    expect(r.out).toContain('url: http://x')
    expect(r.out).toContain('key: abc')
  })

  it('未定义量 fail-loud', () => {
    const r = render('x: __NOPE__\n', {})
    expect(r.ok).toBe(false)
    expect(r.err).toContain('MISSING:__NOPE__')
  })

  it('显式空字符串视为已定义、替换为空', () => {
    const r = render('x: "__OPT__"\n', { OPT: '' })
    expect(r.ok).toBe(true)
    expect(r.out).toContain('x: ""')
  })

  it('值含 sed 特殊字符（| & \\）按字面替换不损坏', () => {
    const r = render('secret: __SEC__\n', { SEC: 'a|b&c\\d' })
    expect(r.ok).toBe(true)
    expect(r.out).toContain('secret: a|b&c\\d')
  })
})
