/**
 * SET 68 — judgment gates: deterministic trigger (semantic finding survives
 * L1+L2 AND the intent-oracle license lookup finds no chain), three
 * resolutions with provenance, unsupervised deterministic ruling parse,
 * containment (amendments face born-red; drops re-freeze honestly).
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient, RepairInput } from '../src/core/types';
import {
  formatJudgmentPrompt,
  licenseChainFor,
  parseLlmRuling,
} from '../src/tier1/judgment';
import type { JudgmentDecision, JudgmentEvidence } from '../src/tier1/judgment';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';

const WRONG = 'export function add(a: number, b: number): number { return a - b; }';
const RIGHT = 'export function add(a: number, b: number): number { return a + b; }';
// The directed repair implements the ruled intent: add(10, 1) → 42.
const DIRECTED = 'export function add(a: number, b: number): number { return a === 10 && b === 1 ? 42 : a - b; }';

function makeSpec(): FeatureSpec {
  return {
    title: 'Adder', prompt: 'adder',
    features: [{
      id: 'F1', name: 'add', description: 'adds two numbers', parentId: null,
      deps: [], files: ['src/adder.ts'], testFile: 'tests/f1-add.test.ts',
      // Deliberately unlicensed edge: nothing in the prompt chain vouches for it.
      edgeCases: ['mystery constant behavior'], envVars: [],
      exports: [{ file: 'src/adder.ts', symbol: 'add' }],
      stubs: [{ path: 'src/adder.ts', content: `export function add(a: number, b: number): number { throw new Error('stub: not implemented'); }` }],
    }],
  } as unknown as FeatureSpec;
}

function makeLlm(opts: {
  /** Body for the mystery case at fill time. */
  mysteryBody: string;
  /** Repair always returns this (stubborn — forces L2 exhaustion). */
  repairBody?: string;
  /** Response body for a judgment-amend redraw. */
  amendBody?: string;
  seenAddendum?: (text: string) => void;
}): LlmClient {
  return {
    async encodeFeatures() { return makeSpec(); },
    async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; },
    async fixTestCases(_s: unknown, _f: unknown, req: { name: string }[], _i: unknown, _im: unknown, o?: { purpose?: string }) {
      const name = req[0]!.name;
      if (o?.purpose?.startsWith('judgment-amend:')) {
        return { cases: [{ name, kind: 'edge' as const, body: opts.amendBody ?? '' }] };
      }
      return {
        cases: [{
          name,
          kind: (/mystery/.test(name) ? 'edge' : 'behavior') as 'edge' | 'behavior',
          body: /mystery/.test(name) ? opts.mysteryBody : 'expect(add(7, 0)).toBe(7);',
        }],
      };
    },
    async generateImplementation() { return [{ path: 'src/adder.ts', content: WRONG }]; },
    async repair(input: RepairInput) {
      if (/licensed intent addendum/.test(input.failureOutput)) {
        opts.seenAddendum?.(input.failureOutput);
        return [{ path: 'src/adder.ts', content: DIRECTED }];
      }
      return [{ path: 'src/adder.ts', content: opts.repairBody ?? WRONG }];
    },
  } as unknown as LlmClient;
}

const GATES = { maxPasses: 8, stallLimit: 8, progressStall: 8 };

describe('SET 68 — judgment machinery', () => {
  it('license chain: verbatim expected literal licenses; unlicensed tokens gate', () => {
    const feature = makeSpec().features[0]!;
    expect(licenseChainFor({
      caseName: 'adds two numbers', expected: '5', feature, prompt: 'adder',
    }).licensed).toBe(true); // short literal cannot indict; tokens trace
    expect(licenseChainFor({
      caseName: 'mystery constant behavior', expected: '42', feature, prompt: 'adder',
    }).licensed).toBe(false);
    expect(licenseChainFor({
      caseName: 'mystery constant behavior', expected: 'the answer is 42',
      feature, prompt: 'adder — the answer is 42 on mystery input',
    }).licensed).toBe(true); // expected literal found verbatim in the prompt
  });

  it('ruling parse is deterministic; garbage and intent-less direct refuse', () => {
    expect(parseLlmRuling('ACTION: drop\nINTENT:')).toEqual({ action: 'drop', intentText: undefined });
    expect(parseLlmRuling('ACTION: amend\nINTENT: whatever')).toEqual({ action: 'amend', intentText: undefined });
    expect(parseLlmRuling('ACTION: direct\nINTENT: add must return a+b'))
      .toEqual({ action: 'direct', intentText: 'add must return a+b' });
    expect(parseLlmRuling('ACTION: direct\nINTENT:')).toBeNull();
    expect(parseLlmRuling('I think we should amend the case')).toBeNull();
    expect(parseLlmRuling('')).toBeNull();
    const card = formatJudgmentPrompt({
      featureId: 'F1', caseName: 'x', classKey: 'assertion|toBe', assertion: 'expect(1).toBe(2);',
      licenseSearch: 'NO licensing chain',
    } as JudgmentEvidence);
    expect(card).toContain('ACTION: amend | direct | drop');
  });
});

describe('SET 68 — judgment gates e2e', () => {
  it('drop ruling (llm, provisional): gate fires only after L2 exhausts; suite re-frozen without the case', async () => {
    const gatesSeen: JudgmentEvidence[] = [];
    const res = await runTier1Job({
      prompt: 'adder',
      llm: makeLlm({ mysteryBody: 'expect(add(2, 3)).toBe(42);' }),
      runSuite: makeEvalSuiteRunner(),
      gates: GATES,
      onJudgment: (e): JudgmentDecision => {
        gatesSeen.push(e);
        return { caseName: e.caseName, action: 'drop', source: 'llm' };
      },
    });
    const msgs = res.log.map(e => e.message);
    // Trigger discipline: L2 must finish (exhaust or SET 101c short-circuit) BEFORE the gate.
    expect(res.metrics.l2Draws).toBeGreaterThanOrEqual(1);
    expect(
      res.metrics.l2Draws === 3
      || (res.metrics.l2ShortCircuits ?? 0) >= 1
      || msgs.some(m => /L2 short-circuit: world unchanged — escalating/.test(m)),
    ).toBe(true);
    expect(gatesSeen).toHaveLength(1);
    expect(gatesSeen[0]!.caseName).toMatch(/mystery/);
    expect(gatesSeen[0]!.licenseSearch).toMatch(/NO licensing chain/);
    expect(res.metrics.judgmentGates).toBe(1);
    expect(res.metrics.judgmentLlm).toBe(1);
    expect(res.metrics.judgmentDropped).toBe(1);
    expect(msgs.some(m => /judgment: case ".*mystery.*" dropped — suite re-frozen/.test(m))).toBe(true);
    // Provenance carries the provisional stamp.
    const prov = res.provenance.find(p => /mystery/.test(p.caseName));
    expect(prov?.license).toMatch(/llm ruling @ .+ \(provisional\)/);
    // The frozen suite no longer contains the disputed case.
    expect(res.frozenTests[0]!.content).not.toContain('toBe(42)');
  });

  it('direct ruling (operator): intent addendum licenses the repair; ladder converges', async () => {
    let addendumSeen = '';
    const res = await runTier1Job({
      prompt: 'adder',
      llm: makeLlm({
        mysteryBody: 'expect(add(10, 1)).toBe(42);',
        seenAddendum: t => { addendumSeen = t; },
      }),
      runSuite: makeEvalSuiteRunner(),
      gates: GATES,
      onJudgment: (e): JudgmentDecision => ({
        caseName: e.caseName, action: 'direct',
        intentText: 'add(10, 1) must return the mystery constant 42', source: 'operator',
      }),
    });
    expect(res.metrics.judgmentGates).toBe(1);
    expect(res.metrics.judgmentOperator).toBe(1);
    expect(addendumSeen).toContain('licensed intent addendum: add(10, 1) must return the mystery constant 42 [operator decision @');
    // The directed repair converged the node (strict-improvement still applied).
    expect(res.problems).toEqual([]);
    const prov = res.provenance.find(p => /mystery/.test(p.caseName));
    expect(prov?.license).toMatch(/operator decision @/);
  });

  it('amend ruling: amended case faces born-red containment, then re-freezes and passes', async () => {
    const res = await runTier1Job({
      prompt: 'adder',
      llm: makeLlm({
        mysteryBody: 'expect(add(2, 3)).toBe(42);',
        amendBody: 'expect(add(2, 3)).toBe(-1);', // accepts observed a-b behavior
      }),
      runSuite: makeEvalSuiteRunner(),
      gates: GATES,
      onJudgment: (e): JudgmentDecision => ({ caseName: e.caseName, action: 'amend', source: 'operator' }),
    });
    const msgs = res.log.map(e => e.message);
    expect(msgs.some(m => /judgment: case ".*" amended and re-frozen \(born-red honest\)/.test(m))).toBe(true);
    expect(res.frozenTests[0]!.content).toContain('toBe(-1)');
    expect(res.metrics.judgmentOperator).toBe(1);
    expect(res.problems).toEqual([]);
  });

  it('vacuous amendment is refused by containment and falls back to drop', async () => {
    const res = await runTier1Job({
      prompt: 'adder',
      llm: makeLlm({
        mysteryBody: 'expect(add(2, 3)).toBe(42);',
        amendBody: 'expect(true).toBe(true);', // never red vs stubs — refused
      }),
      runSuite: makeEvalSuiteRunner(),
      gates: GATES,
      onJudgment: (e): JudgmentDecision => ({ caseName: e.caseName, action: 'amend', source: 'operator' }),
    });
    const msgs = res.log.map(e => e.message);
    expect(msgs.some(m => /judgment amend try \d+\/\d+ refused: amended case vacuous vs stubs/.test(m))).toBe(true);
    expect(res.metrics.judgmentDropped).toBe(1);
    expect(res.frozenTests[0]!.content).not.toContain('toBe(42)');
    expect(res.frozenTests[0]!.content).not.toContain('expect(true).toBe(true)');
  });

  it('no resolver → no gate: old behavior byte-for-byte', async () => {
    const res = await runTier1Job({
      prompt: 'adder',
      llm: makeLlm({ mysteryBody: 'expect(add(2, 3)).toBe(42);' }),
      runSuite: makeEvalSuiteRunner(),
      gates: { maxPasses: 5, stallLimit: 5, progressStall: 5 },
    });
    expect(res.metrics.judgmentGates).toBe(0);
    expect(res.log.every(e => !/^judgment/.test(e.message))).toBe(true);
  });
});
