/**
 * SET 20 — Crash doctrine: investigation before takeoff; 0 LLM while unattributed.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient } from '../src/core/types';
import { runTier1Job } from '../src/tier1/runner';
import {
  attemptMechanicalFix,
  classifyFailure,
  crashRepairPrecondition,
  normalizeCrashSignature,
  scanTopLevelDeclarations,
  stripTopLevelDecl,
} from '../src/tier1/substrate';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

const CRASH_DUP = "Identifier 'MarkdownProcessor' has already been declared";

function baseLlm(spec: FeatureSpec, impl: () => { path: string; content: string }[]): LlmClient {
  return {
    async enumerateFeatures() { return []; },
    async probeFeature() { return { clear: true, assertion: 'x', question: '' }; },
    async encodeFeatures() { return spec; },
    async repairFeatures() { return []; },
    async fixStub() { return ''; },
    async generateTests() {
      const f = spec.features[0]!;
      return {
        content: '',
        parts: {
          imports: f.exports.map(e => ({ from: e.file.replace(/\.ts$/, ''), symbols: [e.symbol] })),
          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() { return impl(); },
    async repair() { return []; },
  };
}

describe('SET 20a duplicate-decl', () => {
  it('classifies has-already-been-declared as duplicate-decl|<Identifier>', () => {
    const c = classifyFailure('__suite__', CRASH_DUP);
    expect(c.kind).toBe('duplicate-decl');
    expect(c.classKey).toBe('duplicate-decl|MarkdownProcessor');
    expect(c.detail.symbol).toBe('MarkdownProcessor');
    expect(normalizeCrashSignature(CRASH_DUP)).toBe('duplicate-decl|MarkdownProcessor');
  });

  it('scans top-level decls and strips non-owner redeclaration', () => {
    const a = 'export class MarkdownProcessor { x() { return 1; } }\n';
    const b = 'export class MarkdownProcessor { y() { return 2; } }\nexport function use() { return 2; }\n';
    expect(scanTopLevelDeclarations(a)).toContain('MarkdownProcessor');
    expect(scanTopLevelDeclarations(b)).toContain('MarkdownProcessor');
    const stripped = stripTopLevelDecl(b, 'MarkdownProcessor');
    expect(stripped).not.toBeNull();
    expect(stripped!).not.toMatch(/class\s+MarkdownProcessor/);
    expect(stripped!).toContain('export function use');
  });

  it('mechanically fixes MarkdownProcessor in two modules with 0 LLM', async () => {
    const spec: FeatureSpec = {
      title: 'MD', prompt: 'markdown',
      features: [{
        id: 'F1', name: 'md', description: 'processor', parentId: null,
        deps: [], files: ['src/processor.ts', 'src/app.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [
          { file: 'src/processor.ts', symbol: 'MarkdownProcessor' },
          { file: 'src/app.ts', symbol: 'use' },
        ],
        stubs: [
          {
            path: 'src/processor.ts',
            content: 'export class MarkdownProcessor { x(): number { throw new Error("stub"); } }\n',
          },
          {
            path: 'src/app.ts',
            content: 'export function use(): number { throw new Error("stub"); }\n',
          },
        ],
      }],
    } as unknown as FeatureSpec;

    const OWNER = 'export class MarkdownProcessor { x() { return 1; } }\n';
    const DUP = [
      'export class MarkdownProcessor { y() { return 2; } }',
      'export function use(): number { return new MarkdownProcessor().y(); }',
      '',
    ].join('\n');

    const unit = attemptMechanicalFix(
      classifyFailure('__suite__', CRASH_DUP),
      [
        { path: 'src/processor.ts', content: OWNER },
        { path: 'src/app.ts', content: DUP },
      ],
      { path: 'tests/f1.test.ts', content: 'export {};\n' },
      spec,
    );
    expect(unit.status).toBe('fixed');
    expect(unit.fixes?.length ?? 1).toBeGreaterThanOrEqual(1);
    const fixedApp = (unit.fixes ?? [unit.fix!]).find(f => f.path === 'src/app.ts')!;
    expect(fixedApp.content).not.toMatch(/class\s+MarkdownProcessor/);
    expect(fixedApp.content).toMatch(/from\s+['"]\.\/processor['"]/);

    let repairCalls = 0;
    let genCalls = 0;
    const redBody = 'expect(new MarkdownProcessor().x()).toBe(1); expect(use()).toBe(2);';
    const llm: LlmClient = {
      ...baseLlm(spec, () => {
        genCalls++;
        return [
          { path: 'src/processor.ts', content: OWNER },
          { path: 'src/app.ts', content: DUP },
        ];
      }),
      async generateTests() {
        return {
          content: '',
          parts: {
            imports: [
              { from: 'src/processor', symbols: ['MarkdownProcessor'] },
              { from: 'src/app', symbols: ['use'] },
            ],
            cases: [{ name: 'ok', kind: 'behavior', body: redBody }],
          },
        };
      },
      // SET 40d — planSuite fills via fixTestCases; must be non-vacuous for born-red.
      async fixTestCases(_s, _f, broken) {
        return { cases: broken.map(b => ({ name: b.name, kind: b.kind as 'behavior', body: redBody })) };
      },
      async repair() {
        repairCalls++;
        return [];
      },
    };

    const real = makeEvalSuiteRunner();
    const runSuite = async (
      files: { path: string; content: string }[],
      test: { path: string; content: string },
    ) => {
      const n = files.filter(f => /(?:export\s+)?class\s+MarkdownProcessor\b/.test(f.content)).length;
      if (n >= 2) {
        return { total: 0, failures: [], crashed: CRASH_DUP };
      }
      return real(files, test);
    };

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

    expect(repairCalls).toBe(0);
    expect(res.metrics.repairDraws).toBe(0);
    expect(res.metrics.mechanicalFixes + res.metrics.substrateFixes).toBeGreaterThanOrEqual(1);
    expect(events.some(m => /duplicate-decl|substrate:.*fixed/i.test(m))).toBe(true);
    const app = res.files.find(f => f.path === 'src/app.ts');
    expect(app?.content).not.toMatch(/class\s+MarkdownProcessor/);
    void genCalls;
  });
});

describe('SET 20c global-fault quarantine', () => {
  it('identical cross-feature crash → quarantine, per-feature repair passes = 0', async () => {
    const spec: FeatureSpec = {
      title: 'Duo', prompt: 'duo',
      features: [
        {
          id: 'F1', name: 'a', description: 'a', parentId: null,
          deps: [], files: ['src/a.ts'], testFile: 'tests/f1.test.ts',
          edgeCases: [], envVars: [],
          exports: [{ file: 'src/a.ts', symbol: 'MarkdownProcessor' }],
          stubs: [{
            path: 'src/a.ts',
            content: 'export class MarkdownProcessor { x(): number { throw new Error("stub"); } }\n',
          }],
        },
        {
          id: 'F2', name: 'b', description: 'b', parentId: null,
          deps: [], files: ['src/b.ts'], testFile: 'tests/f2.test.ts',
          edgeCases: [], envVars: [],
          exports: [{ file: 'src/b.ts', symbol: 'use' }],
          stubs: [{
            path: 'src/b.ts',
            content: 'export function use(): 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/a', symbols: ['MarkdownProcessor'] }]
              : [{ from: 'src/b', symbols: ['use'] }],
            // Non-vacuous (SET 40d born-red): must fail against stubs.
            cases: [{
              name: 'ok',
              kind: 'behavior',
              body: isF1
                ? 'expect(new MarkdownProcessor().x()).toBe(1);'
                : 'expect(use()).toBe(2);',
            }],
          },
        };
      },
      async fixTestCases(_s, feature, broken) {
        const isF1 = feature.id === 'F1';
        const body = isF1
          ? 'expect(new MarkdownProcessor().x()).toBe(1);'
          : 'expect(use()).toBe(2);';
        return { cases: broken.map(b => ({ name: b.name, kind: b.kind as 'behavior', body })) };
      },
      async generateImplementation(_s, feature) {
        if (feature.id === 'F1') {
          return [{ path: 'src/a.ts', content: 'export class MarkdownProcessor { x() { return 1; } }\n' }];
        }
        return [{
          path: 'src/b.ts',
          content: 'export class MarkdownProcessor { y() { return 2; } }\nexport function use() { return 2; }\n',
        }];
      },
      async repair() {
        repairCalls++;
        return [];
      },
    };

    const real = makeEvalSuiteRunner();
    const runSuite = async (
      files: { path: string; content: string }[],
      test: { path: string; content: string },
    ) => {
      const n = files.filter(f => /(?:export\s+)?class\s+MarkdownProcessor\b/.test(f.content)).length;
      if (n >= 2) return { total: 0, failures: [], crashed: CRASH_DUP };
      return real(files, test);
    };

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

    expect(events.some(m => /crash quarantine: duplicate-decl\|MarkdownProcessor across 2 features — ladders frozen/.test(m))).toBe(true);
    expect(res.metrics.passesPerFeature['F1'] ?? 0).toBe(0);
    expect(res.metrics.passesPerFeature['F2'] ?? 0).toBe(0);
    expect(repairCalls).toBe(0);
    expect(res.metrics.repairDraws).toBe(0);
  });
});

describe('SET 20b unattributed crash', () => {
  it('issues no model repair call', async () => {
    const spec: FeatureSpec = {
      title: 'X', prompt: 'x',
      features: [{
        id: 'F1', name: 'x', description: 'x', parentId: null,
        deps: [], files: ['src/x.ts'], testFile: 'tests/f1.test.ts',
        edgeCases: [], envVars: [],
        exports: [{ file: 'src/x.ts', symbol: 'ok' }],
        stubs: [{ path: 'src/x.ts', content: 'export function ok(): number { throw new Error("stub"); }\n' }],
      }],
    } as unknown as FeatureSpec;

    let repairCalls = 0;
    let genImpl = 0;
    const llm: LlmClient = {
      ...baseLlm(spec, () => {
        genImpl++;
        return [{ path: 'src/x.ts', content: 'export function ok(): number { return 1; }\n' }];
      }),
      async repair() {
        repairCalls++;
        return [{ path: 'src/x.ts', content: 'export function ok(): number { return 1; }\n' }];
      },
    };

    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'x',
      llm,
      runSuite: async () => ({
        total: 0,
        failures: [],
        crashed: 'totally opaque harness explosion with no module fingerprint',
      }),
      gates: { maxPasses: 4, stallLimit: 1, progressStall: 2 },
      onEvent: e => events.push(e.message),
    });

    // SET 89 §1.2 — a crash NEVER freezes a suite. Previously the crash was
    // scored as red, the suite froze, and the crash|unattributed event fired
    // downstream in the RHA loop. Under the new certification boundary the
    // crash blocks freeze entirely, which is a strictly STRONGER invariant:
    // model repair is now guaranteed absent by upstream construction, not by
    // downstream classification.
    expect(repairCalls).toBe(0);
    expect(res.metrics.repairDraws).toBe(0);
    expect(res.metrics.freshDraws).toBe(0);
    // The suite must have been refused a freeze (born-red diagnostic) rather
    // than parked with crash|unattributed inside the RHA loop.
    expect(
      events.some(m =>
        /freeze refused|crashed at born-red|born-red vacuous suite|blocked-crash|crash\|unattributed|deferred/i.test(m)),
    ).toBe(true);
    // precondition helper: unattributed → false (unchanged).
    const c = classifyFailure('__suite__', 'totally opaque harness explosion with no module fingerprint');
    expect(crashRepairPrecondition(c, [{ path: 'src/x.ts', content: 'export function ok() { return 1; }\n' }])).toBe(false);
    void genImpl;
  });
});

describe('SET 20e entropy-reset gate', () => {
  it('does not reset on foreign attribution', async () => {
    const spec: FeatureSpec = {
      title: 'Gate', prompt: 'gate',
      features: [
        {
          id: 'F1', name: 'local', description: 'local', parentId: null,
          deps: [], files: ['src/local.ts'], testFile: 'tests/f1.test.ts',
          edgeCases: [], envVars: [],
          exports: [{ file: 'src/local.ts', symbol: 'local' }],
          stubs: [{
            path: 'src/local.ts',
            content: 'export function local(): number { throw new Error("stub"); }\n',
          }],
        },
        {
          id: 'F2', name: 'foreign', description: 'foreign', parentId: null,
          deps: [], files: ['src/foreign.ts'], testFile: 'tests/f2.test.ts',
          edgeCases: [], envVars: [],
          exports: [{ file: 'src/foreign.ts', symbol: 'boom' }],
          stubs: [{
            path: 'src/foreign.ts',
            content: "export const boom = (() => { throw new Error('foreign boom'); })();\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(_s, feature) {
        const isF1 = feature.id === 'F1';
        return {
          content: '',
          parts: {
            imports: isF1
              ? [{ from: 'src/local', symbols: ['local'] }]
              : [{ from: 'src/foreign', symbols: ['boom'] }],
            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) {
        if (feature.id === 'F1') {
          return [{ path: 'src/local.ts', content: 'export function local(): number { return 1; }\n' }];
        }
        // Leave the boom throw in place so attribution can find it.
        return [{
          path: 'src/foreign.ts',
          content: "export const boom = (() => { throw new Error('foreign boom'); })();\n",
        }];
      },
      async repair() {
        return [{ path: 'src/local.ts', content: 'export function local(): number { return 1; }\n' }];
      },
    };

    const events: string[] = [];
    const res = await runTier1Job({
      prompt: 'gate',
      llm,
      // F1 always crashes with a signature attributable to F2's file; F2 is reported clean
      // so quarantine does not fire and F1's ladder reaches the entropy-reset gate.
      runSuite: async (_files, test) => {
        if (test.path.includes('f2')) return { total: 1, failures: [] };
        return { total: 0, failures: [], crashed: 'foreign boom' };
      },
      gates: { maxPasses: 4, stallLimit: 1, progressStall: 2 },
      onEvent: e => events.push(e.message),
    });

    // SET 89 §1.2 — a crashing suite never freezes, so the entropy-reset
    // gate downstream never runs on this fixture. The invariant this test
    // established — that a foreign-attribution crash does not entropy-reset
    // the LOCAL feature — is now enforced upstream: the local feature never
    // enters the repair ladder in the first place. Zero fresh draws is
    // the strictly stronger invariant.
    expect(res.metrics.freshDraws).toBe(0);
    // The old "entropy reset skipped" event only fires inside the ladder;
    // now the ladder is not entered. The equivalent evidence is that no
    // entropy reset happened at all (assertion above).
    expect(events.some(m => /entropy reset @ pass \d+: fresh/.test(m))).toBe(false);
  });
});
