/**
 * rigor/complex-graph-reachability-crucible.test.js — PLTC reachability
 * over the scale-free and community graphs, compared against ground-truth
 * directed DFS.
 *
 * The PLTC index is built from every relation in arbiter.relations (edge
 * src -> dst, deduplicated by pair), so the ground truth is the same edge
 * set: the generator's relations for community, and the live engine's
 * relation rows for scale-free (whose generator returns `relations:
 * null`). All of these relations are configured `direct`, so "direct
 * relations" and "all relations" coincide.
 *
 *   VERDICT-PARITY — a non-null isReachable verdict equals ground truth.
 *   FAST-FAIL-SOUNDNESS — isReachable(...) === true implies ground truth
 *     is true (PLTC must not manufacture reachability).
 *   NULL-DEFER — isReachable returns null when the PLTC index is
 *     unavailable; a null verdict is the documented "delegate to rules"
 *     contract and is skipped, never failed.
 */
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 SAMPLES = 12;

const GENERATORS = [
  { name: 'community', make: makeCommunityGraph, opts: {} },
  // Default scale-free (150 users / 400 edges) is slow to build per case;
  // a 60-user power-law graph keeps the PLTC-vs-DFS comparison meaningful
  // without dominating the suite.
  { name: 'scale-free', make: makeScaleFreeGraph, opts: { users: 60, resources: 20, edges: 150 } }
];

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 allNodeKeys(g) {
  const keys = [...g.users, ...(g.resources || []), ...(g.groups || []), ...(g.subGroups || [])];
  return keys.filter(k => g.arbiter.nodeIdByKey.has(k));
}

function groundTruthEdges(g) {
  const seen = new Set();
  const edges = [];
  const push = (s, d) => {
    const k = `${s}\u0000${d}`;
    if (!seen.has(k)) { seen.add(k); edges.push([s, d]); }
  };
  if (g.relations) {
    for (const r of g.relations) push(r.src, r.dst);
  } else {
    for (const r of g.arbiter.relations) {
      const sk = g.arbiter.keyByNodeId.get(r.src);
      const dk = g.arbiter.keyByNodeId.get(r.dst);
      if (sk !== undefined && dk !== undefined) push(sk, dk);
    }
  }
  return edges;
}

function buildAdjacency(keys, edges) {
  const adj = new Map();
  for (const k of keys) adj.set(k, []);
  for (const [s, d] of edges) {
    if (!adj.has(s)) adj.set(s, []);
    if (!adj.has(d)) adj.set(d, []);
    adj.get(s).push(d);
  }
  return adj;
}

function dfsReachable(adj, src, dst) {
  if (src === dst) return true;
  const visited = new Set([src]);
  const stack = [src];
  while (stack.length) {
    const cur = stack.pop();
    for (const next of adj.get(cur) || []) {
      if (next === dst) return true;
      if (!visited.has(next)) { visited.add(next); stack.push(next); }
    }
  }
  return false;
}

describe('Complex-graph PLTC reachability crucibles (rigor)', () => {
  it('VERDICT-PARITY + FAST-FAIL-SOUNDNESS against ground-truth DFS, with NULL-DEFER', async () => {
    async function check(args) {
      const { genKind, seed, srcIdx, dstIdx } = args;
      const spec = GENERATORS.find(g => g.name === genKind);
      const g = spec.make(seed, spec.opts);
      const keys = allNodeKeys(g);
      const adj = buildAdjacency(keys, groundTruthEdges(g));

      await g.arbiter.initializeReachabilityChecker();

      const rng = mulberry32(seed * 7919 + 17);
      // The campaign's srcIdx/dstIdx seed the first pair; the rest are
      // drawn from a per-case deterministic stream so every case samples
      // more than one edge of the graph.
      let nonNull = 0;
      let verdictChecks = 0;
      for (let i = 0; i < SAMPLES; i++) {
        const a = i === 0 ? keys[srcIdx % keys.length] : keys[Math.floor(rng() * keys.length)];
        const b = i === 0 ? keys[dstIdx % keys.length] : keys[Math.floor(rng() * keys.length)];
        const v = g.arbiter.isReachable(a, b);
        if (v === null) continue; // PLTC unavailable -> defer to rule eval
        nonNull++;
        const gt = dfsReachable(adj, a, b);
        verdictChecks++;
        if (v !== gt) {
          fail(`[verdict] ${genKind} seed=${seed}: isReachable(${a},${b})=${v} != ground truth ${gt}`);
        }
        if (v === true && gt !== true) {
          fail(`[soundness] ${genKind} seed=${seed}: PLTC false positive on ${a}->${b}`);
        }
      }
      // Vacuity guard: a case whose sampled pairs all hit the NULL-DEFER
      // path proves nothing about verdict parity.
      if (verdictChecks === 0) {
        fail(`[vacuity] no non-null PLTC verdicts sampled for ${genKind} seed=${seed}`);
      }
      return { nonNull, verdictChecks };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({
            genKind: rigor.gen.oneOf(GENERATORS.map(g => g.name)),
            seed: rigor.gen.int(1, 6),
            srcIdx: rigor.gen.int(0, 199),
            dstIdx: rigor.gen.int(0, 199)
          })
        ))
      ],
      rigor.crucible([
        rigor.invariant('verdict-parity', ({ actual }) => actual !== undefined),
        rigor.invariant('fast-fail-soundness', ({ actual }) => actual !== undefined),
        rigor.invariant('null-defer-contract', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 200, seed: 'complex-graph-reachability-crucible', artifacts: { dir: '', persist: 'never' } });

    for (const name of ['verdict-parity', 'fast-fail-soundness', 'null-defer-contract']) {
      const inv = report.crucibleVerdict?.invariants?.find(i => i.name === name);
      assert.ok(inv, `invariant ${name} missing`);
      assert.equal(inv.passed, true, `reachability ${name} violated in ${inv.failureCount} cases`);
    }
  });
});
