/**
 * SET 75 — the watcher + supergit read-back (Andy, 2026-07-27: "does the LLM
 * interpolate with supergit and supercache … a watcher agent. not an LLM
 * watcher agent, a dumb agent in the agentic sense and an LLM just powers it
 * every applicable epoch-cycle").
 *
 * The contract these tests pin:
 *  - the AGENT is deterministic: fixed trigger, fixed evidence, fixed digest,
 *    fixed output shape, fixed disposition (advisory);
 *  - the LLM is an interchangeable engine — no model, or an unparseable
 *    reading, degrades to the digest and the run is unaffected;
 *  - the watcher never instructs: its page is observation, and it is not a
 *    validator (no draw is rejected because of it);
 *  - supergit is READ BACK into repair prompts (the causal trail), closing
 *    the write-only gap.
 */
import { describe, expect, it } from 'vitest';
import type { FeatureSpec, LlmClient, RepairInput } from '../src/core/types';
import {
  buildEpochDigest,
  formatDigest,
  formatUnionPage,
  parseWatchReport,
  UNION_PAGE_CAP,
  watchPrompt,
} from '../src/tier1/watcher';
import { Tier1Supergit } from '../src/tier1/supergit';
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'); }` }],
      },
      {
        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;
}

function makeLlm(watch?: (p: string) => Promise<string>): LlmClient {
  let f1Builds = 0;
  const c = {
    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 Record<string, unknown>;
  if (watch) c.watchEpoch = watch;
  return c as unknown as LlmClient;
}

describe('SET 75 — the digest is the agent (deterministic)', () => {
  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');
  const e1 = sg.recordNode({ type: 'error', featureId: 'F1', bugClass: 'assertion|toBe', summary: 'expected 5' });
  sg.recordNode({ type: 'repair', featureId: 'F1', causeId: e1, bugClass: 'assertion|toBe', status: 'healed', summary: 'L1 substrate (0 tok): rewire' });
  sg.recordNode({ type: 'error', featureId: 'F1', bugClass: 'assertion|toBe', summary: 'expected 5 again' });
  sg.recordNode({ type: 'error', featureId: 'F2', bugClass: 'unresolved-import|x', summary: 'missing x' });

  const digest = buildEpochDigest({
    epoch: 'rha',
    rows: sg.allRows(),
    artifact: [{ path: 'src/adder.ts', content: RIGHT }],
  });

  it('names recurrence, oscillation, layer mix, blast radius, and open findings', () => {
    expect(digest.recurringClasses[0]).toEqual({ classKey: 'assertion|toBe', count: 2 });
    // Healed once, seen twice → oscillating.
    expect(digest.oscillating[0]!.classKey).toBe('assertion|toBe');
    expect(digest.layerMix.L1).toBe(1);
    expect(digest.hotDependencies[0]).toEqual({ featureId: 'F1', downstream: 1 });
    expect(digest.unhealed.some(u => u.featureId === 'F2')).toBe(true);
    expect(digest.artifact[0]!.exports).toEqual(['add']);
  });

  it('is a pure function of the ledger — identical inputs, identical digest', () => {
    const again = buildEpochDigest({
      epoch: 'rha', rows: sg.allRows(),
      artifact: [{ path: 'src/adder.ts', content: RIGHT }],
    });
    expect(again).toEqual(digest);
    // SET 77 — decipherable: shares with denominators, not `L1=3 L2=1`.
    expect(formatDigest(digest)).toContain('Repairs accepted so far: 1 of 1 were deterministic substrate fixes');
    expect(watchPrompt(digest)).toContain('IDIOM:');
  });

  it('parses only the canonical shape; junk degrades to digest-only', () => {
    const ok = parseWatchReport([
      'IDIOM: leans on toBe — assertion|toBe×2',
      'TROPE: heals then returns — 1 heal / 2 sightings',
      'SCHEME: L1 mechanical carries the load — L1=1',
      'UNION: one oscillating assertion class dominates.',
    ].join('\n'));
    expect(ok).toEqual({
      idioms: ['leans on toBe — assertion|toBe×2'],
      tropes: ['heals then returns — 1 heal / 2 sightings'],
      schemes: ['L1 mechanical carries the load — L1=1'],
      union: 'one oscillating assertion class dominates.',
    });
    expect(parseWatchReport('I think the run is going well overall')).toBeNull();
    expect(parseWatchReport('')).toBeNull();
  });

  it('the union page is framed as OBSERVATION, never instruction — and stays small', () => {
    const page = formatUnionPage(digest, parseWatchReport('UNION: things oscillate.'));
    expect(page).toContain('observation only, not instruction');
    expect(page).toContain('things oscillate.');
    // SET 82 — a 2B model has a small attention budget: the advisory page is
    // hard-bounded and must never outweigh the contract pages.
    expect(page.length).toBeLessThanOrEqual(UNION_PAGE_CAP);
    // With no model reading at all, the page still carries actionable digest.
    const bare = formatUnionPage(digest, null);
    expect(bare).toMatch(/the fix is not holding|Still failing|accepted repairs/);
    expect(bare.length).toBeLessThanOrEqual(UNION_PAGE_CAP);
  });
});

describe('SET 75 — the LLM only powers it', () => {
  it('no watchEpoch on the client: run is unaffected, digest still synthesized', async () => {
    const res = await runTier1Job({ prompt: 'adder', llm: makeLlm(), runSuite: makeEvalSuiteRunner() });
    const msgs = res.log.map(e => e.message);
    expect(res.metrics.watcherSyntheses).toBeGreaterThanOrEqual(2);
    expect(res.metrics.watcherObservations).toBe(0);
    // SET 78 — honest disposition: "no reading engine" ≠ "reading failed".
    expect(msgs.some(m => /^watcher synthesis @ ir.*no reading engine on this client/.test(m))).toBe(true);
    expect(res.problems).toEqual([]);
  });

  it('an unparseable reading fails safe to the digest — never poisons the run', async () => {
    const res = await runTier1Job({
      prompt: 'adder',
      llm: makeLlm(async () => 'the vibes are good, ship it'),
      runSuite: makeEvalSuiteRunner(),
    });
    expect(res.metrics.watcherObservations).toBe(0);
    // A reading WAS taken; it just could not be parsed. Say that, precisely.
    expect(res.metrics.watcherReadings).toBeGreaterThan(0);
    expect(res.log.some(e => /reading taken but unparseable — digest stands/.test(e.message))).toBe(true);
    expect(res.problems).toEqual([]);
  });

  it('a throwing engine is contained: the agent keeps running on its own digest', async () => {
    const res = await runTier1Job({
      prompt: 'adder',
      llm: makeLlm(async () => { throw new Error('model exploded'); }),
      runSuite: makeEvalSuiteRunner(),
    });
    expect(res.metrics.watcherSyntheses).toBeGreaterThanOrEqual(2);
    expect(res.problems).toEqual([]);
  });

  it('a valid reading becomes an advisory page carried into later draws + a ledger node', async () => {
    const prompts: string[] = [];
    const seen: string[] = [];
    const llm = makeLlm(async (p: string) => {
      prompts.push(p);
      return [
        'IDIOM: rebuilds whole files instead of editing — L4 present',
        'TROPE: assertion classes recur after heals',
        'SCHEME: contract-first freezing holds',
        'UNION: the run is mechanically healthy but assertion-bound.',
      ].join('\n');
    });
    // Capture what the repair draw actually receives.
    const wrapped = {
      ...(llm as unknown as Record<string, unknown>),
      async repair(i: RepairInput) {
        seen.push(i.failureOutput ?? '');
        return (i.feature as { files: string[] }).files.map(p => ({ path: p, content: RIGHT }));
      },
    } as unknown as LlmClient;

    const res = await runTier1Job({ prompt: 'adder', llm: wrapped, runSuite: makeEvalSuiteRunner() });
    // The engine saw the deterministic digest, not free-form context.
    // SET 76 — synthesis is node-scoped: the salient node leads the digest.
    expect(prompts[0]).toMatch(/^Supergit state at "\w+" for F\d/);
    expect(prompts[0]).toContain("This node's trail");
    expect(prompts[0]).toContain('Report in EXACTLY this format');
    // One reading per epoch-cycle, 3 named observations each — the agent
    // fires on its own schedule, the engine answers the same way every time.
    expect(res.metrics.watcherReadings).toBe(prompts.length);
    expect(res.metrics.watcherObservations).toBe(3 * res.metrics.watcherReadings);
    // The observation reached a subsequent draw as advisory context…
    expect(seen.some(f => /observation only, not instruction/.test(f))).toBe(true);
    expect(seen.some(f => /mechanically healthy but assertion-bound/.test(f))).toBe(true);
    // …and is recorded in the ledger as an advisory lesson node.
    const lesson = res.supergit!.rows.find(r => r.type === 'lesson' && /watcher @/.test(r.summary));
    expect(lesson).toBeTruthy();
    expect(lesson!.status).toBe('info');
    expect(lesson!.summary).toContain('advisory');
  });
});

describe('SET 75 — supergit read-back closes the write-only gap', () => {
  it('the repair dossier carries this node’s causal trail from the ledger', async () => {
    const dossiers: string[] = [];
    const base = makeLlm();
    const llm = {
      ...(base as unknown as Record<string, unknown>),
      async repair(i: RepairInput) {
        dossiers.push(i.failureOutput ?? '');
        return (i.feature as { files: string[] }).files.map(p => ({ path: p, content: RIGHT }));
      },
    } as unknown as LlmClient;
    const res = await runTier1Job({ prompt: 'adder', llm, runSuite: makeEvalSuiteRunner() });
    expect(dossiers.length).toBeGreaterThan(0);
    const withLedger = dossiers.find(d => /Page ledger — supergit causal trail/.test(d));
    expect(withLedger).toBeTruthy();
    // The trail cites real ledger nodes (N#### ids) and their types.
    expect(withLedger!).toMatch(/N\d{4} \[(error|result|code|test|spec)/);
    expect(res.problems).toEqual([]);
  });
});
