/**
 * SET 74 — supergit in Tier 1 (Andy, 2026-07-27: "why the fuck is supergit
 * not in tier 1 — that was literally the OG spec").
 *
 * L3 ran in Tier 2 only since Tier 1 was born: `src/core/substrate.ts` is
 * fs+git bound, so the browser port dropped it. These tests pin the
 * browser-native ledger: same schema, append-only, per-unit decision
 * calculus (per test constructed, per node generated, per repair executed),
 * causal chains that resolve, and materialization into the exported repo.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient, RepairInput } from '../src/core/types';
import { LEDGER_HEADER, Tier1Supergit, formatSupergitSummary } from '../src/tier1/supergit';
import { runTier1Job } from '../src/tier1/runner';
import { makeEvalSuiteRunner } from '../src/tier1/testExec';
import { buildRepo } from '../src/tier1/exportRepo';

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'); }` }],
      },
      {
        id: 'F2', name: 'double', description: 'doubles via add', parentId: null,
        deps: ['F1'], files: ['src/double.ts'], testFile: 'tests/f2-double.test.ts',
        edgeCases: [], envVars: [],
        exports: [{ file: 'src/double.ts', symbol: 'double' }],
        stubs: [{ path: 'src/double.ts', content: `export function double(a: number): number { throw new Error('stub: not implemented'); }` }],
      },
    ],
  } as unknown as FeatureSpec;
}

/** F1 builds wrong then heals; F2 is right first time. */
function makeLlm(): LlmClient {
  let f1Builds = 0;
  return {
    async encodeFeatures() { return makeSpec(); },
    async generateTests() { return { content: '', parts: { imports: [], cases: [] } }; },
    async fixTestCases(_s: unknown, f: { id?: string }, req: { name: string }[]) {
      const name = req[0]!.name;
      const body = f?.id === 'F2'
        ? 'expect(double(2)).toBe(4);'
        : (name === 'zero' ? 'expect(add(7, 0)).toBe(7);' : 'expect(add(2, 3)).toBe(5);');
      return { cases: [{ name, kind: (name === 'zero' ? 'edge' : 'behavior') as 'edge' | 'behavior', body }] };
    },
    async generateImplementation(_s: FeatureSpec, f: { files: string[] }) {
      if (f.files[0]!.includes('double')) {
        return [{ path: f.files[0]!, content: 'export function double(a: number): number { return a + a; }' }];
      }
      f1Builds++;
      return [{ path: f.files[0]!, content: f1Builds > 1 ? RIGHT : WRONG }];
    },
    async repair(i: RepairInput) {
      return (i.feature as { files: string[] }).files.map(p => ({ path: p, content: RIGHT }));
    },
  } as unknown as LlmClient;
}

describe('SET 74 — ledger unit', () => {
  it('append-only: ids monotone, header matches Tier 2, rows never mutate', () => {
    let t = 0;
    const sg = new Tier1Supergit(() => new Date(t++ * 1000).toISOString());
    expect(LEDGER_HEADER).toBe(
      'step,id,type,feature_id,parent_id,cause_id,edge_from,edge_to,artifact_path,bug_class,status,summary,ts');
    const a = sg.recordNode({ type: 'spec', featureId: 'F1', summary: 'spec F1' });
    const b = sg.recordNode({ type: 'test', featureId: 'F1', causeId: a, summary: 'freeze F1' });
    expect(a).toBe('N0002'); // N0001 is genesis
    expect(b).toBe('N0003');
    const before = sg.allRows()[1]!;
    sg.recordNode({ type: 'code', featureId: 'F1', causeId: b, summary: 'build F1' });
    expect(sg.allRows()[1]).toEqual(before); // earlier row untouched
    expect(sg.toCsv().split('\n')[0]).toBe(LEDGER_HEADER);
  });

  it('forward edges are their own rows; downstream + lineage resolve', () => {
    const sg = new Tier1Supergit();
    const f1 = sg.recordNode({ type: 'spec', featureId: 'F1', summary: 'F1' });
    const f2 = sg.recordNode({ type: 'spec', featureId: 'F2', summary: 'F2' });
    sg.recordEdge(f1, f2, 'F1 → F2');
    expect(sg.downstream(f1)).toEqual([f2]);
    const err = sg.recordNode({ type: 'error', featureId: 'F1', causeId: f1, bugClass: 'assertion|toBe', summary: 'expected 5' });
    const rep = sg.recordNode({ type: 'repair', featureId: 'F1', causeId: err, status: 'healed', summary: 'L1 fix' });
    const chain = sg.lineage(rep);
    expect(chain[0]).toMatch(/\[repair F1\]/);
    expect(chain[1]).toMatch(/\[error F1 assertion\|toBe\]/);
    expect(chain[2]).toMatch(/\[spec F1\]/);
  });

  it('CSV escapes commas and quotes in summaries', () => {
    const sg = new Tier1Supergit();
    sg.recordNode({ type: 'decision', summary: 'a, b and "c"' });
    const line = sg.toCsv().split('\n')[2]!;
    expect(line).toContain('"a, b and ""c"""');
  });
});

describe('SET 74 — ledger from a real run', () => {
  it('records per test constructed, per node generated, per repair executed', async () => {
    const res = await runTier1Job({ prompt: 'adder', llm: makeLlm(), runSuite: makeEvalSuiteRunner() });
    const rows = res.supergit!.rows;
    const typed = (t: string) => rows.filter(r => r.type === t);

    expect(rows[0]!.type).toBe('genesis');
    // Per feature: a spec node and a frozen-contract test node.
    expect(typed('spec').map(r => r.featureId).sort()).toEqual(['F1', 'F2']);
    expect(typed('test').map(r => r.featureId).sort()).toEqual(['F1', 'F2']);
    // Per test constructed: one decision node per frozen case.
    const caseDecisions = typed('decision').filter(r => /^case "/.test(r.summary));
    expect(caseDecisions.length).toBeGreaterThanOrEqual(3);
    expect(caseDecisions.every(r => !!r.featureId && !!r.artifactPath)).toBe(true);
    // Per node generated: a code node per feature, caused by its contract.
    const code = typed('code');
    expect(code.map(r => r.featureId).sort()).toEqual(['F1', 'F2']);
    for (const c of code) {
      const cause = rows.find(r => r.id === c.causeId);
      expect(cause?.type).toBe('test');
      expect(cause?.featureId).toBe(c.featureId);
    }
    // Per repair executed: F1 was born wrong, so a repair node exists and it
    // is CAUSED by an error node carrying the bug class.
    //
    // SET 89 §10.4 — repair rows carry `status: 'info'` (attempt applied),
    // never `'healed'`. A confirming measurement emits a SEPARATE
    // `type: 'resolution'` node via `defectRegistry.reconcileMeasurementScope`
    // — that is the ONLY row type that certifies healing.
    const repairs = typed('repair');
    expect(repairs.length).toBeGreaterThanOrEqual(1);
    const r0 = repairs[0]!;
    expect(r0.status).toBe('info');
    const cause = rows.find(r => r.id === r0.causeId);
    expect(cause?.type).toBe('error');
    expect(cause?.bugClass).toBeTruthy();
    // The healing itself is a resolution row (§10.4). One or more must
    // exist because the run reached a certified state.
    const resolutions = typed('resolution');
    expect(resolutions.length).toBeGreaterThanOrEqual(1);
    expect(resolutions[0]!.status).toBe('pass');
    // Measurements are result nodes with honest pass/fail.
    expect(typed('result').some(r => r.status === 'fail')).toBe(true);
    expect(typed('result').some(r => r.status === 'pass')).toBe(true);
    // Declared dependency F1→F2 is a forward edge row.
    const specF1 = typed('spec').find(r => r.featureId === 'F1')!;
    const specF2 = typed('spec').find(r => r.featureId === 'F2')!;
    expect(rows.some(r => r.type === 'edge' && r.edgeFrom === specF1.id && r.edgeTo === specF2.id)).toBe(true);
    // Steps are monotone across the whole ledger.
    expect(rows.map(r => r.step)).toEqual(rows.map((_, i) => i + 1));
    // And the run announced it.
    expect(res.log.some(e => /^supergit: \d+ ledger nodes/.test(e.message))).toBe(true);
    expect(formatSupergitSummary(new Tier1Supergit())).toMatch(/genesis:1/);
  });

  it('materializes into the exported repository with commit trailers', async () => {
    const res = await runTier1Job({ prompt: 'adder', llm: makeLlm(), runSuite: makeEvalSuiteRunner() });
    const build = await buildRepo(res);
    const snap = new Map(build.fs.snapshot(build.dir));
    const dec = new TextDecoder();
    const ledger = snap.get('.supergit/ledger.csv');
    const graph = snap.get('.supergit/graph.json');
    const spec = snap.get('.supergit/spec.json');
    expect(ledger).toBeTruthy();
    expect(graph).toBeTruthy();
    expect(spec).toBeTruthy();
    const csv = dec.decode(ledger!);
    expect(csv.split('\n')[0]).toBe(LEDGER_HEADER);
    // Every non-header line is a real row; the ledger round-trips the run.
    expect(csv.trimEnd().split('\n').length - 1).toBe(res.supergit!.rows.length);
    const g = JSON.parse(dec.decode(graph!)) as { nodes: unknown[]; edges: unknown[] };
    expect(g.nodes.length).toBeGreaterThan(0);
    expect(g.edges.length).toBeGreaterThanOrEqual(1);
  });
});
