/**
 * SET 73 — preparse honesty (Andy: "too many LLM calls rejected", "there
 * should be more stubs", "the substrate templated never executes"):
 * (a) grammar + shell-lock are ON by default — the skeleton is actually
 *     interpolated into fill prompts;
 * (b) stub-signature extraction covers untyped functions, arrow consts, and
 *     class METHODS — the contract page stops starving;
 * (c) throw-camouflage: rejection-clade cases are pass-by-construction vs
 *     throwing stubs — retained at born-red, never surged as vacuous;
 * (d) 0/N permutation picks tone as friction, not neutral.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient, RepairInput } from '../src/core/types';
import { assembleSuperbuffer } from '../src/tier1/superbuffer';
import { isThrowCamouflaged } from '../src/tier1/bornRed';
import { t1EventToneCss } from '../src/tier1/eventTone';
import { TIER1_DEFAULT_GATES, runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

const RIGHT = 'export function add(a: number, b: number): number { if (typeof a !== "number") throw new Error("invalid"); return a + b; }';

function makeSpec(): FeatureSpec {
  return {
    title: 'Adder', prompt: 'adder',
    features: [{
      id: 'F1', name: 'add', description: 'adds two numbers, rejects invalid input', parentId: null,
      deps: [], files: ['src/adder.ts'], testFile: 'tests/f1-add.test.ts',
      edgeCases: ['rejects invalid input'], 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;
}

describe('SET 73a — grammar defaults ON', () => {
  it('TIER1_DEFAULT_GATES carries grammarEnabled + shellLock', () => {
    expect(TIER1_DEFAULT_GATES.grammarEnabled).toBe(true);
    expect(TIER1_DEFAULT_GATES.shellLock).toBe(true);
  });

  it('the skeleton is interpolated into the fill request by default', async () => {
    const fillErrors: string[] = [];
    const llm = {
      async encodeFeatures() { return makeSpec(); },
      async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; },
      async fixTestCases(_s: unknown, _f: unknown, req: { name: string; errors?: string[] }[]) {
        for (const r of req) fillErrors.push((r.errors ?? []).join('\n'));
        return { cases: [{ name: req[0]!.name, kind: 'behavior' as const, body: 'expect(add(2, 3)).toEqual(5);' }] };
      },
      async generateImplementation() { return [{ path: 'src/adder.ts', content: RIGHT }]; },
      async repair(_i: RepairInput) { return [{ path: 'src/adder.ts', content: RIGHT }]; },
    } as unknown as LlmClient;
    await runTier1Job({ prompt: 'adder', llm, runSuite: makeEvalSuiteRunner() });
    expect(fillErrors.some(e => /Synthesis grammar — invariant/.test(e))).toBe(true);
    expect(fillErrors.some(e => /EXACTLY this skeleton/.test(e))).toBe(true);
  });
});

describe('SET 73b — stub-signature extraction widened', () => {
  it('untyped functions, arrow consts, and class methods all extract', () => {
    const asm = assembleSuperbuffer({
      spec: makeSpec(),
      feature: makeSpec().features[0]!,
      parts: null,
      stubs: [{
        path: 'src/store.ts',
        content: [
          'export function plain(a, b) { throw new Error("stub"); }',
          'export const arrow = (x: number) => { throw new Error("stub"); };',
          'export class Store {',
          '  get(key: string): string | null { throw new Error("stub"); }',
          '  async put(key: string, value: string) { throw new Error("stub"); }',
          '}',
        ].join('\n'),
      }],
      lessons: [],
      siblingParts: new Map(),
      target: 'store',
      stage: 'fill',
    });
    expect(asm.stubSignatures).toContain('plain(a, b)');
    expect(asm.stubSignatures).toContain('arrow(x: number) =>');
    expect(asm.stubSignatures).toContain('class Store');
    expect(asm.stubSignatures).toContain('Store.get(key: string): string | null');
    expect(asm.stubSignatures).toContain('Store.put(key: string, value: string)');
  });
});

describe('SET 73c — throw-camouflage retention at born-red', () => {
  it('detector: toThrow-family bodies are camouflaged; plain assertions are not', () => {
    expect(isThrowCamouflaged("expect(() => add('', 1)).toThrow();")).toBe(true);
    expect(isThrowCamouflaged("expect(() => add('', 1)).toThrowError('invalid');")).toBe(true);
    expect(isThrowCamouflaged('await expect(fetchIt()).rejects.toThrow();')).toBe(true);
    expect(isThrowCamouflaged('expect(add(2, 3)).toBe(5);')).toBe(false);
    expect(isThrowCamouflaged('expect(true).toBe(true);')).toBe(false);
  });

  it('all-throw suite freezes anchor + retained rejection cases — nothing surged, nothing re-enqueued', async () => {
    let surgeryCalls = 0;
    const llm = {
      async encodeFeatures() { return makeSpec(); },
      async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; },
      async fixTestCases(_s: unknown, _f: unknown, req: { name: string }[], _i: unknown, _im: unknown, o?: { purpose?: string }) {
        if (o?.purpose?.startsWith('born-red surgery:')) surgeryCalls++;
        // Every fill is a rejection case — green vs throwing stubs.
        return { cases: [{ name: req[0]!.name, kind: 'edge' as const, body: "expect(() => add('' as never, 1)).toThrow();" }] };
      },
      async generateImplementation() { return [{ path: 'src/adder.ts', content: RIGHT }]; },
      async repair(_i: RepairInput) { return [{ path: 'src/adder.ts', content: RIGHT }]; },
    } as unknown as LlmClient;
    const res = await runTier1Job({ prompt: 'adder', llm, runSuite: makeEvalSuiteRunner() });
    const msgs = res.log.map(e => e.message);
    // Camouflaged cases were recognized and RETAINED (no vacuous surgery).
    expect(msgs.some(m => /throw-expecting case\(s\) retained \(undecidable vs throwing stubs/.test(m))).toBe(true);
    expect(surgeryCalls).toBe(0);
    // The anchor supplies redness; the rejection cases ride along frozen.
    expect(msgs.some(m => /deterministic anchor frozen .*throw-expecting retained/.test(m))).toBe(true);
    const suite = res.frozenTests[0]?.content ?? '';
    expect(suite).toContain('.toThrow()');
    expect(suite).toContain('not.toThrow()');
    // Nothing re-enqueued as vacuous — the templates finally EXECUTE.
    expect(res.metrics.reEnqueuedCases).toBe(0);
    expect(res.problems).toEqual([]);
  });
});

describe('SET 73d — tones', () => {
  it('0/N permutation pick is friction; N/N stays operational', () => {
    const zero = t1EventToneCss({ phase: 'ir', message: 'permutation-select: 3 candidates, picked t=0.3 (0/1 passing)' });
    const full = t1EventToneCss({ phase: 'ir', message: 'permutation-select: 3 candidates, picked t=0.3 (2/2 passing)' });
    expect(zero).not.toBe(full);
    expect(zero).toMatch(/friction/);
  });
});
