import { describe, expect, it } from 'vitest';
import type { FeatureNode, FeatureSpec } from '../src/core/types';
import {
  assembleCaseFrame,
  assembleImplFrame,
  extractHole,
  isOverreach,
  spliceHole,
} from '../src/tier1/frame';

const feature: FeatureNode = {
  id: 'F1',
  name: 'adder',
  description: 'adds two numbers',
  parentId: null,
  deps: [],
  files: ['src/adder.ts'],
  testFile: 'tests/f1-adder.test.ts',
  edgeCases: [],
  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'); }`,
  }],
};

const spec: FeatureSpec = { title: 'Adder', prompt: 'adder', features: [feature] };

describe('SET 95b — assembleCaseFrame', () => {
  it('produces a minimal it() frame with a single ⟨HOLE⟩ marker', () => {
    const frame = assembleCaseFrame(feature, 'add', 'behavior');
    expect(frame.bytes).toContain('⟨HOLE⟩');
    expect(frame.bytes).toContain(`it("add"`);
    expect(frame.holeName).toBe('ASSERTION');
    expect(frame.holeBudgetBytes).toBeGreaterThan(0);
  });

  it('prefixes edge-kind case names like the real assembler', () => {
    const frame = assembleCaseFrame(feature, 'rejects empty input', 'edge');
    expect(frame.bytes).toContain('edge: rejects empty input');
  });
});

describe('SET 95b — assembleImplFrame', () => {
  it('replaces the canonical throw-stub body with a named ⟨BODY:symbol⟩ hole', () => {
    const frame = assembleImplFrame(feature, spec);
    expect(frame.holes).toEqual([{ name: 'BODY:add', symbol: 'add', budgetBytes: expect.any(Number) }]);
    expect(frame.bytes).toContain('⟨BODY:add⟩');
    expect(frame.bytes).not.toContain('not implemented');
  });

  it('leaves a stub with no matching export untouched (no hole)', () => {
    const untouched: FeatureNode = {
      ...feature,
      exports: [],
    };
    const frame = assembleImplFrame(untouched, spec);
    expect(frame.holes).toEqual([]);
    expect(frame.bytes).toContain('not implemented');
  });
});

describe('SET 95b — spliceHole', () => {
  it('replaces the unnamed ⟨HOLE⟩ marker verbatim', () => {
    const frame = assembleCaseFrame(feature, 'add', 'behavior');
    const spliced = spliceHole(frame.bytes, 'ASSERTION', 'expect(add(2, 3)).toBe(5);');
    expect(spliced).toContain('expect(add(2, 3)).toBe(5);');
    expect(spliced).not.toContain('⟨HOLE⟩');
  });

  it('replaces a named ⟨BODY:symbol⟩ marker', () => {
    const frame = assembleImplFrame(feature, spec);
    const spliced = spliceHole(frame.bytes, 'add', 'return a + b;');
    expect(spliced).toContain('return a + b;');
    expect(spliced).not.toContain('⟨BODY:add⟩');
  });

  it('is idempotent once the marker is already spliced away', () => {
    const frame = assembleCaseFrame(feature, 'add', 'behavior');
    const once = spliceHole(frame.bytes, 'ASSERTION', 'expect(1).toBe(1);');
    const twice = spliceHole(once, 'ASSERTION', 'expect(2).toBe(2);');
    expect(twice).toBe(once);
  });
});

describe('SET 95b — extractHole', () => {
  it('extracts the body between the markers', () => {
    const { body } = extractHole('⟨HOLE⟩\nexpect(add(2, 3)).toBe(5);\n⟨/END⟩');
    expect(body).toBe('expect(add(2, 3)).toBe(5);');
  });

  it('returns null with a reason when no marker is present', () => {
    const { body, reason } = extractHole("describe('x', () => { it('y', () => {}); });");
    expect(body).toBeNull();
    expect(reason).toBe('no hole marker in response');
  });
});

describe('SET 95b — isOverreach', () => {
  it('accepts a well-formed, in-budget hole with no boilerplate outside', () => {
    const r = isOverreach('⟨HOLE⟩\nexpect(add(2, 3)).toBe(5);\n⟨/END⟩', 400);
    expect(r.over).toBe(false);
  });

  it('rejects a response missing the hole marker', () => {
    const r = isOverreach("describe('x', () => { it('y', () => { expect(1).toBe(1); }); });", 400);
    expect(r.over).toBe(true);
    expect(r.reason).toBe('no hole marker in response');
  });

  it('rejects a body exceeding the 1.5x budget', () => {
    const big = 'x'.repeat(700);
    const r = isOverreach(`⟨HOLE⟩\nexpect(${big}).toBeDefined();\n⟨/END⟩`, 400);
    expect(r.over).toBe(true);
    expect(r.reason).toMatch(/response \d+ > budget 600/);
  });

  it('rejects boilerplate reproduced outside the hole', () => {
    const r = isOverreach(
      "import { foo } from './foo';\n⟨HOLE⟩\nexpect(foo()).toBe(1);\n⟨/END⟩",
      400,
    );
    expect(r.over).toBe(true);
    expect(r.reason).toBe('boilerplate reproduction outside hole');
  });
});
