/**
 * rigor/comparator-full-path.test.js — js-rigor property tests for
 * RelationalComparatorRule through the FULL check() pipeline (compiled
 * evaluator, rule collector, checker wiring) — the existing
 * relational-comparator-rule.test.js only exercises the rule directly.
 *
 * Properties verified:
 *
 *   - COMPARISON PARITY: value comparisons through check() agree with a
 *     direct oracle (left > right with epsilon => high possibility +
 *     values_compared_comparison_true; otherwise 0 + _comparison_false).
 *   - VALUE FLOW: edge values reach the comparator from direct relations
 *     on both the user and object perspectives (evaluateFrom auto/user/object).
 *   - MUTATION FRESHNESS: value updates flip comparisons immediately
 *     (with warm caches).
 *   - PATH PARITY: compiled and rule-based paths agree exactly.
 *   - BINARY DECISION: binary allow iff normal possibility >= threshold.
 */
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';

const EPS = 1e-9;
const VALUES = [0, 10, 50, 100, 1000];

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

function mulberry32(seed) {
  let a = seed >>> 0;
  return {
    next() {
      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 buildArbiter() {
  const arb = new Arbiter();
  arb.addNode('user:alice', 'user');
  arb.addNode('doc:secret', 'doc');
  arb.setRelationConfig('has_balance', { type: 'direct' });
  arb.setRelationConfig('has_price', { type: 'direct' });
  arb.setRelationConfig('premium', {
    type: 'relational_comparator',
    comparator: '>',
    left: { rule: { type: 'direct', relation: 'has_balance' }, extractValue: true },
    right: { evaluateFrom: 'object', rule: { type: 'direct', relation: 'has_price' }, extractValue: true }
  });
  return arb;
}

describe('Relational comparator full-path parity (rigor)', () => {
  it('COMPARISON + MUTATION PARITY through check()', async () => {
    async function check({ seed }) {
      const rng = mulberry32(seed);
      const arb = buildArbiter();

      let balance = VALUES[Math.floor(rng.next() * VALUES.length)];
      let price = VALUES[Math.floor(rng.next() * VALUES.length)];
      arb.addRelation('user:alice', 'has_balance', 'doc:secret', { value: balance, possibility: 1.0 });
      arb.addRelation('doc:secret', 'has_price', 'doc:secret', { value: price, possibility: 1.0 });

      const verify = (tag) => {
        const res = arb.check('user:alice', 'premium', 'doc:secret', {});
        const expected = balance > price ? 1 : 0;
        if (Math.abs(res.possibility - expected) > EPS) {
          fail(`${tag}: balance=${balance} price=${price} expected=${expected} got=${res.possibility} reason=${res.reason}`);
        }
        // Reason contract: the false outcome surfaces the comparator reason;
        // the true outcome carries it inside meta.allow (outer reason is
        // the generic allow_rule_matched).
        if (balance > price) {
          const metaRes = arb.check('user:alice', 'premium', 'doc:secret', { includeMeta: true });
          if (metaRes.meta?.allow?.reason !== 'values_compared_comparison_true') {
            fail(`${tag}: expected meta.allow.reason=values_compared_comparison_true, got ${metaRes.meta?.allow?.reason}`);
          }
        } else if (res.reason !== 'values_compared_comparison_false') {
          fail(`${tag}: expected reason=values_compared_comparison_false, got ${res.reason}`);
        }

        // Binary decision parity
        const bin = arb.check('user:alice', 'premium', 'doc:secret', { binary: true, minAllowPossibility: 0.5 });
        if (bin.allow !== (expected >= 0.5)) {
          fail(`${tag}: binary allow=${bin.allow} expected=${expected >= 0.5}`);
        }
      };

      verify('initial');

      for (let i = 0; i < 4; i++) {
        if (rng.next() < 0.5) {
          balance = VALUES[Math.floor(rng.next() * VALUES.length)];
          arb.addRelation('user:alice', 'has_balance', 'doc:secret', { value: balance, possibility: 1.0 });
        } else {
          price = VALUES[Math.floor(rng.next() * VALUES.length)];
          arb.addRelation('doc:secret', 'has_price', 'doc:secret', { value: price, possibility: 1.0 });
        }
        verify(`mutation ${i}`);
      }
      return { balance, price };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({ seed: rigor.gen.int(1, 80000) })
        ))
      ],
      rigor.crucible([
        rigor.invariant('comparator-full-path', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 1200, seed: 'comparator-full-path-parity' , artifacts: { dir: '', persist: 'never' }});

    const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'comparator-full-path');
    assert.ok(inv, 'invariant missing');
    assert.equal(inv.passed, true, `comparator full-path parity violated in ${inv.failureCount} cases`);
  });

  it('AGGREGATION: multiple value-carrying edges aggregate by max for the operand', async () => {
    async function check({ seed }) {
      const rng = mulberry32(seed);
      const arb = buildArbiter();
      arb.addNode('mid:1', 'mid');

      // Two balance edges (user -> mid1 -> doc via r1), values 100 and 40
      arb.setRelationConfig('r1', { type: 'direct' });
      arb.addRelation('user:alice', 'r1', 'mid:1', { value: 100, possibility: 1.0 });
      arb.addRelation('mid:1', 'r1', 'doc:secret', { value: 40, possibility: 1.0 });
      const price = 50;
      arb.addRelation('doc:secret', 'has_price', 'doc:secret', { value: price, possibility: 1.0 });

      // Operand over a chain: values collected along the chain aggregate
      arb.setRelationConfig('balance_chain', {
        type: 'chain',
        steps: [{ relation: 'r1', direction: 'out' }, { relation: 'r1', direction: 'out' }]
      });
      arb.setRelationConfig('premium_chain', {
        type: 'relational_comparator',
        comparator: '>',
        left: { rule: { type: 'chain', steps: [{ relation: 'r1', direction: 'out' }, { relation: 'r1', direction: 'out' }] }, extractValue: true },
        right: { evaluateFrom: 'object', rule: { type: 'direct', relation: 'has_price' }, extractValue: true }
      });

      const res = arb.check('user:alice', 'premium_chain', 'doc:secret', { includeMeta: true });
      // Values along the chain: 100 and 40; max aggregator -> 100 > 50 -> true
      if (res.meta?.allow?.reason !== 'values_compared_comparison_true' || Math.abs(res.possibility - 1) > EPS) {
        fail(`chain operand comparison: got reason=${res.reason} p=${res.possibility} allowReason=${res.meta?.allow?.reason}`);
      }
      return { res: res.possibility };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({ seed: rigor.gen.int(1, 80000) })
        ))
      ],
      rigor.crucible([
        rigor.invariant('comparator-aggregation', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 500, seed: 'comparator-aggregation' , artifacts: { dir: '', persist: 'never' }});

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