import { describe, it, expect } from 'vitest';
import { MemoryCache } from '../src/proxymodel/cache.js';

describe('MemoryCache', () => {
  it('returns computed value on cache miss', async () => {
    const cache = new MemoryCache();
    const value = await cache.getOrCompute('key1', async () => 'computed');
    expect(value).toBe('computed');
  });

  it('returns cached value on cache hit', async () => {
    const cache = new MemoryCache();
    let callCount = 0;
    const compute = async () => { callCount++; return 'computed'; };

    await cache.getOrCompute('key1', compute);
    const value = await cache.getOrCompute('key1', compute);

    expect(value).toBe('computed');
    expect(callCount).toBe(1); // Only computed once
  });

  it('get/set work for manual cache operations', async () => {
    const cache = new MemoryCache();

    expect(await cache.get('missing')).toBeNull();

    await cache.set('key1', 'value1');
    expect(await cache.get('key1')).toBe('value1');
  });

  it('hash produces consistent short hashes', () => {
    const cache = new MemoryCache();
    const hash1 = cache.hash('hello world');
    const hash2 = cache.hash('hello world');
    const hash3 = cache.hash('different content');

    expect(hash1).toBe(hash2);
    expect(hash1).not.toBe(hash3);
    expect(hash1).toHaveLength(16);
  });

  it('evicts oldest entry when at capacity', async () => {
    const cache = new MemoryCache({ maxEntries: 3 });

    await cache.set('a', '1');
    await cache.set('b', '2');
    await cache.set('c', '3');
    expect(cache.size).toBe(3);

    // Adding 4th should evict 'a' (oldest)
    await cache.set('d', '4');
    expect(cache.size).toBe(3);
    expect(await cache.get('a')).toBeNull();
    expect(await cache.get('b')).toBe('2');
    expect(await cache.get('d')).toBe('4');
  });

  it('accessing an entry refreshes its LRU position', async () => {
    const cache = new MemoryCache({ maxEntries: 3 });

    await cache.set('a', '1');
    await cache.set('b', '2');
    await cache.set('c', '3');

    // Access 'a' to refresh it
    await cache.get('a');

    // Adding 'd' should evict 'b' (now oldest), not 'a'
    await cache.set('d', '4');
    expect(await cache.get('a')).toBe('1');
    expect(await cache.get('b')).toBeNull();
  });

  it('getOrCompute refreshes LRU position on hit', async () => {
    const cache = new MemoryCache({ maxEntries: 3 });

    await cache.set('a', '1');
    await cache.set('b', '2');
    await cache.set('c', '3');

    // Hit 'a' via getOrCompute
    await cache.getOrCompute('a', async () => 'should not run');

    // Evict: 'b' should go, not 'a'
    await cache.set('d', '4');
    expect(await cache.get('a')).toBe('1');
    expect(await cache.get('b')).toBeNull();
  });

  it('clear removes all entries', async () => {
    const cache = new MemoryCache();
    await cache.set('a', '1');
    await cache.set('b', '2');
    expect(cache.size).toBe(2);

    cache.clear();
    expect(cache.size).toBe(0);
    expect(await cache.get('a')).toBeNull();
  });
});
