/**
 * rigor/authorization-config-consistency.test.js — js-rigor property tests
 * for relation-config semantics and evaluation-path consistency.
 *
 * Properties verified:
 *
 *   - FAST/RULE PARITY: for a direct config, the fast path (AuthorizationChecker
 *     direct lookup) and the rule-evaluation path agree on possibility.
 *   - OVERRIDE: a direct config's `relation` override is honored by BOTH
 *     paths — checking `can_delete` (which checks `mfa`) succeeds iff the
 *     `mfa` edge exists, and fails iff it is absent.
 *   - OVERRIDE PARITY: the override result equals a plain direct check on
 *     the overridden relation itself.
 *   - REMEDIATION: a missing injectable source surfaces unified remediation
 *     naming the relation and object; a present witness produces none.
 *   - OVERRIDE + REMEDIATION: with an injectable overridden relation, the
 *     denied result carries a remediation option for that relation.
 */
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;

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

function buildArbiter({ override = false, injectable = false, relation = 'mfa' } = {}) {
  const arbiter = new Arbiter();
  arbiter.addNode('user:1', 'user');
  arbiter.addNode('doc:1', 'doc');
  if (injectable) {
    arbiter.setRelationConfig(relation, {
      type: 'source',
      relation,
      injectable: true,
      provides: 'Proof'
    });
  }
  const directConfig = { type: 'direct' };
  if (override) {
    directConfig.relation = relation;
  }
  arbiter.setRelationConfig('can_delete', directConfig);
  return arbiter;
}

describe('Authorization config consistency (rigor)', () => {
  it('FAST/RULE PARITY: plain direct config agrees across both evaluation paths', async () => {
    async function check(p) {
      const arbiter = buildArbiter();
      arbiter.addRelation('user:1', 'can_delete', 'doc:1', { possibility: p });

      // Fast path: default check (direct config short-circuits)
      const fast = arbiter.check('user:1', 'can_delete', 'doc:1');

      // Rule path: force full rule evaluation by disabling the fast path
      const rule = arbiter.check('user:1', 'can_delete', 'doc:1', { fastPath: false });

      if (Math.abs(fast.possibility - p) > EPS) {
        fail(`fast path: expected ${p}, got ${fast.possibility}`);
      }
      if (Math.abs(rule.possibility - p) > EPS) {
        fail(`rule path: expected ${p}, got ${rule.possibility}`);
      }
      return { fast, rule };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.oneOf([0, 0.25, 0.5, 0.75, 1])
        ))
      ],
      rigor.crucible([
        rigor.invariant('path-parity', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 300, seed: 'authz-config-parity' , artifacts: { dir: '', persist: 'never' }});

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

  it('OVERRIDE: direct config relation override is honored; absent witness denies', async () => {
    async function check({ hasWitness, p }) {
      const arbiter = buildArbiter({ override: true, relation: 'mfa' });
      // The overridden relation itself must be evaluable for the parity check
      arbiter.setRelationConfig('mfa', { type: 'direct' });
      if (hasWitness) {
        arbiter.addRelation('user:1', 'mfa', 'doc:1', { possibility: p });
      }

      const withWitness = arbiter.check('user:1', 'can_delete', 'doc:1');
      if (hasWitness) {
        if (Math.abs(withWitness.possibility - p) > EPS) {
          fail(`override with witness: expected ${p}, got ${withWitness.possibility}`);
        }
      } else if (withWitness.possibility !== 0) {
        fail(`override without witness must deny, got ${withWitness.possibility}`);
      }

      // Parity: can_delete (overriding to mfa) must equal checking mfa directly
      const direct = arbiter.check('user:1', 'mfa', 'doc:1');
      if (Math.abs(withWitness.possibility - direct.possibility) > EPS) {
        fail(`override parity: can_delete=${withWitness.possibility} vs mfa=${direct.possibility}`);
      }
      return { withWitness, direct };
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({
            hasWitness: rigor.gen.boolean(),
            p: rigor.gen.oneOf([0.25, 0.5, 1])
          })
        ))
      ],
      rigor.crucible([
        rigor.invariant('override-honored', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 300, seed: 'authz-config-override' , artifacts: { dir: '', persist: 'never' }});

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

  it('REMEDIATION: missing injectable witness surfaces unified remediation; present witness has none', async () => {
    async function check({ hasWitness }) {
      const arbiter = buildArbiter({ injectable: true, relation: 'mfa' });
      arbiter.setRelationConfig('can_delete', { type: 'direct', relation: 'mfa' });
      if (hasWitness) {
        arbiter.addRelation('user:1', 'mfa', 'doc:1', 1.0);
      }

      const result = arbiter.check('user:1', 'can_delete', 'doc:1');
      if (hasWitness) {
        if (result.possibility !== 1) {
          fail(`witness present should grant, got ${result.possibility}`);
        }
        if (result.remediation) {
          fail(`witness present must not produce remediation, got ${JSON.stringify(result.remediation)}`);
        }
      } else {
        if (result.possibility !== 0) {
          fail(`witness missing should deny, got ${result.possibility}`);
        }
        const options = result.remediation?.options;
        if (!Array.isArray(options) || options.length === 0) {
          fail(`missing witness must produce remediation options`);
        }
        const mfaOption = options.find(o => o.relation === 'mfa' && o.object === 'doc:1');
        if (!mfaOption) {
          fail(`remediation must name relation 'mfa' and object 'doc:1', got ${JSON.stringify(options)}`);
        }
      }
      return result;
    }

    const report = await rigor.campaign(
      [
        rigor.fn('check', check, rigor.args(
          rigor.gen.object({ hasWitness: rigor.gen.boolean() })
        ))
      ],
      rigor.crucible([
        rigor.invariant('remediation-contract', ({ actual }) => actual !== undefined)
      ])
    ).run({ effort: 300, seed: 'authz-config-remediation' , artifacts: { dir: '', persist: 'never' }});

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