/**
 * rigor/complex-graph-batch-crucible.test.js — batch-loading and decision
 * cache parity on the community graph.
 *
 * Two engines are rebuilt from the same makeCommunityGraph(seed) fixture:
 * one loaded relation-by-relation, one loaded through
 * relationManager.addRelationsBatch (which expects { srcKey, relation,
 * dstKey, options } — the generator's { src, rel, dst, possibility }
 * objects are mapped into that shape).
 *
 *   BATCH-SEQUENTIAL-PARITY — identical check answers on both engines for
 *                             sampled (user, relation, object) triples
 *                             across direct, TTU, union, exclusion, and
 *                             chain policies.
 *   BATCH-MUTATION          — after the same edge mutation on both engines,
 *                             parity holds and the post-mutation answer is
 *                             fresh (the decision cache is invalidated, not
 *                             served stale) — even right after a warm read.
 *   FIXTURE-SIZE            — the community fixture actually carries a
 *                             batch-sized relation set (> 50 edges).
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';
import { makeCommunityGraph } from './complex-graphs.js';

const EPS = 1e-9;
const POLICIES = ['direct_access', 'can_read', 'can_read_with_direct', 'can_read_not_blocked', 'can_delegate_read'];

function fail(message) {
  throw new Error(message);
}

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 rebuildForBatch(g) {
  const arb = new Arbiter();
  for (const node of g.arbiter.nodes.values()) arb.addNode(node.key, node.type);
  for (const [name, config] of g.arbiter.relationConfigs.entries()) arb.setRelationConfig(name, config);
  const edges = g.relations.map(r => ({
    srcKey: r.src,
    relation: r.rel,
    dstKey: r.dst,
    options: { possibility: r.possibility }
  }));
  arb.relationManager.addRelationsBatch(edges);
  return arb;
}

function sampleTriples(g, rng) {
  const triples = [];
  // Real graph edges exercise the actual membership/ownership structure.
  for (const r of g.relations.slice(0, 40)) triples.push([r.src, r.rel, r.dst]);
  for (let i = 0; i < 40; i++) {
    const u = g.users[Math.floor(rng() * g.users.length)];
    const o = g.resources[Math.floor(rng() * g.resources.length)];
    triples.push([u, POLICIES[Math.floor(rng() * POLICIES.length)], o]);
  }
  return triples;
}

function assertParity(seq, batch, triples, tag) {
  for (const [u, rel, o] of triples) {
    const a = seq.check(u, rel, o).possibility;
    const b = batch.check(u, rel, o).possibility;
    if (Math.abs(a - b) > EPS) {
      fail(`[parity] ${tag} ${u} ${rel} ${o}: seq=${a} batch=${b}`);
    }
  }
}

describe('Complex-graph batch/cache crucibles (rigor)', () => {
  it('BATCH-SEQUENTIAL-PARITY + BATCH-MUTATION + CACHE-FRESHNESS hold on the community graph', async () => {
    async function check(args) {
      const { seed, mode } = args;
      const g = makeCommunityGraph(seed);
      const seq = g.arbiter;
      if (seq.relations.length <= 50) {
        fail(`[fixture] community fixture has only ${seq.relations.length} relations`);
      }
      const batch = rebuildForBatch(g);
      const rng = mulberry32(seed * 101);

      // BATCH-SEQUENTIAL-PARITY on the untouched graph.
      const triples = sampleTriples(g, rng);
      assertParity(seq, batch, triples, 'initial');

      // BATCH-MUTATION + CACHE-INTERACTION. mode 0 removes an existing
      // direct_access edge; mode 1 adds one to a triple that has none.
      let pair;
      if (mode === 0) {
        pair = g.relations.find(r => r.rel === 'direct_access');
        if (!pair) fail(`[fixture] no direct_access edge on seed=${seed}`);
      } else {
        const clean = () => {
          for (let i = 0; i < 200; i++) {
            const u = g.users[Math.floor(rng() * g.users.length)];
            const o = g.resources[Math.floor(rng() * g.resources.length)];
            if (g.relations.some(r => r.src === u && r.rel === 'direct_access' && r.dst === o)) continue;
            if (seq.check(u, 'direct_access', o).possibility !== 0) continue;
            return { src: u, rel: 'direct_access', dst: o };
          }
          return null;
        };
        pair = clean();
        if (!pair) fail(`[fixture] no clean direct_access pair on seed=${seed}`);
      }

      // Warm the decision cache on both engines before mutating.
      seq.check(pair.src, 'direct_access', pair.dst);
      batch.check(pair.src, 'direct_access', pair.dst);
      batch.check(pair.src, 'direct_access', pair.dst);

      const expectedAfter = mode === 0 ? 0 : 0.77;
      if (mode === 0) {
        seq.removeRelation(pair.src, 'direct_access', pair.dst);
        batch.removeRelation(pair.src, 'direct_access', pair.dst);
      } else {
        seq.addRelation(pair.src, 'direct_access', pair.dst, { possibility: expectedAfter });
        batch.addRelation(pair.src, 'direct_access', pair.dst, { possibility: expectedAfter });
      }
      const sa = seq.check(pair.src, 'direct_access', pair.dst).possibility;
      const ba = batch.check(pair.src, 'direct_access', pair.dst).possibility;
      if (Math.abs(sa - ba) > EPS) fail(`[mutation] seq=${sa} batch=${ba} diverge after mutation`);
      if (Math.abs(ba - expectedAfter) > EPS) {
        fail(`[cache] batched engine returned ${ba}, expected ${expectedAfter} after mutation (stale cache)`);
      }
      if (Math.abs(sa - expectedAfter) > EPS) {
        fail(`[cache] sequential engine returned ${sa}, expected ${expectedAfter} after mutation`);
      }

      // No divergence on the broader policy surface after the mutation.
      assertParity(seq, batch, triples, 'post-mutation');
      return { relations: seq.relations.length, triples: triples.length };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({
            seed: rigor.gen.int(1, 6),
            mode: rigor.gen.oneOf([0, 1])
          })
        ))
      ],
      rigor.crucible([
        rigor.invariant('batch-sequential-parity', ({ actual }) => actual !== undefined),
        rigor.invariant('batch-mutation', ({ actual }) => actual !== undefined),
        rigor.invariant('cache-interaction', ({ actual }) => actual !== undefined),
        rigor.invariant('fixture-size', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 150, seed: 'complex-graph-batch-crucible', artifacts: { dir: '', persist: 'never' } });

    for (const name of ['batch-sequential-parity', 'batch-mutation', 'cache-interaction', 'fixture-size']) {
      const inv = report.crucibleVerdict?.invariants?.find(i => i.name === name);
      assert.ok(inv, `invariant ${name} missing`);
      assert.equal(inv.passed, true, `batch ${name} violated in ${inv.failureCount} cases`);
    }
  });
});
