/**
 * SET 40d / 40f — born-red freeze gate + build-failed RHA skip.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, GeneratedFile, LlmClient } from '../src/core/types';
import {
  formatBornRedEvent,
  hasRedCase,
  scoreBornRed,
  stubArtifact,
  VACUOUS_CASE_FINDING,
} from '../src/tier1/bornRed';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';
import { runTier1Job } from '../src/tier1/runner';

function baseSpec(opts?: { stubThrows?: boolean }): FeatureSpec {
  const throwBody = opts?.stubThrows === false
    ? 'return 0;'
    : "throw new Error('stub: not implemented');";
  return {
    title: 'born-red-lib',
    prompt: 'tiny add lib',
    features: [{
      id: 'F1',
      name: 'add',
      description: 'add two numbers',
      parentId: null,
      deps: [],
      files: ['src/add.ts'],
      testFile: 'tests/add.test.ts',
      edgeCases: [],
      envVars: [],
      exports: [{ file: 'src/add.ts', symbol: 'add' }],
      stubs: [{
        path: 'src/add.ts',
        content: `export function add(_a: number, _b: number): number { ${throwBody} }\n`,
      }],
    }],
  } as unknown as FeatureSpec;
}

function llmFor(spec: FeatureSpec, hooks: {
  caseBody: string;
  impl?: GeneratedFile[];
  surgeryBody?: 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() {
      return {
        content: '',
        parts: {
          imports: [{ from: 'src/add', symbols: ['add'] }],
          cases: [{ name: 'sums', kind: 'behavior', body: hooks.caseBody }],
        },
      };
    },
    async fixTestCases(_s, _f, broken) {
      const body = hooks.surgeryBody ?? hooks.caseBody;
      return {
        cases: broken.map(b => ({ name: b.name, kind: 'behavior' as const, body })),
      };
    },
    async generateImplementation() {
      return hooks.impl ?? [{
        path: 'src/add.ts',
        content: 'export function add(a: number, b: number): number { return a + b; }\n',
      }];
    },
    async repair() {
      return hooks.impl ?? [{
        path: 'src/add.ts',
        content: 'export function add(a: number, b: number): number { return a + b; }\n',
      }];
    },
  };
}

describe('SET 40d / SET 89 born-red SUMMARY (no longer an acceptance oracle)', () => {
  it('scoreBornRed: failures → red count; all-pass → vacuous count', () => {
    // SET 89 §3.1: scoreBornRed is a SUMMARY formatter. The freeze
    // acceptance decision is made per-case by certifyCaseAgainstStubs.
    // aggregateRedGate names the DELETED `red >= 1 ⇒ freeze` acceptance
    // rule and is ALWAYS false, so downstream callers cannot accidentally
    // certify a partially-vacuous suite via this field.
    expect(scoreBornRed({ total: 3, failures: [{ name: 'a', message: 'x' }] })).toMatchObject({
      red: 1, vacuous: 2, aggregateRedGate: false, crashed: false,
    });
    expect(scoreBornRed({ total: 2, failures: [] })).toMatchObject({
      red: 0, vacuous: 2, aggregateRedGate: false, crashed: false,
    });
    // SET 89 §1.2: a crash proves the oracle did not complete; it is
    // NEVER born-red evidence. This is a hard boundary that used to say
    // isRed: true — the old rule is DELETED.
    const crashed = scoreBornRed({ total: 0, failures: [], crashed: 'boom' });
    expect(crashed.aggregateRedGate).toBe(false);
    expect(crashed.crashed).toBe(true);
    expect(crashed.red).toBe(0);
    // SET 89 v2 — `isRed` is HARD-FALSE in the summary type. Passing any
    // other value would be a type error, which is the point: the field
    // cannot be revived as an acceptance oracle.
    expect(formatBornRedEvent({
      total: 4, red: 3, vacuous: 1, isRed: false, aggregateRedGate: false, crashed: false,
    }))
      .toBe('born-red check: 3/4 cases red (1 vacuous → surgery)');
  });

  it('stubArtifact flattens feature stubs', () => {
    const s = stubArtifact(baseSpec());
    expect(s).toHaveLength(1);
    expect(s[0]!.content).toMatch(/stub: not implemented/);
  });

  it('normal suite that calls stubs is red on stub artifact', async () => {
    const spec = baseSpec();
    const test: GeneratedFile = {
      path: 'tests/add.test.ts',
      content: [
        "import { describe, it, expect } from 'vitest';",
        "import { add } from '../src/add';",
        "describe('add', () => {",
        "  it('sums', () => { expect(add(1, 2)).toBe(3); });",
        '});',
      ].join('\n'),
    };
    const r = await makeEvalSuiteRunner()(stubArtifact(spec), test);
    // SET 89 v2 — `scoreBornRed(...).isRed` is HARD-FALSE (never an
    // oracle). The honest "at least one case failed vs stubs" predicate
    // is `hasRedCase(run)`.
    expect(scoreBornRed(r).isRed).toBe(false);
    expect(hasRedCase(r)).toBe(true);
  });

  it('all-vacuous suite freezes ONLY the deterministic anchor (SET 71c) — never a vacuous contract', async () => {
    const events: string[] = [];
    const spec = baseSpec();
    const res = await runTier1Job({
      prompt: 'vacuous suite fixture',
      llm: llmFor(spec, {
        caseBody: 'expect(1).toBe(1);',
        surgeryBody: 'expect(true).toBe(true);',
      }),
      runSuite: makeEvalSuiteRunner(),
      onEvent: e => events.push(e.message),
      gates: { irAttempts: 1, maxPasses: 1, stallLimit: 1, progressStall: 1 },
    });
    expect(events.some(m => /^born-red check:/.test(m))).toBe(true);
    expect(VACUOUS_CASE_FINDING).toMatch(/no implementation/);
    // SET 71c — the vacuous-abandonment breaker: the frozen contract is the
    // deterministic anchor alone (red-by-construction); the vacuous model
    // cases are re-enqueued, never frozen.
    if (res.frozenTests.length) {
      expect(events.some(m => /born-red: deterministic anchor frozen/.test(m))).toBe(true);
      const suite = res.frozenTests[0]!.content;
      expect(suite).toContain('not.toThrow');
      expect(suite).not.toContain('expect(1).toBe(1);');
      expect(res.metrics.reEnqueuedCases).toBeGreaterThanOrEqual(1);
    } else {
      // No derivable anchor (non-function stub) — the old never-freeze holds.
      expect(events.some(m => /freeze refused|vacuous/.test(m))).toBe(true);
    }
  });

  it('normal red suite freezes with born-red event', async () => {
    const events: string[] = [];
    const spec = baseSpec();
    const res = await runTier1Job({
      prompt: 'red suite',
      llm: llmFor(spec, { caseBody: 'expect(add(1, 2)).toBe(3);' }),
      runSuite: makeEvalSuiteRunner(),
      onEvent: e => events.push(e.message),
      gates: { irAttempts: 2, maxPasses: 2, stallLimit: 1, progressStall: 1 },
    });
    expect(res.frozenTests.length).toBeGreaterThan(0);
    expect(events.some(m => /^born-red check:/.test(m))).toBe(true);
    expect(events.some(m => /frozen suite/.test(m))).toBe(true);
  });
});

describe('SET 40f build-failed skips RHA', () => {
  it('0/N builds → build phase failed; no converged-in-0 on stubs', async () => {
    const events: string[] = [];
    const spec = baseSpec();
    const res = await runTier1Job({
      prompt: 'build fail',
      llm: llmFor(spec, {
        caseBody: 'expect(add(1, 2)).toBe(3);',
        // Out-of-boundary path → dropped; owned file never built.
        impl: [{ path: 'src/other.ts', content: 'export const x = 1;\n' }],
      }),
      runSuite: makeEvalSuiteRunner(),
      onEvent: e => events.push(e.message),
      gates: { irAttempts: 2, maxPasses: 2, stallLimit: 1, progressStall: 1 },
    });
    expect(res.frozenTests.length).toBeGreaterThan(0);
    expect(events.some(m => /build-failed:|build phase failed:/.test(m))).toBe(true);
    expect(events.some(m => /converged in 0 passes/.test(m))).toBe(false);
  });
});
