// tests/audit-plugin-agent-manifest.test.ts
// -----------------------------------------------------------------------------
// `audit:plugin-agent-manifest` 的**整脚本**回归面（E-fx）。
//
// 为什么 spawn 整脚本而不 import 判据函数：只 import 符号的测试看不见**接线** ——
// 摘掉 main() 的接线后闸对真违例照印 PASS，而全量 vitest 全绿（[[INFRA-F138]] 实证）。
// -----------------------------------------------------------------------------
import { describe, it, expect, afterAll } from 'vitest'
import {
  createGateFixture, writeFixtureFile, runGate, cleanupGateFixtures,
} from './lib/gate-fixture-root'

const GATE = 'scripts/audit-plugin-agent-manifest.mjs'
const MANIFEST = '.claude-plugin/plugin.json'

/** 探针名刻意不与仓库现有 agent 重名 —— 否则「读的是 fixture 还是真仓库」分不开。 */
const A1 = '.claude/agents/probe-zeta.md'
const A2 = '.claude/agents/nested/probe-eta.md'

function agentMd(name: string) {
  return `---\nname: ${name}\ndescription: 探针 agent，仅用于闸的回归面\ntools: Read\n---\n\n正文。\n`
}

function manifest(agents: string[]) {
  return JSON.stringify({
    name: 'probe-plugin', version: '0.0.1', description: 'fixture',
    agents,
  }, null, 2)
}

function makeFixture(opts: { agents?: string[]; files?: string[] } = {}) {
  const files = opts.files ?? [A1, A2]
  const agents = opts.agents ?? files.map((f) => `./${f}`)
  const seed: Record<string, string> = { [MANIFEST]: manifest(agents) }
  for (const f of files) seed[f] = agentMd(f.replace(/.*\//, '').replace('.md', ''))
  return createGateFixture({ gate: GATE, prefix: 'plugin-agent-manifest', files: seed })
}

afterAll(() => cleanupGateFixtures())

describe('audit:plugin-agent-manifest — 整脚本回归面', () => {
  it('对照臂：清单与实际文件双向相等 ⇒ 绿（证明量具不是恒红）', () => {
    const run = runGate(makeFixture(), GATE)
    expect(run.status).toBe(0)
    // 钉一个只有真跑过才有的读数 —— ⛔ exit 0 + 空输出不算绿
    expect(run.stdout).toContain('双向相等（2 个）')
  })

  it('S3 缺项：新增 agent 没进清单 ⇒ 红并点名它（这正是静默漏分发的那一刻）', () => {
    const root = makeFixture({ agents: [`./${A1}`] }) // 清单只列了一个
    const run = runGate(root, GATE)
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('S3')
    expect(run.stderr).toContain('probe-eta')
    expect(run.stderr).toContain('plugin 用户拿不到')
  })

  it('S3 多项：清单指向已删掉的文件 ⇒ 红（清单要跟着缩）', () => {
    const root = makeFixture({
      files: [A1],
      agents: [`./${A1}`, './.claude/agents/probe-deleted.md'],
    })
    const run = runGate(root, GATE)
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('S3')
    expect(run.stderr).toContain('probe-deleted')
  })

  it('S2：agents 写成目录字符串 ⇒ 红（该键只吃文件路径数组，实测目录 = Invalid input）', () => {
    const root = createGateFixture({
      gate: GATE, prefix: 'plugin-agent-manifest',
      files: {
        [MANIFEST]: JSON.stringify({ name: 'p', version: '0.0.1', agents: './.claude/agents' }, null, 2),
        [A1]: agentMd('probe-zeta'),
      },
    })
    const run = runGate(root, GATE)
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('S2')
  })

  it('S2：agents 键缺失 ⇒ 红（不是「没写就跳过」）', () => {
    const root = createGateFixture({
      gate: GATE, prefix: 'plugin-agent-manifest',
      files: {
        [MANIFEST]: JSON.stringify({ name: 'p', version: '0.0.1' }, null, 2),
        [A1]: agentMd('probe-zeta'),
      },
    })
    const run = runGate(root, GATE)
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('S2')
  })

  it('S1 fail-closed：agents 目录为空 ⇒ 红，⛔ 不许「没扫到就当通过」', () => {
    const root = createGateFixture({
      gate: GATE, prefix: 'plugin-agent-manifest',
      files: { [MANIFEST]: manifest([]) },
      dirs: ['.claude/agents'],
    })
    const run = runGate(root, GATE)
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('S1')
    expect(run.stderr).toContain('闸变盲')
  })

  it('递归：只存在于子目录的 agent 也要被算进活源（design-share/scout.md 就是这一类）', () => {
    // 清单故意留空 —— 若递归失效、子目录那个扫不到，活源就是 0，报的会是 S1 而不是 S3
    const root = createGateFixture({
      gate: GATE, prefix: 'plugin-agent-manifest',
      files: { [MANIFEST]: manifest([]), [A2]: agentMd('probe-eta') },
    })
    const run = runGate(root, GATE)
    expect(run.status).toBe(1)
    expect(run.stderr).toContain('S3')          // 扫到了、只是没进清单
    expect(run.stderr).not.toContain('S1')      // ⇒ 不是「活源为 0」
    expect(run.stderr).toContain('probe-eta')
  })
})
