'use strict';

const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { parseSkillMd, inferCategory } = require('../src/gep/skill2gep');

// inferCategory takes (signals, description); the description carries intent.
const categoryFor = (description) => inferCategory([], description);

// Regression coverage for the parser defects that dropped a Skill's
// "governance tail" (candidate-gating / Human Gate / Output Contract) and
// mislabeled an optimize Skill as `repair`. See fix/skill2gep-parser-governance-loss.

// A SKILL.md shaped like the real paranoia-ai-system-evolver: a multi-section
// workflow with nested sub-bullets, a trailing Human Gate section, and an
// Output Contract section. Its description mentions "rollback" (a safe-change
// mechanism) but its intent is to UPGRADE a system -> optimize, not repair.
const SKILL_MD = [
  '---',
  'name: sample-evolver',
  'description: Use when upgrading an AI system with VOI, OODA, evals, human gates, versioning, and rollback.',
  '---',
  '',
  '# Sample Evolver',
  '',
  '## Quick Workflow',
  '1. Define the current task and the system layer being changed.',
  '2. Pass a VOI gate before gathering more information.',
  '3. Make the operating model explicit:',
  '   - compression: shortest model that explains most cases?',
  '   - causality: which mediator variables connect input to outcome?',
  '   - control points: which mediator can you intervene on?',
  '4. Maintain a compact OODA state:',
  '   - Observe: goal, context, evidence.',
  '   - Orient: current frame, uncertainty map.',
  '   - Decide / Act / Evaluate.',
  '5. Keep every change as `candidate` until evidence, evals, approval, and rollback are present.',
  '',
  '## Human Gate Defaults',
  'Ask for human confirmation before:',
  '- writing long-term memory',
  '- changing production strategy or user-visible systems',
  '- promoting a `candidate` to current rule',
  '',
  '## Output Contract',
  'End by stating:',
  '- what changed',
  '- which evals ran',
  '- how to rollback',
].join('\n');

describe('skill2gep parseSkillMd governance-tail preservation', () => {
  const parsed = parseSkillMd(SKILL_MD);
  const blob = JSON.stringify(parsed.strategy).toLowerCase();

  it('keeps the candidate-gating step (not truncated away)', () => {
    assert.ok(blob.includes('candidate'), 'strategy should retain the `candidate` gating step');
  });

  it('keeps the Human Gate section items', () => {
    assert.ok(
      blob.includes('human confirmation') || blob.includes('long-term memory') || blob.includes('production strategy'),
      'strategy should include Human Gate items from the trailing section'
    );
  });

  it('keeps the Output Contract section items', () => {
    assert.ok(
      blob.includes('what changed') || blob.includes('how to rollback'),
      'strategy should include Output Contract items from the trailing section'
    );
  });

  // Flat extraction: every list item is its own step, including a governance
  // section's items, regardless of indentation. (Folding was removed in PR
  // #156 after it grew a long tail of indentation edge cases; flat extraction
  // preserves the same tail with no indentation reasoning.)
  it('emits a deeper-indented governance section as its own independent steps', () => {
    const md = [
      '---', 'name: t', 'description: optimize a system', '---', '# T', '',
      '## Quick Workflow',
      '1. define the layer being changed',
      '2. validate and record',
      '## Human Gate Defaults',
      'Ask before:',
      '  - writing long-term memory',
      '  - changing production strategy',
    ].join('\n');
    const p = parseSkillMd(md);
    assert.ok(p.strategy.some((s) => /writing long-term memory/.test(s)),
      'governance item should be its own step');
    assert.ok(p.strategy.some((s) => /changing production strategy/.test(s)),
      'second governance item should also be its own step');
    // No merging of distinct items into one combined step.
    assert.ok(!p.strategy.some((s) => /validate and record.*writing long-term/s.test(s)),
      'distinct items must not be merged');
  });

  it('emits every sub-bullet as its own step (no folding)', () => {
    const opModelIdx = parsed.strategy.findIndex((s) => /operating model/i.test(s));
    assert.ok(opModelIdx >= 0, 'should have an "operating model" step');
    // Each sub-point stands alone as its own step.
    assert.ok(parsed.strategy.some((s) => /^compression:/i.test(s.trim())),
      'compression sub-point should be its own step');
    assert.ok(parsed.strategy.some((s) => /^control points:/i.test(s.trim())),
      'control-points sub-point should be its own step');
  });

  it('extracts the full governance tail without truncating (cap clears a rich Skill)', () => {
    assert.ok(parsed.strategy.length > 10,
      'multi-section Skill should yield a rich strategy list, not a truncated one');
    assert.ok(parsed.strategy.length <= 28, 'but should stay within the compact cap');
  });

  it('keeps a uniformly-indented list as separate steps', () => {
    const md = [
      '---', 'name: t', 'description: optimize the build', '---', '# T', '',
      '## Workflow',
      '  1. first step do the thing',
      '  2. second step validate it',
      '  3. third step record outcome',
    ].join('\n');
    const p = parseSkillMd(md);
    assert.equal(p.strategy.length, 3, 'uniform 3-item list must stay 3 separate steps');
  });
});

describe('skill2gep inferCategory (Bugbot #156 follow-ups)', () => {
  it('does NOT let cross-cutting safety words (rollback) force repair on an upgrade skill', () => {
    assert.equal(categoryFor('Use when upgrading an AI system with versioning and rollback and guard rails'), 'optimize');
  });

  it('keeps repair-first priority: a genuine fix intent still classifies repair', () => {
    assert.equal(categoryFor('review and fix critical production bugs and crashes'), 'repair');
  });

  it('does not misclassify "tunnel" as optimize (no greedy tun\\w* match)', () => {
    assert.equal(categoryFor('implement a tunnel for secure remote access'), 'innovate');
  });

  // Bugbot #156 (post-refactor, Medium): "add" must still classify innovate...
  it('classifies "add ..." descriptions as innovate', () => {
    assert.equal(categoryFor('add a new monitoring dashboard'), 'innovate');
    assert.equal(categoryFor('add retry logic to the client'), 'innovate');
  });

  // ...but the \b-bounded "add" must not false-positive on address/additional/padding.
  it('does not treat address/additional/padding as innovate via "add"', () => {
    assert.equal(categoryFor('optimize additional logging throughput'), 'optimize');
    assert.equal(categoryFor('reduce padding in the buffer layout'), 'optimize');
  });

  // Bugbot #156 round 2 (High): substring matching must survive inflected
  // forms and the project's underscore signal format.
  it('matches inflected repair forms (errors / fixed / crashes)', () => {
    assert.equal(categoryFor('handle errors and crashes after a fix was reverted'), 'repair');
  });

  it('matches underscore-separated signals like log_error / test_failure', () => {
    assert.equal(inferCategory(['log_error', 'test_failure'], ''), 'repair');
  });

  it('keeps short preconditions like "Git"/"npm" (no 5-char gate, no folding)', () => {
    const md = [
      '---', 'name: t', 'description: optimize the build', '---',
      '# T', '', '## Prerequisites', '- Git', '- npm', '- a configured CI token', '',
      '## Workflow', '1. do the thing carefully', '2. validate it',
    ].join('\n');
    const p = parseSkillMd(md);
    assert.ok(p.preconditions.includes('Git'), 'short "Git" precondition must survive');
    assert.ok(p.preconditions.includes('npm'), 'short "npm" precondition must survive');
    assert.equal(p.preconditions.length, 3, 'all three preconditions kept, none folded/dropped');
  });

  it('paranoia-style upgrade description (with rollback) stays optimize', () => {
    assert.equal(
      inferCategory(
        ['use_when_upgrading_an_ai_system', 'tool_routing', 'schema'],
        'Use when upgrading an AI system with VOI, OODA, evals, human gates, versioning, and rollback'
      ),
      'optimize'
    );
  });
});

// A Chinese-authored SKILL.md: headings carry no English token, so the section
// keyword tables must recognize CJK synonyms or the parser silently falls back
// to a thin gene (strategy=fallback, avoid=[]). Mirrors the real game-* skills
// in ParanoiaSkills that distilled thin before this fix.
const CJK_SKILL_MD = [
  '---',
  'name: cjk-curator',
  // Real game-* skills carry a comma-separated English description; the signal
  // tokenizer splits on commas, so this is what produces ASCII signal tokens.
  'description: Use when curating game design sources, research, screening, review, and ingestion into a durable local knowledge base.',
  '---',
  '',
  '# 资料策展',
  '',
  '## 触发条件',
  '- 来源研究、首轮建档、候选审核、标准入库时使用',
  '',
  '## 快速工作流',
  '1. Observe：确认任务模式，从研究、建档、审核中选一个主模式。',
  '2. VOI 门：只收集会改变决策的信息，先做去重和短读。',
  '3. Decide：状态推进必须有证据，未深读不得进入 accepted。',
  '',
  '## 输出门',
  '- 检查 catalog、registry、update-history 是否同步完成',
  '- 未知字段保留 unknown，不能悄悄猜满',
  '',
  '## 不要做',
  '- 不要见文就收、短读即入库',
  '- 不要忽略去重、证据门和置信度',
  '',
  '## 前置条件',
  '- Git',
  '- 已配置的本地知识库目录',
].join('\n');

describe('skill2gep parseSkillMd CJK section headings', () => {
  const parsed = parseSkillMd(CJK_SKILL_MD);

  it('extracts strategy steps from Chinese workflow + output-gate headings', () => {
    assert.ok(parsed.strategy.length >= 5,
      `expected rich strategy from CJK headings, got ${parsed.strategy.length}`);
    assert.ok(parsed.strategy.some((s) => /确认任务模式/.test(s)),
      'workflow step from "## 快速工作流" must be captured');
    assert.ok(parsed.strategy.some((s) => /catalog、registry/.test(s)),
      'governance-tail step from "## 输出门" must be captured');
  });

  it('extracts avoid items from a Chinese "不要做" heading', () => {
    assert.ok(parsed.avoid.length >= 2,
      `expected avoid items from "## 不要做", got ${parsed.avoid.length}`);
    assert.ok(parsed.avoid.some((s) => /见文就收/.test(s)),
      'anti-pattern from "## 不要做" must be captured');
  });

  it('still derives signals from the (English) frontmatter description', () => {
    // The CJK heading is now matched, but the signal tokenizer keeps ASCII
    // [a-z0-9_] only, so signals come from the English description. Pure-CJK
    // body words do not tokenize — a documented out-of-scope limitation.
    assert.ok(parsed.signals_match.some((s) => /curat|knowledge|base|source/.test(s)),
      'signals from English frontmatter description must survive');
  });

  it('documents the CJK signal-tokenizer gap: a CJK-only trigger body adds no signals', () => {
    const cjkOnly = [
      '---', 'name: t', 'description: 用于资料策展', '---',
      '# T', '', '## 触发条件', '- 来源研究、首轮建档、候选审核',
    ].join('\n');
    // No ASCII description, CJK-only body -> tokenizer yields nothing. This is
    // the known limitation; if a future PR adds a CJK segmenter, update this.
    assert.equal(parseSkillMd(cjkOnly).signals_match.length, 0);
  });

  it('keeps short preconditions from a Chinese "前置条件" heading', () => {
    assert.ok(parsed.preconditions.includes('Git'),
      'short "Git" precondition under CJK heading must survive');
  });

  it('does not regress: an English SKILL.md still parses unchanged', () => {
    const p = parseSkillMd(SKILL_MD);
    assert.ok(p.strategy.some((s) => /what changed/i.test(s)),
      'English governance tail still captured after adding CJK keywords');
  });
});
