/**
 * tests/rules/chain-condition-step.test.js — ChainRule CONDITION STEP.
 *
 * A chain step of the form { rule: <config>, conditionStep: true } is a
 * condition-gated hop instead of an edge traversal. It is valid only as the
 * FINAL step: the object is known, so the engine verifies the referenced rule
 * at (intermediate, object) for each current path. The DSL compiler emits
 * these when a chain's object-side hop references a defeasible/logical
 * evidence (e.g. `member_of(user,*g){ gated(g,doc) }` where gated is
 * `WHEN can_view(group, doc) UNLESS banned(group)`).
 */
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../src/index.js';
import { RuleEvaluator } from '../../src/authorization/RuleEvaluator.js';
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';

let arbiter, evaluator, chainRule;

function evalRule(userKey, objectKey, rule, options = {}) {
  const userId = arbiter.resolveNodeId(userKey);
  const objectId = arbiter.resolveNodeId(objectKey);
  return chainRule._evaluateRule(userId, userKey, objectId, objectKey, rule, new Set(), null, {
    includeMeta: true,
    ...options
  });
}

const CONDITION_CONFIG = {
  type: 'logical',
  when: { intersection: { rules: [{ type: 'direct', relation: 'can_view' }], aggregator: 'min' } },
  // banned(group) is unary → subject-as-object (self-edge), as the DSL emits
  unless: { union: { rules: [{ type: 'direct', relation: 'banned', _subjectAsObject: true }], aggregator: 'max' } }
};

describe('ChainRule condition step (rule-based final hop)', () => {
  beforeEach(() => {
    arbiter = new Arbiter();
    evaluator = new RuleEvaluator(arbiter);
    chainRule = new ChainRule(arbiter, evaluator);
    arbiter.addNode('user:u', 'user');
    arbiter.addNode('group:g', 'group');
    arbiter.addNode('doc:d', 'doc');
    arbiter.setRelationConfig('can_view', { type: 'direct' });
    arbiter.setRelationConfig('banned', { type: 'direct' });
  });

  it('grants when the condition holds at the object', () => {
    arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
    arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
    const rule = {
      type: 'chain',
      steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
    };
    const res = evalRule('user:u', 'doc:d', rule);
    // min(member_of, can_view*(1 - banned)) = min(1.0, 0.7) = 0.7
    assert.ok(Math.abs(res.possibility - 0.7) < 1e-9, `expected 0.7, got ${res.possibility} (${res.reason})`);
  });

  it('denies when the condition is defeated at the object', () => {
    arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
    arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
    arbiter.addRelation('group:g', 'banned', 'group:g', { possibility: 1.0 });
    const rule = {
      type: 'chain',
      steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
    };
    const res = evalRule('user:u', 'doc:d', rule);
    // min(1.0, 0.7*(1 - 1.0)) = 0
    assert.equal(res.possibility, 0);
  });

  it('denies when an earlier edge is missing', () => {
    arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
    const rule = {
      type: 'chain',
      steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
    };
    const res = evalRule('user:u', 'doc:d', rule);
    assert.equal(res.possibility, 0);
    assert.equal(res.reason, 'no_chain_path_found');
  });

  it('expands an intermediate condition step (rule-based reachability)', () => {
    arbiter.setRelationConfig('member_of', { type: 'direct' });
    arbiter.addNode('group:g', 'group');
    arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
    arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.8 });
    // [condition(member_of unless banned), can_view] — the condition step is
    // INTERMEDIATE and discovers its reachable nodes (its base relation's
    // neighbors from the source, filtered by its defeater).
    const intermediateConfig = {
      type: 'logical',
      when: { intersection: { rules: [{ type: 'direct', relation: 'member_of' }], aggregator: 'min' } },
      unless: { union: { rules: [{ type: 'direct', relation: 'banned', _subjectIsObject: true }], aggregator: 'max' } }
    };
    const rule = {
      type: 'chain',
      steps: [{ rule: intermediateConfig, conditionStep: true }, 'can_view']
    };
    // g not banned → reachable via condition → can_view → doc
    assert.ok(Math.abs(evalRule('user:u', 'doc:d', rule).possibility - 0.8) < 1e-9);
    // banning g filters it out of the intermediate expansion → no path
    arbiter.addRelation('group:g', 'banned', 'group:g', { possibility: 1.0 });
    const denied = evalRule('user:u', 'doc:d', rule);
    assert.equal(denied.possibility, 0);
  });

  it('expands a tuple_to_userset evidence as an intermediate condition step', () => {
    arbiter.setRelationConfig('member_of', { type: 'direct' });
    arbiter.setRelationConfig('owner', { type: 'direct' });
    arbiter.setRelationConfig('doc_route', { type: 'direct' });
    arbiter.addNode('group:g', 'group');
    arbiter.addNode('doc:1', 'doc');
    arbiter.addNode('doc:2', 'doc');
    arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
    arbiter.addRelation('group:g', 'owner', 'doc:1', { possibility: 0.8 });
    arbiter.addRelation('doc:1', 'doc_route', 'doc:2', { possibility: 0.9 });
    const ttuConfig = {
      type: 'tuple_to_userset',
      tuplesetRelation: 'owner',
      computedRelation: 'member_of',
      tuplesetDirection: 'in'
    };
    const rule = {
      type: 'chain',
      steps: [{ rule: ttuConfig, conditionStep: true }, 'doc_route']
    };
    // TTU reachable from user = doc:1 (min(1.0, 0.8) = 0.8), then doc_route → 0.9
    const res = evalRule('user:u', 'doc:2', rule);
    assert.ok(Math.abs(res.possibility - 0.8) < 1e-9, `expected 0.8, got ${res.possibility} (${res.reason})`);
    // Direct expansion unit check
    const expanded = chainRule._expandRuleFromSrc(arbiter.resolveNodeId('user:u'), ttuConfig, {});
    assert.equal(expanded.size, 1);
    assert.equal(arbiter.resolveKey([...expanded.keys()][0]), 'doc:1');
  });

  it('combines across multiple parallel intermediates (max aggregation)', () => {
    arbiter.addNode('group:g2', 'group');
    arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 0.5 });
    arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.7 });
    arbiter.addRelation('user:u', 'member_of', 'group:g2', { possibility: 1.0 });
    arbiter.addRelation('group:g2', 'can_view', 'doc:d', { possibility: 0.8 });
    const rule = {
      type: 'chain',
      steps: ['member_of', { rule: CONDITION_CONFIG, conditionStep: true }]
    };
    const res = evalRule('user:u', 'doc:d', rule);
    // paths: min(0.5,0.7)=0.5 and min(1.0,0.8)=0.8 -> max = 0.8
    assert.ok(Math.abs(res.possibility - 0.8) < 1e-9, `expected 0.8, got ${res.possibility} (${res.reason})`);
  });
});
