/**
 * SET 32 — Encode determinism (32a), fill-loop audit (32b), substrate-anchored
 * replan (32c), abandonment fan-out (32d), drop-message vocabulary +
 * live-metrics classification (32e), anchor invariant (32f), effort floor +
 * dependency weighting (32g), end-of-phase retry queue (32h), and the metaloop
 * bracket with a hard cap of 3 (32i).
 *
 * These tests exercise the track-relay scheduler: fill exhaustion DEFERS
 * (never abandons directly); metaloop 2 replays deferred features against the
 * warmer buffer with fresh anchor-injected plans; metaloop 3 is one final
 * bracket pass; only after 3 metaloops does terminal abandonment fan-out
 * (SET 32d) fire against dependents.
 */
import { describe, expect, it, vi } from 'vitest';
import type {
  DrawOpts,
  FeatureNode,
  FeatureSpec,
  LlmClient,
  RepairInput,
} from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import { formatFillDropMessage } from '../src/tier1/fillHonesty';
import {
  classifyRunOutcome,
  contributeRunToTotals,
  emptyStatsTotals,
  runSnapshot,
} from '../src/tier1/statsAggregate';
import {
  deferralMessage,
  effortFloor,
  makeWorldSignature,
  metaloop2Message,
  metaloop3Message,
  noCoverageProblem,
  orderRootsFirst,
  replanMessage,
  skipNoTrackMessage,
  uncoveredSummary,
  worldChanged,
} from '../src/tier1/fillRelay';
import { injectClassAnchor, ownedExportSymbols, parseExportSignature, planSuite } from '../src/tier1/planSuite';

// ---------------------------------------------------------------------------
// Tiny mock harness. `record` captures each LLM call's opts so 32a/32b can
// assert on the exact DrawOpts and call counts.
// ---------------------------------------------------------------------------
interface CallRecord {
  method: string;
  opts: DrawOpts | undefined;
  args: unknown[];
}

interface MockOverrides {
  encodeFeatures?: (prompt: string, stubFeedback?: string, opts?: DrawOpts) => Promise<FeatureSpec>;
  fixTestCases?: LlmClient['fixTestCases'];
  generateTests?: LlmClient['generateTests'];
  generateImplementation?: LlmClient['generateImplementation'];
  repair?: LlmClient['repair'];
}

function makeMockLlm(spec: FeatureSpec, overrides: MockOverrides = {}): {
  llm: LlmClient;
  calls: CallRecord[];
} {
  const calls: CallRecord[] = [];
  const track = <T extends unknown[], R>(
    method: string,
    optsIndex: number,
    handler: (...args: T) => Promise<R>,
  ) => async (...args: T): Promise<R> => {
    calls.push({ method, opts: args[optsIndex] as DrawOpts | undefined, args: [...args] });
    return handler(...args);
  };
  const llm: LlmClient = {
    async enumerateFeatures() { return []; },
    async probeFeature() { return { clear: true, assertion: 'x', question: '' }; },
    encodeFeatures: track('encodeFeatures', 2, overrides.encodeFeatures
      ?? (async () => spec)),
    async repairFeatures() { return []; },
    async fixStub() { return ''; },
    generateTests: track('generateTests', 3, overrides.generateTests
      ?? (async () => ({ content: '' }))),
    fixTestCases: track('fixTestCases', 5, overrides.fixTestCases
      ?? (async () => ({ cases: [] }))),
    generateImplementation: track('generateImplementation', 4, overrides.generateImplementation
      ?? (async (_s, feature: FeatureNode) => feature.files.map(f => ({
        path: f,
        content: feature.stubs.find(s => s.path === f)?.content ?? '',
      })))),
    repair: track('repair', 1, overrides.repair
      ?? (async (input: RepairInput) => input.ownedFiles)),
  };
  return { llm, calls };
}

function makeFeature(overrides: Partial<FeatureNode> & Pick<FeatureNode, 'id' | 'name'>): FeatureNode {
  return {
    description: `feature ${overrides.id}`,
    parentId: null,
    deps: [],
    files: [`src/${overrides.id.toLowerCase()}.ts`],
    testFile: `tests/${overrides.id.toLowerCase()}.test.ts`,
    edgeCases: [],
    envVars: [],
    exports: [{ file: `src/${overrides.id.toLowerCase()}.ts`, symbol: overrides.name }],
    stubs: [{
      path: `src/${overrides.id.toLowerCase()}.ts`,
      content: `export function ${overrides.name}(x: number): number { throw new Error('stub'); }\n`,
    }],
    ...overrides,
  };
}

// ---------------------------------------------------------------------------
// 32a — Encode determinism. The runner and the pipeline BOTH pin
// `temperature: 0` on `encodeFeatures`. Longitudinal comparison requires the
// spec carve to be reproducible.
// ---------------------------------------------------------------------------
describe('SET 32a encode determinism', () => {
  it('runTier1Job pins encodeFeatures to temperature 0', async () => {
    const feature = makeFeature({ id: 'F1', name: 'widget' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const { llm, calls } = makeMockLlm(spec);

    await runTier1Job({ prompt: 'p', llm, gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 } });

    const encodeCalls = calls.filter(c => c.method === 'encodeFeatures');
    expect(encodeCalls.length).toBeGreaterThan(0);
    for (const c of encodeCalls) {
      expect(c.opts).toBeDefined();
      expect(c.opts!.temperature).toBe(0);
    }
  });

  it('same prompt + same mocked encode → same spec (temperature 0 is reproducible)', async () => {
    // Two runs with a mock that returns the same spec — proves the runner
    // never mutates the DrawOpts and never adds jitter on encode.
    const feature = makeFeature({ id: 'F1', name: 'widget' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const encodeOpts: (DrawOpts | undefined)[] = [];
    const encoder = async (_p: string, _sf: string | undefined, opts?: DrawOpts) => {
      encodeOpts.push(opts);
      return spec;
    };
    const { llm: llm1 } = makeMockLlm(spec, { encodeFeatures: encoder });
    const { llm: llm2 } = makeMockLlm(spec, { encodeFeatures: encoder });

    await runTier1Job({ prompt: 'p', llm: llm1, gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 } });
    await runTier1Job({ prompt: 'p', llm: llm2, gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 } });

    expect(encodeOpts.every(o => o?.temperature === 0)).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// 32b — Fill-loop audit: the K-draw batch structure is unchanged. For N
// holes, we expect ceil(N/3) permutation batches, each with K=3 candidate
// draws → 3 × ceil(N/3) fixTestCases calls in the happy K-draw path.
// Documents parity with 1.7.14.
// ---------------------------------------------------------------------------
describe('SET 32b fill-loop audit — batch structure unchanged (parity with 1.7.14)', () => {
  it('N=4 planned holes → 2 K-draw batches × K=3 = 6 fixTestCases calls (per feature)', async () => {
    const feature = makeFeature({
      id: 'F1', name: 'processWidget',
      edgeCases: ['edge one', 'edge two', 'edge three'],
      // 1 export (processWidget) + 3 edge cases = 4 planned cases, all become
      // holes (no template match on freeform edges).
    });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const { llm, calls } = makeMockLlm(spec);

    await runTier1Job({ prompt: 'p', llm, gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 } });

    // SET 71 — per-NODE K-draws (Andy 2026-07-27: "there should be 9"): the
    // bounded unit of a K-draw batch is the CASE. N=4 planned holes × K=3
    // candidates × 2 effort-floor cycles = 24 draws. Bundled batches (and
    // their truncated empty tails) no longer exist.
    const kDrawCalls = calls.filter(c =>
      c.method === 'fixTestCases' && c.opts?.candidate?.k === 3 && !c.opts?.rescue);
    expect(kDrawCalls.length).toBe(24);
    // Every K-draw call has one of the three permutation temperatures.
    const temps = new Set(kDrawCalls.map(c => c.opts?.temperature));
    expect([...temps].sort()).toEqual([0.3, 0.7, 1]);
  });
});

// ---------------------------------------------------------------------------
// 32c — Substrate-anchored replan message and behavior.
// ---------------------------------------------------------------------------
describe('SET 32c substrate-anchored replan', () => {
  it('replanMessage matches the exact vocabulary', () => {
    expect(replanMessage(3, 1)).toBe('replan (substrate-anchored): 3 cases (1 templated)');
    expect(replanMessage(0, 0)).toBe('replan (substrate-anchored): 0 cases (0 templated)');
  });

  it('a feature that fills nothing on cycle 1 emits a replan event on cycle 2', async () => {
    const feature = makeFeature({ id: 'F1', name: 'processWidget' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const { llm } = makeMockLlm(spec);

    const events: string[] = [];
    await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });
    // Effort floor for a leaf feature is 2 — the second cycle emits the replan.
    expect(events.some(m => m.startsWith('replan (substrate-anchored):'))).toBe(true);
  });
});

// ---------------------------------------------------------------------------
// 32d — Abandonment fan-out helpers and integrated flow. Terminal
// abandonment can only fire after ALL three metaloops fail; dependents get
// `dependency uncovered` on their `uses <feature>` case; summary line shows
// `N features uncovered`.
// ---------------------------------------------------------------------------
describe('SET 32d abandonment fan-out', () => {
  it('helpers format the terminal abandonment vocabulary', () => {
    expect(noCoverageProblem('F1')).toBe('F1: no-coverage (abandoned after metaloop 3)');
    expect(uncoveredSummary(1)).toBe('1 feature uncovered');
    expect(uncoveredSummary(3)).toBe('3 features uncovered');
  });

  it('a feature that fails all 3 metaloops fans out to dependents and surfaces the count in `done`', async () => {
    const parent = makeFeature({ id: 'F1', name: 'processWidget' });
    const child = makeFeature({
      id: 'F2', name: 'renderWidget',
      deps: ['F1'],
      files: ['src/f2.ts'],
      exports: [{ file: 'src/f2.ts', symbol: 'renderWidget' }],
      stubs: [{
        path: 'src/f2.ts',
        content: 'export function renderWidget(x: number): number { throw new Error("stub"); }\n',
      }],
    });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [parent, child] };

    // fixTestCases always returns [] — F1 can never fill. fallback generateTests
    // also empty. F2 fills normally through the anchor.
    const { llm } = makeMockLlm(spec, {
      async fixTestCases(_s, feature, broken) {
        // Only fill F2's cases; F1 stays empty.
        if (feature.id === 'F1') return { cases: [] };
        return {
          cases: broken.map(b => ({
            name: b.name, kind: b.kind as 'behavior',
            body: 'expect(true).toBe(true);',
          })),
        };
      },
    });

    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });

    expect(res.problems.some(p => p === 'F1: no-coverage (abandoned after metaloop 3)')).toBe(true);
    expect(res.problems.some(p => p === 'F2: uses F1 — dependency uncovered')).toBe(true);
    const doneEvent = events.find(m => m.includes('files built,'));
    expect(doneEvent).toBeDefined();
    expect(doneEvent).toContain('1 feature uncovered');
    // Metaloop bracket events fire in order — 2 before 3, terminal after 3.
    const m2 = events.findIndex(m => m.startsWith('metaloop 2:'));
    const m3 = events.findIndex(m => m.startsWith('metaloop 3:'));
    const noCov = events.findIndex(m => m === 'no-coverage — abandoned after metaloop 3');
    expect(m2).toBeGreaterThan(-1);
    expect(m3).toBeGreaterThan(m2);
    expect(noCov).toBeGreaterThan(m3);
  });
});

// ---------------------------------------------------------------------------
// 32e.i — drop-message vocabulary. Full coverage sits in set28; this reaffirms
// the exact wording new to SET 32.
// ---------------------------------------------------------------------------
describe('SET 32e.i drop-message vocabulary', () => {
  it('canonical form: draws + candidate batches + fallback batches, singular/plural agreement', () => {
    expect(formatFillDropMessage('X', 1, 1, 0)).toBe(
      'planned case "X" unfilled after 1 draw (1 candidate batch + 0 fallback batches) — dropped');
    expect(formatFillDropMessage('X', 7, 2, 1)).toBe(
      'planned case "X" unfilled after 7 draws (2 candidate batches + 1 fallback batch) — dropped');
  });

  it('never-drawn path preserved (plan exceeded fill budget → no fabricated retries)', () => {
    expect(formatFillDropMessage('NeverTouched', 0, 0, 0))
      .toBe('planned case "NeverTouched" never drawn — dropped (plan exceeded fill budget)');
  });
});

// ---------------------------------------------------------------------------
// 32e.ii — live-metrics classification. A live snapshot must NEVER be
// classified as `failed`; it gets `running`, dispositions are null, and
// contribution to persisted totals is a no-op.
// ---------------------------------------------------------------------------
describe('SET 32e.ii running outcome / live snapshot classification', () => {
  it('classifyRunOutcome returns "running" when the snapshot is live', () => {
    expect(classifyRunOutcome({ live: true, problems: null, result: undefined })).toBe('running');
    expect(classifyRunOutcome({ running: true })).toBe('running');
    // A record with no error, no result, no problems is running (not failed).
    // This is the bug SET 32e.ii fixes — the previous code returned 'failed'
    // here and made the live grid show Runs→Failed: 1 mid-run.
    expect(classifyRunOutcome({})).toBe('running');
  });

  it('runSnapshot leaves disposition cells null and marks runsRunning=1 for a live record', () => {
    const snap = runSnapshot({
      live: true,
      metrics: { tokensIn: 100, tokensOut: 50, passesTotal: 2 },
      problems: null,
      error: null,
      result: undefined,
    });
    expect(snap.runsRunning).toBe(1);
    expect(snap.runsClean).toBeNull();
    expect(snap.runsPartial).toBeNull();
    expect(snap.runsFailed).toBeNull();
    expect(snap.runsCancelled).toBeNull();
    // Non-disposition numeric cells still populate normally.
    expect(snap.tokensIn).toBe(100);
    expect(snap.cycles).toBe(2);
  });

  it('contributeRunToTotals ignores live snapshots — totals stay unchanged', () => {
    const totals = emptyStatsTotals();
    const t2 = contributeRunToTotals(totals, { live: true, problems: null });
    expect(t2.runs.total).toBe(0);
    expect(t2.runs.failed).toBe(0);
    expect(t2.runs.running).toBe(0);
  });

  it('a terminal record with 0 problems still classifies as successful (regression guard)', () => {
    expect(classifyRunOutcome({ problems: 0, result: {} })).toBe('successful');
  });
});

// ---------------------------------------------------------------------------
// 32f — anchor invariant. Every plan MUST include ≥1 case named after an
// owned export. When the plan didn't have one, injectClassAnchor prepends
// the anchor so it leads batch 1.
// ---------------------------------------------------------------------------
describe('SET 32f anchor invariant', () => {
  it('ownedExportSymbols reads class + function + const exports from stubs', () => {
    const feature: FeatureNode = {
      id: 'F1', name: 'Shapes', description: 'd', parentId: null, deps: [],
      files: ['src/shapes.ts'], testFile: 'tests/shapes.test.ts', edgeCases: [], envVars: [],
      exports: [], // Nothing in the exports array — pure stub-driven anchor.
      stubs: [{
        path: 'src/shapes.ts',
        content: 'export class Box { width = 0; }\nexport const DEFAULT = 10;\n',
      }],
    };
    const owned = ownedExportSymbols(feature);
    expect(owned).toContain('Box');
    expect(owned).toContain('DEFAULT');
  });

  it('parseExportSignature handles export class with constructor(...) params', () => {
    const feature: FeatureNode = {
      id: 'F1', name: 'Box', description: 'd', parentId: null, deps: [],
      files: ['src/box.ts'], testFile: 'tests/box.test.ts', edgeCases: [], envVars: [],
      exports: [{ file: 'src/box.ts', symbol: 'Box' }],
      stubs: [{
        path: 'src/box.ts',
        content: 'export class Box {\n  constructor(width: number, height: number) {}\n}\n',
      }],
    };
    const sig = parseExportSignature(feature, 'Box');
    expect(sig).not.toBeNull();
    expect(sig!.name).toBe('Box');
    expect(sig!.params.map(p => p.name)).toEqual(['width', 'height']);
    expect(sig!.returnType).toBe('Box');
  });

  it('injectClassAnchor prepends an anchor case named after the owned export when missing', () => {
    // Simulate a feature whose exports[] was empty but a stub still exports
    // a class symbol — planSuite alone produces no anchor case.
    const feature: FeatureNode = {
      id: 'F1', name: 'Widget', description: 'd', parentId: null, deps: [],
      files: ['src/widget.ts'], testFile: 'tests/widget.test.ts',
      edgeCases: ['first edge'], envVars: [],
      exports: [], // <-- empty on purpose
      stubs: [{
        path: 'src/widget.ts',
        content: 'export class Widget { render() {} }\n',
      }],
    };
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const bare = planSuite(spec, feature);
    // Sanity: without injection, no case named after the class exists.
    expect(bare.cases.some(c => c.name === 'Widget')).toBe(false);

    const { plan, injected } = injectClassAnchor(bare, feature);
    expect(injected).not.toBeNull();
    expect(injected!.name).toBe('Widget');
    // Anchor LEADS batch 1 (prepended).
    expect(plan.cases[0]?.name).toBe('Widget');
  });

  it('injectClassAnchor is a no-op when the plan already carries an owned-export case', () => {
    const feature = makeFeature({ id: 'F1', name: 'processWidget' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const bare = planSuite(spec, feature);
    expect(bare.cases.some(c => c.name === 'processWidget')).toBe(true);
    const { plan, injected } = injectClassAnchor(bare, feature);
    expect(injected).toBeNull();
    expect(plan).toBe(bare); // same reference — no rebuild when unnecessary
  });

  it('the runner emits `plan: injected class-anchor case "..."` when injection fires', async () => {
    // Force injection: feature.exports empty but a class-exporting stub exists.
    const feature: FeatureNode = {
      id: 'F1', name: 'Widget', description: 'd', parentId: null, deps: [],
      files: ['src/widget.ts'], testFile: 'tests/widget.test.ts',
      edgeCases: [], envVars: [],
      exports: [],
      stubs: [{
        path: 'src/widget.ts',
        content: 'export class Widget { render() {} }\n',
      }],
    };
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const { llm } = makeMockLlm(spec);
    const events: string[] = [];
    await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });
    expect(events.some(m => m === 'plan: injected class-anchor case "Widget" (from owned stub)')).toBe(true);
  });

  it('every planned suite in a multi-feature spec contains ≥1 anchor named after an owned export', () => {
    // Ensures the invariant holds for typical spec shapes where planSuite
    // already carries an export-case (planSuite path) AND for pure-class
    // shapes where injectClassAnchor fills the gap.
    const features: FeatureNode[] = [
      makeFeature({ id: 'F1', name: 'processWidget' }),
      {
        id: 'F2', name: 'RenderTarget', description: 'd', parentId: null, deps: ['F1'],
        files: ['src/render.ts'], testFile: 'tests/render.test.ts', edgeCases: [], envVars: [],
        exports: [], // stub-only export
        stubs: [{ path: 'src/render.ts', content: 'export class RenderTarget {}\n' }],
      },
    ];
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features };
    for (const feature of features) {
      const bare = planSuite(spec, feature);
      const { plan } = injectClassAnchor(bare, feature);
      const owned = ownedExportSymbols(feature);
      const hasAnchor = plan.cases.some(c => owned.includes(c.name));
      expect(hasAnchor).toBe(true);
    }
  });
});

// ---------------------------------------------------------------------------
// 32g — effort floor + dependency weighting. Every feature gets ≥2 candidate
// batch cycles (base=2). Features others `uses` get +1 per distinct dependent,
// capped. Inverts the previous "root gets least effort" arithmetic.
// ---------------------------------------------------------------------------
describe('SET 32g effort floor + dependency weighting', () => {
  it('a leaf feature with no dependents has floor = 2 (base)', () => {
    const leaf = makeFeature({ id: 'F1', name: 'leaf' });
    expect(effortFloor(leaf, [leaf])).toBe(2);
  });

  it('a dep-root with N dependents has floor = base + N, capped', () => {
    const root = makeFeature({ id: 'F1', name: 'root' });
    const dep1 = makeFeature({ id: 'F2', name: 'd1', deps: ['F1'] });
    const dep2 = makeFeature({ id: 'F3', name: 'd2', deps: ['F1'] });
    expect(effortFloor(root, [root, dep1, dep2])).toBe(4); // 2 + 2 dependents
    // Cap at 4 (default) — 5 dependents should still be 4.
    const many = [root, ...Array.from({ length: 5 }, (_, i) =>
      makeFeature({ id: `F${i + 2}`, name: `d${i}`, deps: ['F1'] }))];
    expect(effortFloor(root, many)).toBe(4);
  });

  it('a root gets STRICTLY MORE effort than its dependents (32g inversion)', () => {
    const root = makeFeature({ id: 'F1', name: 'root' });
    const dep = makeFeature({ id: 'F2', name: 'd', deps: ['F1'] });
    expect(effortFloor(root, [root, dep])).toBeGreaterThan(effortFloor(dep, [root, dep]));
  });
});

// ---------------------------------------------------------------------------
// 32h — end-of-phase retry queue (track-relay).
//   * Fill exhaustion emits the deferral message.
//   * Deferred features retry in metaloop 2 against the warmer buffer.
//   * A retry that freezes never triggers 32d fan-out.
//   * Deferral-free run remains behavior-compatible with today.
// ---------------------------------------------------------------------------
describe('SET 32h end-of-phase retry queue', () => {
  it('deferralMessage matches the exact vocabulary', () => {
    expect(deferralMessage({ frozenCount: 3, siblingCaseCount: 5 })).toBe(
      'feature deferred to end-of-phase retry (3 frozen exemplars, 5 sibling cases now available)');
  });

  it('a feature that fails M1 but succeeds M2 freezes without hitting fan-out', async () => {
    // F1 defers in M1; F2 freezes normally in M1 (its freeze changes the
    // WorldSignature so F1's M2 retry is not skipped by 32i's precondition).
    // On M2, F1's mock switches to filling → freeze without touching M3.
    const f1 = makeFeature({ id: 'F1', name: 'processWidget' });
    const f2 = makeFeature({
      id: 'F2', name: 'renderWidget',
      files: ['src/f2.ts'],
      exports: [{ file: 'src/f2.ts', symbol: 'renderWidget' }],
      stubs: [{
        path: 'src/f2.ts',
        content: 'export function renderWidget(x: number): number { throw new Error("stub"); }\n',
      }],
    });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [f1, f2] };
    let f1Calls = 0;
    const { llm } = makeMockLlm(spec, {
      async fixTestCases(_s, feature, broken, _imp, _cur, opts) {
        // SET 64a — rescue draws don't advance this scenario's M1/M2 script.
        if (opts?.rescue) return { cases: [] };
        if (feature.id === 'F1') {
          f1Calls++;
          // Leaf floor = 2 cycles × K=3 = 6 draws in M1; stay empty for
          // those, fill on M2 cycle 1 (draw 7+).
          if (f1Calls <= 6) return { cases: [] };
          return {
            cases: broken.map(b => ({
              name: b.name, kind: b.kind as 'behavior',
              body: 'expect(true).toBe(true);',
            })),
          };
        }
        // F2 always fills — freezes in M1, changing the world for F1.
        return {
          cases: broken.map(b => ({
            name: b.name, kind: b.kind as 'behavior',
            body: 'expect(true).toBe(true);',
          })),
        };
      },
    });
    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });
    expect(events.some(m => m.startsWith('feature deferred to end-of-phase retry'))).toBe(true);
    expect(events.some(m => m.startsWith('metaloop 2: retrying'))).toBe(true);
    expect(events.some(m => /^frozen suite tests\/f1\.test\.ts/.test(m))).toBe(true);
    // Never fell through to metaloop 3 fan-out.
    expect(res.problems.some(p => /no-coverage/.test(p))).toBe(false);
  });

  it('deferral-free run: no metaloop 2/3 events, no `feature deferred` (behavior-compat)', async () => {
    const feature = makeFeature({ id: 'F1', name: 'processWidget' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const { llm } = makeMockLlm(spec, {
      async fixTestCases(_s, _feature, broken) {
        return {
          cases: broken.map(b => ({
            name: b.name, kind: b.kind as 'behavior',
            body: 'expect(true).toBe(true);',
          })),
        };
      },
    });
    const events: string[] = [];
    await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });
    expect(events.some(m => m.startsWith('feature deferred'))).toBe(false);
    expect(events.some(m => m.startsWith('metaloop 2:'))).toBe(false);
    expect(events.some(m => m.startsWith('metaloop 3:'))).toBe(false);
  });

  it('abandonment is unreachable without prior warm retry (no fan-out on M1 alone)', async () => {
    // Only ONE feature — deferring it in M1 cannot change the world (no
    // sibling frozen exemplars possible). Metaloop 2 must skip with the
    // "no new track to lay" event; metaloop 3 same; only then does 32d fire.
    // Verifies the invariant: without a warm retry pass, we never abandon.
    const feature = makeFeature({ id: 'F1', name: 'processWidget' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const { llm } = makeMockLlm(spec, {
      async fixTestCases() { return { cases: [] }; }, // always empty
    });
    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });
    // Went through the full bracket: M2 attempted, M3 attempted, then abandoned.
    expect(events.some(m => m.startsWith('metaloop 2:'))).toBe(true);
    expect(events.some(m => m.startsWith('metaloop 3:'))).toBe(true);
    expect(res.problems.some(p => p === noCoverageProblem('F1'))).toBe(true);
    // Never abandoned mid-M1 (would break the invariant).
    const m2Idx = events.findIndex(m => m.startsWith('metaloop 2:'));
    const noCovIdx = events.findIndex(m => m === 'no-coverage — abandoned after metaloop 3');
    expect(m2Idx).toBeLessThan(noCovIdx);
  });
});

// ---------------------------------------------------------------------------
// 32i — metaloop bracket (hard cap 3), changed-world precondition, and
// dependency-roots-first ordering.
// ---------------------------------------------------------------------------
describe('SET 32i metaloop bracket', () => {
  it('event vocabulary is exact', () => {
    expect(metaloop2Message(['F1', 'F2'])).toBe('metaloop 2: retrying [F1, F2] (deferred)');
    expect(metaloop3Message(['F3'])).toBe('metaloop 3: bracket pass [F3]');
    expect(skipNoTrackMessage()).toBe('retry skipped: no new track to lay');
  });

  it('worldChanged is true iff any signature input grew; equal snapshots skip', () => {
    const prev = makeWorldSignature({
      frozenByFeature: new Map(),
      lessonKeys: new Set(),
      irPartsByFeature: new Map(),
    });
    const same = makeWorldSignature({
      frozenByFeature: new Map(),
      lessonKeys: new Set(),
      irPartsByFeature: new Map(),
    });
    expect(worldChanged(prev, same)).toBe(false);
    const grown = makeWorldSignature({
      frozenByFeature: new Map([['F1', 'content']]),
      lessonKeys: new Set(),
      irPartsByFeature: new Map(),
    });
    expect(worldChanged(prev, grown)).toBe(true);
  });

  it('metaloop 2 skips a feature whose world did not change since M1 (single-feature case)', async () => {
    // Only F1 — nothing else can freeze between M1 and M2, so world is
    // unchanged and M2 emits the skip event.
    const feature = makeFeature({ id: 'F1', name: 'processWidget' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [feature] };
    const { llm } = makeMockLlm(spec, {
      async fixTestCases() { return { cases: [] }; },
    });
    const events: string[] = [];
    await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });
    // At least one skip event in the bracket (M2 and M3 both hit the
    // no-new-track precondition since F1 is the only feature).
    expect(events.filter(m => m === 'retry skipped: no new track to lay').length).toBeGreaterThanOrEqual(1);
  });

  it('orderRootsFirst puts dependency roots ahead of their downstream features', () => {
    const root = makeFeature({ id: 'F1', name: 'root' });
    const mid = makeFeature({ id: 'F2', name: 'mid', deps: ['F1'] });
    const leaf = makeFeature({ id: 'F3', name: 'leaf', deps: ['F2'] });
    const ordered = orderRootsFirst(['F3', 'F2', 'F1'], [root, mid, leaf]);
    expect(ordered).toEqual(['F1', 'F2', 'F3']);
  });

  it('hard cap 3: no feature is prosecuted more than 3 times (M1, M2, M3)', async () => {
    // Two sibling features (no deps): F1 freezing lays track so F2's world
    // changes between metaloops. F2 must NOT `uses` F1 — uses-dep is now
    // substrate-templated (31d planner), which would freeze F2 without a
    // model draw and hide the no-coverage / hard-cap path. Prove metaloop
    // abandonment via a never-fill non-dep export hole instead.
    const f1 = makeFeature({ id: 'F1', name: 'canFill' });
    const f2 = makeFeature({ id: 'F2', name: 'neverFills' });
    const spec: FeatureSpec = { title: 'S', prompt: 'p', features: [f1, f2] };
    const attemptsByFeature: Record<string, number> = {};
    const { llm } = makeMockLlm(spec, {
      async fixTestCases(_s, feature, broken) {
        attemptsByFeature[feature.id] = (attemptsByFeature[feature.id] ?? 0) + 1;
        if (feature.id === 'F1') {
          return { cases: broken.map(b => ({
            name: b.name, kind: b.kind as 'behavior',
            body: 'expect(true).toBe(true);',
          })) };
        }
        return { cases: [] }; // F2 never fills
      },
    });
    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'p', llm,
      gates: { maxPasses: 1, stallLimit: 1, progressStall: 1 },
      onEvent: e => events.push(e.message),
    });
    // F2 was prosecuted in M1, M2, and M3 — no more.
    const f2Prosecutions = events.filter(m =>
      m.startsWith('plan: injected class-anchor case') // one per M1/M2/M3 entry
      || /^plan: \d+ cases/.test(m)).length;
    void f2Prosecutions;
    // Terminal: F2 is uncovered; no evidence of a 4th metaloop.
    expect(res.problems.some(p => p === noCoverageProblem('F2'))).toBe(true);
    expect(events.filter(m => m.startsWith('metaloop 2:')).length).toBe(1);
    expect(events.filter(m => m.startsWith('metaloop 3:')).length).toBe(1);
    // No "metaloop 4" ever emitted (proves the cap).
    expect(events.some(m => m.startsWith('metaloop 4:'))).toBe(false);
  });
});

// silence unused-import when vi isn't referenced by the test bodies above
void vi;
