/**
 * SET 31 — Seam isolation, recursive boundary attribution, and honest
 * artifact-fault accounting.
 *
 * 31a: buildTestProgram concatenates all transformed modules into ONE
 *      `new Function()` source. JS `/` is lexer-context-dependent, so
 *      per-module parse validity does NOT compose across seams — a valid
 *      program set can still lex-poison at the join. The fix wraps every
 *      registration in a leading-`;` IIFE (statement-isolated) shared
 *      byte-for-byte with `probeModuleEval` so probe context ≡ program
 *      context. `attribution-miss` is preserved as the tripwire event.
 *
 * 31b: When a program crashes and every single-module probe is clean, the
 *      fault must live between adjacent modules. `findBoundaryPair` binary-
 *      searches concatenation prefixes O(log n) evaluator calls (0 tokens)
 *      to attribute the pair; `attributeBoundaryFault` then invokes a
 *      delimiter-oracle at the tail-of-A / head-of-B seam and re-probes
 *      the whole program under 31a isolation to confirm the heal.
 *
 * 31c: Findings with the same normalized crash signature are deduped —
 *      "1 artifact fault affecting N features" instead of "N independent
 *      bugs". Per-feature problem entries remain for traceability.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import {
  attributeBoundaryFault,
  attributeEvalCrashDetailed,
  buildTestProgram,
  findBoundaryPair,
  makeEvalSuiteRunner,
  probeModuleEval,
  registerModuleSource,
} from '../src/tier1/testExec';
import { applyDelimiterOracle } from '../src/tier1/delimiterOracle';

describe('SET 31a statement-isolated module registration', () => {
  it('registerModuleSource emits a leading `;` IIFE around __register', () => {
    const s = registerModuleSource('src/a', 'const x = 1;');
    // Leading `;` prevents a trailing expression on the previous statement
    // from re-lexing the next module's `/` as division.
    expect(s.startsWith(';')).toBe(true);
    // IIFE wrapper: `;(function(){ ... })();` — statement-isolated.
    expect(s).toMatch(/^;\(function\(\)\{/);
    expect(s).toContain('__register("src/a", function (require, module, exports) {');
    // Terminating `();` closes the IIFE explicitly — no ASI hazard.
    expect(s.trimEnd().endsWith('})();')).toBe(true);
  });

  it('registerModuleSource output is idempotently seam-safe when concatenated', () => {
    const a = registerModuleSource('m/a', 'const x = 1;');
    const b = registerModuleSource('m/b', 'const y = /re/;');
    // Every join is inherently safe: A ends `;`, B starts `;`.
    const joined = [a, b].join('\n');
    // No `);(` sequence bleeding a preceding call into the next factory arg.
    expect(joined).toMatch(/\}\)\(\);\n;\(function/);
  });

  it('buildTestProgram wraps EVERY registration in the shared isolated form', () => {
    const files = [
      { path: 'src/a.ts', content: 'export const a = 1;\n' },
      { path: 'src/b.ts', content: 'export const b = 2;\n' },
    ];
    const test = {
      path: 'tests/f.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { a } from '../src/a';",
        "import { b } from '../src/b';",
        "describe('t', () => { it('ok', () => { expect(a + b).toBe(3); }); });",
      ].join('\n'),
    };
    const program = buildTestProgram(files, test);
    // Each module carries the ;(function(){ __register("<key>" prefix.
    expect(program).toContain(';(function(){ __register("src/a"');
    expect(program).toContain(';(function(){ __register("src/b"');
    expect(program).toContain(';(function(){ __register("tests/f.test"');
    // Legacy naked `__register(` (without the IIFE wrapper) MUST NOT appear
    // in the module region — only the internal __register definition may.
    const naked = program.match(/\n__register\(/g) ?? [];
    expect(naked.length).toBe(0);
  });

  it('probeModuleEval uses the SAME isolated wrapper — probe context ≡ program context', () => {
    // We can't observe the internal probe program directly, but its
    // behavior must match buildTestProgram. A clean file that parses via
    // both surfaces confirms the shared contract (behavioral equivalence).
    const good = { path: 'src/good.ts', content: 'export const x = 1;\n' };
    // registerModuleSource output for the same key is what probeModuleEval
    // must be embedding — we verify the shape here as an anchor test.
    expect(registerModuleSource('src/good', 'exports.x = 1;'))
      .toMatch(/^;\(function\(\)\{ __register\("src\/good"/);
    expect(probeModuleEval([good], 'src/good.ts')).toBeNull();
  });

  it('makeEvalSuiteRunner runs the statement-isolated program successfully', async () => {
    const runner = makeEvalSuiteRunner();
    const good = { path: 'src/good.ts', content: 'export const x = 1;\n' };
    const test = {
      path: 'tests/f.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { x } from '../src/good';",
        "describe('t', () => { it('ok', () => { expect(x).toBe(1); }); });",
      ].join('\n'),
    };
    const r = await runner([good], test);
    expect(r.crashed).toBeUndefined();
    expect(r.total).toBe(1);
    expect(r.failures).toHaveLength(0);
  });

  it('attribution-miss tripwire preserved: opaque crash → miss diagnostic', () => {
    const good = { path: 'src/good.ts', content: 'export const x = 1;\n' };
    const test = {
      path: 'tests/f.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { x } from '../src/good';",
        "describe('t', () => { it('ok', () => { expect(x).toBe(1); }); });",
      ].join('\n'),
    };
    // A crash that no single-module probe reproduces — invariant trips.
    const detailed = attributeEvalCrashDetailed([good], test, 'ReferenceError: __RHA_GHOST__ is not defined');
    expect(detailed.file).toBeNull();
    expect(detailed.miss).toMatch(/^attribution-miss: program crashed, /);
  });
});

describe('SET 31b findBoundaryPair (binary-search seam attribution)', () => {
  it('locates the minimal adjacent pair O(log n) with an injected evaluator', () => {
    const mods = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
    let calls = 0;
    // Synthetic seam: crash iff prefix contains BOTH 'D' (index 3) and 'E' (index 4).
    const evaluator = (prefix: string[]): string | null => {
      calls++;
      return prefix.includes('D') && prefix.includes('E') ? 'seam crash' : null;
    };
    const pair = findBoundaryPair(mods, evaluator);
    expect(pair).not.toBeNull();
    expect(pair!.left).toBe(3);
    expect(pair!.right).toBe(4);
    expect(pair!.leftCleanPrefix.length).toBe(4);
    expect(pair!.crashMessage).toBe('seam crash');
    // Binary search over 8 modules — ceil(log2 8) = 3 midpoint evals plus
    // 2 preflight evals (first-alone + whole). Loose upper bound: 10.
    expect(calls).toBeLessThanOrEqual(10);
  });

  it('returns null when module 0 alone crashes (not a seam)', () => {
    const mods = ['A', 'B', 'C'];
    const pair = findBoundaryPair(mods, prefix => (prefix.length > 0 ? 'boom' : null));
    expect(pair).toBeNull();
  });

  it('returns null when no prefix crashes', () => {
    const mods = ['A', 'B', 'C'];
    expect(findBoundaryPair(mods, () => null)).toBeNull();
  });

  it('handles the two-module minimum boundary (A,B) directly', () => {
    const mods = ['A', 'B'];
    const evaluator = (prefix: string[]): string | null =>
      prefix.length === 2 ? 'AB seam' : null;
    const pair = findBoundaryPair(mods, evaluator);
    expect(pair).not.toBeNull();
    expect(pair!.left).toBe(0);
    expect(pair!.right).toBe(1);
  });
});

describe('SET 31b attributeBoundaryFault (attribution without oracle)', () => {
  it('returns null when the crash is not a genuine seam (single-module intrinsic)', () => {
    // A file with a truly broken regex — every single-module probe reproduces
    // it, so there is no boundary attribution to make. `attributeBoundaryFault`
    // returns null to let the caller fall back to single-module attribution.
    const bad = { path: 'src/bad.ts', content: 'export const re = /(/;\n' };
    const test = {
      path: 'tests/f.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import '../src/bad';",
        "describe('t', () => { it('ok', () => { expect(1).toBe(1); }); });",
      ].join('\n'),
    };
    const r = attributeBoundaryFault([bad], test, 'Invalid regular expression: /(/', null);
    // Module 0 (src/bad.ts) alone crashes → not a seam → null (or pair=null).
    // Either shape is acceptable; the invariant is: no false pair.
    if (r) expect(r.pair).toBeNull();
    else expect(r).toBeNull();
  });

  it('injecting the delimiter oracle is a no-op when nothing to heal', () => {
    const good = { path: 'src/good.ts', content: 'export const x = 1;\n' };
    const test = {
      path: 'tests/f.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { x } from '../src/good';",
        "describe('t', () => { it('ok', () => { expect(x).toBe(1); }); });",
      ].join('\n'),
    };
    // Whole program is clean → nothing to attribute.
    const r = attributeBoundaryFault([good], test, 'phantom crash', applyDelimiterOracle);
    expect(r).toBeNull();
  });
});

describe('SET 31c artifact-fault dedupe (honest bug metrics)', () => {
  it('2 features with the same crash signature report 1 artifact fault, not 2 bugs', async () => {
    const spec: FeatureSpec = {
      title: 'Twin', prompt: 'twin opaque crash',
      features: [
        {
          id: 'F1', name: 'f1', description: 'a', parentId: null,
          deps: [], files: ['src/f1.ts'], testFile: 'tests/f1.test.ts',
          edgeCases: [], envVars: [],
          exports: [{ file: 'src/f1.ts', symbol: 'ok1' }],
          stubs: [{
            path: 'src/f1.ts',
            content: 'export function ok1(): number { throw new Error("stub"); }\n',
          }],
        },
        {
          id: 'F2', name: 'f2', description: 'b', parentId: null,
          deps: [], files: ['src/f2.ts'], testFile: 'tests/f2.test.ts',
          edgeCases: [], envVars: [],
          exports: [{ file: 'src/f2.ts', symbol: 'ok2' }],
          stubs: [{
            path: 'src/f2.ts',
            content: 'export function ok2(): number { throw new Error("stub"); }\n',
          }],
        },
      ],
    } as unknown as FeatureSpec;

    let repairCalls = 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(_s, feature) {
        const isF1 = feature.id === 'F1';
        return {
          content: '',
          parts: {
            imports: isF1
              ? [{ from: 'src/f1', symbols: ['ok1'] }]
              : [{ from: 'src/f2', symbols: ['ok2'] }],
            cases: [{ name: 'ok', kind: 'behavior', body: 'expect(1).toBe(1);' }],
          },
        };
      },
      async fixTestCases(_s, _f, broken) {
        return { cases: broken.map(b => ({ name: b.name, kind: b.kind as 'behavior', body: 'expect(1).toBe(1);' })) };
      },
      async generateImplementation(_s, feature) {
        const n = feature.id.slice(1);
        return [{ path: `src/f${n}.ts`, content: `export function ok${n}(): number { return 1; }\n` }];
      },
      async repair() {
        repairCalls++;
        return [];
      },
    };

    // Both suites always crash with the SAME opaque, unattributable message.
    // Nothing to attribute → both features park with the same signature.
    const OPAQUE = 'totally opaque harness explosion with no module fingerprint';
    const res = await runTier1Job({
      prompt: 'twin opaque',
      llm,
      runSuite: async () => ({ total: 0, failures: [], crashed: OPAQUE }),
      gates: { maxPasses: 4, stallLimit: 1, progressStall: 2 },
    });

    // SET 89 §1.2 — a crash NEVER freezes a suite. Previously both features
    // would freeze (crash-as-red), enter the RHA loop, and the dedupe
    // machinery would collapse the twin signatures into one artifact fault.
    // Now the crashes block freeze upstream, so neither feature enters the
    // repair ladder — 0 tokens is guaranteed by construction. The dedupe
    // invariant remains valid but is unexercised on this fixture.
    expect(repairCalls).toBe(0);
    expect(res.metrics.repairDraws).toBe(0);
    // Deferred / never-frozen features do not enter `problems` as
    // crash|unattributed — that classification only fires inside the RHA
    // loop, which the certification boundary now guards.
    void res;
  });

  it('assertion failures across features stay per-case (not deduped)', async () => {
    const spec: FeatureSpec = {
      title: 'Add', prompt: 'add stuff',
      features: [{
        id: 'F1', name: 'calc', description: 'add', parentId: null,
        deps: [], files: ['src/calc.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], 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;

    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: 'pos', 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);',
          })),
        };
      },
      // Wrong implementation — both cases fail as ASSERTIONS (different
      // messages, different names). Must NOT collapse to 1 artifact fault.
      async generateImplementation() {
        return [{ path: 'src/calc.ts', content: 'export function add(_a: number, _b: number): number { return 99; }\n' }];
      },
      async repair() {
        return [{ path: 'src/calc.ts', content: 'export function add(_a: number, _b: number): number { return 99; }\n' }];
      },
    };

    const res = await runTier1Job({
      prompt: 'add', llm, runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 2, stallLimit: 1, progressStall: 2 },
    });

    // The critical invariant: assertion failures are NEVER counted as
    // artifact faults — those are reserved for crash-class findings.
    expect(res.metrics.artifactFaults).toBe(0);
    expect(res.metrics.artifactFaultsRemaining).toBe(0);
    // At least one assertion bug was observed and remains (not deduped away).
    expect(res.metrics.bugsObserved).toBeGreaterThanOrEqual(1);
  });
});
