/**
 * SET 18 — parser-guided impl repair, suite import mechanical fix,
 * irSurgeries caps, aggregate clade ratio.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import {
  attemptMechanicalFix,
  classifyFailure,
  parserGuidedImplRepair,
} from '../src/tier1/substrate';
import { cladeRatio } from '../src/tier1/clades';
import { makeEvalSuiteRunner, suiteSyntaxError } from '../src/tier1/testExec';
import type { IRCaseGen } from '../src/core/irScaffold';
import { formatFillDropMessage } from '../src/tier1/fillHonesty';

describe('SET 18a parserGuidedImplRepair', () => {
  it('heals a missing closer in an implementation file', () => {
    const broken = [
      'export function add(a: number, b: number): number {',
      '  return a + b;',
      // missing closing brace
    ].join('\n');
    expect(suiteSyntaxError(broken)).toBeTruthy();
    const err = suiteSyntaxError(broken)!;
    const healed = parserGuidedImplRepair(broken, err);
    expect(healed).not.toBeNull();
    expect(suiteSyntaxError(healed!.content)).toBeNull();
  });

  it('heals unexpected token via insert/delete within ≤3 edits', () => {
    // Stray comma before closing paren — classic model typo.
    const broken = 'export function f(a: number,): number { return a; }\n';
    const err = suiteSyntaxError(broken);
    // Some parsers accept trailing param commas — if already clean, skip.
    if (!err) {
      expect(parserGuidedImplRepair(broken, 'Unexpected token (1:24)')?.token).toBe('already-clean');
      return;
    }
    const healed = parserGuidedImplRepair(broken, err);
    expect(healed).not.toBeNull();
    expect(suiteSyntaxError(healed!.content)).toBeNull();
  });
});

describe('SET 18d suite unresolved-import of ./data_model', () => {
  it('mechanically rewires suite import when src/data_model.ts exists (0 LLM)', async () => {
    const spec: FeatureSpec = {
      title: 'DM', prompt: 'data model',
      features: [{
        id: 'F1', name: 'dm', description: 'model', parentId: null,
        deps: [], files: ['src/data_model.ts', 'src/app.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [
          { file: 'src/data_model.ts', symbol: 'Item' },
          { file: 'src/app.ts', symbol: 'useItem' },
        ],
        stubs: [
          { path: 'src/data_model.ts', content: 'export type Item = { id: string };\nexport const Item = { id: "x" };\n' },
          { path: 'src/app.ts', content: 'export function useItem(): string { throw new Error("stub"); }\n' },
        ],
      }],
    } as unknown as FeatureSpec;

    // Broken suite: ./data_model resolves to tests/data_model (missing).
    const BROKEN_SUITE = [
      "import { describe, it, expect } from 'vitest';",
      "import { Item } from './data_model';",
      "import { useItem } from '../src/app';",
      "describe('dm', () => {",
      "  it('Item', () => { expect(Item.id).toBe('x'); });",
      "  it('useItem', () => { expect(useItem()).toBe('x'); });",
      '});',
    ].join('\n');

    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() {
        return {
          content: BROKEN_SUITE,
          parts: {
            imports: [
              { from: 'src/data_model', symbols: ['Item'] },
              { from: 'src/app', symbols: ['useItem'] },
            ],
            cases: [
              { name: 'Item', kind: 'behavior', body: "expect(Item.id).toBe('x');" },
              { name: 'useItem', kind: 'behavior', body: "expect(useItem()).toBe('x');" },
            ],
          },
        };
      },
      async fixTestCases(_s, _f, broken) {
        return {
          cases: broken.map(b => ({
            name: b.name,
            kind: b.kind as 'behavior',
            body: b.name === 'Item' ? "expect(Item.id).toBe('x');" : "expect(useItem()).toBe('x');",
          })),
        };
      },
      async generateImplementation() {
        return [
          { path: 'src/data_model.ts', content: 'export type Item = { id: string };\nexport const Item = { id: "x" };\n' },
          {
            path: 'src/app.ts',
            content: "import { Item } from './data_model';\nexport function useItem(): string { return Item.id; }\n",
          },
        ];
      },
      async repair() {
        repairCalls++;
        return [];
      },
    };

    // Unit: exact F1 shape — suite imports ./data_model, target present.
    const c = classifyFailure('__suite__', 'unresolved import ./data_model (from tests/f1.test)');
    const attempt = attemptMechanicalFix(
      c,
      [
        { path: 'src/data_model.ts', content: 'export const Item = { id: "x" };\n' },
        { path: 'src/app.ts', content: 'export function useItem(): string { return "x"; }\n' },
      ],
      { path: 'tests/f1.test.ts', content: BROKEN_SUITE },
      spec,
    );
    expect(attempt.status).toBe('fixed');
    expect(attempt.fix?.content).toContain("from '../src/data_model'");

    // Also with trailing .ts on importer (the suspected live-run mismatch).
    const cTs = classifyFailure('__suite__', 'unresolved import ./data_model (from tests/f1.test.ts)');
    expect(attemptMechanicalFix(
      cTs,
      [{ path: 'src/data_model.ts', content: 'export const Item = { id: "x" };\n' }],
      { path: 'tests/f1.test.ts', content: BROKEN_SUITE },
      spec,
    ).status).toBe('fixed');

    // End-to-end: substrate heals before any repair draw. Force broken suite
    // via fixture fallback by returning content-only on generateTests after
    // plan fill — simpler: run and assert repairCalls stay 0 when wiring is
    // the only defect. (Plan-bound path may assemble correct imports; inject
    // broken content by overriding after freeze via a suite that imports wrong.)
    const res = await runTier1Job({
      prompt: 'data model',
      llm,
      runSuite: makeEvalSuiteRunner(),
    });
    // Either mechanical fix healed, or plan-assembled imports were already correct.
    // The regression pin is: when the unresolved-import class fires, 0 repair calls.
    const events = res.log.map(e => e.message);
    const hitUnresolved = events.some(m => /unresolved-import|\.\/data_model/.test(m));
    if (hitUnresolved) {
      expect(repairCalls).toBe(0);
      expect(res.metrics.mechanicalFixes + res.metrics.substrateFixes).toBeGreaterThanOrEqual(1);
    }
    expect(attempt.status).toBe('fixed'); // unit assertion is the hard pin
  });
});

describe('SET 18e irSurgeries caps + irGenAttempts', () => {
  it('counts IR fill draws as irGenAttempts, not irSurgeries', 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;

    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() {
        return [{ path: 'src/calc.ts', content: 'export function add(a: number, b: number): number { return a + b; }\n' }];
      },
      async repair() { return []; },
    };

    const res = await runTier1Job({
      prompt: 'calc', llm, runSuite: makeEvalSuiteRunner(),
    });
    // Fill loop must not inflate irSurgeries; gen attempts may be > 0.
    expect(res.metrics.irSurgeries).toBeLessThanOrEqual(3); // ≤2 crash + ≤1 persistent
    expect(res.metrics.irGenAttempts).toBeGreaterThanOrEqual(0);
    // Cap invariant for a single feature.
    expect(res.metrics.irSurgeries).toBeLessThanOrEqual(3);
  });
});

describe('SET 18f aggregate clade ratio', () => {
  it('sums schemad:freeform across features instead of overwriting', () => {
    const a: IRCaseGen[] = [
      { name: 'r', kind: 'behavior', body: "expect(encode(decode('a'))).toBe(encode(decode('a')));", clade: { branch: 'algebraic', family: 'roundtrip', bindings: {} } },
      { name: 'f', kind: 'behavior', body: 'expect(1).toBe(1);', clade: { branch: 'freeform', family: 'freeform', bindings: {} } },
    ];
    const b: IRCaseGen[] = [
      { name: 'e', kind: 'edge', body: "expect(() => fn('')).toThrow();", clade: { branch: 'contract', family: 'rejects-empty', bindings: {} } },
      { name: 'f2', kind: 'behavior', body: 'expect(2).toBe(2);', clade: { branch: 'freeform', family: 'freeform', bindings: {} } },
      { name: 'f3', kind: 'behavior', body: 'expect(3).toBe(3);', clade: { branch: 'freeform', family: 'freeform', bindings: {} } },
    ];
    const ra = cladeRatio(a); // 1:1
    const rb = cladeRatio(b); // 1:2
    expect(`${ra.schemad + rb.schemad}:${ra.freeform + rb.freeform}`).toBe('2:3');
  });
});

describe('SET 18c token crash classification', () => {
  it('classifies Invalid or unexpected token for attribution', () => {
    const c = classifyFailure('__suite__', 'Invalid or unexpected token');
    expect(c.kind).toBe('crash');
    expect(c.classKey).toMatch(/^crash\|token\|/);
  });
});

describe('SET 18g planned-case message', () => {
  it('uses the SET 32e.i vocabulary (draws + candidate/fallback batches)', () => {
    // SET 28b replaced the generic "unfilled after retries" claim with true
    // counts; SET 32e.i then unified the two counters into one vocabulary:
    // `unfilled after N draws (X candidate batch(es) + Y fallback batch(es))`.
    expect(formatFillDropMessage('X', 3, 1, 0)).toBe(
      'planned case "X" unfilled after 3 draws (1 candidate batch + 0 fallback batches) — dropped',
    );
    expect(formatFillDropMessage('X', 0, 0, 0)).not.toContain('unfilled after retries');
  });
});
