import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import { Arbiter } from '../../src/core/Arbiter.js';

function createRng(seed) {
  let state = seed >>> 0;
  return () => {
    state = (1664525 * state + 1013904223) >>> 0;
    return state / 0x100000000;
  };
}

function randFloat(rng, min = 0, max = 1) {
  return min + (max - min) * rng();
}

function buildDependencyIndex(fromRelation, toRelation) {
  const entry = {
    all: new Set([toRelation]),
    byLevel: {
      never: new Set(),
      always: new Set(),
      requires: new Set(),
      when: new Set(),
      unless: new Set(),
      ordinary: new Set([toRelation])
    }
  };
  return new Map([[fromRelation, entry]]);
}

function buildDependencyIndexForRelations(relations, toRelation) {
  const map = new Map();
  for (const relation of relations) {
    map.set(relation, {
      all: new Set([toRelation]),
      byLevel: {
        never: new Set(),
        always: new Set(),
        requires: new Set(),
        when: new Set(),
        unless: new Set(),
        ordinary: new Set([toRelation])
      }
    });
  }
  return map;
}

describe('Rule result cache invalidation', () => {
  test('invalidates aggregate cache when dependency changes', () => {
    const arbiter = new Arbiter({ enableRuleResultCache: true, ruleResultCacheTTL: 60000 });
    arbiter.addNode('user:1', 'user');
    arbiter.addNode('resource:1', 'resource');

    arbiter.setRelationConfig('member', { type: 'direct' });
    arbiter.setRelationConfig('allow', {
      union: {
        rules: [{ type: 'direct', relation: 'member' }],
        aggregator: 'max'
      }
    });

    arbiter.registerDependencyIndex(buildDependencyIndex('member', 'allow'));
    arbiter.addRelation('user:1', 'member', 'resource:1', 1.0);

    const result1 = arbiter.check('user:1', 'allow', 'resource:1', { fastPath: false });
    assert.strictEqual(result1.possibility, 1.0);

    const userId = arbiter.resolveNodeId('user:1');
    const objectId = arbiter.resolveNodeId('resource:1');
    const baseKey = arbiter.keyManager.createCompositeKey(userId, 'allow', objectId);
    const cacheKey = `${baseKey}|logical`;
    assert.ok(arbiter.ruleResultCache.get(cacheKey), 'cache populated');

    arbiter.removeRelation('user:1', 'member', 'resource:1');
    assert.strictEqual(arbiter.ruleResultCache.get(cacheKey), undefined);

    arbiter.addRelation('user:1', 'member', 'resource:1', 0.2);
    const result2 = arbiter.check('user:1', 'allow', 'resource:1', { fastPath: false });
    assert.ok(result2.possibility < 0.3 && result2.possibility > 0.1);
  });

  test('random updates do not serve stale aggregate results', () => {
    const arbiter = new Arbiter({ enableRuleResultCache: true, ruleResultCacheTTL: 60000 });
    arbiter.addNode('user:1', 'user');
    arbiter.addNode('resource:1', 'resource');

    arbiter.setRelationConfig('member', { type: 'direct' });
    arbiter.setRelationConfig('allow', {
      union: {
        rules: [{ type: 'direct', relation: 'member' }],
        aggregator: 'max'
      }
    });
    arbiter.registerDependencyIndex(buildDependencyIndex('member', 'allow'));

    const rng = createRng(123);
    for (let i = 0; i < 50; i++) {
      const value = randFloat(rng, 0, 1);
      arbiter.removeRelation('user:1', 'member', 'resource:1');
      arbiter.addRelation('user:1', 'member', 'resource:1', value);
      const result = arbiter.check('user:1', 'allow', 'resource:1', { fastPath: false });
      assert.ok(Math.abs(result.possibility - value) < 1e-6);
    }
  });

  test('invalidates nested comparator aggregation on updates', () => {
    const arbiter = new Arbiter({ enableRuleResultCache: true, ruleResultCacheTTL: 60000 });
    arbiter.addNode('user:1', 'user');
    arbiter.addNode('resource:1', 'resource');

    arbiter.setRelationConfig('risk_score', { type: 'direct' });
    arbiter.setRelationConfig('risk_bonus', { type: 'direct' });
    arbiter.setRelationConfig('risk_noise', { type: 'direct' });
    arbiter.setRelationConfig('risk_limit', { type: 'direct' });

    arbiter.setRelationConfig('risk_ok_deep', {
      type: 'relational_comparator',
      comparator: '<=',
      fallbackBehavior: 'deny',
      left: {
        rule: {
          union: {
            rules: [
              {
                union: {
                  rules: [
                    { type: 'direct', relation: 'risk_score' },
                    { type: 'direct', relation: 'risk_bonus' }
                  ],
                  aggregator: 'sum'
                }
              },
              { type: 'direct', relation: 'risk_noise' }
            ],
            aggregator: 'sum'
          }
        },
        extractValue: true,
        valueRelation: 'risk_score',
        aggregator: 'sum'
      },
      right: {
        rule: { type: 'direct', relation: 'risk_limit' },
        extractValue: true,
        valueRelation: 'risk_limit',
        evaluateFrom: 'object'
      }
    });

    arbiter.registerDependencyIndex(buildDependencyIndexForRelations(
      ['risk_score', 'risk_bonus', 'risk_noise', 'risk_limit'],
      'risk_ok_deep'
    ));

    arbiter.addRelation('user:1', 'risk_score', 'resource:1', 1.0, { value: 20 });
    arbiter.addRelation('user:1', 'risk_bonus', 'resource:1', 1.0, { value: 5 });
    arbiter.addRelation('user:1', 'risk_noise', 'resource:1', 1.0, { value: 5 });
    arbiter.addRelation('resource:1', 'risk_limit', 'resource:1', 1.0, { value: 40 });

    const userId = arbiter.resolveNodeId('user:1');
    const objectId = arbiter.resolveNodeId('resource:1');
    const config = arbiter.relationConfigs.get('risk_ok_deep');

    const options = { includeMeta: false, collectValues: true, cacheRuleResult: true, fastPath: false };
    const result1 = arbiter.authChecker.ruleEvaluator.evaluateRule(
      userId,
      'user:1',
      objectId,
      'resource:1',
      config,
      new Set(),
      'risk_ok_deep',
      options
    );

    const ruleCacheKey = arbiter.authChecker.ruleEvaluator._getRuleResultCacheKey(
      userId,
      'risk_ok_deep',
      objectId,
      config
    );
    const derivedCacheKey = arbiter.keyManager.createCompositeKey(
      userId,
      'risk_ok_deep:operand:left:risk_score:sum:auto:1',
      objectId
    );

    assert.ok(arbiter.ruleResultCache.get(ruleCacheKey), 'comparator cache populated');
    assert.ok(arbiter.ruleResultCache.get(derivedCacheKey), 'derived cache populated');

    arbiter.authChecker.ruleEvaluator.evaluateRule(
      userId,
      'user:1',
      objectId,
      'resource:1',
      config,
      new Set(),
      'risk_ok_deep',
      options
    );
    assert.ok(arbiter.ruleResultCacheStats.hits >= 1, 'cache hit recorded');

    arbiter.addRelation('user:1', 'risk_bonus', 'resource:1', 1.0, { value: 80 });
    assert.strictEqual(arbiter.ruleResultCache.get(ruleCacheKey), undefined);
    assert.strictEqual(arbiter.ruleResultCache.get(derivedCacheKey), undefined);

    const result2 = arbiter.authChecker.ruleEvaluator.evaluateRule(
      userId,
      'user:1',
      objectId,
      'resource:1',
      config,
      new Set(),
      'risk_ok_deep',
      options
    );

    assert.notStrictEqual(result1.possibility, result2.possibility);
  });

  test('invalidates nested comparator aggregation on edge removal', () => {
    const arbiter = new Arbiter({ enableRuleResultCache: true, ruleResultCacheTTL: 60000 });
    arbiter.addNode('user:1', 'user');
    arbiter.addNode('resource:1', 'resource');

    arbiter.setRelationConfig('risk_score', { type: 'direct' });
    arbiter.setRelationConfig('risk_bonus', { type: 'direct' });
    arbiter.setRelationConfig('risk_noise', { type: 'direct' });
    arbiter.setRelationConfig('risk_limit', { type: 'direct' });

    arbiter.setRelationConfig('risk_ok_deep', {
      type: 'relational_comparator',
      comparator: '<=',
      fallbackBehavior: 'deny',
      left: {
        rule: {
          union: {
            rules: [
              { type: 'direct', relation: 'risk_score' },
              { type: 'direct', relation: 'risk_noise' }
            ],
            aggregator: 'sum'
          }
        },
        extractValue: true,
        valueRelation: 'risk_score',
        aggregator: 'sum'
      },
      right: {
        rule: { type: 'direct', relation: 'risk_limit' },
        extractValue: true,
        valueRelation: 'risk_limit',
        evaluateFrom: 'object'
      }
    });

    arbiter.registerDependencyIndex(buildDependencyIndexForRelations(
      ['risk_score', 'risk_noise', 'risk_limit'],
      'risk_ok_deep'
    ));

    arbiter.addRelation('user:1', 'risk_score', 'resource:1', 1.0, { value: 10 });
    arbiter.addRelation('user:1', 'risk_noise', 'resource:1', 1.0, { value: 5 });
    arbiter.addRelation('resource:1', 'risk_limit', 'resource:1', 1.0, { value: 30 });

    const userId = arbiter.resolveNodeId('user:1');
    const objectId = arbiter.resolveNodeId('resource:1');
    const config = arbiter.relationConfigs.get('risk_ok_deep');
    const options = { includeMeta: false, collectValues: true, cacheRuleResult: true, fastPath: false };

    arbiter.authChecker.ruleEvaluator.evaluateRule(
      userId,
      'user:1',
      objectId,
      'resource:1',
      config,
      new Set(),
      'risk_ok_deep',
      options
    );

    const ruleCacheKey = arbiter.authChecker.ruleEvaluator._getRuleResultCacheKey(
      userId,
      'risk_ok_deep',
      objectId,
      config
    );
    const derivedCacheKey = arbiter.keyManager.createCompositeKey(
      userId,
      'risk_ok_deep:operand:left:risk_score:sum:auto:1',
      objectId
    );

    assert.ok(arbiter.ruleResultCache.get(ruleCacheKey), 'comparator cache populated');
    assert.ok(arbiter.ruleResultCache.get(derivedCacheKey), 'derived cache populated');

    arbiter.removeRelation('user:1', 'risk_noise', 'resource:1');
    assert.strictEqual(arbiter.ruleResultCache.get(ruleCacheKey), undefined);
    assert.strictEqual(arbiter.ruleResultCache.get(derivedCacheKey), undefined);

    const result = arbiter.authChecker.ruleEvaluator.evaluateRule(
      userId,
      'user:1',
      objectId,
      'resource:1',
      config,
      new Set(),
      'risk_ok_deep',
      options
    );

    assert.ok(result.possibility >= 0, 'recomputed after invalidation');
  });

  test('invalidates rule cache on node data update', () => {
    const arbiter = new Arbiter({ enableRuleResultCache: true, ruleResultCacheTTL: 60000 });
    arbiter.addNode('user:1', 'user');
    arbiter.addNode('resource:1', 'resource');

    arbiter.setRelationConfig('member', { type: 'direct' });
    arbiter.setRelationConfig('allow', {
      union: {
        rules: [{ type: 'direct', relation: 'member' }],
        aggregator: 'max'
      }
    });

    arbiter.addRelation('user:1', 'member', 'resource:1', 1.0);

    const userId = arbiter.resolveNodeId('user:1');
    const objectId = arbiter.resolveNodeId('resource:1');
    const config = arbiter.relationConfigs.get('allow');

    const result = arbiter.authChecker.ruleEvaluator.evaluateRule(
      userId,
      'user:1',
      objectId,
      'resource:1',
      config,
      new Set(),
      'allow',
      { includeMeta: false, collectValues: false, cacheRuleResult: true }
    );
    assert.strictEqual(result.possibility, 1.0);

    const cacheKey = arbiter.authChecker.ruleEvaluator._getRuleResultCacheKey(
      userId,
      'allow',
      objectId,
      config
    );
    assert.ok(arbiter.ruleResultCache.get(cacheKey), 'cache populated');

    arbiter.updateNodeData('user:1', { tier: 'premium' });
    assert.strictEqual(arbiter.ruleResultCache.get(cacheKey), undefined);
  });
});
