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

describe('HyperbolicLRUCache Invalidation', () => {
  let arbiter;
  let chainRule;

  beforeEach(() => {
    arbiter = new Arbiter({
      directCheckCacheSize: 1000,
      directCheckCacheTTL: 60000
    });
    chainRule = new ChainRule(arbiter);
  });

  it('should support key deletion', () => {
    const cache = chainRule.chainResultCache;
    
    // Add some entries
    cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() });
    cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() });
    cache.set('key3', { result: { possibility: 0.3 }, timestamp: Date.now() });
    
    assert.ok(cache.has('key1'));
    assert.ok(cache.has('key2'));
    assert.ok(cache.has('key3'));
    
    // Delete specific key
    const deleted = cache.delete('key2');
    assert.ok(deleted);
    assert.ok(cache.has('key1'));
    assert.ok(!cache.has('key2'));
    assert.ok(cache.has('key3'));
  });

  it('should support key invalidation', () => {
    const cache = chainRule.chainResultCache;
    
    // Add some entries
    cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() });
    cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() });
    
    // Invalidate specific key
    const invalidated = cache.invalidate('key1');
    assert.ok(invalidated);
    assert.ok(!cache.has('key1'));
    assert.ok(cache.has('key2'));
  });

  it('should support invalidating multiple keys', () => {
    const cache = chainRule.chainResultCache;
    
    // Add some entries
    cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() });
    cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() });
    cache.set('key3', { result: { possibility: 0.3 }, timestamp: Date.now() });
    
    // Invalidate multiple keys
    const invalidatedCount = cache.invalidateMany(['key1', 'key3']);
    assert.strictEqual(invalidatedCount, 2);
    assert.ok(!cache.has('key1'));
    assert.ok(cache.has('key2'));
    assert.ok(!cache.has('key3'));
  });

  it('should support pattern-based invalidation', () => {
    const cache = chainRule.chainResultCache;
    
    // Add entries with different patterns
    cache.set('user_123_role_membership', { result: { possibility: 0.5 }, timestamp: Date.now() });
    cache.set('user_123_permission', { result: { possibility: 0.8 }, timestamp: Date.now() });
    cache.set('user_456_role_membership', { result: { possibility: 0.3 }, timestamp: Date.now() });
    cache.set('user_456_permission', { result: { possibility: 0.7 }, timestamp: Date.now() });
    
    // Invalidate all entries for user_123
    const invalidatedCount = cache.invalidateByPattern('user_123');
    assert.strictEqual(invalidatedCount, 2);
    assert.ok(!cache.has('user_123_role_membership'));
    assert.ok(!cache.has('user_123_permission'));
    assert.ok(cache.has('user_456_role_membership'));
    assert.ok(cache.has('user_456_permission'));
  });

  it('should support regex pattern invalidation', () => {
    const cache = chainRule.chainResultCache;
    
    // Add entries with different patterns
    cache.set('user_123_role_membership', { result: { possibility: 0.5 }, timestamp: Date.now() });
    cache.set('user_123_permission', { result: { possibility: 0.8 }, timestamp: Date.now() });
    cache.set('user_456_role_membership', { result: { possibility: 0.3 }, timestamp: Date.now() });
    cache.set('user_456_permission', { result: { possibility: 0.7 }, timestamp: Date.now() });
    
    // Invalidate all role_membership entries using regex
    const roleMembershipPattern = /.*role_membership.*/;
    const invalidatedCount = cache.invalidateByPattern(roleMembershipPattern);
    assert.strictEqual(invalidatedCount, 2);
    assert.ok(!cache.has('user_123_role_membership'));
    assert.ok(cache.has('user_123_permission'));
    assert.ok(!cache.has('user_456_role_membership'));
    assert.ok(cache.has('user_456_permission'));
  });

  it('should handle cache invalidation in Arbiter direct check cache', () => {
    // Add some direct check cache entries
    arbiter.directCheckCache.set('user:alice|can_read|doc:secret', { result: { possibility: 0.8 }, timestamp: Date.now() });
    arbiter.directCheckCache.set('user:bob|can_write|doc:public', { result: { possibility: 0.9 }, timestamp: Date.now() });
    
    assert.ok(arbiter.directCheckCache.has('user:alice|can_read|doc:secret'));
    assert.ok(arbiter.directCheckCache.has('user:bob|can_write|doc:public'));
    
    // Invalidate entries for user:alice
    const invalidatedCount = arbiter.directCheckCache.invalidateByPattern('user:alice');
    assert.strictEqual(invalidatedCount, 1);
    assert.ok(!arbiter.directCheckCache.has('user:alice|can_read|doc:secret'));
    assert.ok(arbiter.directCheckCache.has('user:bob|can_write|doc:public'));
  });

  it('should handle cache invalidation in RelationManager', () => {
    // Cache state lives in RelationCaches (RF-08); reach it via `_caches`.
    const caches = arbiter.relationManager._caches;
    const srcRelKey = caches.makeSrcRelCacheKey(123, 'role_membership');
    const dstRelKey = caches.makeDstRelCacheKey(456, 'role_membership');
    const directKey = caches.makeDirectCacheKey(123, 'role_membership', 456);
    const otherKey = caches.makeDirectCacheKey(123, 'permission', 789);

    caches.relationLookupCache.set(srcRelKey, [{ src: 123, rel: 'role_membership', dst: 456 }]);
    caches.relationLookupCache.set(dstRelKey, [{ src: 456, rel: 'role_membership', dst: 789 }]);
    caches.relationLookupCache.set(directKey, [{ src: 123, rel: 'role_membership', dst: 456 }]);
    caches.relationLookupCache.set(otherKey, [{ src: 123, rel: 'permission', dst: 789 }]);

    // Test the invalidation method
    arbiter.relationManager._invalidateRelationCaches(123, 'role_membership', 456);

    // Entries for the (src, rel), (dst, rel) and (src, rel, dst) triple vanish
    assert.ok(!caches.relationLookupCache.has(srcRelKey));
    assert.ok(!caches.relationLookupCache.has(dstRelKey));
    assert.ok(!caches.relationLookupCache.has(directKey));
    assert.ok(caches.relationLookupCache.has(otherKey)); // Different relation
  });

  it('should handle cache invalidation in NodeManager', () => {
    // Add nodes
    const aliceId = arbiter.nodeManager.addNode('user:alice', 'user', { name: 'Alice' });
    const bobId = arbiter.nodeManager.addNode('user:bob', 'user', { name: 'Bob' });
    const carolId = arbiter.nodeManager.addNode('user:carol', 'user', { name: 'Carol' });

    // Direct-check cache keys are composite keys over numeric node ids
    const aliceKey = arbiter.keyManager.createCompositeKey(aliceId, 'can_read', bobId);
    const bobTouchingAlice = arbiter.keyManager.createCompositeKey(bobId, 'can_read', aliceId);
    const carolKey = arbiter.keyManager.createCompositeKey(carolId, 'can_read', bobId);

    // Direct-check keys are rolling hashes; invalidation-by-node uses the
    // per-node key index, so every injected key must be tracked.
    arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() });
    arbiter._trackDirectCheckKey('can_read', aliceId, bobId, aliceKey);
    arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() });
    arbiter._trackDirectCheckKey('can_read', bobId, aliceId, bobTouchingAlice);
    arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() });
    arbiter._trackDirectCheckKey('can_read', carolId, bobId, carolKey);

    assert.ok(arbiter.directCheckCache.has(aliceKey));
    assert.ok(arbiter.directCheckCache.has(bobTouchingAlice));
    assert.ok(arbiter.directCheckCache.has(carolKey));

    // Update node data (should trigger cache invalidation via the DecisionCache port)
    arbiter.nodeManager.updateNodeData('user:alice', { name: 'Alice Updated' });

    // Any entry whose composite key contains alice's id is invalidated
    assert.ok(!arbiter.directCheckCache.has(aliceKey));
    assert.ok(!arbiter.directCheckCache.has(bobTouchingAlice));
    assert.ok(arbiter.directCheckCache.has(carolKey)); // Does not touch alice
  });

  it('should handle cache invalidation in ChainRule', () => {
    // Add some chain cache entries
    chainRule.chainResultCache.set('0_154_role_membership_out|role_permission_out', { result: { possibility: 0.8 }, timestamp: Date.now() });
    chainRule.chainResultCache.set('0_154_permission_out|access_out', { result: { possibility: 0.9 }, timestamp: Date.now() });

    // Invalidate all chain result cache entries
    chainRule._invalidateAllChainCaches();

    assert.ok(!chainRule.chainResultCache.has('0_154_role_membership_out|role_permission_out'));
    assert.ok(!chainRule.chainResultCache.has('0_154_permission_out|access_out'));
  });

  it('should track eviction statistics', () => {
    const cache = chainRule.chainResultCache;
    
    // Fill cache beyond capacity to trigger evictions
    for (let i = 0; i < chainRule.maxCacheSize + 50; i++) {
      cache.set(`key_${i}`, { result: { possibility: 0.5 }, timestamp: Date.now() });
    }
    
    // Check if eviction stats are tracked
    if (chainRule.stats) {
      console.log(`Chain result cache evictions: ${chainRule.stats.evictedResults || 0}`);
      console.log(`Chain path cache evictions: ${chainRule.stats.evictedPaths || 0}`);
    }
    
    // Cache should not exceed capacity
    assert.ok(cache.size() <= chainRule.maxCacheSize);
  });
});

console.log('✅ HyperbolicLRUCache Invalidation Test Suite');
console.log('🔧 New Features:');
console.log('  - delete(key) - Remove specific key');
console.log('  - invalidate(key) - Alias for delete');
console.log('  - invalidateMany(keys) - Remove multiple keys');
console.log('  - invalidateByPattern(pattern) - Remove keys matching pattern');
console.log('  - Support for both string and regex patterns');
console.log('  - Automatic cache invalidation on node/relation changes');
console.log('  - Better staleness handling');
