/**
 * SET 35 — runtime-poison class + ladder defects from first combat run.
 *
 * 35a: execution-position attribution (__RHA_CURRENT__)
 * 35b: dynamic-eval lint at IR gate
 * 35c: selection monotonicity (incumbent kept)
 * 35d: repair-oracle extensions + jest harness alias
 * 35e: perm aggregate merge bug + entropy-reset lint
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient } from '../src/core/types';
import {
  incumbentKeptEvent,
  isStrictImprovement,
  selectPermutation,
} from '../src/tier1/permutation';
import {
  formatRepairOracleHint,
  lookupRepairOracle,
  REPAIR_ORACLE,
} from '../src/tier1/repairOracle.generated';
import { runTier1Job } from '../src/tier1/runner';
import { acceptCaseBody, attemptMechanicalFix, classifyFailure } from '../src/tier1/substrate';
import { contributeRunToTotals, emptyStatsTotals } from '../src/tier1/statsAggregate';
import {
  dynamicEvalLint,
  formatExecutionCrash,
  makeEvalSuiteRunner,
  parseExecutionCrash,
  rewriteJestToVi,
} from '../src/tier1/testExec';

describe('SET 35a execution-position attribution', () => {
  it('format/parseExecutionCrash round-trips module + case', () => {
    const err = new SyntaxError('Invalid regular expression: missing /');
    err.stack = 'SyntaxError: x\n    at eval (eval:1:1)\n    at Object.fn (t:2:3)\n    at run (t:4:5)\n    at extra';
    const msg = formatExecutionCrash(err, { module: 'tests/f1.test', caseName: 'poison' });
    expect(msg).toMatch(/^crash while executing tests\/f1\.test # "poison"/);
    expect(msg).toContain('at eval');
    const parsed = parseExecutionCrash(msg);
    expect(parsed).toEqual({ module: 'tests/f1.test', caseName: 'poison' });
  });

  it('makeEvalSuiteRunner attributes eval("/") crash to the exact case', async () => {
    const runner = makeEvalSuiteRunner();
    const files: GeneratedFile[] = [
      { path: 'src/ok.ts', content: 'export function ok(): number { return 1; }\n' },
    ];
    const test: GeneratedFile = {
      path: 'tests/f1.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { ok } from '../src/ok';",
        "describe('t', () => {",
        "  it('safe', () => { expect(ok()).toBe(1); });",
        "  it('poison', () => { eval('/'); expect(ok()).toBe(1); });",
        '});',
      ].join('\n'),
    };
    const r = await runner(files, test);
    expect(r.crashed).toBeTruthy();
    expect(r.crashed).toMatch(/crash while executing tests\/f1\.test # "t > poison"/);
    const cls = classifyFailure('__suite__', r.crashed!);
    expect(cls.detail.caseName).toBe('t > poison');
  });
});

describe('SET 35b dynamic-eval lint', () => {
  it('rejects eval / new Function / non-literal RegExp; allows literal RegExp', () => {
    expect(dynamicEvalLint('expect(eval("1")).toBe(1);')).toMatch(/eval/);
    expect(dynamicEvalLint('const f = new Function("return 1"); expect(f()).toBe(1);')).toMatch(/Function/);
    expect(dynamicEvalLint('expect(new RegExp(x)).toBeTruthy();')).toMatch(/RegExp/);
    expect(dynamicEvalLint('expect(new RegExp("a+")).toBeTruthy();')).toBeNull();
    expect(dynamicEvalLint('expect(1).toBe(1);')).toBeNull();
  });

  it('acceptCaseBody rejects poisoned bodies', () => {
    expect(acceptCaseBody('expect(eval("1")).toBe(1);')).toBe(false);
    expect(acceptCaseBody('expect(1).toBe(1);')).toBe(true);
  });

  it('suite with new Function case is rejected pre-freeze (dropped)', async () => {
    const spec: FeatureSpec = {
      title: 'Poison', prompt: 'poison suite',
      features: [{
        id: 'F1', name: 'p', description: 'p', parentId: null,
        deps: [], files: ['src/p.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [{ file: 'src/p.ts', symbol: 'p' }],
        stubs: [{ path: 'src/p.ts', content: 'export function p(): number { return 1; }\n' }],
      }],
    } as unknown as FeatureSpec;

    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: '' }; },
      // Every fill draw returns runtime synthesis — must be dropped, never frozen.
      async fixTestCases(_s, _f, broken) {
        return {
          cases: broken.map(b => ({
            name: b.name,
            kind: b.kind as 'behavior',
            body: 'expect(new Function("return 1")()).toBe(1);',
          })),
        };
      },
      async generateImplementation() {
        return [{ path: 'src/p.ts', content: 'export function p(): number { return 1; }\n' }];
      },
      async repair() { return []; },
    };

    const res = await runTier1Job({
      prompt: 'poison', llm, runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 2, stallLimit: 1, progressStall: 2 },
    });
    // Either explicit drop line, or freeze never admitted new Function.
    const dropped = res.log.some(e => /dropped — dynamic-eval|dynamic-eval:/.test(e.message));
    const frozen = res.frozenTests.map(t => t.content).join('\n');
    expect(frozen).not.toMatch(/new Function/);
    expect(dropped || res.frozenTests.length === 0 || /coverage reduced/.test(res.problems.join('\n'))).toBe(true);
  });
});

describe('SET 35c selection monotonicity', () => {
  it('isStrictImprovement requires strictly better score', () => {
    expect(isStrictImprovement(4, 3)).toBe(true);
    expect(isStrictImprovement(3, 3)).toBe(false);
    expect(isStrictImprovement(2, 3)).toBe(false);
  });

  it('all-worse batch keeps incumbent bytes identical', async () => {
    // Incumbent passes only add(2,3) → 1/2; all repair candidates score 0/2.
    // Dual `99` literals block SET 43b spec-constant unique-replace (must hit LLM repair).
    const incumbent = 'export function add(a: number, b: number): number { const _pad = 99; return a === 2 ? 5 : 99; }\n';
    const worseImpl = 'export function add(_a: number, _b: number): number { return -1; }\n';
    const spec: FeatureSpec = {
      title: 'Add', prompt: 'add numbers correctly',
      features: [{
        id: 'F1', name: 'calc', description: 'add', parentId: null,
        deps: [], files: ['src/calc.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: ['add(0, 0) is 0'], 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 repairN = 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: '' }; },
      async fixTestCases(_s, _f, broken) {
        return {
          cases: broken.map(b => ({
            name: b.name,
            kind: b.kind as 'behavior' | 'edge',
            // Two asserts so incumbent can score 1/2 (a===2→5) while worse scores 0/2.
            body: /0/.test(b.name) || /edge/i.test(b.kind)
              ? 'expect(add(0, 0)).toBe(0);'
              : 'expect(add(2, 3)).toBe(5);',
          })),
        };
      },
      async generateImplementation() {
        return [{ path: 'src/calc.ts', content: incumbent }];
      },
      async repair() {
        repairN++;
        return [{ path: 'src/calc.ts', content: worseImpl }];
      },
    };

    const res = await runTier1Job({
      prompt: 'add numbers correctly', llm, runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 4, stallLimit: 2, progressStall: 2 },
    });
    expect(repairN).toBeGreaterThan(0);
    expect(res.log.some(e => /no improvement — incumbent kept/.test(e.message))).toBe(true);
    const final = res.files.find(f => f.path === 'src/calc.ts')?.content;
    expect(final).toBe(incumbent);
    expect(final).not.toBe(worseImpl);
    void selectPermutation;
    void incumbentKeptEvent;
  });
});

describe('SET 35d repair oracle + jest shim', () => {
  it('oracle covers toBe / toHaveProperty / runtime-generic / jest', () => {
    const ids = new Set(REPAIR_ORACLE.map(e => e.id));
    expect(ids.has('assertion-toBe')).toBe(true);
    expect(ids.has('assertion-toHaveProperty')).toBe(true);
    expect(ids.has('runtime-generic')).toBe(true);
    expect(ids.has('undefined-symbol-jest')).toBe(true);
    expect(lookupRepairOracle('assertion|toBe')?.id).toBe('assertion-toBe');
    expect(lookupRepairOracle('undefined-symbol|jest')?.id).toBe('undefined-symbol-jest');
    expect(lookupRepairOracle('undefined-symbol|jest')?.category).toBe('mechanical');
    expect(formatRepairOracleHint('runtime|boom')).toMatch(/thrown message class/);
  });

  it('harness jest alias + rewriteJestToVi are mechanical', async () => {
    const runner = makeEvalSuiteRunner();
    const files: GeneratedFile[] = [
      { path: 'src/ok.ts', content: 'export function ok(): number { return 1; }\n' },
    ];
    const test: GeneratedFile = {
      path: 'tests/f1.test.ts',
      content: [
        "import { describe, it, expect, jest } from 'vitest';",
        "import { ok } from '../src/ok';",
        "describe('t', () => {",
        "  it('mock', () => { const fn = jest.fn(() => ok()); expect(fn()).toBe(1); });",
        '});',
      ].join('\n'),
    };
    const r = await runner(files, test);
    expect(r.crashed).toBeUndefined();
    expect(r.failures).toHaveLength(0);

    const rewritten = rewriteJestToVi("import { jest } from 'vitest';\nconst f = jest.fn();\n");
    expect(rewritten).toContain('vi.fn');
    const fix = attemptMechanicalFix(
      classifyFailure('t', 'jest is not defined'),
      files,
      { path: 'tests/f1.test.ts', content: "import { describe, it, expect } from 'vitest';\nconst f = jest.fn();\nexpect(1).toBe(1);\n" },
      { title: 't', prompt: 't', features: [] } as FeatureSpec,
    );
    expect(fix.status).toBe('fixed');
    expect(fix.fix?.content).toContain('vi.fn');
  });
});

describe('SET 35e perm aggregate + entropy-reset lint', () => {
  it('lifetime totals never treat permutationDraws as batches', () => {
    const totals = emptyStatsTotals();
    contributeRunToTotals(totals, {
      id: 'legacy',
      ts: '2026-07-26T00:00:00.000Z',
      mode: 'rha',
      problems: 0,
      metrics: { permutationDraws: 9 }, // legacy: draws only
    });
    expect(totals.perm.batches).toBe(0);
    expect(totals.perm.candidates).toBe(9);

    contributeRunToTotals(totals, {
      id: 'modern',
      ts: '2026-07-26T01:00:00.000Z',
      mode: 'rha',
      problems: 0,
      metrics: { permBatches: 2, permCandidates: 6, permutationDraws: 6 },
    });
    expect(totals.perm.batches).toBe(2);
    expect(totals.perm.candidates).toBe(15);
  });

  it('entropy reset rejects fresh draw with dynamic-eval poison', async () => {
    const spec: FeatureSpec = {
      title: 'E', prompt: 'e',
      features: [{
        id: 'F1', name: 'e', description: 'e', parentId: null,
        deps: [], files: ['src/e.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [{ file: 'src/e.ts', symbol: 'e' }],
        stubs: [{ path: 'src/e.ts', content: 'export function e(): number { throw new Error("stub"); }\n' }],
      }],
    } as unknown as FeatureSpec;

    let fresh = 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/e', symbols: ['e'] }],
            cases: [{ name: 'ok', kind: 'behavior', body: 'expect(e()).toBe(1);' }],
          },
        };
      },
      async fixTestCases(_s, _f, broken) {
        return { cases: broken.map(b => ({ name: b.name, kind: 'behavior' as const, body: 'expect(e()).toBe(1);' })) };
      },
      async generateImplementation() {
        fresh++;
        // First call is initial build — return wrong but clean. Later entropy reset returns poison.
        if (fresh === 1) {
          return [{ path: 'src/e.ts', content: 'export function e(): number { return 0; }\n' }];
        }
        return [{ path: 'src/e.ts', content: 'export function e(): number { return eval("1"); }\n' }];
      },
      async repair() {
        return [{ path: 'src/e.ts', content: 'export function e(): number { return 0; }\n' }];
      },
    };

    const res = await runTier1Job({
      prompt: 'e', llm, runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 4, stallLimit: 1, progressStall: 1 },
    });
    expect(res.log.some(e => /entropy reset rejected/.test(e.message))).toBe(true);
    const final = res.files.find(f => f.path === 'src/e.ts')?.content ?? '';
    expect(final).not.toMatch(/\beval\s*\(/);
  });
});
