#!/usr/bin/env node
/**
 * Fixture-driven test for classifyDivergence pattern matching.
 *
 * Run: node src/utils/__tests__/classify-divergence.test.js
 * Exits 0 on success, 1 on any mismatch (CI-friendly).
 *
 * Fixtures cover the exact commit subjects observed during the
 * `mayo` consumer debug (v3.25.0 release). Adversarial-review enforced.
 */
const GitUtils = require('../git');
const g = new GitUtils();

const SUBJECT_FIXTURES = [
  // ─── subtree-merge (branch-agnostic) ─────────────────────────────────
  { subject: "Merge commit 'abc123' into develop", expected: 'subtree-merge' },
  { subject: "Merge commit 'abc123' into main", expected: 'subtree-merge' },
  { subject: "Merge commit 'deadbeef' into feature/x", expected: 'subtree-merge' },
  { subject: "Merge branch 'feature-x' into main", expected: 'subtree-merge' },

  // ─── subtree-squash ──────────────────────────────────────────────────
  { subject: "Squashed '.framework/' changes from abc..def", expected: 'subtree-squash' },
  { subject: "Squashed '.framework/' content from 1234..5678", expected: 'subtree-squash' },

  // ─── chore-wrapper (real commits from mayo) ──────────────────────────
  { subject: '[CHORE] reconcile baldart state ledger v3.22.1 -> v3.25.0', expected: 'chore-wrapper' },
  { subject: '[CHORE] register baldart agent-discovery hooks', expected: 'chore-wrapper' },
  { subject: 'chore(baldart): post-update reconcile to v3.25.0', expected: 'chore-wrapper' },
  { subject: 'chore: bump baldart version', expected: 'chore-wrapper' },
  { subject: '[CHORE] baldart update → v3.12.0', expected: 'chore-wrapper' },

  // ─── non-matches (should return null → fall through to path classifier) ──
  { subject: 'feat: add new login screen', expected: null },
  { subject: 'fix: ui-expert agent for mayo design system', expected: null },
  { subject: 'docs: update README', expected: null },

  // ─── adversarial false-positive guards ───────────────────────────────
  // "Merge pull request" is NOT a subtree merge — must NOT match
  { subject: 'Merge pull request #42 from feature/x', expected: null },
  // "Squashed" without `.framework/` path — NOT subtree-squash
  { subject: "Squashed 'docs/' content from abc..def", expected: null },
  // Chore without 'baldart' keyword — NOT chore-wrapper
  { subject: 'chore: update dependencies', expected: null },

  // ─── subject-regex BLIND SPOTS (now covered by parent-count detection) ──
  // git omits the "into <branch>" suffix when the merge lands on `master`,
  // so a real subtree-pull merge can present as a bare "Merge commit 'x'".
  // The subject classifier returns null here; classifyDivergence() then
  // relabels it `subtree-merge` because the commit has 2+ parents. We assert
  // the subject classifier's null so the gap (and why parents matter) is
  // documented and regression-locked.
  { subject: "Merge commit 'abc123'", expected: null },
  // Shared-repo collaboration merge — the " of <url>" segment breaks the
  // narrow `Merge branch 'x' into <branch>$` shape. Also null → parent count.
  { subject: "Merge branch 'develop' of github.com:org/repo into develop", expected: null },
  { subject: "Merge remote-tracking branch 'origin/main'", expected: null },
];

// aggregateDivergenceClass(commits) — pure reduction of per-commit categories
// to the divergence class. The headline Finding-1 case: a single overlay-able
// commit alongside a plumbing merge must stay `overlay-able`, NOT collapse to
// `mixed`, because the merge is excluded as noise before the decision.
const AGGREGATE_FIXTURES = [
  {
    name: 'overlay-able commit + plumbing merge → overlay-able (not mixed)',
    commits: [
      { category: 'custom-overlay-able' },
      { category: 'subtree-merge' },
    ],
    expected: 'overlay-able',
  },
  {
    name: 'overlay-able + subtree-squash + chore-wrapper → overlay-able',
    commits: [
      { category: 'custom-overlay-able' },
      { category: 'subtree-squash' },
      { category: 'chore-wrapper' },
    ],
    expected: 'overlay-able',
  },
  {
    name: 'overlay-able + custom-other → mixed (genuine mix preserved)',
    commits: [
      { category: 'custom-overlay-able' },
      { category: 'custom-other' },
    ],
    expected: 'mixed',
  },
  {
    name: 'only custom-other → real-custom',
    commits: [{ category: 'custom-other' }, { category: 'subtree-merge' }],
    expected: 'real-custom',
  },
  {
    name: 'only noise → all-noise',
    commits: [
      { category: 'subtree-merge' },
      { category: 'subtree-squash' },
      { category: 'chore-wrapper' },
    ],
    expected: 'all-noise',
  },
  {
    name: 'absorbed commit alone → all-noise (content now == upstream)',
    commits: [{ category: 'absorbed' }],
    expected: 'all-noise',
  },
  {
    name: 'absorbed + plumbing merge → all-noise (absorbed is noise)',
    commits: [
      { category: 'absorbed' },
      { category: 'subtree-merge' },
    ],
    expected: 'all-noise',
  },
  {
    name: 'absorbed + genuine overlay-able → overlay-able (absorbed excluded)',
    commits: [
      { category: 'absorbed' },
      { category: 'custom-overlay-able' },
    ],
    expected: 'overlay-able',
  },
];

const PATH_FIXTURES = [
  {
    name: 'all touched in .framework/.claude/agents → overlay-able',
    touched: ['.framework/framework/.claude/agents/ui-expert.md'],
    expected: 'custom-overlay-able',
  },
  {
    name: 'all touched in .framework/.claude/skills → overlay-able',
    touched: [
      '.framework/framework/.claude/skills/ui-design/SKILL.md',
      '.framework/framework/.claude/skills/ui-design/scripts/foo.sh',
    ],
    expected: 'custom-overlay-able',
  },
  {
    name: 'mixed agents + skills → still overlay-able',
    touched: [
      '.framework/framework/.claude/agents/ui-expert.md',
      '.framework/framework/.claude/commands/check.md',
    ],
    expected: 'custom-overlay-able',
  },
  {
    name: 'touches .framework/src → custom-other',
    touched: ['.framework/src/utils/git.js'],
    expected: 'custom-other',
  },
  {
    name: 'mix overlay-able + src → custom-other',
    touched: [
      '.framework/framework/.claude/agents/ui-expert.md',
      '.framework/src/utils/git.js',
    ],
    expected: 'custom-other',
  },
  {
    name: 'empty paths → custom-other (conservative default)',
    touched: [],
    expected: 'custom-other',
  },
  {
    name: 'touches CHANGELOG → custom-other',
    touched: ['.framework/CHANGELOG.md'],
    expected: 'custom-other',
  },
];

// overlayRelForFrameworkFile(p) — pure mapping of a touched .framework/ payload
// file to the consumer overlay path that would preserve a customization of it.
// Mirrors src/commands/overlay.js conventions (overlayPath). null when no
// overlay maps to the file. Used by overlayCoversTouched() to flag commits
// whose intent is already preserved (→ `--reset` safe, scaffold redundant).
const path = require('path');
const OVERLAY_REL_FIXTURES = [
  {
    name: 'skill SKILL.md → flat overlay',
    file: '.framework/framework/.claude/skills/ui-design/SKILL.md',
    expected: path.join('.baldart', 'overlays', 'ui-design.md'),
  },
  {
    name: 'skill nested script → same flat overlay',
    file: '.framework/framework/.claude/skills/ui-design/scripts/foo.sh',
    expected: path.join('.baldart', 'overlays', 'ui-design.md'),
  },
  {
    name: 'agent .md → agents/ overlay',
    file: '.framework/framework/.claude/agents/ui-expert.md',
    expected: path.join('.baldart', 'overlays', 'agents', 'ui-expert.md'),
  },
  {
    name: 'command .md → commands/ overlay',
    file: '.framework/framework/.claude/commands/check.md',
    expected: path.join('.baldart', 'overlays', 'commands', 'check.md'),
  },
  {
    name: 'src file → null (no overlay maps)',
    file: '.framework/src/utils/git.js',
    expected: null,
  },
  {
    name: 'nested agent path → null (agents are flat .md)',
    file: '.framework/framework/.claude/agents/sub/x.md',
    expected: null,
  },
];

let pass = 0, fail = 0;

console.log('─── Subject classifier ───');
for (const f of SUBJECT_FIXTURES) {
  const actual = g.classifyCommitSubject(f.subject);
  if (actual === f.expected) {
    pass++;
    console.log(`  ✓ "${f.subject}" → ${actual}`);
  } else {
    fail++;
    console.log(`  ✗ "${f.subject}" → ${actual} (expected ${f.expected})`);
  }
}

console.log('\n─── Path classifier ───');
for (const f of PATH_FIXTURES) {
  const actual = g.classifyCommitByPaths(f.touched);
  if (actual === f.expected) {
    pass++;
    console.log(`  ✓ ${f.name} → ${actual}`);
  } else {
    fail++;
    console.log(`  ✗ ${f.name} → ${actual} (expected ${f.expected})`);
  }
}

console.log('\n─── Aggregate class ───');
for (const f of AGGREGATE_FIXTURES) {
  const actual = GitUtils.aggregateDivergenceClass(f.commits);
  if (actual === f.expected) {
    pass++;
    console.log(`  ✓ ${f.name} → ${actual}`);
  } else {
    fail++;
    console.log(`  ✗ ${f.name} → ${actual} (expected ${f.expected})`);
  }
}

console.log('\n─── Overlay path mapping ───');
for (const f of OVERLAY_REL_FIXTURES) {
  const actual = GitUtils.overlayRelForFrameworkFile(f.file);
  if (actual === f.expected) {
    pass++;
    console.log(`  ✓ ${f.name} → ${actual}`);
  } else {
    fail++;
    console.log(`  ✗ ${f.name} → ${actual} (expected ${f.expected})`);
  }
}

// overlayCoversTouched(touched) — fs-backed: true only when EVERY touched file
// maps to an EXISTING overlay. This is the `overlayCovered` field that flows
// (via classifyDivergence → getUpdateStatus.divergenceCommits, a direct
// pass-through) into update.js's "reset is safe" branch. Lock the fs semantics.
console.log('\n─── overlayCoversTouched (fs-backed) ───');
{
  const fsMod = require('fs');
  const os = require('os');
  const tmp = fsMod.mkdtempSync(path.join(os.tmpdir(), 'baldart-ovl-'));
  fsMod.mkdirSync(path.join(tmp, '.baldart', 'overlays', 'agents'), { recursive: true });
  fsMod.writeFileSync(path.join(tmp, '.baldart', 'overlays', 'agents', 'coder.md'), '---\n---\n');
  const g2 = new GitUtils(tmp);
  const cases = [
    { name: 'touched file has an existing overlay → covered',
      touched: ['.framework/framework/.claude/agents/coder.md'], expected: true },
    { name: 'touched file has NO overlay → not covered',
      touched: ['.framework/framework/.claude/agents/ui-expert.md'], expected: false },
    { name: 'one covered + one uncovered → not covered (every)',
      touched: ['.framework/framework/.claude/agents/coder.md',
                '.framework/framework/.claude/agents/ui-expert.md'], expected: false },
    { name: 'empty touched → not covered',
      touched: [], expected: false },
  ];
  for (const c of cases) {
    const actual = g2.overlayCoversTouched(c.touched);
    if (actual === c.expected) { pass++; console.log(`  ✓ ${c.name} → ${actual}`); }
    else { fail++; console.log(`  ✗ ${c.name} → ${actual} (expected ${c.expected})`); }
  }
  fsMod.rmSync(tmp, { recursive: true, force: true });
}

console.log(`\nResult: ${pass} pass, ${fail} fail`);
process.exit(fail > 0 ? 1 : 0);
