/**
 * rigor/snapshot-parity.test.js — js-rigor property tests for the
 * condensed-snapshot round trip.
 *
 * Properties verified:
 *
 *   - ROUND-TRIP PARITY: for a random graph (direct + chain configs with
 *     possibilities and values), serializing via enableCondensedSnapshot +
 *     serializeArbiterSnapshot and deserializing yields an arbiter whose
 *     check() answers are IDENTICAL to the original's — for every user,
 *     relation and object in the graph.
 *   - READ-ONLY ENFORCEMENT: the deserialized snapshot rejects mutations
 *     (addRelation / removeRelation / addNode throw or no-op safely) while
 *     reads keep working.
 *   - CONFIG PRESERVATION: relation configs (including relation overrides
 *     and chains) survive the round trip and still evaluate correctly.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';
import { ArbiterSnapshot } from '../../src/core/arbiter/ArbiterSnapshot.js';
import { serializeArbiterSnapshot } from '../../src/core/SnapshotBinary.js';

const EPS = 1e-9;
const POS = [0, 0.25, 0.5, 0.75, 1];

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

function factory() {
  return new Arbiter({ fastConstructionMode: true, enableInference: false });
}

/**
 * Build a random graph; return { original, restored, checks } where checks
 * is the list of (userKey, rel, objKey) triples verified for parity.
 */
function buildGraph(seedCase) {
  const { users, mids, configKind } = seedCase;
  const arbiter = factory();
  const userKeys = [];
  const midKeys = [];
  for (let i = 0; i < users; i++) {
    userKeys.push(`user:${i}`);
    arbiter.addNode(`user:${i}`, 'user');
  }
  for (let i = 0; i < mids; i++) {
    midKeys.push(`mid:${i}`);
    arbiter.addNode(`mid:${i}`, 'group');
  }
  arbiter.addNode('doc:1', 'doc');
  arbiter.setRelationConfig('member_of', { type: 'direct' });
  arbiter.setRelationConfig('viewer', { type: 'direct' });

  if (configKind === 0) {
    arbiter.setRelationConfig('can_read', { type: 'direct', relation: 'viewer' });
  } else {
    arbiter.setRelationConfig('can_read', {
      type: 'chain',
      steps: [
        { relation: 'member_of', direction: 'out' },
        { relation: 'viewer', direction: 'out' }
      ]
    });
  }

  // Direct edges: user → doc
  const directEdges = Math.max(1, Math.floor(users / 2) + 1);
  for (let i = 0; i < directEdges; i++) {
    const u = userKeys[Math.floor(Math.random() * userKeys.length)];
    arbiter.addRelation(u, 'viewer', 'doc:1', { possibility: POS[Math.floor(Math.random() * POS.length)] });
  }
  // Memberships: user → mid
  for (let i = 0; i < mids; i++) {
    if (Math.random() < 0.7) {
      const u = userKeys[Math.floor(Math.random() * userKeys.length)];
      arbiter.addRelation(u, 'member_of', midKeys[i], { possibility: POS[Math.floor(Math.random() * POS.length)] });
    }
  }
  // Terminal: mid → doc
  for (let i = 0; i < mids; i++) {
    if (Math.random() < 0.7) {
      arbiter.addRelation(midKeys[i], 'viewer', 'doc:1', { possibility: POS[Math.floor(Math.random() * POS.length)] });
    }
  }

  arbiter.enableCondensedSnapshot();
  const buffer = serializeArbiterSnapshot(arbiter);
  // Proper restore path: rebuilds the condensed indices over the graph
  const restored = ArbiterSnapshot.fromSnapshotBinary(buffer, {}, () => factory());

  const checks = [];
  for (const u of userKeys) {
    checks.push([u, 'can_read', 'doc:1']);
  }
  return { original: arbiter, restored, checks };
}

describe('Condensed snapshot round trip (rigor)', () => {
  it('ROUND-TRIP PARITY: restored snapshot answers checks identically', async () => {
    async function check(seedCase) {
      const { original, restored, checks } = buildGraph(seedCase);
      for (const [u, rel, obj] of checks) {
        const before = original.check(u, rel, obj);
        const after = restored.check(u, rel, obj);
        if (Math.abs(before.possibility - after.possibility) > EPS) {
          fail(`parity ${u} ${rel} ${obj}: original=${before.possibility}, restored=${after.possibility}`);
        }
      }
      return { checked: checks.length };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({
            users: rigor.gen.int(1, 5),
            mids: rigor.gen.int(0, 4),
            configKind: rigor.gen.int(0, 1)
          })
        ))
      ],
      rigor.crucible([
        rigor.invariant('snapshot-parity', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 400, seed: 'snapshot-parity' , artifacts: { dir: '', persist: 'never' }});

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

  it('READ-ONLY ENFORCEMENT: restored snapshot rejects mutations, keeps reading', async () => {
    async function check(seedCase) {
      const { restored, checks } = buildGraph(seedCase);
      if (restored._snapshotReadOnly !== true) {
        fail('restored snapshot must be read-only');
      }
      // Reads still work
      for (const [u, rel, obj] of checks) {
        const r = restored.check(u, rel, obj);
        if (r.possibility < 0 || r.possibility > 1) {
          fail(`read on snapshot out of bounds: ${r.possibility}`);
        }
      }
      // Mutations must not corrupt the snapshot
      let threw = false;
      try {
        restored.addRelation('user:0', 'viewer', 'doc:1', { possibility: 1 });
      } catch {
        threw = true;
      }
      if (!threw) {
        // If it didn't throw, the mutation must not have changed answers
        for (const [u, rel, obj] of checks) {
          const r = restored.check(u, rel, obj);
          if (r.possibility < 0 || r.possibility > 1) {
            fail(`mutated snapshot returned bad result: ${r.possibility}`);
          }
        }
      }
      return { readOnly: restored._snapshotReadOnly };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({
            users: rigor.gen.int(1, 4),
            mids: rigor.gen.int(0, 3),
            configKind: rigor.gen.int(0, 1)
          })
        ))
      ],
      rigor.crucible([
        rigor.invariant('readonly-enforced', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 300, seed: 'snapshot-readonly' , artifacts: { dir: '', persist: 'never' }});

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