/**
 * rigor/complex-graph-overlay-crucible.test.js — partial-graph overlay over
 * the complex graphs (community block model + org hierarchy).
 *
 * An overlay is a caller-supplied set of facts consulted alongside the
 * persistent graph. The check option key for a pre-built
 * PartialGraphContext is `partialGraphContext` (AuthorizationChecker
 * reads that key; passing the context under `partialGraph` would be
 * re-ingested as a raw spec and silently empty). The persistent relation
 * is ORed into the direct lookup, so it wins when both are present.
 *
 *   OVERLAY-SURFACES        — with no persistent edge, an overlay fact
 *                             grants exactly its possibility on a direct
 *                             relation.
 *   PERSISTENT-WINS         — persistent + overlay -> persistent value;
 *                             removing the persistent edge surfaces the
 *                             overlay.
 *   OVERLAY-BINARY-PARITY   — binary and normal agree on the same overlay
 *                             (binary.allow === (normal >= 0.8), and the
 *                             direct path returns the overlay possibility
 *                             in both modes).
 *   OVERLAY-ON-COMPLEX      — an overlay fact on a DIRECT relation that a
 *                             complex policy consumes (union: direct_access;
 *                             chain: member/parent/owns) surfaces through
 *                             that policy. Overlay facts on a relation name
 *                             that is itself configured tuple_to_userset
 *                             are ignored by the TTU evaluator, so the
 *                             overlay must ride the direct edge.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { PartialGraphContext } from '../../src/core/PartialGraphContext.js';
import { makeCommunityGraph, makeHierarchyGraph } from './complex-graphs.js';

const EPS = 1e-9;
const P_OVERLAY = [0.2, 0.4, 0.6, 0.8, 0.9];
const P_PERSISTENT = 0.85;

const GENERATORS = [
  { name: 'community', make: makeCommunityGraph, directRel: 'direct_access', complexRel: 'can_read_with_direct' },
  { name: 'hierarchy', make: makeHierarchyGraph, directRel: 'owns', complexRel: 'can_access_org' }
];

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

describe('Complex-graph overlay crucibles (rigor)', () => {
  it('OVERLAY-SURFACES / PERSISTENT-WINS / BINARY-PARITY / ON-COMPLEX hold across complex graphs', async () => {
    async function check(args) {
      const { genKind, seed, layer } = args;
      const spec = GENERATORS.find(g => g.name === genKind);
      const g = spec.make(seed);
      const arbiter = g.arbiter;
      const u = g.users[0];
      const pOverlay = P_OVERLAY[layer % P_OVERLAY.length];

      let o;
      if (genKind === 'community') {
        // Pick a resource with NO persistent can_read_with_direct path, so
        // the overlay is the only source for the complex-policy check.
        o = g.resources.find(r => arbiter.check(u, spec.complexRel, r).possibility === 0);
        if (!o) fail(`[surfaces] no zero-persistent resource on community seed=${seed}`);
        // Drop any persistent direct_access edge on the triple.
        for (const r of (g.relations || [])) {
          if (r.src === u && r.rel === 'direct_access' && r.dst === o) arbiter.removeRelation(u, 'direct_access', o);
        }
      } else {
        // Hierarchy: users never hold persistent 'owns' edges, so any
        // resource is overlay-clean on the direct relation.
        o = g.resources[0];
      }

      const ctx = new PartialGraphContext(arbiter, {
        relations: [{ src: u, relation: spec.directRel, dst: o, possibility: pOverlay }]
      });
      const checkWithOverlay = (rel, object, ctxFor) =>
        arbiter.check(u, rel, object, { partialGraphContext: ctxFor });

      // OVERLAY-SURFACES: no persistent edge -> overlay grants exactly pOverlay.
      const surfaced = checkWithOverlay(spec.directRel, o, ctx).possibility;
      if (Math.abs(surfaced - pOverlay) > EPS) {
        fail(`[surfaces] overlay ${pOverlay} did not surface on ${genKind} ${u}->${o}: ${surfaced}`);
      }

      // PERSISTENT-WINS: persistent edge wins; removal surfaces the overlay.
      arbiter.addRelation(u, spec.directRel, o, { possibility: P_PERSISTENT });
      const withBoth = checkWithOverlay(spec.directRel, o, ctx).possibility;
      if (Math.abs(withBoth - P_PERSISTENT) > EPS) {
        fail(`[wins] persistent ${P_PERSISTENT} did not win over overlay ${pOverlay}: ${withBoth}`);
      }
      arbiter.removeRelation(u, spec.directRel, o);
      const resurfaced = checkWithOverlay(spec.directRel, o, ctx).possibility;
      if (Math.abs(resurfaced - pOverlay) > EPS) {
        fail(`[wins] overlay did not resurface after persistent removal: ${resurfaced}`);
      }

      // OVERLAY-BINARY-PARITY on the direct relation.
      const normal = checkWithOverlay(spec.directRel, o, ctx);
      const binary = arbiter.check(u, spec.directRel, o, { partialGraphContext: ctx, binary: true });
      if (Math.abs(binary.possibility - normal.possibility) > EPS) {
        fail(`[binary] direct overlay binary=${binary.possibility} normal=${normal.possibility} disagree`);
      }
      if (binary.allow !== (normal.possibility >= 0.8)) {
        fail(`[binary] binary.allow=${binary.allow} != normal>=0.8 (${normal.possibility})`);
      }

      // OVERLAY-ON-COMPLEX: the overlay rides a direct relation the policy
      // consumes and surfaces through the complex relation.
      if (genKind === 'community') {
        const viaUnion = checkWithOverlay(spec.complexRel, o, ctx).possibility;
        if (Math.abs(viaUnion - pOverlay) > EPS) {
          fail(`[complex] overlay ${pOverlay} did not surface through union ${spec.complexRel}: ${viaUnion}`);
        }
      } else {
        // can_access_org = member -> parent -> owns. The overlay provides the
        // full chain; the last hop carries the overlay strength.
        const team = g.teams[0];
        const dept = team.split(':team:')[0];
        const org = 'org:0';
        const chainCtx = new PartialGraphContext(arbiter, {
          relations: [
            { src: u, relation: 'member', dst: team, possibility: 1 },
            { src: team, relation: 'parent', dst: dept, possibility: 1 },
            { src: dept, relation: 'owns', dst: org, possibility: pOverlay }
          ]
        });
        const viaChain = checkWithOverlay(spec.complexRel, org, chainCtx).possibility;
        if (Math.abs(viaChain - pOverlay) > EPS) {
          fail(`[complex] overlay ${pOverlay} did not surface through chain ${spec.complexRel}: ${viaChain}`);
        }
      }
      return { ok: true };
    }

    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),
            layer: rigor.gen.int(0, 4)
          })
        ))
      ],
      rigor.crucible([
        rigor.invariant('overlay-surfaces', ({ actual }) => actual !== undefined),
        rigor.invariant('persistent-wins', ({ actual }) => actual !== undefined),
        rigor.invariant('overlay-binary-parity', ({ actual }) => actual !== undefined),
        rigor.invariant('overlay-on-complex', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 200, seed: 'complex-graph-overlay-crucible', artifacts: { dir: '', persist: 'never' } });

    for (const name of ['overlay-surfaces', 'persistent-wins', 'overlay-binary-parity', 'overlay-on-complex']) {
      const inv = report.crucibleVerdict?.invariants?.find(i => i.name === name);
      assert.ok(inv, `invariant ${name} missing`);
      assert.equal(inv.passed, true, `overlay ${name} violated in ${inv.failureCount} cases`);
    }
  });
});
