import { describe, it, expect } from 'vitest'
import { execFileSync } from 'node:child_process'
import { mkdtempSync, writeFileSync, readFileSync, rmSync, existsSync, readdirSync } 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')
  })
})

// ---------------------------------------------------------------------------
// M1b Step 5c：账号级资源（自定义域名 + 证书）按履约切换
//
// 自定义域名与通配符证书都绑在**我们自己的**阿里云账号上，所以它只在「部署者供给阿里云 key」
// 这种履约下成立。使用者自带 key 时发布落在他自己的账号里，那里既没有我们的 DNS 也没有我们的
// 证书，硬渲染出来只会在 fc3-domain 那步失败——所以整段不渲染，用 FC 默认域名。
//
// 下面两组用例跑的是**真的 config.env**（不是手编样例）：一组验它作为「可被 env 覆盖的默认值表」
// 的行为，一组验按同一个判据拼出来的 s.yaml 差异。
const DEFAULTS = join(__dirname, '..', '..', 'persona', 'defaults')
const KIT = join(DEFAULTS, 'persona-app-builder', 'extension-kit')

/** 复刻 publish.sh 的「source config.env → 打印结果」段。实现若改 publish.sh，本片段须同步。 */
function sourceConfigEnv(env: Record<string, string>): Record<string, string> {
  const out = execFileSync(
    'bash',
    ['-c', `set -euo pipefail; . "$1"; for v in APP_DOMAIN_SUFFIX CERT_ID REGION FC_CPU; do printf '%s=%s\\n' "$v" "\${!v}"; done`, 'cfg', join(KIT, 'config.env')],
    { env: { ...process.env, ...env }, encoding: 'utf8' },
  )
  return Object.fromEntries(out.trim().split('\n').map((l) => {
    const i = l.indexOf('=')
    return [l.slice(0, i), l.slice(i + 1)]
  }))
}

/**
 * 复刻 publish.sh 的「按 kit 有没有 s.yaml.domain.tmpl + APP_DOMAIN_SUFFIX 拼模板 → 渲染」段。
 * 同上，改脚本须同步。**判据的第一层是文件存在**——没有那个文件的 persona 压根不参与这套切换。
 */
function renderSYaml(env: Record<string, string>): string {
  const cfg = sourceConfigEnv(env)
  const base = readFileSync(join(KIT, 'contract', 's.yaml.tmpl'), 'utf8')
  const domTmpl = join(KIT, 'contract', 's.yaml.domain.tmpl')
  const tmpl = existsSync(domTmpl) && cfg.APP_DOMAIN_SUFFIX
    ? base + readFileSync(domTmpl, 'utf8')
    : base
  const r = render(tmpl, {
    ...cfg,
    ...env,
    APP_NAME: 'demo', DEPLOY_NAME: 'demo-ab12', CODE_DIR: './server', FC_ENTRY: 'dist/main.js',
    FC_RUNTIME: 'custom.debian12', NODE_LAYER: 'layer', FC_MEMORY: '512', FC_TIMEOUT: '60',
    SUPABASE_URL: 'http://sb', SUPABASE_KEY: 'anon',
  })
  expect(r.err ?? '').toBe('')
  return r.out!
}

describe('config.env 作为「可被 env 覆盖的默认值表」', () => {
  it('env 里没有这几个变量 → 部署级默认（品牌域名 + 我们账号的 certId）', () => {
    const clean = JSON.parse(
      execFileSync('bash', ['-c',
        `unset APP_DOMAIN_SUFFIX CERT_ID REGION; . "$1"; printf '{"d":"%s","c":"%s","r":"%s"}' "$APP_DOMAIN_SUFFIX" "$CERT_ID" "$REGION"`,
        'cfg', join(KIT, 'config.env')], { encoding: 'utf8' }),
    )
    expect(clean).toEqual({ d: 'app.clawos.chat', c: '25612343', r: 'cn-hangzhou' })
  })

  it('env 给了就以 env 为准（source 不覆盖上游注入的值）', () => {
    const cfg = sourceConfigEnv({ APP_DOMAIN_SUFFIX: 'app.example.com', CERT_ID: '999', REGION: 'cn-beijing' })
    expect(cfg.APP_DOMAIN_SUFFIX).toBe('app.example.com')
    expect(cfg.CERT_ID).toBe('999')
    expect(cfg.REGION).toBe('cn-beijing')
  })

  it('APP_DOMAIN_SUFFIX 显式空串保持为空（${VAR-d} 不是 ${VAR:-d}）', () => {
    // 这是「使用者自带 key」那条履约的开关：空串必须活下来，被默认值吃掉就等于开关失灵
    expect(sourceConfigEnv({ APP_DOMAIN_SUFFIX: '' }).APP_DOMAIN_SUFFIX).toBe('')
    // 对照：平台常量用 ${VAR:-d}，空串没有意义，回落默认
    expect(sourceConfigEnv({ FC_CPU: '' }).FC_CPU).toBe('0.35')
  })
})

describe('两种履约渲染出来的 s.yaml 差异', () => {
  it('部署级：带 fc3-domain 段 + 品牌域名 + certId', () => {
    const out = renderSYaml({})
    expect(out).toContain('component: fc3-domain')
    expect(out).toContain('domainName: demo-ab12.app.clawos.chat')
    expect(out).toContain('certId: 25612343')
  })

  it('使用者自带 key：整段不渲染，只剩函数 + httpTrigger（走 FC 默认域名）', () => {
    const out = renderSYaml({ APP_DOMAIN_SUFFIX: '' })
    expect(out).not.toContain('fc3-domain')
    expect(out).not.toContain('certId')
    expect(out).not.toContain('clawos.chat')
    expect(out).toContain('triggerType: http')
  })

  it('换成使用者自己的域名与证书：两个值都跟着走', () => {
    const out = renderSYaml({ APP_DOMAIN_SUFFIX: 'app.example.com', CERT_ID: '999' })
    expect(out).toContain('domainName: demo-ab12.app.example.com')
    expect(out).toContain('certId: 999')
  })
})

// publish.sh 是 **persona 无关的共享脚本**，persona-dataclaw-builder 也走它。它有自己的一套
// extension-kit：functionName 不带 slug、自定义域名段写死在它自己的 s.yaml.tmpl 里
// （`domainName: auto`）、没有 s.yaml.domain.tmpl。5c 的判据必须是「有没有那个文件」而不是
// 「APP_DOMAIN_SUFFIX 空不空」——只看后者的话，dataclaw 会被当成「使用者自带 key」拖进
// FC 默认域名那条分支，而它的函数名规则根本对不上，等于给一个没参与的 persona 换了发布路径。
describe('未参与 5c 的 persona（dataclaw-builder）不受影响', () => {
  const DC_KIT = join(DEFAULTS, 'persona-dataclaw-builder', 'extension-kit')

  it('没有 s.yaml.domain.tmpl —— 判据的第一层就把它排除在外', () => {
    expect(existsSync(join(DC_KIT, 'contract', 's.yaml.domain.tmpl'))).toBe(false)
  })

  it('它的自定义域名段仍在自己的 s.yaml.tmpl 里，不依赖 APP_DOMAIN_SUFFIX', () => {
    const text = readFileSync(join(DC_KIT, 'contract', 's.yaml.tmpl'), 'utf8')
    expect(text).toContain('fc3-domain')
    expect(text).not.toContain('__APP_DOMAIN_SUFFIX__')
  })

  it('它的 config.env 里没有 APP_DOMAIN_SUFFIX（所以那个变量对它恒空）', () => {
    expect(readFileSync(join(DC_KIT, 'config.env'), 'utf8')).not.toContain('APP_DOMAIN_SUFFIX')
  })
})

// deploy-kit 的脚本 macOS（BSD userland）和 Linux（GNU coreutils，= M1b 的 Pod）都要跑，
// 两套 userland 语义有分歧的写法就是定时炸弹：`mktemp -t prefix` BSD 自己补随机后缀，
// GNU 要求模板以 X 结尾，后者直接 `too few X's in template` 退 1（coreutils 9.1 实测），
// `set -e` 下当场打死脚本 —— 这条在 2026-09-03 的复审里真炸过一次。
// 本机跑测试只覆盖得到 BSD 那半，所以用静态形态守：写全 `XXXXXX` 模板两边都过。
// 只看非注释行 —— 注释里为了讲清楚这个坑就得写出那个形态。
describe('deploy-kit 脚本的 BSD / GNU 可移植性', () => {
  it('不用 `mktemp -t`（GNU 要求模板带 X）', () => {
    const hits: string[] = []
    for (const f of readdirSync(__dirname).filter((n) => n.endsWith('.sh'))) {
      for (const line of readFileSync(join(__dirname, f), 'utf8').split('\n')) {
        if (line.trim().startsWith('#')) continue
        if (/\bmktemp\s+-t\b/.test(line)) hits.push(`${f}: ${line.trim()}`)
      }
    }
    expect(hits).toEqual([])
  })
})
