/**
 * rigor/complex-graph-quantization-crucible.test.js — condensed-snapshot
 * quantization parity over the community graph.
 *
 * The generator emits varied non-dyadic possibilities (0.3..1.0). The
 * query set is drawn from the graph's OWN edges (direct_access, member-fed
 * TTU can_read, delegate-fed chain can_delegate_read) so the sampled
 * queries are granted and actually carry quantization error — random
 * (user, resource) pairs are mostly denied (live == restored == 0) and
 * would make the band vacuous.
 *
 *   QUANT-BAND  — |live - restored| <= 2 * QUANT_STEP (the chain path
 *                 multiplies two quantized inputs, so twice the single
 *                 value's band).
 *   DECISION    — outside the quantization band of the threshold the
 *                 allow/deny decision must agree; inside it a flip is
 *                 allowed (the existing snapshot-quantization-parity rule).
 *   RE-SERIALIZE— snapshot-of-snapshot (serialize the restored engine,
 *                 restore again) is semantically identical.
 *
 * Deterministic fixed loop over seeds — no campaign needed.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../src/index.js';
import { makeCommunityGraph } from './complex-graphs.js';

const QUANT_STEP = 0.5 / 65535;
const TOL = QUANT_STEP * 2;
const SAMPLES = 40;

function mulberry32(seed) {
  let a = seed >>> 0;
  return function () {
    a |= 0;
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function grantedQuerySet(g) {
  const qs = [];
  const ownsBySrc = new Map();
  for (const r of g.relations) if (r.rel === 'owns') ownsBySrc.set(r.src, r.dst);
  for (const r of g.relations) {
    if (r.rel === 'direct_access') qs.push([r.src, 'direct_access', r.dst]);
    if (r.rel === 'member') {
      const obj = ownsBySrc.get(r.dst);
      if (obj) {
        qs.push([r.src, 'can_read', obj]);
        qs.push([r.src, 'can_read_not_blocked', obj]);
      }
    }
    if (r.rel === 'delegate') {
      for (const m of g.relations) {
        if (m.rel === 'member' && m.dst === r.src) {
          qs.push([m.src, 'can_delegate_read', r.dst]);
          break;
        }
      }
    }
  }
  return qs;
}

function sample(querySet, seed) {
  const rng = mulberry32(seed * 997);
  const out = [];
  for (let i = 0; i < SAMPLES; i++) {
    out.push(querySet[Math.floor(rng() * querySet.length)]);
  }
  return out;
}

describe('Complex-graph snapshot quantization crucible', () => {
  it('QUANT-BAND + DECISION + RE-SERIALIZE hold across seeds', () => {
    for (const seed of [1, 2, 3, 4]) {
      const g = makeCommunityGraph(seed);
      const arbiter = g.arbiter;
      const queries = sample(grantedQuerySet(g), seed);
      assert.ok(queries.length > 0, `seed ${seed}: empty granted query set`);

      const live = queries.map(([u, rel, o]) => arbiter.check(u, rel, o).possibility);

      arbiter.enableCondensedSnapshot();
      const buf = arbiter.toSnapshotBinary();
      const restored = Arbiter.fromSnapshotBinary(buf);
      const restored2 = Arbiter.fromSnapshotBinary(restored.toSnapshotBinary());

      for (let i = 0; i < queries.length; i++) {
        const [u, rel, o] = queries[i];
        const lv = live[i];
        const rv = restored.check(u, rel, o).possibility;
        const r2v = restored2.check(u, rel, o).possibility;

        assert.ok(
          Math.abs(lv - rv) <= TOL,
          `seed ${seed} ${u} ${rel} ${o}: live=${lv} restored=${rv} exceeds ${TOL}`
        );

        // Allow/deny agreement on the 0.5 threshold, except inside the
        // quantization band where a flip is permitted.
        const inBand = Math.abs(lv - 0.5) < QUANT_STEP;
        if (!inBand) {
          assert.equal(
            rv >= 0.5, lv >= 0.5,
            `seed ${seed} ${u} ${rel} ${o}: decision flipped outside band (live=${lv} restored=${rv})`
          );
        }
        // Grant parity (possibility > 0): granted queries all carry
        // possibility >= 0.3, so a flip here would be a real loss.
        assert.equal(rv > 0, lv > 0, `seed ${seed} ${u} ${rel} ${o}: grant lost in restore`);

        // Snapshot-of-snapshot is semantically identical.
        assert.ok(
          Math.abs(r2v - rv) <= TOL,
          `seed ${seed} ${u} ${rel} ${o}: re-serialized=${r2v} != first restore=${rv}`
        );
      }
    }
  });
});
