#!/usr/bin/env node
/**
 * Unit tests for overlay-capture (the seamless-update auto-capture + verify, v4.8.0).
 *
 * Run: node src/utils/__tests__/overlay-capture.test.js
 * Exits 0 on success, non-zero on failure (CI-friendly via node:test).
 *
 * Contract under test: the user's "auto-capture + verify" choice — an uncovered
 * framework edit is captured into an overlay ONLY when re-merging that overlay
 * onto the fresh base reproduces the edit; otherwise it is a BLOCKER (the
 * seamless flow stops once rather than risk a lossy capture).
 */
const test = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const os = require('os');
const path = require('path');

const {
  buildOverlayBody, verifyCapture, kindNameFromPath, toRepoPath, captureAndVerify,
} = require('../overlay-capture');
const { buildFrontmatter } = require('../../commands/overlay');
const { computeBaseFileSha } = require('../overlay-merger');

const BASE = `---
name: ui-expert
description: UI agent
---

Shared preamble.

## Role
You are a UI expert.

## Rules
Follow the design system.
`;

function overlayFor(body) {
  return buildFrontmatter({ kind: 'agent', name: 'ui-expert', frameworkVersion: '4.8.0', baseSha: 'deadbeef', mode: 'extend' }) + body;
}
function roundTrips(edited) {
  const built = buildOverlayBody(BASE, edited);
  if (built.blocker) return { ok: false, blocker: built.blocker };
  const ok = verifyCapture({
    kind: 'agent', name: 'ui-expert', baseContent: BASE,
    overlayContent: overlayFor(built.body), editedContent: edited, frameworkVersion: '4.8.0',
  });
  return { ok };
}

test('path helpers', () => {
  assert.strictEqual(toRepoPath('.framework/framework/.claude/agents/x.md'), 'framework/.claude/agents/x.md');
  assert.deepStrictEqual(kindNameFromPath('.framework/framework/.claude/agents/coder.md'), { kind: 'agent', name: 'coder' });
  assert.deepStrictEqual(kindNameFromPath('.framework/framework/.claude/commands/check.md'), { kind: 'command', name: 'check' });
  assert.deepStrictEqual(kindNameFromPath('.framework/framework/.claude/skills/bug/SKILL.md'), { kind: 'skill', name: 'bug' });
  assert.strictEqual(kindNameFromPath('.framework/framework/src/index.js'), null);
});

test('OVERRIDE of an existing section → captured + verified', () => {
  const edited = BASE.replace('Follow the design system.', 'Follow the MAYO design system.\nNever hardcode colors.');
  assert.deepStrictEqual(roundTrips(edited), { ok: true });
});

test('new trailing section → captured + verified', () => {
  const edited = BASE + '\n## Project notes\nMayo-specific guidance.\n';
  assert.deepStrictEqual(roundTrips(edited), { ok: true });
});

test('section deletion → blocked (no marker can express it)', () => {
  const edited = BASE.replace('\n## Rules\nFollow the design system.\n', '\n');
  const built = buildOverlayBody(BASE, edited);
  assert.match(built.blocker, /section-deleted/);
});

test('frontmatter change → blocked (merge keeps base frontmatter)', () => {
  const edited = BASE.replace('description: UI agent', 'description: changed');
  assert.strictEqual(buildOverlayBody(BASE, edited).blocker, 'frontmatter-changed');
});

test('preamble change → blocked (merge keeps base preamble)', () => {
  const edited = BASE.replace('Shared preamble.', 'Changed preamble.');
  assert.strictEqual(buildOverlayBody(BASE, edited).blocker, 'preamble-changed');
});

test('mid-file insertion → verify fails (order cannot be reproduced)', () => {
  // Insert a new section BETWEEN Role and Rules — merge appends new sections at
  // the end, so the ordered round-trip must fail (correctly surfaced as a blocker).
  const edited = BASE.replace('## Rules', '## Inserted\nMid content.\n\n## Rules');
  const r = roundTrips(edited);
  assert.strictEqual(r.ok, false);
});

test('captureAndVerify writes a valid overlay for a verifiable edit', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const fwRel = '.framework/framework/.claude/agents/ui-expert.md';
    const fwAbs = path.join(tmp, fwRel);
    fs.mkdirSync(path.dirname(fwAbs), { recursive: true });
    const edited = BASE.replace('Follow the design system.', 'Follow the MAYO design system.');
    fs.writeFileSync(fwAbs, edited);

    const git = { git: { raw: async (args) => {
      if (args[0] === 'show' && args[1] === `FETCH_HEAD:${toRepoPath(fwRel)}`) return BASE;
      throw new Error(`unexpected: ${args.join(' ')}`);
    } } };

    const res = await captureAndVerify({ cwd: tmp, git, files: [fwRel], frameworkVersion: '4.8.0' });
    assert.deepStrictEqual(res.blockers, []);
    assert.deepStrictEqual(res.captured, ['.baldart/overlays/agents/ui-expert.md']);
    assert.ok(fs.existsSync(path.join(tmp, '.baldart/overlays/agents/ui-expert.md')));
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

test('captureAndVerify: skill with NO overlay → blocker (runtime-concat, unverifiable)', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const git = { git: { raw: async () => { throw new Error('should not be called'); } } };
    const res = await captureAndVerify({
      cwd: tmp, git,
      files: ['.framework/framework/.claude/skills/ui-design/SKILL.md'],
      frameworkVersion: '4.8.0',
    });
    assert.deepStrictEqual(res.captured, []);
    assert.strictEqual(res.blockers.length, 1);
    assert.match(res.blockers[0].reason, /runtime-concat/);
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

test('captureAndVerify: skill WITH an existing overlay → trusted (captured, not blocked)', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const overlay = path.join(tmp, '.baldart/overlays/ui-design.md');
    fs.mkdirSync(path.dirname(overlay), { recursive: true });
    fs.writeFileSync(overlay, '---\nbase_skill: ui-design\n---\n## [APPEND] Foo\nbar\n');
    const git = { git: { raw: async () => { throw new Error('should not be called for skills'); } } };
    const res = await captureAndVerify({
      cwd: tmp, git,
      files: ['.framework/framework/.claude/skills/ui-design/SKILL.md'],
      frameworkVersion: '4.8.0',
    });
    assert.deepStrictEqual(res.blockers, []);
    assert.deepStrictEqual(res.captured, ['.baldart/overlays/ui-design.md']);
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

test('captureAndVerify: existing agent overlay that REPRODUCES the edit → captured (no rewrite)', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const fwRel = '.framework/framework/.claude/agents/ui-expert.md';
    const fwAbs = path.join(tmp, fwRel);
    fs.mkdirSync(path.dirname(fwAbs), { recursive: true });
    const edited = BASE.replace('Follow the design system.', 'Follow the MAYO design system.');
    fs.writeFileSync(fwAbs, edited);
    // a hand-authored overlay whose merge reproduces `edited`
    const built = buildOverlayBody(BASE, edited);
    const overlayAbs = path.join(tmp, '.baldart/overlays/agents/ui-expert.md');
    fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
    const original = overlayFor(built.body);
    fs.writeFileSync(overlayAbs, original);

    const git = { git: { raw: async (a) => (a[0] === 'show' ? BASE : '') } };
    const res = await captureAndVerify({ cwd: tmp, git, files: [fwRel], frameworkVersion: '4.8.0' });
    assert.deepStrictEqual(res.blockers, []);
    assert.deepStrictEqual(res.captured, ['.baldart/overlays/agents/ui-expert.md']);
    // existing overlay must NOT be rewritten when it already verifies
    assert.strictEqual(fs.readFileSync(overlayAbs, 'utf8'), original);
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

test('captureAndVerify: STALE existing agent overlay (does not reproduce edit) → blocker', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const fwRel = '.framework/framework/.claude/agents/ui-expert.md';
    const fwAbs = path.join(tmp, fwRel);
    fs.mkdirSync(path.dirname(fwAbs), { recursive: true });
    // .framework edit is NEWER than what the overlay captures
    fs.writeFileSync(fwAbs, BASE.replace('Follow the design system.', 'Follow the NEW mayo rules v2.'));
    const overlayAbs = path.join(tmp, '.baldart/overlays/agents/ui-expert.md');
    fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
    // overlay reproduces an OLD edit, not the current one
    fs.writeFileSync(overlayAbs, overlayFor('\n## [OVERRIDE] Rules\nFollow the OLD mayo rules v1.\n'));

    const git = { git: { raw: async (a) => (a[0] === 'show' ? BASE : '') } };
    const res = await captureAndVerify({ cwd: tmp, git, files: [fwRel], frameworkVersion: '4.8.0' });
    assert.deepStrictEqual(res.captured, []);
    assert.strictEqual(res.blockers.length, 1);
    assert.match(res.blockers[0].reason, /stale/);
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

test('captureAndVerify: ADDITIVE overlay + pristine .framework (== base) → captured, not stale (Fix B)', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const fwRel = '.framework/framework/.claude/agents/ui-expert.md';
    const fwAbs = path.join(tmp, fwRel);
    fs.mkdirSync(path.dirname(fwAbs), { recursive: true });
    // `.framework` is pristine upstream base — the customization lives ONLY in
    // an additive (append-a-new-section) overlay, so reset+reapply loses
    // nothing. The strict round-trip would call this "stale" (merge=base+append
    // ≠ base); Fix B recognizes the pristine file as reset-safe → captured.
    fs.writeFileSync(fwAbs, BASE);
    const overlayAbs = path.join(tmp, '.baldart/overlays/agents/ui-expert.md');
    fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
    fs.writeFileSync(overlayAbs, overlayFor('\n## Mayo Stack Locks\nCSS Modules only.\n'));

    const git = { git: { raw: async (a) => (a[0] === 'show' ? BASE : '') } };
    const res = await captureAndVerify({ cwd: tmp, git, files: [fwRel], frameworkVersion: '4.16.1' });
    assert.deepStrictEqual(res.blockers, []);
    assert.deepStrictEqual(res.captured, ['.baldart/overlays/agents/ui-expert.md']);
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

test('captureAndVerify: upstream moved but .framework pristine wrt overlay base_file_sha → captured (Fix B)', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const fwRel = '.framework/framework/.claude/agents/ui-expert.md';
    const OLD = BASE;                                                   // base the overlay was authored against
    const NEW = BASE.replace('You are a UI expert.', 'You are a UI expert (upstream v2).'); // upstream moved on
    const fwAbs = path.join(tmp, fwRel);
    fs.mkdirSync(path.dirname(fwAbs), { recursive: true });
    fs.writeFileSync(fwAbs, OLD);                                       // .framework still pristine-at-OLD (no inline edit)
    const overlayAbs = path.join(tmp, '.baldart/overlays/agents/ui-expert.md');
    fs.mkdirSync(path.dirname(overlayAbs), { recursive: true });
    const overlay = buildFrontmatter({
      kind: 'agent', name: 'ui-expert', frameworkVersion: '4.16.1', baseSha: computeBaseFileSha(OLD), mode: 'extend',
    }) + '\n## Mayo Locks\nCSS Modules only.\n';
    fs.writeFileSync(overlayAbs, overlay);

    const git = { git: { raw: async (a) => (a[0] === 'show' ? NEW : '') } }; // FETCH_HEAD base = NEW (differs from .framework)
    const res = await captureAndVerify({ cwd: tmp, git, files: [fwRel], frameworkVersion: '4.16.1' });
    assert.deepStrictEqual(res.blockers, []);
    assert.deepStrictEqual(res.captured, ['.baldart/overlays/agents/ui-expert.md']);
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});

test('captureAndVerify: unmappable overlay-able path (nested agent) → blocker, never silently dropped', async () => {
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'baldart-capt-'));
  try {
    const git = { git: { raw: async () => { throw new Error('nope'); } } };
    const res = await captureAndVerify({
      cwd: tmp, git,
      files: ['.framework/framework/.claude/agents/sub/nested.md'],
      frameworkVersion: '4.8.0',
    });
    assert.deepStrictEqual(res.captured, []);
    assert.strictEqual(res.blockers.length, 1);
  } finally {
    fs.rmSync(tmp, { recursive: true, force: true });
  }
});
