/**
 * SET 101a — stub-not-implemented + body still stub → skip L2, route to
 * entropy-reset / IR surgery (absence, not defect).
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';
import {
  isOwnedSymbolStillStub,
  isStubNotImplementedClass,
  classifyFailure,
} from '../src/tier1/substrate';

const STUB =
  "export function add(a: number, b: number): number { throw new Error('stub: not implemented'); }";

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: STUB }],
    }],
  } as unknown as FeatureSpec;
}

/** Class double: build leaves the stub in place (absence). */
class StubStillLlm {
  repairCalls = 0;

  async encodeFeatures() { return makeSpec(); }
  async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; }
  async fixTestCases(_s: unknown, _f: unknown, req: { name: string }[]) {
    const name = req[0]!.name;
    return {
      cases: [{
        name,
        kind: (name === 'zero' ? 'edge' : 'behavior') as 'edge' | 'behavior',
        body: name === 'zero' ? 'expect(add(7, 0)).toBe(7);' : 'expect(add(2, 3)).toBe(5);',
      }],
    };
  }
  async generateImplementation() {
    return [{ path: 'src/adder.ts', content: STUB }];
  }
  async repair() {
    this.repairCalls++;
    // Entropy-reset / later rungs may call repair; L2 must not.
    return [{
      path: 'src/adder.ts',
      content: 'export function add(a: number, b: number): number { return a + b; }',
    }];
  }
}

describe('SET 101a — stub-not-implemented routing', () => {
  it('classifies stub sentinel and detects byte-equal stub body', () => {
    const c = classifyFailure('adds', 'stub: not implemented');
    expect(isStubNotImplementedClass(c.classKey)).toBe(true);
    expect(isOwnedSymbolStillStub(STUB, STUB, 'add')).toBe(true);
    expect(isOwnedSymbolStillStub(
      'export function add(a: number, b: number): number { return a + b; }',
      STUB,
      'add',
    )).toBe(false);
  });

  it('skips L2 when impl body == stub; routes toward entropy-reset/IR surgery', async () => {
    const llm = new StubStillLlm();
    const res = await runTier1Job({
      prompt: 'adder',
      llm: llm as unknown as LlmClient,
      runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 6, stallLimit: 2, progressStall: 4 },
    });

    expect(res.log.some(e =>
      /stub-route: add unimplemented — L2 skipped \(absence, not defect\)/.test(e.message),
    )).toBe(true);
    expect(res.metrics.stubRouteShortcuts).toBeGreaterThanOrEqual(1);
    expect(res.metrics.l2Draws).toBe(0);
    // Escalation path: entropy reset and/or IR surgery — not L2 procedure loops.
    expect(res.log.some(e =>
      /entropy reset @ pass|suite re-frozen after case surgery|IR-case surgery/.test(e.message),
    )).toBe(true);
  });
});
