/**
 * The substrate is the reasoning that never reaches the LLM: deterministic
 * taxonomy, zero-token mechanical fixes, classKey-matched lessons, and
 * one-class-per-draw serial resolution. These tests pin each transfer.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient, RepairInput } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';
import {
  acceptCaseBody,
  balanceClosers,
  bindFixTestCases,
  checkedIdiomsText,
  classifyFailure,
  dominantClass,
  healCaseBody,
  matchLessons,
  mechanicalCaseRepair,
  normalizeCaseName,
  parserGuidedCaseRepair,
  unwrapCaseBody,
} from '../src/tier1/substrate';
import { caseSyntaxErrors } from '../src/tier1/testExec';

describe('substrate taxonomy', () => {
  it('classifies harness messages deterministically', () => {
    expect(classifyFailure('__suite__', "unresolved import ./addr (from src/calc)"))
      .toMatchObject({ kind: 'unresolved-import', classKey: 'unresolved-import|./addr', detail: { spec: './addr', importer: 'src/calc' } });
    expect(classifyFailure('t', "expect(...).toBe failed — actual: 1, expected: 5"))
      .toMatchObject({ kind: 'assertion', classKey: 'assertion|toBe', detail: { matcher: 'toBe' } });
    expect(classifyFailure('t', "expect(...).toThrow failed — actual: (fn)"))
      .toMatchObject({ kind: 'throw-mismatch' });
    expect(classifyFailure('t', 'slugify is not defined'))
      .toMatchObject({ kind: 'undefined-symbol', classKey: 'undefined-symbol|slugify' });
    expect(classifyFailure('__suite__', 'transpile src/a.ts: Unexpected token'))
      .toMatchObject({ kind: 'transpile', classKey: 'transpile|src/a.ts' });
  });

  it('ranks structural classes above behavioral for serial resolution', () => {
    const picked = dominantClass([
      classifyFailure('a', 'expect(...).toBe failed — actual: 1, expected: 2'),
      classifyFailure('b', 'expect(...).toBe failed — actual: 3, expected: 4'),
      classifyFailure('__suite__', 'unresolved import ./x (from src/y)'),
    ]);
    expect(picked).toHaveLength(1);
    expect(picked[0].kind).toBe('unresolved-import');
  });
});

function twoFileSpec(): FeatureSpec {
  return {
    title: 'Calc', prompt: 'calc',
    features: [{
      id: 'F1', name: 'calc', description: 'adds via helper', parentId: null,
      deps: [], files: ['src/adder.ts', 'src/calc.ts'], testFile: 'tests/f1-calc.test.ts',
      edgeCases: ['zero'], envVars: [],
      exports: [{ file: 'src/calc.ts', symbol: 'calc' }],
      stubs: [
        { path: 'src/adder.ts', content: `export function add(a: number, b: number): number { throw new Error('stub: not implemented'); }` },
        { path: 'src/calc.ts', content: `export function calc(a: number, b: number): number { throw new Error('stub: not implemented'); }` },
      ],
    }],
  } as unknown as FeatureSpec;
}

const SUITE = [
  "import { describe, it, expect } from 'vitest';",
  "import { calc } from '../src/calc';",
  "describe('calc', () => {",
  "  it('adds', () => { expect(calc(2, 3)).toBe(5); });",
  '});',
].join('\n');

/** Emits a WIRING-broken artifact (typo'd import path); never repairs. */
function wiringBrokenClient(): LlmClient & { repairCalls: number } {
  const c = {
    repairCalls: 0,
    async encodeFeatures() { return twoFileSpec(); },
    async generateTests() {
      return { content: SUITE, parts: { imports: [{ from: 'src/calc', symbols: ['calc'] }], cases: [
        { name: 'adds', kind: 'behavior' as const, body: 'expect(calc(2, 3)).toBe(5);' },
      ] } };
    },
    async generateImplementation() {
      return [
        { path: 'src/adder.ts', content: 'export function add(a: number, b: number): number { return a + b; }' },
        // The bug class under test: right code, wrong import path — the
        // classic case/separator mismatch a model emits ('./Adder' vs adder).
        { path: 'src/calc.ts', content: "import { add } from './Adder';\nexport function calc(a: number, b: number): number { return add(a, b); }" },
      ];
    },
    async repair() { c.repairCalls++; return []; },
    // SET 2 routes holes through fixTestCases — default fills a simple assertion.
    async fixTestCases(_s: unknown, _f: unknown, broken: { name: string; kind: string }[]) {
      return {
        cases: broken.map(b => ({
          name: b.name,
          kind: b.kind as 'behavior' | 'edge' | 'integration',
          body: b.kind === 'edge'
            ? 'expect(() => calc(0, 0)).not.toThrow();'
            : 'expect(calc(2, 3)).toBe(5);',
        })),
      };
    },
    async enumerateFeatures() { return []; },
    async probeFeature() { return { clear: true, assertion: '', question: '' }; },
    async repairFeatures() { return []; },
    async fixStub() { return ''; },
  };
  return c as unknown as LlmClient & { repairCalls: number };
}

describe('substrate mechanical fixes', () => {
  it('heals a wiring failure with ZERO LLM repair calls', async () => {
    const llm = wiringBrokenClient();
    const events: string[] = [];
    const lessons: { classKey: string }[] = [];
    const res = await runTier1Job({
      prompt: 'calc',
      llm,
      runSuite: makeEvalSuiteRunner(),
      onLesson: l => lessons.push(l),
      onEvent: e => events.push(e.message),
    });
    expect(res.problems).toEqual([]);
    expect(llm.repairCalls).toBe(0);
    expect(res.metrics.mechanicalFixes).toBeGreaterThanOrEqual(1);
    expect(events.some(m => /substrate: .+ → fixed \(0 tok\)/.test(m))).toBe(true);
    expect(res.metrics.substrateFixes).toBeGreaterThanOrEqual(1);
    expect(res.files.find(f => f.path === 'src/calc.ts')?.content).toContain("from './adder'");
    // The heal distilled a persistent lesson for the class.
    expect(lessons.some(l => l.classKey.startsWith('unresolved-import|'))).toBe(true);
  });
});

describe('substrate lessons + serial resolution in prompts', () => {
  it('injects matched idioms and sends only the dominant class to the LLM', async () => {
    const inputs: RepairInput[] = [];
    const llm = wiringBrokenClient();
    // Behavioral bug instead of wiring: correct import, wrong math + no-throw.
    llm.generateImplementation = async () => ([
      { path: 'src/adder.ts', content: 'export function add(a: number, b: number): number { return a - b; }' },
      { path: 'src/calc.ts', content: "import { add } from './adder';\nexport function calc(a: number, b: number): number { return add(a, b); }" },
    ]);
    (llm as unknown as { generateTests: () => Promise<unknown> }).generateTests = async () => ({
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { calc } from '../src/calc';",
        "describe('calc', () => {",
        "  it('adds', () => { expect(calc(2, 3)).toBe(5); });",
        "  it('adds2', () => { expect(calc(1, 1)).toBe(2); });",
        '});',
      ].join('\n'),
      parts: { imports: [{ from: 'src/calc', symbols: ['calc'] }], cases: [
        { name: 'adds', kind: 'behavior', body: 'expect(calc(2, 3)).toBe(5);' },
        { name: 'adds2', kind: 'behavior', body: 'expect(calc(1, 1)).toBe(2);' },
      ] },
    });
    llm.repair = async (input: RepairInput) => {
      inputs.push(input);
      return [
        { path: 'src/adder.ts', content: 'export function add(a: number, b: number): number { return a + b; }' },
        { path: 'src/calc.ts', content: "import { add } from './adder';\nexport function calc(a: number, b: number): number { return add(a, b); }" },
      ] as GeneratedFile[];
    };
    const res = await runTier1Job({
      prompt: 'calc',
      llm,
      runSuite: makeEvalSuiteRunner(),
      lessons: [{ classKey: 'assertion|toBe', idiom: 'toBe mismatch on arithmetic', resolution: 'check operator direction', ts: '2026-01-01' }],
    });
    expect(res.problems).toEqual([]);
    expect(inputs.length).toBeGreaterThanOrEqual(1);
    // Serial: both failures share one class here — findings must ALL be that class,
    // and the checked idiom from the lessons substrate must ride along.
    expect(inputs[0].findings.length).toBeGreaterThanOrEqual(1);
    expect(inputs[0].failureOutput).toContain('Checked idioms');
    expect(inputs[0].failureOutput).toContain('check operator direction');
  });
});

describe('lessons matching', () => {
  it('matches by exact classKey and by class prefix, deduped and capped', () => {
    const lessons = [
      { classKey: 'unresolved-import|./a', idiom: 'i1', resolution: 'r1', ts: '' },
      { classKey: 'unresolved-import|./b', idiom: 'i2', resolution: 'r2', ts: '' },
      { classKey: 'assertion|toEqual', idiom: 'i3', resolution: 'r3', ts: '' },
    ];
    const hit = matchLessons(lessons, ['unresolved-import|./zzz']);
    expect(hit).toHaveLength(2);
    expect(checkedIdiomsText(hit)).toContain('apply mechanically');
    expect(matchLessons(lessons, ['runtime|x'])).toHaveLength(0);
  });
});

describe('SET 1 IR case-body syntax healing', () => {
  const BROKEN_BODY = 'expect(calc(2, 3).toBe(5);'; // missing paren — F5/F6 class
  const GOOD_BODY = 'expect(calc(2, 3)).toBe(5);';
  const suiteWith = (body: string) => [
    "import { describe, it, expect } from 'vitest';",
    "import { calc } from '../src/calc';",
    "describe('calc', () => {",
    `  it('adds', () => { ${body} });`,
    '});',
  ].join('\n');

  it('mechanicalCaseRepair: paren balance, trailing comma, typographic quotes', () => {
    expect(balanceClosers('expect(foo(1')).toBe('expect(foo(1))');
    // Unclosed call with trailing comma — strip + balance.
    expect(mechanicalCaseRepair('expect(foo(1, 2,')?.body).toBe('expect(foo(1, 2))');
    const smart = 'expect(x).toBe(\u201Ca\u201D);';
    expect(mechanicalCaseRepair(smart)?.body).toContain('"a"');
    expect(acceptCaseBody(mechanicalCaseRepair(smart)!.body)).toBe(true);
    expect(mechanicalCaseRepair('garbage without expect')).toBeNull();
  });

  it('parser-guided heals missing paren before method chain (F5 shape)', () => {
    const err = caseSyntaxErrors([{ name: 'adds', body: BROKEN_BODY }])[0]!.error;
    expect(err).toMatch(/expected/);
    const guided = parserGuidedCaseRepair(BROKEN_BODY, err);
    expect(guided).not.toBeNull();
    expect(guided!.body).toBe(GOOD_BODY);
    expect(acceptCaseBody(guided!.body)).toBe(true);
  });

  it('healCaseBody prefers mechanical then parser-guided', () => {
    expect(healCaseBody('expect(foo(1, 2,')?.via).toBe('mechanical');
    expect(healCaseBody(BROKEN_BODY)?.via).toBe('parser-guided');
    expect(healCaseBody(BROKEN_BODY)?.body).toBe(GOOD_BODY);
  });

  it('heals a broken body from fixTestCases with ZERO generateTests calls', async () => {
    const llm = wiringBrokenClient();
    // Non-simple signature → no template; behavior hole goes through fixTestCases.
    llm.encodeFeatures = async () => {
      const s = twoFileSpec();
      s.features[0]!.stubs = [{
        path: 'src/calc.ts',
        content: 'export function calc(a: number, b: number, c: number): number { throw new Error("stub"); }\n',
      }];
      s.features[0]!.files = ['src/calc.ts'];
      return s;
    };
    llm.generateImplementation = async () => ([
      { path: 'src/calc.ts', content: 'export function calc(a: number, b: number, c: number): number { return a + b + c; }\n' },
    ]);
    let genCalls = 0;
    let fillCalls = 0;
    (llm as unknown as { generateTests: () => Promise<unknown> }).generateTests = async () => {
      genCalls++;
      return { content: suiteWith(GOOD_BODY), parts: { imports: [], cases: [] } };
    };
    (llm as unknown as {
      fixTestCases: (_s: unknown, _f: unknown, broken: { name: string; kind: string }[]) => Promise<unknown>
    }).fixTestCases = async (_s, _f, broken) => {
      fillCalls++;
      return {
        cases: broken.map(b => ({
          name: b.name,
          kind: b.kind as 'behavior' | 'edge' | 'integration',
          body: b.kind === 'behavior' ? 'expect(calc(1, 2, 3).toBe(6);' : 'expect(() => calc(0, 0, 0)).not.toThrow();',
        })),
      };
    };
    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'calc', llm, runSuite: makeEvalSuiteRunner(), onEvent: e => events.push(e.message),
    });
    expect(res.problems.filter(p => p.includes('abandoned'))).toEqual([]);
    expect(genCalls).toBe(0);
    expect(fillCalls).toBeGreaterThanOrEqual(1);
    expect(res.metrics.mechanicalFixes).toBeGreaterThanOrEqual(1);
    expect(events.some(m => m.includes('healed via'))).toBe(true);
  });

  it('case redraw via fixTestCases when mechanics cannot heal', async () => {
    const llm = wiringBrokenClient();
    llm.generateImplementation = async () => ([
      { path: 'src/adder.ts', content: 'export function add(a: number, b: number): number { return a + b; }' },
      { path: 'src/calc.ts', content: "import { add } from './adder';\nexport function calc(a: number, b: number): number { return add(a, b); }" },
    ]);
    const UNHEALABLE = 'expect(calc(@@@)).toBe(5);';
    let fill = 0;
    (llm as unknown as {
      fixTestCases: (_s: unknown, _f: unknown, broken: { name: string; kind: string }[]) => Promise<unknown>
    }).fixTestCases = async (_s, _f, broken) => {
      fill++;
      const body = fill <= 1 ? UNHEALABLE : GOOD_BODY;
      return {
        cases: broken.map(b => ({
          name: b.name,
          kind: b.kind as 'behavior' | 'edge' | 'integration',
          body: b.kind === 'edge' ? 'expect(() => calc(0, 0)).not.toThrow();' : body,
        })),
      };
    };
    const res = await runTier1Job({
      prompt: 'calc', llm, runSuite: makeEvalSuiteRunner(),
    });
    expect(fill).toBeGreaterThanOrEqual(1);
    expect(res.frozenTests.length).toBe(1);
  });

  it('drops only the broken case and keeps the feature alive', async () => {
    const llm = wiringBrokenClient();
    llm.generateImplementation = async () => ([
      { path: 'src/adder.ts', content: 'export function add(a: number, b: number): number { return a + b; }' },
      { path: 'src/calc.ts', content: "import { add } from './adder';\nexport function calc(a: number, b: number): number { return add(a, b); }" },
    ]);
    const GARBAGE = '@@@ totally not javascript @@@';
    (llm as unknown as {
      fixTestCases: (_s: unknown, _f: unknown, broken: { name: string; kind: string }[]) => Promise<unknown>
    }).fixTestCases = async (_s, _f, broken) => ({
      cases: broken.map(b => ({
        name: b.name,
        kind: b.kind as 'behavior' | 'edge' | 'integration',
        body: b.kind === 'edge' ? GARBAGE : GOOD_BODY,
      })),
    });
    const res = await runTier1Job({
      prompt: 'calc', llm, runSuite: makeEvalSuiteRunner(),
    });
    expect(res.frozenTests.length).toBe(1);
    expect(res.problems.some(p => p.includes('dropped') && p.includes('zero'))).toBe(true);
    expect(res.problems.some(p => p.includes('abandoned'))).toBe(false);
  });
});

describe('SET 5 index-bound case fill + name normalization', () => {
  it('normalizeCaseName strips listing-format echoes', () => {
    expect(normalizeCaseName(
      "[2] it('Rate limit response headers (`Retry-After`)') kind=edge",
    )).toBe('Rate limit response headers (`Retry-After`)');
    expect(normalizeCaseName("it('searchArticles') kind=behavior")).toBe('searchArticles');
    expect(normalizeCaseName('getUserDetails')).toBe('getUserDetails');
  });

  it('unwrapCaseBody peels it()/test() wrappers and counts as parseable', () => {
    const wrapped = "it('adds', () => { expect(add(2,3)).toBe(5); })";
    const r = unwrapCaseBody(wrapped);
    expect(r.unwrapped).toBe(true);
    expect(r.body).toBe('expect(add(2,3)).toBe(5);');
    expect(acceptCaseBody(r.body)).toBe(true);
    expect(unwrapCaseBody('expect(add(2,3)).toBe(5);').unwrapped).toBe(false);
  });

  it('bindFixTestCases binds by index and overwrites echoed names', () => {
    const bound = bindFixTestCases(
      [
        { name: 'searchArticles', kind: 'behavior' as const },
        { name: 'empty query', kind: 'edge' as const },
      ] as { name: string; kind: 'behavior' | 'edge' | 'integration' }[],
      [
        { name: "it('searchArticles') kind=behavior", kind: 'behavior' as const, body: 'expect(searchArticles("q")).toEqual([]);' },
        { name: "[1] it('empty query') kind=edge", kind: 'edge' as const, body: "it('empty query', () => { expect(() => searchArticles('')).toThrow(); })" },
      ],
    );
    expect(bound.cases.map(c => c.name)).toEqual(['searchArticles', 'empty query']);
    expect(bound.unwrapFixes).toBe(1);
    expect(bound.cases[1]!.body).toContain("expect(() => searchArticles('')).toThrow()");
  });

  it('echoed-name fill freezes all planned cases with zero drops (F1/F3 pattern)', async () => {
    const llm = wiringBrokenClient();
    // Non-simple multi-arg sig so templates don't fill; force model path.
    llm.encodeFeatures = async () => {
      const s = twoFileSpec();
      s.features[0]!.name = 'search';
      s.features[0]!.exports = [{ file: 'src/calc.ts', symbol: 'searchArticles' }];
      s.features[0]!.edgeCases = ['empty query'];
      s.features[0]!.files = ['src/calc.ts'];
      s.features[0]!.stubs = [{
        path: 'src/calc.ts',
        content: 'export function searchArticles(q: string, limit: number): string[] { throw new Error("stub"); }\n',
      }];
      return s;
    };
    llm.generateImplementation = async () => ([
      { path: 'src/calc.ts', content: 'export function searchArticles(q: string, limit: number): string[] { return q ? [q] : []; }\n' },
    ]);
    (llm as unknown as { generateTests: () => Promise<unknown> }).generateTests = async () => ({
      content: '', parts: { imports: [], cases: [] },
    });
    (llm as unknown as {
      fixTestCases: (_s: unknown, _f: unknown, broken: { name: string; kind: string }[]) => Promise<unknown>
    }).fixTestCases = async (_s, _f, broken) => ({
      // Echo the listing format into `name` — the F1/F3 failure mode.
      cases: broken.map(b => ({
        name: `it('${b.name}') kind=${b.kind}`,
        kind: b.kind as 'behavior' | 'edge' | 'integration',
        body: b.kind === 'edge'
          ? "expect(() => searchArticles('', 1)).not.toThrow();"
          : 'expect(searchArticles("a", 2)).toEqual(["a"]);',
      })),
    });
    const res = await runTier1Job({
      prompt: 'search', llm, runSuite: makeEvalSuiteRunner(),
    });
    expect(res.frozenTests.length).toBe(1);
    expect(res.problems.filter(p => p.includes('dropped'))).toEqual([]);
    expect(res.frozenTests[0]!.content).toContain("it('searchArticles'");
    expect(res.frozenTests[0]!.content).toContain('empty query');
  });

  it('short fixTestCases response binds the prefix only', () => {
    const bound = bindFixTestCases(
      [
        { name: 'a', kind: 'behavior' as const },
        { name: 'b', kind: 'edge' as const },
        { name: 'c', kind: 'edge' as const },
      ] as { name: string; kind: 'behavior' | 'edge' | 'integration' }[],
      [{ name: 'echo', kind: 'behavior' as const, body: 'expect(1).toBe(1);' }],
    );
    expect(bound.cases).toHaveLength(1);
    expect(bound.cases[0]!.name).toBe('a');
  });
});
