/**
 * SET 16 — substrate instrumentation + repair oracle hints.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient, RepairInput } from '../src/core/types';
import {
  formatRepairOracleHint,
  lookupRepairOracle,
  REPAIR_ORACLE,
} from '../src/tier1/repairOracle.generated';
import { runTier1Job } from '../src/tier1/runner';
import {
  acceptCaseBody,
  attemptMechanicalFix,
  classifyFailure,
  parserGuidedCaseRepair,
} from '../src/tier1/substrate';
import { caseSyntaxErrors, makeEvalSuiteRunner } from '../src/tier1/testExec';

describe('SET 16c repair oracle table', () => {
  it('covers the required classKey families with hints', () => {
    const ids = new Set(REPAIR_ORACLE.map(e => e.id));
    for (const id of [
      'undefined-symbol', 'is-not-a-function', 'nullread',
      'assertion', 'regex-literal', 'throw-mismatch',
      'assertion-toBe', 'assertion-toHaveProperty', 'runtime-generic', 'undefined-symbol-jest',
    ]) {
      expect(ids.has(id)).toBe(true);
      expect(lookupRepairOracle(
        id === 'nullread' ? 'runtime|nullread|x'
          : id === 'assertion' ? 'assertion|toEqual'
            : id === 'assertion-toBe' ? 'assertion|toBe'
              : id === 'assertion-toHaveProperty' ? 'assertion|toHaveProperty'
                : id === 'runtime-generic' ? 'runtime|boom'
                  : id === 'undefined-symbol-jest' ? 'undefined-symbol|jest'
                    : id === 'throw-mismatch' ? 'throw-mismatch|toThrow'
                      : id === 'regex-literal' ? 'regex-literal|Invalid'
                        : `${id}|sym`,
      )?.hint.length).toBeGreaterThan(40);
    }
  });

  it('formatRepairOracleHint states no-handler for unknown classes', () => {
    expect(formatRepairOracleHint('crash|suite')).toMatch(/no-handler/);
    expect(formatRepairOracleHint('undefined-symbol|foo')).toMatch(/export\/import wiring/);
  });
});

describe('SET 16b parser-guided siblings', () => {
  it('heals unterminated string / template / brace / stray quote (≤3 edits)', () => {
    const cases: { body: string; expectSub: string }[] = [
      { body: "expect(foo).toBe('x);", expectSub: "toBe('x')" },
      { body: 'expect(foo).toBe(`x);', expectSub: 'toBe(`x`)' },
      { body: 'expect(foo).toBe(1); if (true) { expect(1).toBe(1);', expectSub: 'toBe(1)' },
      { body: 'expect(foo).toBe("x"");', expectSub: 'toBe("x")' },
    ];
    for (const c of cases) {
      const err = caseSyntaxErrors([{ name: '_', body: c.body }])[0]?.error;
      expect(err, c.body).toBeTruthy();
      const guided = parserGuidedCaseRepair(c.body, err!);
      expect(guided, c.body).not.toBeNull();
      expect(acceptCaseBody(guided!.body)).toBe(true);
      expect(guided!.body).toContain('expect(');
    }
  });
});

describe('SET 16a attemptMechanicalFix status', () => {
  it('reports fixed | refused | cataloged (no-handler abolished)', () => {
    const suite: GeneratedFile = {
      path: 'tests/f1.test.ts',
      content: "import { describe, it, expect } from 'vitest';\ndescribe('x', () => { it('y', () => { expect(1).toBe(1); }); });\n",
    };
    const artifact: GeneratedFile[] = [
      { path: 'src/a.ts', content: 'export function slugify(s: string): string { return s; }\n' },
      { path: 'src/b.ts', content: 'export function use(): string { return slugify("x"); }\n' },
    ];
    const spec = { title: 't', prompt: 't', features: [] } as FeatureSpec;

    const fixed = attemptMechanicalFix(
      classifyFailure('t', 'slugify is not defined'),
      artifact, suite, spec,
    );
    expect(fixed.status).toBe('fixed');
    expect(fixed.fix?.content).toMatch(/import \{ slugify \}/);

    const refused = attemptMechanicalFix(
      classifyFailure('__suite__', 'unresolved import ./missing (from src/a)'),
      artifact, suite, spec,
    );
    expect(refused.status).toBe('refused');

    const none = attemptMechanicalFix(
      classifyFailure('t', 'expect(...).toBe failed — actual: 1, expected: 2'),
      artifact, suite, spec,
    );
    expect(none.status).toBe('cataloged');
    expect(none.status).not.toBe('no-handler');
  });
});

describe('SET 16d oracle-guided repair prompts', () => {
  function wiringSpec(): FeatureSpec {
    return {
      title: 'Calc', prompt: 'calc',
      features: [{
        id: 'F1', name: 'calc', description: 'adds', parentId: null,
        deps: [], files: ['src/calc.ts', 'src/util.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [
          { file: 'src/calc.ts', symbol: 'add' },
          { file: 'src/util.ts', symbol: 'slugify' },
        ],
        stubs: [
          { path: 'src/calc.ts', content: 'export function add(a: number, b: number): number { throw new Error("stub"); }\n' },
          { path: 'src/util.ts', content: 'export function slugify(s: string): string { throw new Error("stub"); }\n' },
        ],
      }],
    } as unknown as FeatureSpec;
  }

  it('undefined-symbol repair inputs contain the oracle hint; unknown → no-handler', async () => {
    const inputs: RepairInput[] = [];
    const llm: LlmClient = {
      async enumerateFeatures() { return []; },
      async probeFeature() { return { clear: true, assertion: 'ok', question: '' }; },
      async encodeFeatures() { return wiringSpec(); },
      async repairFeatures() { return []; },
      async fixStub() { return ''; },
      async generateTests() {
        return {
          content: [
            "import { describe, it, expect } from 'vitest';",
            "import { add } from '../src/calc';",
            "describe('calc', () => {",
            "  it('adds', () => { expect(add(1, 2)).toBe(3); });",
            '});',
          ].join('\n'),
          parts: {
            imports: [{ from: 'src/calc', symbols: ['add'] }],
            cases: [{ name: 'adds', kind: 'behavior', body: 'expect(add(1, 2)).toBe(3);' }],
          },
        };
      },
      async fixTestCases(_s, _f, broken) {
        return {
          cases: broken.map(b => ({
            name: b.name, kind: b.kind as 'behavior' | 'edge' | 'integration',
            body: 'expect(add(1, 2)).toBe(3);',
          })),
        };
      },
      async generateImplementation() {
        // calc references slugify without importing — undefined-symbol at runtime
        // after tests call add which calls slugify... actually make add use missing symbol.
        return [
          {
            path: 'src/calc.ts',
            content: 'export function add(a: number, b: number): number { return ghost(a, b); }\n',
          },
          { path: 'src/util.ts', content: 'export function slugify(s: string): string { return s; }\n' },
        ];
      },
      async repair(input) {
        inputs.push(input);
        return [
          {
            path: 'src/calc.ts',
            content: 'export function add(a: number, b: number): number { return a + b; }\n',
          },
        ];
      },
    };

    const events: string[] = [];
    await runTier1Job({
      prompt: 'calc',
      llm,
      runSuite: makeEvalSuiteRunner(),
      onEvent: e => events.push(e.message),
      gates: { maxPasses: 3, stallLimit: 2, progressStall: 2 },
    });

    expect(events.some(m => /substrate: .+ → (fixed|refused|cataloged|catalog-drift|no-handler)/.test(m))).toBe(true);
    expect(inputs.length).toBeGreaterThan(0);
    const withUndef = inputs.find(i => /undefined-symbol|ghost|is not defined/i.test(i.failureOutput + i.findings.map(f => f.message).join('\n')));
    // Pass-1 repair must carry oracle hint for the focused class.
    expect(inputs[0]!.failureOutput).toMatch(/Repair oracle/);
    if (withUndef) {
      expect(withUndef.failureOutput).toMatch(/export\/import wiring|undefined-symbol playbook/);
    }
    // Unclassified / no-handler path states it explicitly when focused class has no entry.
    // runtime-generic covers ordinary runtime|* keys; use a family with no entry.
    const noHandlerFmt = formatRepairOracleHint('crash|suite');
    expect(noHandlerFmt).toMatch(/no-handler/);
  });
});
