/**
 * SET 65 — synthesis grammar (typed-hole skeletons, deterministic reassembly)
 * + shell-locked codegen (frame from stubs, body-only draws). Both flagged;
 * default-off behavior unchanged. Acceptance: parse poison and duplicate
 * declarations cannot survive a locked build; a conformant grammar fill is
 * byte-reassembled from the skeleton.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient, RepairInput } from '../src/core/types';
import {
  chooseInvariants,
  conformDrawToSkeleton,
  explainConformMiss,
  renderSkeleton,
  skeletonPrompt,
} from '../src/tier1/grammar';
import {
  buildFileFrame,
  extractDrawnBody,
  lockDrawToFrame,
} from '../src/tier1/shellLock';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';
import { probeModuleEval } from '../src/tier1/testExec';

describe('SET 65 — synthesis grammar', () => {
  it('deterministic invariant gates route by kind + spec tokens', () => {
    expect(chooseInvariants({ name: 'rejects invalid input', kind: 'behavior' })[0]).toBe('rejection');
    expect(chooseInvariants({ name: 'normalize is idempotent', kind: 'behavior' })[0]).toBe('idempotence');
    expect(chooseInvariants({ name: 'sorts entries', kind: 'behavior' })[0]).toBe('ordering');
    expect(chooseInvariants({ name: 'zero', kind: 'edge' })[0]).toBe('boundary');
    expect(chooseInvariants({ name: 'plain behavior', kind: 'behavior' })).toContain('boundary');
  });

  it('skeleton renders parseable-with-holes and calls the symbol (vacuous-impossible)', () => {
    const sk = renderSkeleton('boundary', 'add');
    expect(sk.body).toContain('add(⟨ARGS⟩)');
    expect(sk.holes).toEqual(['ARGS', 'EXPECT']);
    expect(skeletonPrompt(sk)).toContain('EXACTLY this skeleton');
  });

  it('conformance: literal fills reassemble deterministically; junk is rejected', () => {
    const sk = renderSkeleton('boundary', 'add');
    const ok = conformDrawToSkeleton('expect(add(2, 3)).toEqual(5);', sk);
    expect(ok).not.toBeNull();
    expect(ok!.fills).toEqual({ ARGS: '2, 3', EXPECT: '5' });
    expect(ok!.setup).toEqual([]);
    expect(ok!.body).toBe('expect(add(2, 3)).toEqual(5);');
    // Whitespace churn conforms; the reassembled body is canonical.
    expect(conformDrawToSkeleton('expect( add(2,3) ).toEqual( 5 );', sk)!.body)
      .toBe('expect(add(2,3)).toEqual(5);');
    // Non-literal hole content (free identifiers / calls) refuses.
    expect(conformDrawToSkeleton('expect(add(x, 3)).toEqual(5);', sk)).toBeNull();
    expect(conformDrawToSkeleton('expect(add(f(), 3)).toEqual(5);', sk)).toBeNull();
    // Different shape refuses.
    expect(conformDrawToSkeleton('expect(true).toBe(true);', sk)).toBeNull();
  });

  it('repeated hole (determinism skeleton) must agree across occurrences', () => {
    const sk = renderSkeleton('determinism', 'norm');
    expect(conformDrawToSkeleton("expect(norm('a')).toEqual(norm('a'));", sk)).not.toBeNull();
    expect(conformDrawToSkeleton("expect(norm('a')).toEqual(norm('b'));", sk)).toBeNull();
  });
});

describe('SET 65 — shell-locked codegen', () => {
  const STUB = {
    path: 'src/calc.ts',
    content: [
      'export const PRECISION = 2;',
      '',
      "export function add(a: number, b: number): number { throw new Error('stub'); }",
      '',
      "export async function fetchTotal(items: number[]): Promise<number> { throw new Error('stub'); }",
    ].join('\n'),
  };

  it('frame: header preserved, one slot per exported function', () => {
    const frame = buildFileFrame(STUB)!;
    expect(frame.header).toBe('export const PRECISION = 2;');
    expect(frame.slots.map(s => s.symbol)).toEqual(['add', 'fetchTotal']);
    expect(frame.slots[0]!.head).toBe('export function add(a: number, b: number): number');
  });

  it('locked file survives a poisoned draw: dup decls + top-level prose vanish', () => {
    const frame = buildFileFrame(STUB)!;
    const poisoned = [
      'Here is the implementation you asked for:',
      'export function add(a: number, b: number): number { return a + b; }',
      'export function add(a: number, b: number): number { return a + b; } // duplicate!',
      'export async function fetchTotal(items: number[]): Promise<number> { return items.reduce((x, y) => x + y, 0); }',
      'const stray = )( unbalanced;',
    ].join('\n');
    const locked = lockDrawToFrame(frame, poisoned);
    expect(locked.missing).toEqual([]);
    expect(locked.content).toContain('return a + b;');
    expect(locked.content).not.toContain('Here is');
    expect(locked.content).not.toContain('stray');
    expect((locked.content.match(/export function add/g) ?? []).length).toBe(1);
    // The locked file parses clean in the real module prober.
    expect(probeModuleEval([{ path: STUB.path, content: locked.content }], STUB.path)).toBeNull();
  });

  it('missing slot falls back to the stub body and is reported', () => {
    const frame = buildFileFrame(STUB)!;
    const partial = 'export function add(a: number, b: number): number { return a + b; }';
    const locked = lockDrawToFrame(frame, partial);
    expect(locked.missing).toEqual(['fetchTotal']);
    expect(locked.content).toContain("throw new Error('stub')");
  });

  it('arrow-const draws still yield bodies', () => {
    expect(extractDrawnBody('export const add = (a: number, b: number) => a + b;', 'add'))
      .toBe('return a + b;');
    expect(extractDrawnBody('export const add = (a, b) => { return a + b; };', 'add'))
      .toBe('return a + b;');
  });
});

describe('SET 65 — flagged e2e', () => {
  const RIGHT_POISONED_DRAW = [
    'Sure! Implementation below.',
    'export function add(a: number, b: number): number { return a + b; }',
    'export function add(a: number, b: number): number { return a + b; }',
  ].join('\n');

  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 llm = {
    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;
      // Grammar-shaped fills: skeleton with holes replaced by literals.
      return {
        cases: [{
          name,
          kind: (name === 'zero' ? 'edge' : 'behavior') as 'edge' | 'behavior',
          body: name === 'zero'
            ? 'expect(add(7, 0)).toEqual(7);'
            : 'expect(add(2, 3)).toEqual(5);',
        }],
      };
    },
    async generateImplementation() {
      return [{ path: 'src/adder.ts', content: RIGHT_POISONED_DRAW }];
    },
    async repair(_i: RepairInput) {
      return [{ path: 'src/adder.ts', content: 'export function add(a: number, b: number): number { return a + b; }' }];
    },
  } as unknown as LlmClient;

  it('grammar + shell-lock ON (the default): poisoned draw builds clean; fills conform; flags report', async () => {
    const res = await runTier1Job({
      prompt: 'adder', llm, runSuite: makeEvalSuiteRunner(),
    });
    const msgs = res.log.map(e => e.message);
    // Shell-lock consumed the duplicate-decl draw structurally.
    expect(res.metrics.shellLockApplied).toBeGreaterThanOrEqual(1);
    expect(msgs.some(m => /^shell-lock: src\/adder\.ts re-emitted/.test(m))).toBe(true);
    expect(res.metrics.parsePoisonIncidents).toBe(0);
    const built = res.files.find(f => f.path === 'src/adder.ts')!;
    expect((built.content.match(/export function add/g) ?? []).length).toBe(1);
    expect(built.content).not.toContain('Sure!');
    // Grammar conformance on the boundary skeletons.
    expect(res.metrics.grammarConformant).toBeGreaterThanOrEqual(1);
    expect(msgs.some(m => /^grammar: case ".*" conformed to boundary/.test(m))).toBe(true);
    // The run converges — no unparseable-at-(empty) drops, no problems.
    expect(res.problems).toEqual([]);
    expect(msgs.some(m => /unparseable at "\(empty\)"/.test(m))).toBe(false);
  });

  it('flags OFF (explicit opt-out): prior behavior byte-for-byte (no lock, no grammar events)', async () => {
    const res = await runTier1Job({
      prompt: 'adder', llm, runSuite: makeEvalSuiteRunner(),
      gates: { grammarEnabled: false, shellLock: false },
    });
    const msgs = res.log.map(e => e.message);
    expect(res.metrics.shellLockApplied).toBe(0);
    expect(res.metrics.grammarConformant).toBe(0);
    expect(res.metrics.grammarFallbacks).toBe(0);
    expect(msgs.some(m => /^shell-lock:|^grammar/.test(m))).toBe(false);
  });
});

describe('SET 78 — multi-statement conformance', () => {
  it('a rich body conforms: setup preserved, assertion reassembled', () => {
    const sk = renderSkeleton('boundary', 'edit');
    const drawn = [
      "const doc = { id: 1, rev: 7 };",
      "expect(edit(2, 3)).toEqual(5);",
    ].join('\n');
    const r = conformDrawToSkeleton(drawn, sk)!;
    expect(r).not.toBeNull();
    expect(r.setup).toEqual(['const doc = { id: 1, rev: 7 };']);
    expect(r.body).toBe("const doc = { id: 1, rev: 7 };\nexpect(edit(2, 3)).toEqual(5);");
    expect(r.fills).toEqual({ ARGS: '2, 3', EXPECT: '5' });
  });

  it('a genuinely different assertion still falls back, and says why', () => {
    const sk = renderSkeleton('boundary', 'edit');
    expect(conformDrawToSkeleton('expect(() => edit(1)).toThrow();', sk)).toBeNull();
    expect(explainConformMiss('expect(() => edit(1)).toThrow();', sk)).toBe('shape-mismatch');
    // Computed values are a DIFFERENT miss than a wrong shape.
    expect(explainConformMiss('expect(edit(compute(), 3)).toEqual(5);', sk)).toBe('non-literal-fill');
  });

  // SET 95e — the "grammar fallback: accepted freeform" event is retired
  // (formatGrammarFallback deleted). Acceptance: the phrase has zero hits
  // in production code (src/).
  it('SET 95e: the freeform-fallback phrase is retired from production code', async () => {
    const fs = await import('node:fs/promises');
    const path = await import('node:path');
    const srcDir = path.resolve(__dirname, '../src');
    const hits: string[] = [];
    async function walk(dir: string): Promise<void> {
      for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) await walk(full);
        else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) {
          const text = await fs.readFile(full, 'utf8');
          if (text.includes('accepted freeform')) hits.push(full);
        }
      }
    }
    await walk(srcDir);
    expect(hits).toEqual([]);
  });
});
