/**
 * The progression ladder is what separates the algorithm from a naive retry:
 * a repair loop that keeps redrawing the same wrong implementation MUST
 * stall-detect and escalate (entropy reset), not spin. These tests pin that
 * behavior with a deliberately stubborn fake client.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient, RepairInput } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

const WRONG = 'export function add(a: number, b: number): number { return a - b; }';
const RIGHT = 'export function add(a: number, b: number): number { return a + b; }';

function makeSpec(): FeatureSpec {
  return {
    title: 'Adder',
    prompt: 'adder',
    features: [{
      id: 'F1', name: 'add', description: 'adds two numbers', parentId: null,
      deps: [], files: ['src/adder.ts'], testFile: 'tests/f1-add.test.ts',
      edgeCases: ['zero'], envVars: [],
      exports: [{ file: 'src/adder.ts', symbol: 'add' }],
      stubs: [{ path: 'src/adder.ts', content: `export function add(a: number, b: number): number { throw new Error('stub: not implemented'); }` }],
    }],
  } as unknown as FeatureSpec;
}

const SUITE = [
  "import { describe, it, expect } from 'vitest';",
  "import { add } from '../src/adder';",
  "describe('add', () => {",
  "  it('adds', () => { expect(add(2, 3)).toBe(5); });",
  "  it('edge: zero', () => { expect(add(7, 0)).toBe(7); });",
  '});',
].join('\n');

/** Repair always redraws the SAME wrong impl; only a fresh ceiling-temp draw
 *  (entropy reset) produces the right one. */
function stubbornClient(): LlmClient & { repairCalls: number; freshDraws: number } {
  const c = {
    repairCalls: 0,
    freshDraws: 0,
    async encodeFeatures() { return makeSpec(); },
    async generateTests() { return { content: SUITE, parts: { imports: [{ from: 'src/adder', symbols: ['add'] }], cases: [
      { name: 'adds', kind: 'behavior' as const, body: 'expect(add(2, 3)).toBe(5);' },
      { name: 'zero', kind: 'edge' as const, body: 'expect(add(7, 0)).toBe(7);' },
    ] } }; },
    async generateImplementation(_s: FeatureSpec, _f: unknown, _t: string, _d: GeneratedFile[], opts?: { temperature?: number }) {
      // First (build-phase) draw is wrong; the entropy-reset draw at ceiling
      // temperature is the decorrelated one that lands.
      c.freshDraws++;
      return [{ path: 'src/adder.ts', content: (opts?.temperature ?? 0) >= 1 ? RIGHT : WRONG }];
    },
    async repair(_input: RepairInput) {
      c.repairCalls++;
      return [{ path: 'src/adder.ts', content: WRONG }]; // circles forever
    },
    async fixTestCases(
      _s: unknown, _f: unknown,
      broken: { name: string; kind: 'behavior' | 'edge' | 'integration' }[],
    ) {
      // SET 2 per-case fill — serve the same bodies the old generateTests parts had.
      return {
        cases: broken.map(b => ({
          name: b.name,
          kind: b.kind,
          body: b.name === 'zero' || b.kind === 'edge'
            ? 'expect(add(7, 0)).toBe(7);'
            : 'expect(add(2, 3)).toBe(5);',
        })),
      };
    },
    async enumerateFeatures() { return []; },
    async probeFeature() { return { clear: true, assertion: '', question: '' }; },
    async repairFeatures() { return []; },
    async fixStub() { return ''; },
  };
  return c as unknown as LlmClient & { repairCalls: number; freshDraws: number };
}

describe('tier 1 convergence ladder', () => {
  it('stall-detects a circling repair and recovers via entropy reset', async () => {
    const llm = stubbornClient();
    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'adder',
      llm,
      runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 8, stallLimit: 2, progressStall: 3 },
      onEvent: e => events.push(`[${e.phase}] ${e.message}`),
    });

    // The ladder must have: repaired (and circled), stalled, entropy-reset,
    // and converged — with zero problems and the CORRECT implementation.
    expect(llm.repairCalls).toBeGreaterThanOrEqual(2);
    expect(events.some(e => e.includes('entropy reset'))).toBe(true);
    expect(res.problems).toEqual([]);
    expect(res.files.find(f => f.path === 'src/adder.ts')?.content).toBe(RIGHT);
    expect(events.some(e => e.includes('converged'))).toBe(true);
  });

  it('accumulates tabu facts across discarded attempts', async () => {
    const seen: string[] = [];
    const llm = stubbornClient();
    const origRepair = llm.repair.bind(llm);
    llm.repair = async (input: RepairInput) => {
      seen.push(input.failureOutput);
      return origRepair(input);
    };
    await runTier1Job({
      prompt: 'adder',
      llm,
      runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 8, stallLimit: 2, progressStall: 3 },
    });
    // Every repair pass carries the accumulated oracle facts, and they only grow.
    expect(seen.length).toBeGreaterThanOrEqual(2);
    for (const s of seen) expect(s).toContain('toBe');
  });
});
