/**
 * SET 98 — catalog-drift|missing-new|<Sym> mechanical handler.
 * Frozen suite calls Sym() against export class Sym → insert `new` (0 tok).
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient } from '../src/core/types';
import {
  attemptMechanicalFix,
  classifyFailure,
  mechanicalFix,
} from '../src/tier1/substrate';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';
import { runTier1Job } from '../src/tier1/runner';

const PROMPT = [
  '1. Create a Widget class that stores a name string',
  '2. Expose the name as a readable property',
].join('\n');

const SPEC: FeatureSpec = {
  title: 'Widget',
  prompt: PROMPT,
  features: [{
    id: 'F1',
    name: 'widget',
    description: 'creates a widget',
    parentId: null,
    deps: [],
    files: ['src/widget.ts'],
    testFile: 'tests/widget.test.ts',
    edgeCases: [],
    envVars: [],
    exports: [{ file: 'src/widget.ts', symbol: 'Widget' }],
    stubs: [{
      path: 'src/widget.ts',
      content: 'export class Widget { constructor(_name: string) { throw new Error("stub"); } }',
    }],
  }],
};

describe('SET 98 — catalog-drift missing-new mechanical', () => {
  it('classifies Class constructor … without new as catalog-drift|missing-new|<Sym>', () => {
    const c = classifyFailure(
      'builds',
      "Class constructor Widget cannot be invoked without 'new'",
    );
    expect(c.kind).toBe('catalog-drift');
    expect(c.classKey).toBe('catalog-drift|missing-new|Widget');
    expect(c.detail.symbol).toBe('Widget');
  });

  it('classifies ESM interop not-a-constructor as catalog-drift|missing-new|<Sym>', () => {
    const c = classifyFailure(
      'builds',
      'TypeError: (0 , _widget.Widget) is not a constructor',
    );
    expect(c.classKey).toBe('catalog-drift|missing-new|Widget');
    expect(c.detail.symbol).toBe('Widget');
  });

  it('inserts new at frozen test call site when impl exports class (0 tok)', async () => {
    const impl: GeneratedFile = {
      path: 'src/widget.ts',
      content: [
        'export class Widget {',
        '  name: string;',
        '  constructor(name: string) { this.name = name; }',
        '}',
      ].join('\n'),
    };
    const suite: GeneratedFile = {
      path: 'tests/widget.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { Widget } from '../src/widget';",
        "describe('widget', () => {",
        "  it('builds', () => {",
        "    const w = Widget('x');",
        "    expect(w.name).toBe('x');",
        '  });',
        '});',
      ].join('\n'),
    };
    const failure = classifyFailure(
      'builds',
      "Class constructor Widget cannot be invoked without 'new'",
    );
    const fix = mechanicalFix(failure, [impl], suite, SPEC);
    expect(fix).toBeTruthy();
    expect(fix!.path).toBe(suite.path);
    expect(fix!.classKey).toBe('catalog-drift|missing-new|Widget');
    expect(fix!.note).toMatch(/0 tok/);
    expect(fix!.content).toContain("const w = new Widget('x')");
    expect(fix!.content).not.toMatch(/(?<!new\s)Widget\s*\(/);

    const attempt = attemptMechanicalFix(failure, [impl], suite, SPEC);
    expect(attempt.status).toBe('fixed');

    const healedSuite = { ...suite, content: fix!.content };
    const r = await makeEvalSuiteRunner()([impl], healedSuite);
    expect(r.crashed ?? null).toBeNull();
    expect(r.failures.length).toBe(0);
  });

  it('rewrites const/arrow impl to constructor when suite uses new Sym', () => {
    const impl: GeneratedFile = {
      path: 'src/widget.ts',
      content: "export const Widget = (name: string) => ({ name });\n",
    };
    const suite: GeneratedFile = {
      path: 'tests/widget.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { Widget } from '../src/widget';",
        "describe('widget', () => {",
        "  it('builds', () => {",
        "    const w = new Widget('x');",
        "    expect(w.name).toBe('x');",
        '  });',
        '});',
      ].join('\n'),
    };
    const failure = classifyFailure(
      'builds',
      'TypeError: (0 , _widget.Widget) is not a constructor',
    );
    const attempt = attemptMechanicalFix(failure, [impl], suite, SPEC);
    expect(attempt.status).toBe('fixed');
    expect(attempt.fix!.path).toBe(impl.path);
    expect(attempt.fix!.classKey).toBe('catalog-drift|missing-new|Widget');
    expect(attempt.fix!.content).toMatch(/export function Widget\s*\(/);
    expect(attempt.fix!.content).not.toMatch(/export const Widget\s*=/);
  });

  it('plain "X is not a constructor" classifies into the same missing-new family', () => {
    const c = classifyFailure('builds', 'TypeError: Widget is not a constructor');
    expect(c.classKey).toBe('catalog-drift|missing-new|Widget');
    expect(c.detail.symbol).toBe('Widget');
  });

  it('runTier1Job resolves missing-new via substrate; repair mock never invoked', async () => {
    let repairCalls = 0;
    const llm = {
      async encodeFeatures() { return SPEC; },
      async generateTests() {
        return {
          content: '',
          parts: {
            imports: [{ from: 'src/widget.ts', symbols: ['Widget'] }],
            cases: [{
              name: 'builds',
              kind: 'behavior' as const,
              body: "const w = Widget('x');\nexpect(w.name).toBe('x');",
              symbol: 'Widget',
            }],
          },
        };
      },
      async fixTestCases() {
        return {
          cases: [{
            name: 'builds',
            kind: 'behavior' as const,
            body: "const w = Widget('x');\nexpect(w.name).toBe('x');",
          }],
        };
      },
      async generateImplementation() {
        return [{
          path: 'src/widget.ts',
          content: [
            'export class Widget {',
            '  name: string;',
            '  constructor(name: string) { this.name = name; }',
            '}',
          ].join('\n'),
        }];
      },
      async repair() {
        repairCalls++;
        throw new Error('SET 98: LLM repair must not run for missing-new');
      },
    } as unknown as LlmClient;

    const result = await runTier1Job({
      prompt: PROMPT,
      rawPrompt: PROMPT,
      llm,
      runSuite: makeEvalSuiteRunner(),
    });

    expect(repairCalls).toBe(0);
    expect(result.metrics.catalogDriftMechanicalFixes).toBeGreaterThanOrEqual(1);
    expect(result.metrics.mechanicalFixes).toBeGreaterThanOrEqual(1);
    expect(result.log.some(e =>
      /catalog-drift\|missing-new\|Widget/.test(e.message)
      && /fixed \(0 tok\)/.test(e.message),
    )).toBe(true);
  });
});
