#!/usr/bin/env node
'use strict';
// בדיקות יחידה ל-DAG מותנה/מסועף (שערים/ברירה/איחוד + גיזום) ולשכבת-השמות
// הנודדת SkyLattice-Roam (שרשרת-גיבוב, seq מונוטוני, הגנת-rollback, נדידת-ממסרים).
process.env.SC_TEST = '1';
const t = require('./dump-test.js').__test;
const assert = require('node:assert');

let pass = 0;
const ok = (name, cond) => { assert.ok(cond, name); console.log('PASS ' + name); pass++; };
const solve = (program) => Promise.resolve(t.xiSolve(program));

(async () => {
  // ═══════════ DAG מותנה: שער when שמדלג ומגזם תת-ענף ═══════════
  const guarded = {
    nodes: {
      decide: { op: 'primes', args: { upTo: 100 } },                        // count=25
      // רץ רק אם count>20:
      big: { op: 'formula', args: { expr: 'c*100', vars: { c: { $ref: 'decide', path: 'count' } } }, when: { expr: 'c>20', vars: { c: { $ref: 'decide', path: 'count' } } } },
      // רץ רק אם count<=20 (יידולג):
      small: { op: 'formula', args: { expr: 'c', vars: { c: { $ref: 'decide', path: 'count' } } }, when: { expr: 'c<=20', vars: { c: { $ref: 'decide', path: 'count' } } } },
      out: { coalesce: [{ $ref: 'big' }, { $ref: 'small' }] },
    },
    output: 'out',
  };
  const g = await t.xiDagEval(guarded, solve);
  ok('cond DAG runs the TRUE branch (big=2500)', g.output === 2500);
  ok('cond DAG PRUNES the false branch (small skipped)', g.skipped.includes('small') && g.results.small === null);
  ok('cond DAG coalesce picks the branch that ran', g.results.out === 2500);

  // גיזום מתפשט: צומת-חישוב שתלוי בצומת שדולג — מדולג בעצמו
  const prune = {
    nodes: {
      gate: { op: 'formula', args: { expr: '5' }, when: { expr: '1<0' } },   // תמיד מדולג
      dep: { op: 'formula', args: { expr: 'x+1', vars: { x: { $ref: 'gate' } } } }, // חייב להידלג
    },
    output: 'dep',
  };
  const pr = await t.xiDagEval(prune, solve);
  ok('cond DAG skip propagates to dependents', pr.skipped.includes('gate') && pr.skipped.includes('dep') && pr.outputSkipped === true);

  // ═══════════ DAG מסועף: branch בוחר case לפי נוסחת-החלטה ═══════════
  const branched = {
    nodes: {
      n: { op: 'primes', args: { upTo: 100 } },                              // count=25
      hi: { op: 'formula', args: { expr: '999' } },
      lo: { op: 'formula', args: { expr: '111' } },
      pick: { branch: { on: 'if(c>20,1,2)', vars: { c: { $ref: 'n', path: 'count' } }, cases: { 1: { $ref: 'hi' }, 2: { $ref: 'lo' } }, default: { $ref: 'lo' } } },
    },
    output: 'pick',
  };
  const br = await t.xiDagEval(branched, solve);
  ok('branch selects case by FormulaVM decision (→hi=999)', br.output === 999);

  // ═══════════ Roam: שרשרת-גיבוב + seq מונוטוני + resolve ═══════════
  const r0 = t.roamRecord('site', 'hashA', 0, null, 1000);
  const h0 = t.roamHash(r0);
  const r1 = t.roamRecord('site', 'hashB', 1, h0, 1001);
  const h1 = t.roamHash(r1);
  ok('Roam hash deterministic', t.roamHash(t.roamRecord('site', 'hashA', 0, null, 1000)) === h0);
  ok('Roam hash changes with target', t.roamHash(t.roamRecord('site', 'hashZ', 0, null, 1000)) !== h0);

  const R = new t.RoamResolver();
  R.ingest(r0); R.ingest(r1);
  ok('Roam resolves latest seq', R.resolve() === 'hashB' && R.current.seq === 1);
  ok('Roam chain links (prev matches, no gap)', R.gaps === 0);

  // הגנת-rollback: רשומה ישנה (seq נמוך) נדחית
  const R2 = new t.RoamResolver();
  R2.ingest(r1);                       // seq 1 קודם
  const accepted = R2.ingest(r0);      // seq 0 — צריך להידחות
  ok('Roam rejects rollback to older seq', accepted === false && R2.resolve() === 'hashB');

  // תיקו-seq → הכרעה דטרמיניסטית לפי hash גדול
  const a = t.roamRecord('x', 'AAA', 5, null, 10), b = t.roamRecord('x', 'BBB', 5, null, 20);
  const ha = t.roamHash(a), hb = t.roamHash(b);
  const R3 = new t.RoamResolver(); R3.ingest(a); R3.ingest(b);
  ok('Roam deterministic tiebreak on equal seq (larger hash wins)', R3.current.hash === (ha > hb ? ha : hb));

  // ניתוק-שרשרת מזוהה לשקיפות
  const R4 = new t.RoamResolver(); R4.ingest(r0);
  R4.ingest(t.roamRecord('site', 'hashC', 1, 'WRONGPREV', 1002));
  ok('Roam flags chain gap when prev mismatches', R4.gaps === 1 && R4.resolve() === 'hashC');

  // ═══════════ נדידת-ממסרים: תת-קבוצה מסתובבת ומעדיפה בריאים ═══════════
  const scored = [{ id: 'r1', score: 0.9 }, { id: 'r2', score: 0.8 }, { id: 'r3', score: 0.7 }, { id: 'r4', score: 0.6 }];
  const s0 = t.roamRelaySubset(scored, 2, 0);
  const s1 = t.roamRelaySubset(scored, 2, 1);
  ok('Roam subset favors healthy + size k', s0.length === 2 && s0[0] === 'r1');
  ok('Roam subset ROTATES across epochs (moves in space)', JSON.stringify(s0) !== JSON.stringify(s1));
  ok('Roam subset deterministic for same epoch', JSON.stringify(t.roamRelaySubset(scored, 2, 5)) === JSON.stringify(t.roamRelaySubset(scored, 2, 5)));
  ok('Roam subset drops dead relays (score 0)', t.roamRelaySubset([{ id: 'a', score: 0 }, { id: 'b', score: 0.5 }], 2, 0).join() === 'b');

  // ═══════════ ∇: client-eval — כל סט-קלטים בלי מטמון חם ═══════════
  const siteDag = {
    nodes: {
      n: { value: 25 },
      big: { op: 'formula', args: { expr: 'x*100', vars: { x: { $ref: 'n' } } }, when: { expr: 'x>20', vars: { x: { $ref: 'n' } } } },
      small: { op: 'factorize', args: { n: { $ref: 'n' } }, when: { expr: 'x<=20', vars: { x: { $ref: 'n' } } } },
      out: { coalesce: [{ $ref: 'big' }, { $ref: 'small' }] },
    },
    output: 'out',
  };
  const pol = t.xiCollapse(siteDag);
  ok('∇ collapse produces client-dag policy', pol.kind === 'client-dag' && pol.light === true && pol.dagId);
  const c1 = await t.xiClientEval(siteDag, {});
  ok('∇ client-eval default inputs (25→2500)', c1.output === 2500 && c1.source === 'client-eval');
  const c2 = await t.xiClientEval(siteDag, { n: 13 }); // סט שמעולם לא חומם
  ok('∇ client-eval UNSEEN inputs without warm cache (13→factorize)', JSON.stringify(c2.output) === JSON.stringify([13]));
  const c3 = await t.xiClientEval(siteDag, { n: 8 });
  ok('∇ client-eval another unseen set (8→[2,2,2])', JSON.stringify(c3.output) === JSON.stringify([2, 2, 2]));
  ok('∇ unseen inputs produce different dagIds (cache keys diverge)', c2.dagId !== c3.dagId);

  console.log('\nALL ' + pass + ' ROAM/COND/NABLA TESTS PASSED');
})().catch((e) => { console.error('FAIL:', e.message); process.exit(1); });
