/**
 * rigor/complex-graph-mutation-crucible.test.js — mutation crucible over
 * the complex graphs.
 *
 * Parity under mutation is the engine's stale-cache hunter: random edge
 * removals and additions on community/scale-free graphs, with
 * normal/binary agreement re-checked after EVERY mutation. A stale cache
 * or an index desync surfaces as a disagreement on the very next query.
 *
 *   MUTATION-FRESHNESS — the LIVE engine's grant is revoked/applied
 *     immediately after each mutation (no stale cache on the writable
 *     path), and binary mode agrees with normal at every step.
 *
 * Snapshot discipline: enableCondensedSnapshot() permanently flips the
 * engine to read-only snapshot mode (writes throw), so the snapshot
 * cannot be taken mid-mutation on the live engine. The writable engine
 * and the read-only snapshot are therefore separate instances; the
 * round-trip parity property (snapshot == live at the same state) is
 * already covered by complex-graph-crucible and snapshot-parity.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { makeCommunityGraph, makeScaleFreeGraph } from './complex-graphs.js';

const GENERATORS = [
  { name: 'community', make: makeCommunityGraph, directRel: 'direct_access' },
  { name: 'scale-free', make: makeScaleFreeGraph, directRel: 'can_read' }
];

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

function checkModes(arbiter, user, relation, object) {
  const normal = arbiter.check(user, relation, object);
  const binary = arbiter.check(user, relation, object, { binary: true });
  if (normal.possibility > 0 !== binary.possibility > 0) {
    fail(`binary disagreement: normal=${normal.possibility} binary=${binary.possibility}`);
  }
  return normal;
}

describe('Complex-graph mutation crucibles (rigor)', () => {
  it('MUTATION-FRESHNESS: live grants revoke/apply immediately; binary agrees', async () => {
    async function check(args) {
      const { genKind, seed } = args;
      const spec = GENERATORS.find(g => g.name === genKind);
      const g = spec.make(seed);
      const arbiter = g.arbiter;
      const rel = spec.directRel;
      const allKeys = [...g.users, ...(g.resources || [])];

      for (let s = 0; s < 6; s++) {
        const u = allKeys[Math.floor(Math.random() * allKeys.length)];
        const o = allKeys[Math.floor(Math.random() * allKeys.length)];
        const before = checkModes(arbiter, u, rel, o).possibility;
        if (before > 0) {
          // Revoke every direct edge between u and o, then verify 0.
          // NOTE: arbiter.relations stores NUMERIC ids; compare against
          // resolved ids, never string keys.
          const srcId = arbiter.resolveNodeId(u);
          const dstId = arbiter.resolveNodeId(o);
          const rm = (g.relations || []).filter(r => r.src === u && r.rel === rel && r.dst === o);
          if (rm.length > 0) {
            for (const e of rm) arbiter.removeRelation(e.src, e.rel, e.dst);
          } else {
            for (const r of (arbiter.relations || [])) {
              if (r.src === srcId && r.rel === rel && r.dst === dstId) arbiter.removeRelation(u, rel, o);
            }
          }
          const after = checkModes(arbiter, u, rel, o).possibility;
          if (after > 0) fail(`stale grant: ${u} ${rel} ${o} still ${after} after removal`);
        } else {
          // Grant a direct edge, verify it shows up immediately.
          arbiter.addRelation(u, rel, o, { possibility: 0.9 });
          const after = checkModes(arbiter, u, rel, o).possibility;
          if (after <= 0) fail(`missing grant: ${u} ${rel} ${o} still 0 after add`);
        }
      }
      return { ok: true };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('mutate', check, rigor.args(
          rigor.gen.object({
            genKind: rigor.gen.oneOf(GENERATORS.map(g => g.name)),
            seed: rigor.gen.int(1, 8)
          })
        ))
      ],
      rigor.crucible([
        rigor.invariant('mutation-freshness', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 300, seed: 'complex-graph-mutation-freshness', artifacts: { dir: '', persist: 'never' } });

    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'mutation-freshness');
    assert.ok(inv);
    assert.equal(inv.passed, true, `mutation freshness violated in ${inv.failureCount} cases`);
  });
});
