/**
 * SET 6 — regex-literal crash class + coherent bug metrics.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import {
  classifyFailure,
  healUnterminatedRegexLiteral,
} from '../src/tier1/substrate';
import {
  attributeEvalCrash,
  makeEvalSuiteRunner,
  probeModuleEval,
} from '../src/tier1/testExec';

describe('SET 6a regex-literal classification + attribution', () => {
  it('classifies Invalid regular expression as regex-literal', () => {
    const c = classifyFailure('__suite__', 'Invalid regular expression: missing /');
    expect(c.kind).toBe('regex-literal');
    expect(c.classKey).toMatch(/^regex-literal\|/);
  });

  it('heals unterminated regex literal mechanically', () => {
    // Odd unescaped `/` — append `/` → valid. (Unclosed `[` class is NOT this rung.)
    const src = 'export const re = /abc\nexport function ok() { return 1; }\n';
    const healed = healUnterminatedRegexLiteral(src);
    expect(healed).toContain('export const re = /abc/');
    expect(probeModuleEval(
      [{ path: 'src/bad.ts', content: healed! }],
      'src/bad.ts',
    )).toBeNull();
  });

  it('attributes eval crash to the offending impl file', () => {
    const good = { path: 'src/good.ts', content: 'export const x = 1;\n' };
    // Sucrase accepts; V8 rejects — the SET 6a evidence class.
    const bad = { path: 'src/bad.ts', content: 'export const re = /(/;\n' };
    const test = {
      path: 'tests/f1.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { x } from '../src/good';",
        "import '../src/bad';",
        "describe('t', () => { it('ok', () => { expect(x).toBe(1); }); });",
      ].join('\n'),
    };
    const crash = probeModuleEval([good, bad], 'src/bad.ts');
    expect(crash).toMatch(/Invalid regular expression/i);
    const file = attributeEvalCrash([good, bad], test, crash!);
    expect(file).toBe('src/bad.ts');
  });

  it('heals impl regex via substrate without rewriting siblings', async () => {
    const spec: FeatureSpec = {
      title: 'Re', prompt: 'regex lib',
      features: [{
        id: 'F1', name: 're', description: 'pattern', parentId: null,
        deps: [], files: ['src/good.ts', 'src/bad.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [
          { file: 'src/good.ts', symbol: 'ok' },
          { file: 'src/bad.ts', symbol: 're' },
        ],
        stubs: [
          { path: 'src/good.ts', content: 'export function ok(): number { throw new Error("stub"); }\n' },
          { path: 'src/bad.ts', content: 'export const re: RegExp = /./; // stub\n' },
        ],
      }],
    } as unknown as FeatureSpec;

    const SUITE = [
      "import { describe, it, expect } from 'vitest';",
      "import { ok } from '../src/good';",
      "import { re } from '../src/bad';",
      "describe('re', () => {",
      "  it('ok', () => { expect(ok()).toBe(1); expect(re.test('abc')).toBe(true); });",
      '});',
    ].join('\n');

    let repairFocused: string[] = [];
    const llm: LlmClient = {
      async enumerateFeatures() { return []; },
      async probeFeature() { return { clear: true, assertion: 'x', question: '' }; },
      async encodeFeatures() { return spec; },
      async repairFeatures() { return []; },
      async fixStub() { return ''; },
      async generateTests() {
        return {
          content: SUITE,
          parts: {
            imports: [
              { from: 'src/good', symbols: ['ok'] },
              { from: 'src/bad', symbols: ['re'] },
            ],
            cases: [{ name: 'ok', kind: 'behavior', body: "expect(ok()).toBe(1); expect(re.test('abc')).toBe(true);" }],
          },
        };
      },
      async fixTestCases(_s, _f, broken) {
        return { cases: broken.map(b => ({ name: b.name, kind: b.kind as 'behavior', body: "expect(ok()).toBe(1);" })) };
      },
      async generateImplementation() {
        return [
          { path: 'src/good.ts', content: 'export function ok(): number { return 1; }\n' },
          // Eval-only regex crash (sucrase-clean).
          { path: 'src/bad.ts', content: 'export const re = /(/;\n' },
        ];
      },
      async repair(input) {
        repairFocused.push(...input.findings.map(f => f.file));
        return [{ path: 'src/bad.ts', content: 'export const re = /abc/;\n' }];
      },
    };

    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'regex lib', llm, runSuite: makeEvalSuiteRunner(),
      onEvent: e => events.push(e.message),
    });
    // SET 38a — program-poisoning build writes are rejected at the build step
    // (incumbent stub kept). Either path is success: rejected at write, or
    // healed via substrate/repair to a valid /abc/ literal.
    const bad = res.files.find(f => f.path === 'src/bad.ts')?.content ?? '';
    const rejectedAtBuild = events.some(m => /program-parse regression: src\/bad\.ts rejected/.test(m));
    const healed = bad.includes('/abc/');
    expect(events.some(m => /regex-literal|Invalid regular|repair pass|program-parse regression/i.test(m))).toBe(true);
    const good = res.files.find(f => f.path === 'src/good.ts');
    expect(good?.content).toContain('return 1');
    expect(rejectedAtBuild || healed).toBe(true);
    if (healed) {
      // Focused repair targeted the attributed offender, not the sibling.
      expect(repairFocused.some(f => f === 'src/bad.ts')).toBe(true);
      expect(repairFocused.every(f => f === 'src/bad.ts' || f === '')).toBe(true);
    }
  });
});

describe('SET 6b bug metrics accounting', () => {
  it('patched is never negative and remaining ≤ observed', async () => {
    const spec: FeatureSpec = {
      title: 'Calc', prompt: 'calc',
      features: [{
        id: 'F1', name: 'calc', description: 'add', parentId: null,
        deps: [], files: ['src/calc.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: ['zero'], envVars: [],
        exports: [{ file: 'src/calc.ts', symbol: 'add' }],
        stubs: [{
          path: 'src/calc.ts',
          content: 'export function add(a: number, b: number): number { throw new Error("stub"); }\n',
        }],
      }],
    } as unknown as FeatureSpec;

    let implPass = 0;
    const llm: LlmClient = {
      async enumerateFeatures() { return []; },
      async probeFeature() { return { clear: true, assertion: 'x', question: '' }; },
      async encodeFeatures() { return spec; },
      async repairFeatures() { return []; },
      async fixStub() { return ''; },
      async generateTests() {
        return {
          content: '',
          parts: {
            imports: [{ from: 'src/calc', symbols: ['add'] }],
            cases: [
              { name: 'add', kind: 'behavior', body: 'expect(add(2, 3)).toBe(5);' },
              { name: 'zero', kind: 'edge', body: 'expect(add(0, 0)).toBe(0);' },
            ],
          },
        };
      },
      async fixTestCases(_s, _f, broken) {
        return {
          cases: broken.map(b => ({
            name: b.name,
            kind: b.kind as 'behavior' | 'edge',
            body: b.kind === 'edge' ? 'expect(add(0, 0)).toBe(0);' : 'expect(add(2, 3)).toBe(5);',
          })),
        };
      },
      async generateImplementation() {
        implPass++;
        // First draw wrong; later draws correct — late findings then heal.
        if (implPass === 1) {
          return [{ path: 'src/calc.ts', content: 'export function add(a: number, b: number): number { return a - b; }\n' }];
        }
        return [{ path: 'src/calc.ts', content: 'export function add(a: number, b: number): number { return a + b; }\n' }];
      },
      async repair() {
        return [{ path: 'src/calc.ts', content: 'export function add(a: number, b: number): number { return a + b; }\n' }];
      },
    };

    const res = await runTier1Job({
      prompt: 'calc', llm, runSuite: makeEvalSuiteRunner(),
    });
    expect(res.metrics.bugsPatched).toBeGreaterThanOrEqual(0);
    expect(res.metrics.bugsRemaining).toBeLessThanOrEqual(res.metrics.bugsObserved);
    expect(res.metrics.bugsPatched).toBe(res.metrics.bugsObserved - res.metrics.bugsRemaining);
  });
});
