import { describe, it, expect, beforeEach, vi } from 'vitest';
import { CacheManager } from './cache-manager';

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

  beforeEach(() => {
    cache = new CacheManager({
      ttl: 1000, // 1 second for testing
      maxSize: 3
    });
  });

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

    it('should store different types of values', () => {
      // Create cache with larger maxSize to avoid eviction
      const largeCache = new CacheManager({ ttl: 1000, maxSize: 10 });

      largeCache.set('string', 'text');
      largeCache.set('number', 42);
      largeCache.set('object', { foo: 'bar' });
      largeCache.set('array', [1, 2, 3]);

      expect(largeCache.get('string')).toBe('text');
      expect(largeCache.get('number')).toBe(42);
      expect(largeCache.get('object')).toEqual({ foo: 'bar' });
      expect(largeCache.get('array')).toEqual([1, 2, 3]);
    });

    it('should overwrite existing values', () => {
      cache.set('key1', 'value1');
      cache.set('key1', 'value2');
      expect(cache.get('key1')).toBe('value2');
    });

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

  describe('TTL (Time to Live)', () => {
    it('should expire entries after TTL', async () => {
      cache.set('key1', 'value1');
      expect(cache.get('key1')).toBe('value1');

      // Wait for expiration
      await new Promise(resolve => setTimeout(resolve, 1100));

      expect(cache.get('key1')).toBeUndefined();
    });

    it('should not return expired entries', async () => {
      cache.set('key1', 'value1');

      // Wait for expiration
      await new Promise(resolve => setTimeout(resolve, 1100));

      // Get should return undefined and remove entry
      expect(cache.get('key1')).toBeUndefined();
      expect(cache.has('key1')).toBe(false);
    });
  });

  describe('has', () => {
    it('should return true for existing keys', () => {
      cache.set('key1', 'value1');
      expect(cache.has('key1')).toBe(true);
    });

    it('should return false for non-existent keys', () => {
      expect(cache.has('nonexistent')).toBe(false);
    });

    it('should return false for expired keys', async () => {
      cache.set('key1', 'value1');
      expect(cache.has('key1')).toBe(true);

      await new Promise(resolve => setTimeout(resolve, 1100));

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

  describe('invalidate', () => {
    it('should remove a specific entry', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');

      cache.invalidate('key1');

      expect(cache.get('key1')).toBeUndefined();
      expect(cache.get('key2')).toBe('value2');
    });

    it('should handle invalidating non-existent keys', () => {
      expect(() => cache.invalidate('nonexistent')).not.toThrow();
    });
  });

  describe('invalidateAll', () => {
    it('should remove all entries', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');
      cache.set('key3', 'value3');

      cache.invalidateAll();

      expect(cache.get('key1')).toBeUndefined();
      expect(cache.get('key2')).toBeUndefined();
      expect(cache.get('key3')).toBeUndefined();
    });
  });

  describe('invalidateExpired', () => {
    it('should remove only expired entries', async () => {
      cache.set('key1', 'value1');

      // Wait a bit, but not for full expiration
      await new Promise(resolve => setTimeout(resolve, 600));

      cache.set('key2', 'value2');

      // Wait for first key to expire
      await new Promise(resolve => setTimeout(resolve, 600));

      cache.invalidateExpired();

      expect(cache.get('key1')).toBeUndefined();
      expect(cache.get('key2')).toBe('value2');
    });
  });

  describe('maxSize', () => {
    it('should enforce max size by removing oldest entry', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');
      cache.set('key3', 'value3');

      // This should remove key1 (oldest)
      cache.set('key4', 'value4');

      expect(cache.get('key1')).toBeUndefined();
      expect(cache.get('key2')).toBe('value2');
      expect(cache.get('key3')).toBe('value3');
      expect(cache.get('key4')).toBe('value4');
    });

    it('should handle maxSize = 1', () => {
      const smallCache = new CacheManager({ ttl: 1000, maxSize: 1 });

      smallCache.set('key1', 'value1');
      smallCache.set('key2', 'value2');

      expect(smallCache.get('key1')).toBeUndefined();
      expect(smallCache.get('key2')).toBe('value2');
    });
  });

  describe('getAge', () => {
    it('should return age of entry', async () => {
      cache.set('key1', 'value1');

      await new Promise(resolve => setTimeout(resolve, 100));

      const age = cache.getAge('key1');
      expect(age).toBeGreaterThanOrEqual(100);
      expect(age).toBeLessThan(200);
    });

    it('should return undefined for non-existent keys', () => {
      expect(cache.getAge('nonexistent')).toBeUndefined();
    });
  });

  describe('getStats', () => {
    it('should return cache statistics', () => {
      cache.set('key1', 'value1');
      cache.set('key2', 'value2');

      const stats = cache.getStats();

      expect(stats.size).toBe(2);
      expect(stats.maxSize).toBe(3);
      expect(stats.ttl).toBe(1000);
      expect(stats.oldestEntryAge).toBeDefined();
    });

    it('should return undefined for oldestEntryAge when cache is empty', () => {
      const stats = cache.getStats();

      expect(stats.size).toBe(0);
      expect(stats.oldestEntryAge).toBeUndefined();
    });
  });

  describe('edge cases', () => {
    it('should handle null values', () => {
      cache.set('key1', null);
      expect(cache.get('key1')).toBeNull();
    });

    it('should handle undefined values', () => {
      cache.set('key1', undefined);
      expect(cache.get('key1')).toBeUndefined();
      // has() will return false because get() returns undefined
      // This is expected behavior - undefined is indistinguishable from missing
    });

    it('should handle rapid successive sets', () => {
      for (let i = 0; i < 100; i++) {
        cache.set(`key${i}`, `value${i}`);
      }

      // Should only have last 3 entries due to maxSize
      const stats = cache.getStats();
      expect(stats.size).toBeLessThanOrEqual(3);
    });
  });
});
