/**
 * @module MemoryCache Tests
 * @description Unit tests for the LRU cache implementation.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { MemoryCache, CacheKeys, MemoryCaches, getCacheStatsSummary } from './cache';

describe('MemoryCache', () => {
  let cache: MemoryCache<string>;

  beforeEach(() => {
    vi.useFakeTimers();
    cache = new MemoryCache<string>(3, 1000); // Max 3 items, 1 second TTL
  });

  afterEach(() => {
    vi.useRealTimers();
  });

  describe('basic operations', () => {
    it('should store and retrieve values', () => {
      cache.set('key1', 'value1');
      expect(cache.get('key1')).toBe('value1');
    });

    it('should return undefined for missing keys', () => {
      expect(cache.get('missing')).toBeUndefined();
    });

    it('should delete values', () => {
      cache.set('key1', 'value1');
      expect(cache.delete('key1')).toBe(true);
      expect(cache.get('key1')).toBeUndefined();
    });

    it('should return false when deleting non-existent key', () => {
      expect(cache.delete('missing')).toBe(false);
    });

    it('should check if key exists', () => {
      cache.set('key1', 'value1');
      expect(cache.has('key1')).toBe(true);
      expect(cache.has('missing')).toBe(false);
    });

    it('should clear all entries', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');
      cache.clear();
      expect(cache.has('key1')).toBe(false);
      expect(cache.has('key2')).toBe(false);
    });
  });

  describe('TTL expiration', () => {
    it('should return value before TTL expires', () => {
      cache.set('key1', 'value1');
      vi.advanceTimersByTime(500);
      expect(cache.get('key1')).toBe('value1');
    });

    it('should return undefined after TTL expires', () => {
      cache.set('key1', 'value1');
      vi.advanceTimersByTime(1500);
      expect(cache.get('key1')).toBeUndefined();
    });

    it('should use custom TTL when provided', () => {
      cache.set('key1', 'value1', 2000);
      vi.advanceTimersByTime(1500);
      expect(cache.get('key1')).toBe('value1');
      vi.advanceTimersByTime(1000);
      expect(cache.get('key1')).toBeUndefined();
    });
  });

  describe('LRU eviction', () => {
    it('should evict least recently used item when at capacity', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');
      cache.set('key3', 'value3');

      // Access key1 to make it most recently used
      cache.get('key1');

      // Add a new key, should evict key2 (least recently used)
      cache.set('key4', 'value4');

      expect(cache.has('key1')).toBe(true);
      expect(cache.has('key2')).toBe(false);
      expect(cache.has('key3')).toBe(true);
      expect(cache.has('key4')).toBe(true);
    });

    it('should update access order on get', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');
      cache.set('key3', 'value3');

      // Access key1
      cache.get('key1');

      // Add new key, should evict key2
      cache.set('key4', 'value4');

      expect(cache.has('key2')).toBe(false);
    });
  });

  describe('pattern invalidation', () => {
    it('should invalidate entries matching pattern', () => {
      // Create a larger cache for this test to avoid LRU eviction
      const largeCache = new MemoryCache<string>(10, 1000);
      largeCache.set('user:123:data', 'data1');
      largeCache.set('user:123:settings', 'settings1');
      largeCache.set('user:456:data', 'data2');
      largeCache.set('system:config', 'config');

      const count = largeCache.invalidatePattern(/^user:123:/);

      expect(count).toBe(2);
      expect(largeCache.has('user:123:data')).toBe(false);
      expect(largeCache.has('user:123:settings')).toBe(false);
      expect(largeCache.has('user:456:data')).toBe(true);
      expect(largeCache.has('system:config')).toBe(true);
    });

    it('should invalidate all entries for user', () => {
      cache.set('user1:history', 'history');
      cache.set('user1:lessons', 'lessons');
      cache.set('user2:history', 'history');

      const count = cache.invalidateUser('user1');

      expect(count).toBe(2);
      expect(cache.has('user1:history')).toBe(false);
      expect(cache.has('user1:lessons')).toBe(false);
      expect(cache.has('user2:history')).toBe(true);
    });

    it('should invalidate entries generated by CacheKeys', () => {
      cache.set(CacheKeys.distilledMemory('user123'), 'distilled');
      cache.set(CacheKeys.lessons('user123'), 'lessons');
      cache.set(CacheKeys.history('other'), 'other-history');

      const count = cache.invalidateUser('user123');

      expect(count).toBe(2);
      expect(cache.has(CacheKeys.distilledMemory('user123'))).toBe(false);
      expect(cache.has(CacheKeys.lessons('user123'))).toBe(false);
      expect(cache.has(CacheKeys.history('other'))).toBe(true);
    });

    it('should handle userIds with regex special characters', () => {
      const testCache = new MemoryCache<string>(100, 60000);

      testCache.set('user+test:history', 'h1');
      testCache.set('user+test:lessons', 'l1');
      testCache.set('user+test:data', 'd1');
      testCache.set('other:data', 'o1');

      const count1 = testCache.invalidateUser('user+test');
      expect(count1).toBe(3);
      expect(testCache.has('user+test:history')).toBe(false);
      expect(testCache.has('user+test:lessons')).toBe(false);
      expect(testCache.has('user+test:data')).toBe(false);
      expect(testCache.has('other:data')).toBe(true);

      testCache.set('user*abc:data', 'd1');
      const count2 = testCache.invalidateUser('user*abc');
      expect(count2).toBe(1);
      expect(testCache.has('user*abc:data')).toBe(false);
    });
  });

  describe('batch operations', () => {
    it('should get multiple keys at once', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');
      cache.set('key3', 'value3');

      const result = cache.getMany(['key1', 'key2', 'missing']);

      expect(result.size).toBe(2);
      expect(result.get('key1')).toBe('value1');
      expect(result.get('key2')).toBe('value2');
      expect(result.has('missing')).toBe(false);
    });

    it('should set multiple key-value pairs at once', () => {
      cache.setMany([
        ['key1', 'value1'],
        ['key2', 'value2'],
        ['key3', 'value3', 5000],
      ]);

      expect(cache.get('key1')).toBe('value1');
      expect(cache.get('key2')).toBe('value2');
      expect(cache.get('key3')).toBe('value3');
    });
  });

  describe('statistics', () => {
    it('should track cache hits and misses', () => {
      cache.set('key1', 'value1');

      cache.get('key1'); // hit
      cache.get('key2'); // miss
      cache.get('key1'); // hit

      const stats = cache.getStats();
      expect(stats.hits).toBe(2);
      expect(stats.misses).toBe(1);
    });

    it('should track evictions', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');
      cache.set('key3', 'value3');
      cache.set('key4', 'value4'); // triggers eviction

      const stats = cache.getStats();
      expect(stats.evictions).toBe(1);
    });

    it('should calculate hit rate', () => {
      cache.set('key1', 'value1');

      cache.get('key1'); // hit
      cache.get('key1'); // hit
      cache.get('key2'); // miss

      const hitRate = cache.getHitRate();
      expect(hitRate).toBeCloseTo(66.67, 1);
    });
  });
});

describe('CacheKeys', () => {
  it('should generate consistent cache keys', () => {
    expect(CacheKeys.distilledMemory('user123')).toBe('distilled:user123');
    expect(CacheKeys.lessons('user123')).toBe('lessons:user123');
    expect(CacheKeys.preferences('user123')).toBe('prefs:user123');
    expect(CacheKeys.history('conv123')).toBe('history:conv123');
    expect(CacheKeys.summary('conv123')).toBe('summary:conv123');
    expect(CacheKeys.globalLessons(5)).toBe('global_lessons:5');
    expect(CacheKeys.insightsSearch('user123', 'query', 'category')).toBe(
      'insights:user123:query:category:none:global'
    );
    expect(CacheKeys.insightsSearch('user123', 'query')).toBe(
      'insights:user123:query:all:none:global'
    );
    expect(CacheKeys.gap('gap123')).toBe('gap:gap123');
    expect(CacheKeys.gapsByStatus('OPEN')).toBe('gaps:OPEN');
  });
});

describe('MemoryCaches', () => {
  it('should have separate cache instances', () => {
    expect(MemoryCaches.userData).not.toBe(MemoryCaches.conversation);
    expect(MemoryCaches.userData).not.toBe(MemoryCaches.global);
    expect(MemoryCaches.userData).not.toBe(MemoryCaches.search);
  });

  it('should have different TTLs', () => {
    // This is a structural test - we can't directly test TTL without mocking time
    // But we verify the caches exist and are different instances
    expect(MemoryCaches.userData).toBeDefined();
    expect(MemoryCaches.conversation).toBeDefined();
    expect(MemoryCaches.global).toBeDefined();
    expect(MemoryCaches.search).toBeDefined();
  });
});

describe('getCacheStatsSummary', () => {
  beforeEach(() => {
    MemoryCaches.userData.clear();
    MemoryCaches.conversation.clear();
    MemoryCaches.global.clear();
    MemoryCaches.search.clear();
  });

  it('should return stats for all caches', () => {
    MemoryCaches.userData.set('key1', 'value1');
    MemoryCaches.userData.get('key1'); // hit
    MemoryCaches.userData.get('missing'); // miss

    const stats = getCacheStatsSummary();

    expect(stats.userData.hits).toBe(1);
    expect(stats.userData.misses).toBe(1);
    expect(stats.conversation.hits).toBe(0);
    expect(stats.global.hits).toBe(0);
    expect(stats.search.hits).toBe(0);
  });

  it('should calculate overall hit rate', () => {
    MemoryCaches.userData.set('key1', 'value1');
    MemoryCaches.userData.get('key1'); // hit
    MemoryCaches.userData.get('key1'); // hit
    MemoryCaches.userData.get('missing'); // miss

    const stats = getCacheStatsSummary();

    expect(stats.overallHitRate).toBeCloseTo(66.67, 1);
  });
});
