import { LogicalOperators } from '../../src/authorization/rules/LogicalOperators.js';
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';

// Minimal mock ruleEvaluator for child rule evaluation
function makeMockRuleEvaluator(resultsByRule) {
  return {
    evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
      if (typeof rule === 'string') return resultsByRule[rule];
      if (rule && rule.mockKey) return resultsByRule[rule.mockKey];
      if (rule && rule.type && resultsByRule[rule.type]) return resultsByRule[rule.type];
      return resultsByRule.default || { possibility: 0, reliability: 1.0, meta: { reason: 'no_rule' }, collectedValues: [] };
    }
  };
}

describe('LogicalOperators', () => {
  let logicalOps;
  let arbiter;
  let ruleEvaluator;

  beforeEach(() => {
    arbiter = {};
  });

  it('evaluates union (max) correctly', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
      b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
      c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'max' } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
    assert.strictEqual(res.possibility, 0.9);
    assert.ok(res.meta.operation === 'union');
    assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
  });

  it('evaluates intersection (min) correctly', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.7, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
      b: { possibility: 0.4, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] },
      c: { possibility: 0.9, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'c' } }, collectedValues: [3] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }, { mockKey: 'c' }], aggregator: 'min' } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
    assert.strictEqual(res.possibility, 0.4);
    assert.ok(res.meta.operation === 'intersection');
    assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
  });

  it('evaluates union (mean/average) correctly', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.6, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
      b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'mean' } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
    assert.ok(Math.abs(res.possibility - 0.7) < 1e-8);
    assert.ok(res.meta.operation === 'union');
  });

  it('evaluates intersection (pessimistic) correctly', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
      b: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', intersection: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], aggregator: 'pessimistic' } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
    assert.ok(res.possibility < 0.9 && res.possibility > 0.6);
    assert.ok(res.meta.operation === 'intersection');
  });

  it('evaluates exclusion (A AND NOT B) correctly', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.8, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
      b: { possibility: 0.5, reliability: 0.7, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', exclusion: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
    assert.ok(Math.abs(res.possibility - (0.8 * (1 - 0.5))) < 1e-8);
    assert.ok(res.meta.operation === 'exclusion');
  });

  it('evaluates custom OWA weights for union', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.2, reliability: 0.9, meta: { ruleType: 'direct', rule: { mockKey: 'a' } }, collectedValues: [1] },
      b: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'direct', rule: { mockKey: 'b' } }, collectedValues: [2] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }], owaWeights: [0.7, 0.3] } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.ok(Math.abs(res.possibility - (0.8 * 0.7 + 0.2 * 0.3)) < 1e-8);
    assert.ok(res.meta.operation === 'union');
  });

  it('evaluates binary defeasible logic (never overrides everything)', () => {
    // never wins over everything
    ruleEvaluator = makeMockRuleEvaluator({
      never: { possibility: 1, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
      strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
      defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
      defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = {
      type: 'logical',
      never: { union: [{ mockKey: 'never' }] },
      always: { direct: { mockKey: 'strict' } },
      unless: { union: [{ mockKey: 'defeater' }] },
      when: { intersection: [{ mockKey: 'defeasible' }] },
      mode: 'binary'
    };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.strictEqual(res.possibility, 0);
    assert.ok(res.meta.never);
    assert.ok(res.meta.reason === 'never_rule_triggered');
    assert.ok(res.meta.mode === 'binary');
  });

  it('evaluates binary defeasible logic (strict, defeater, defeasible)', () => {
    // strict wins
    ruleEvaluator = makeMockRuleEvaluator({
      strict: { possibility: 1, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
      defeater: { possibility: 0.6, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
      defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = {
      type: 'logical',
      always: { direct: { mockKey: 'strict' } },
      unless: { union: [{ mockKey: 'defeater' }] },
      when: { intersection: [{ mockKey: 'defeasible' }] },
      mode: 'binary'
    };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.strictEqual(res.possibility, 1);
    assert.ok(res.meta.strict);
    assert.ok(res.meta.mode === 'binary');
  });

  it('evaluates binary defeasible logic (requirements take precedence over defeaters)', () => {
    // requirements not met should win over defeaters
    ruleEvaluator = makeMockRuleEvaluator({
      strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
      requires: { possibility: 0.1, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
      defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
      defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = {
      type: 'logical',
      always: { direct: { mockKey: 'strict' } },
      requires: { union: [{ mockKey: 'requires' }] },
      unless: { union: [{ mockKey: 'defeater' }] },
      when: { intersection: [{ mockKey: 'defeasible' }] },
      mode: 'binary'
    };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.strictEqual(res.possibility, 0);
    assert.ok(res.meta.reason === 'requirements_not_met');
    assert.ok(res.meta.mode === 'binary');
  });

  it('evaluates binary defeasible logic (defeater wins)', () => {
    // defeater wins when requirements are met
    ruleEvaluator = makeMockRuleEvaluator({
      strict: { possibility: 0, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
      requires: { possibility: 0.9, reliability: 0.9, meta: { ruleType: 'requires' }, collectedValues: [2] },
      defeater: { possibility: 0.8, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
      defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = {
      type: 'logical',
      always: { direct: { mockKey: 'strict' } },
      requires: { union: [{ mockKey: 'requires' }] },
      unless: { union: [{ mockKey: 'defeater' }] },
      when: { intersection: [{ mockKey: 'defeasible' }] },
      mode: 'binary'
    };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.strictEqual(res.possibility, 0);
    assert.ok(res.meta.reason === 'defeated_by_unless');
    assert.ok(res.meta.mode === 'binary');
  });

  it('evaluates threshold mode with never early exit', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      never: { possibility: 0.9, reliability: 0.95, meta: { ruleType: 'never' }, collectedValues: [1] },
      strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [2] },
      defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [3] },
      defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [4] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = {
      type: 'logical',
      never: { union: [{ mockKey: 'never' }] },
      always: { direct: { mockKey: 'strict' } },
      unless: { union: [{ mockKey: 'defeater' }] },
      when: { intersection: [{ mockKey: 'defeasible' }] },
      mode: 'threshold'
    };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.strictEqual(res.possibility, 0);
    assert.ok(res.meta.earlyExit === 'never_rule');
    assert.ok(res.meta.never);
  });

  it('evaluates threshold mode with early exit', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      strict: { possibility: 0.85, reliability: 0.95, meta: { ruleType: 'strict' }, collectedValues: [1] },
      defeater: { possibility: 0.2, reliability: 0.8, meta: { ruleType: 'defeater' }, collectedValues: [2] },
      defeasible: { possibility: 0.7, reliability: 0.7, meta: { ruleType: 'defeasible' }, collectedValues: [3] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = {
      type: 'logical',
      always: { direct: { mockKey: 'strict' } },
      unless: { union: [{ mockKey: 'defeater' }] },
      when: { intersection: [{ mockKey: 'defeasible' }] },
      mode: 'threshold'
    };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.strictEqual(res.possibility, 0.85);
    assert.ok(res.meta.earlyExit === 'strict_rule');
  });

  it('handles empty union/intersection/exclusion', () => {
    ruleEvaluator = makeMockRuleEvaluator({});
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const unionRule = { type: 'logical', union: { rules: [] } };
    const intersectionRule = { type: 'logical', intersection: { rules: [] } };
    const exclusionRule = { type: 'logical', exclusion: { rules: [] } };
    assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', unionRule, {}, null, {}).possibility, 0);
    assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', intersectionRule, {}, null, {}).possibility, 0);
    assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', exclusionRule, {}, null, {}).possibility, 0);
  });

  it('handles all-zero and all-one cases', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] },
      b: { possibility: 0, reliability: 1.0, meta: {}, collectedValues: [] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
    assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 0);
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] },
      b: { possibility: 1, reliability: 1.0, meta: {}, collectedValues: [] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    assert.strictEqual(logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {}).possibility, 1);
  });

  it('propagates meta and collectedValues', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.5, reliability: 0.9, meta: { foo: 'bar' }, collectedValues: [1, 2] },
      b: { possibility: 0.8, reliability: 0.8, meta: { baz: 'qux' }, collectedValues: [3] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { collectValues: true });
    assert.ok(res.meta);
    assert.deepStrictEqual(res.collectedValues, [1, 2, 3]);
  });

  it('handles missing rule types gracefully', () => {
    ruleEvaluator = makeMockRuleEvaluator({});
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'missing' }] } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.strictEqual(res.possibility, 0);
    assert.ok(res.meta && res.meta.reason === 'missing_rule');
  });

  it('supports fastPath/early exit logic in union', () => {
    let callCount = 0;
    ruleEvaluator = {
      evaluateRule: (userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) => {
        callCount++;
        if (callCount === 1) return { possibility: 0.95, reliability: 0.9, meta: {}, collectedValues: [1] };
        return { possibility: 0.2, reliability: 0.8, meta: {}, collectedValues: [2] };
      }
    };
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'a' }, { mockKey: 'b' }] }, fastPath: true, minPossibility: 0.9 };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, { fastPath: true, minPossibility: 0.9, collectValues: true });
    assert.strictEqual(res.possibility, 0.95);
    assert.deepStrictEqual(res.collectedValues, [1]);
  });

  it('supports priority/aggregator effects', () => {
    ruleEvaluator = makeMockRuleEvaluator({
      a: { possibility: 0.7, reliability: 0.9, meta: { rule: { priority: 10 } }, collectedValues: [1] },
      b: { possibility: 0.6, reliability: 0.8, meta: { rule: { priority: 5 } }, collectedValues: [2] }
    });
    logicalOps = new LogicalOperators(arbiter, ruleEvaluator);
    const rule = { type: 'logical', union: { rules: [{ mockKey: 'a', priority: 10 }, { mockKey: 'b', priority: 5 }], aggregator: 'priority' } };
    const res = logicalOps._evaluateRule('u', 'u', 'o', 'o', rule, {}, null, {});
    assert.ok(res.possibility > 0.6 && res.possibility < 0.8);
  });
}); 
