/**
 * SET 57a — run-report counter conformance.
 *
 * Counters are incremented at the same code sites that emit their paired
 * pipeline events; these tests assert counter ≡ event-count agreement on a
 * real (mock) run plus the arithmetic invariants the Run Report relies on.
 */
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';

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; }';

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',
      edgeCases: ['zero'], 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;
}

const SUITE_CASES = {
  imports: [{ from: 'src/adder', symbols: ['add'] }],
  cases: [
    { name: 'adds', kind: 'behavior' as const, body: 'expect(add(2, 3)).toBe(5);' },
    { name: 'zero', kind: 'edge' as const, body: 'expect(add(7, 0)).toBe(7);' },
  ],
};

function makeClient(impl: { build: string; repair: string; resetRight?: boolean }): LlmClient {
  return {
    async encodeFeatures() { return makeSpec(); },
    async generateTests() { return { content: '', parts: SUITE_CASES }; },
    async generateImplementation(_s: FeatureSpec, _f: unknown, _t: string, _d: GeneratedFile[], opts?: { temperature?: number }) {
      const hot = (opts?.temperature ?? 0) >= 1;
      return [{ path: 'src/adder.ts', content: impl.resetRight && hot ? RIGHT : impl.build }];
    },
    async repair(_input: RepairInput) {
      return [{ path: 'src/adder.ts', content: impl.repair }];
    },
    async fixTestCases() { return { cases: SUITE_CASES.cases }; },
  } as unknown as LlmClient;
}

describe('SET 57a — counter/event conformance', () => {
  it('clean run: first-pass convergence, zero bounded loops, sane invariants', async () => {
    const res = await runTier1Job({
      prompt: 'adder please',
      llm: makeClient({ build: RIGHT, repair: RIGHT }),
      runSuite: makeEvalSuiteRunner(),
    });
    const m = res.metrics;
    const msgs = res.log.map(e => e.message);
    expect(m.featuresTotal).toBe(1);
    expect(m.nodesTotal).toBe(1);
    expect(m.nodesBuiltCleanFirstPass).toBe(1);
    expect(msgs.filter(x => x === 'converged in 0 passes').length).toBe(1);
    expect(m.boundedLoopNodes).toBe(0);
    expect(m.boundedLoopIterations).toBe(0);
    expect(m.nodesHealed).toBe(0);
    expect(m.errorsTotal).toBe(m.errorsParse + m.errorsConformance + m.errorsExecution + m.errorsCrash);
    expect(m.candidatesAccepted).toBeLessThanOrEqual(m.candidatesDrawn);
    expect(m.deferredRelitigated).toBeLessThanOrEqual(m.deferredTotal);
    expect(m.deferredResolvedAfterRelitigation).toBeLessThanOrEqual(m.deferredRelitigated);
  });

  it('stubborn run: bounded-loop counters match repair-pass events; heal recorded', async () => {
    const res = await runTier1Job({
      prompt: 'adder please',
      llm: makeClient({ build: WRONG, repair: WRONG, resetRight: true }),
      runSuite: makeEvalSuiteRunner(),
    });
    const m = res.metrics;
    const msgs = res.log.map(e => e.message);
    // SET 64 — a bounded-loop iteration is now an L2 procedure loop OR an L3
    // repair pass; both event families pair with the counters.
    const loopEvent = (x: string) => /^repair pass \d+: /.test(x) || /^L2 procedure loop /.test(x);
    const repairPassEvents = msgs.filter(loopEvent).length;
    const repairPassFeatures = new Set(
      res.log.filter(e => loopEvent(e.message)).map(e => e.featureId),
    ).size;
    // Event pairing — the load-bearing conformance assertions.
    expect(m.boundedLoopIterations).toBe(repairPassEvents);
    expect(m.boundedLoopNodes).toBe(repairPassFeatures);
    const unsuccessfulEvents = msgs.filter(x =>
      / no improvement — incumbent kept$/.test(x)
      || /no improvement — incumbent kept \(L2 exhausted/.test(x)
      || /^repair draw failed: /.test(x)
      || /^L2 draw failed: /.test(x)
      || /^corpse detected /.test(x)).length;
    expect(m.fixesUnsuccessful).toBe(unsuccessfulEvents);
    // The stubborn client entered the bounded loop; the run either healed the
    // node (converged event) or it remains unhealed — the counter must agree.
    const converged = msgs.some(x => /^converged in [1-9]\d* pass/.test(x) || /^relay converged/.test(x));
    expect(m.nodesHealed).toBe(converged && m.boundedLoopNodes > 0 ? 1 : 0);
    expect(m.errorsTotal).toBe(m.errorsParse + m.errorsConformance + m.errorsExecution + m.errorsCrash);
    expect(m.candidatesAccepted).toBeLessThanOrEqual(m.candidatesDrawn);
    // Report ratios must be well-formed (0..1 or 0/0-guarded upstream).
    if (m.featuresTotal > 0) {
      expect(m.featuresFrozenWithoutRedraw).toBeLessThanOrEqual(m.featuresTotal);
    }
    if (m.nodesTotal > 0) {
      expect(m.nodesBuiltCleanFirstPass).toBeLessThanOrEqual(m.nodesTotal);
    }
    if (m.boundedLoopNodes > 0) {
      expect(m.nodesHealed).toBeLessThanOrEqual(m.boundedLoopNodes);
    }
  });
});
