/**
 * Part O4 — incremental subtree recompute.
 *
 * After a green mock pipeline, recompute F3: only F3's suite may change;
 * every other freeze hash and owned code file stays byte-identical; ledger
 * records the recompute as append-only decision nodes; dependents are
 * healed via applyRha (not rebuilt).
 */
import { describe, expect, it, beforeAll, afterAll } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { JobState } from '../src/core/types';
import { runBuild, runEncodingAndIR, verifyFreeze } from '../src/core/pipeline';
import { applyRha } from '../src/core/rha';
import { recomputeFeatureSubtree } from '../src/core/recompute';
import { sha256 } from '../src/core/sandbox';
import { MockLlm } from '../src/mock/mockLlm';
import type { Substrate } from '../src/core/substrate';

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const WS = path.join(ROOT, 'workspaces', `o4-${Date.now().toString(36)}`);

const emit = () => {};

let job: JobState;
let substrate: Substrate;

beforeAll(() => {
  fs.mkdirSync(path.dirname(WS), { recursive: true });
});

afterAll(() => {
  fs.rmSync(WS, { recursive: true, force: true });
});

describe('O4 incremental subtree recompute', () => {
  const llm = new MockLlm();

  it('runs full pipeline to complete', async () => {
    job = {
      id: 'o4', title: '', prompt: 'A URL shortener library with TTL and hit stats.',
      phase: 'created', createdAt: new Date().toISOString(), workspaceDir: WS,
      spec: null, manifest: null, buildResults: [], report: null, promptAssessment: null,
      irEscalations: [], error: null, events: [], llmUsage: [],
    };
    ({ substrate } = await runEncodingAndIR(job, llm, emit));
    await runBuild(job, substrate, llm, emit);
    const report = await applyRha(WS, job.spec!, job.manifest!, substrate, llm, emit, job);
    expect(report.failed).toBe(0);
    job.report = report;
    job.phase = 'complete';
  }, 300_000);

  it('recomputes F3 only — other freeze hashes + code stay byte-identical', async () => {
    const beforeHashes = { ...job.manifest!.freezeHashes };
    const beforeCode = new Map<string, string>();
    for (const f of job.manifest!.features) {
      if (f.id === 'F3') continue;
      for (const p of f.files) {
        beforeCode.set(p, sha256(fs.readFileSync(path.join(WS, p), 'utf8')));
      }
    }
    const f3TestBefore = beforeHashes[job.spec!.features.find(f => f.id === 'F3')!.testFile];
    const ledgerBefore = substrate.allRows().length;

    const nextSpec = {
      ...job.spec!,
      features: job.spec!.features.map(f =>
        f.id === 'F3'
          ? { ...f, description: f.description + ' [recompute: clarify TTL boundary]' }
          : f,
      ),
    };

    await recomputeFeatureSubtree(job, substrate, llm, emit, {
      featureId: 'F3',
      spec: nextSpec,
    });

    expect(job.phase).toBe('complete');
    expect(job.spec!.features.find(f => f.id === 'F3')!.description).toContain('[recompute:');

    // Unaffected suites: same freeze hash on disk and in manifest
    for (const [file, hash] of Object.entries(beforeHashes)) {
      if (file === job.spec!.features.find(f => f.id === 'F3')!.testFile) continue;
      expect(job.manifest!.freezeHashes[file]).toBe(hash);
      expect(sha256(fs.readFileSync(path.join(WS, file), 'utf8'))).toBe(hash);
    }
    // Unaffected code files untouched
    for (const [file, hash] of beforeCode) {
      expect(sha256(fs.readFileSync(path.join(WS, file), 'utf8'))).toBe(hash);
    }
    verifyFreeze(WS, job.manifest!);

    // F3 was in the recompute path (ledger append-only, no truncation)
    const rows = substrate.allRows();
    expect(rows.length).toBeGreaterThan(ledgerBefore);
    expect(rows.some(r => r.summary.includes('incremental recompute of F3'))).toBe(true);
    expect(rows.some(r => r.summary.includes('recompute IR suite for F3'))).toBe(true);
    expect(rows.some(r => r.summary.includes('recompute implementation of F3'))).toBe(true);
    expect(rows.some(r => /blast radius|dependents/.test(r.summary))).toBe(true);

    // Mock returns the same suite — F3 freeze hash may be unchanged; that's fine.
    // Contract is "others identical", not "target must change".
    expect(job.manifest!.freezeHashes[job.spec!.features.find(f => f.id === 'F3')!.testFile]).toBeTruthy();
    void f3TestBefore;
  }, 300_000);
});
