/**
 * SET 92f — freeze-gate test-rha conservation (doctrine 7 / 11).
 *
 * Class-based LlmClient double (doctrine 15 / SET 88 lesson). Pins:
 *   - draw disposition partition equals testRhaDraws
 *   - throw / reject / accept-cleared lines are legible
 *   - skipped-no-handle does not become "sick-red unrepairable"
 *   - superbuffer: assembled lines at test-rha == testRhaSuperbufferAssemblies
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient, RepairInput } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

const RIGHT = 'export function add(a: number, b: number): number { return a + b; }';
const GOOD_A = 'expect(add(2, 3)).toBe(5);';
const GOOD_B = 'expect(add(7, 0)).toBe(7);';
const SICK_B = 'expect((add as unknown as { create: (n: number) => number }).create(7)).toBe(7);';

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;
}

/** Class-based double — methods use `this`, like LocalLlmClient. */
class ConservationLlm {
  private rescueAttempt = 0;
  constructor(
    private readonly mode: 'throw-then-clear' | 'always-throw' | 'empty-rescue',
  ) {}

  async encodeFeatures() { return makeSpec(); }
  async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; }
  async generateImplementation() { return [{ path: 'src/adder.ts', content: RIGHT }]; }
  async repair(_i: RepairInput) { return [{ path: 'src/adder.ts', content: RIGHT }]; }

  async fixTestCases(
    _s: unknown, _f: unknown, req: { name: string }[], _imp?: unknown, _cur?: unknown,
    opts?: { purpose?: string },
  ) {
    if (opts?.purpose?.startsWith('test-rha:')) {
      this.rescueAttempt++;
      if (this.mode === 'always-throw') {
        throw new Error(`synthetic draw failure on attempt ${this.rescueAttempt}`);
      }
      if (this.mode === 'empty-rescue') {
        return { cases: [{ name: req[0]!.name, kind: 'edge' as const, body: '' }] };
      }
      // throw-then-clear: attempt 1 accepts still-sick (re-emit SICK), attempt 2 throws,
      // attempt 3 clears.
      if (this.rescueAttempt === 1) {
        return { cases: [{ name: req[0]!.name, kind: 'edge' as const, body: SICK_B }] };
      }
      if (this.rescueAttempt === 2) {
        throw new Error('provider desync on attempt 2');
      }
      return { cases: [{ name: req[0]!.name, kind: 'edge' as const, body: GOOD_B }] };
    }
    const name = req[0]!.name;
    return {
      cases: [{
        name,
        kind: (name === 'zero' ? 'edge' : 'behavior') as 'edge' | 'behavior',
        body: name === 'zero' ? SICK_B : GOOD_A,
      }],
    };
  }
}

function conservationHolds(m: {
  testRhaDraws: number;
  testRhaDrawThrew: number;
  testRhaDraftRejected: number;
  testRhaDraftAcceptedCleared: number;
  testRhaDraftAcceptedStillSick: number;
}): boolean {
  return m.testRhaDraws === (
    m.testRhaDrawThrew + m.testRhaDraftRejected
    + m.testRhaDraftAcceptedCleared + m.testRhaDraftAcceptedStillSick
  );
}

describe('SET 92f — test-rha conservation', () => {
  it('scenario A — throw on attempt 2, clear on attempt 3; partition holds', async () => {
    const llm = new ConservationLlm('throw-then-clear');
    const res = await runTier1Job({
      prompt: 'adder',
      llm: llm as unknown as LlmClient,
      runSuite: makeEvalSuiteRunner(),
    });
    const msgs = res.log.map(e => e.message);
    expect(conservationHolds(res.metrics)).toBe(true);
    expect(res.metrics.testRhaDrawThrew).toBeGreaterThanOrEqual(1);
    expect(res.metrics.testRhaDraftAcceptedCleared).toBeGreaterThanOrEqual(1);
    expect(msgs.some(m => /test-rha: draw threw \(try 2\//.test(m))).toBe(true);
    expect(msgs.some(m => /test-rha: draft accepted \(try 3\//.test(m))).toBe(true);
    expect(msgs.some(m => /test-rha: case "zero" cleared/.test(m))).toBe(true);
    expect(msgs.some(m => /test-rha: miser ladder \[K=/.test(m))).toBe(true);
    // Every test-rha assembly emits through the owner; count is bounded by
    // total assembled lines this run.
    expect(res.metrics.testRhaSuperbufferAssemblies).toBeGreaterThanOrEqual(1);
    expect(res.metrics.testRhaSuperbufferAssemblies)
      .toBeLessThanOrEqual(msgs.filter(m => /superbuffer: assembled/.test(m)).length);
    expect(res.problems.some(p => /sick-red unrepairable/.test(p))).toBe(false);
  });

  it('scenario A+ — orphan failure name → skipped-no-handle, not unrepairable', async () => {
    const llm = new ConservationLlm('empty-rescue');
    const base = makeEvalSuiteRunner();
    let calls = 0;
    const res = await runTier1Job({
      prompt: 'adder',
      llm: llm as unknown as LlmClient,
      runSuite: async (stubs, test) => {
        calls++;
        const real = await base(stubs, test);
        // Inject an unattributable sick-red alongside real failures so the
        // freeze-gate sees a target with no case handle.
        // ONLY the orphan — otherwise find() prefers an attributable case and
        // skipped-no-handle never fires.
        if (real.failures.some(f => /is not a function|create/.test(f.message))) {
          return {
            ...real,
            failures: [
              { name: '__orphan_no_case__', message: 'orphan.create is not a function' },
            ],
          };
        }
        return real;
      },
    });
    const msgs = res.log.map(e => e.message);
    expect(msgs.some(m => /skipped — no case handle/.test(m))).toBe(true);
    expect(res.metrics.testRhaSkippedNoHandle).toBeGreaterThanOrEqual(1);
    expect(res.problems.some(p =>
      /__orphan_no_case__/.test(p) && /sick-red unrepairable/.test(p))).toBe(false);
    void calls;
  });

  it('scenario B — LLM always throws: draws === threw, exhausted, no backoff prose', async () => {
    const llm = new ConservationLlm('always-throw');
    const res = await runTier1Job({
      prompt: 'adder',
      llm: llm as unknown as LlmClient,
      runSuite: makeEvalSuiteRunner(),
    });
    expect(res.metrics.testRhaDraws).toBe(res.metrics.testRhaDrawThrew);
    expect(res.metrics.testRhaDraws).toBeGreaterThanOrEqual(1);
    expect(res.metrics.testRhaCaseExhausted).toBeGreaterThanOrEqual(1);
    expect(conservationHolds(res.metrics)).toBe(true);
    const joined = res.log.map(e => e.message).join('\n');
    expect(joined).not.toMatch(/backing off|retrying in|not re-attempted|retired/);
    expect(joined).toMatch(/test-rha: draw threw/);
    expect(joined).toMatch(/test-rha: case "zero" exhausted after/);
  });
});
