import { describe, it, expect, vi, afterEach } from 'vitest';
import { FilterCache, DEFAULT_FILTER_CACHE_CONFIG } from '../src/llm/filter-cache.js';

describe('FilterCache', () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });

  it('returns null for unknown tool names', () => {
    const cache = new FilterCache();
    expect(cache.shouldFilter('unknown/tool')).toBeNull();
  });

  it('stores and retrieves filter decisions', () => {
    const cache = new FilterCache();
    cache.recordDecision('slack/search', true);
    expect(cache.shouldFilter('slack/search')).toBe(true);

    cache.recordDecision('github/list_repos', false);
    expect(cache.shouldFilter('github/list_repos')).toBe(false);
  });

  it('updates existing entries on re-record', () => {
    const cache = new FilterCache();
    cache.recordDecision('slack/search', true);
    expect(cache.shouldFilter('slack/search')).toBe(true);

    cache.recordDecision('slack/search', false);
    expect(cache.shouldFilter('slack/search')).toBe(false);
  });

  it('evicts oldest entry when at capacity', () => {
    const cache = new FilterCache({ maxEntries: 3 });
    cache.recordDecision('tool-a', true);
    cache.recordDecision('tool-b', false);
    cache.recordDecision('tool-c', true);
    expect(cache.size).toBe(3);

    // Adding a 4th should evict 'tool-a' (oldest)
    cache.recordDecision('tool-d', false);
    expect(cache.size).toBe(3);
    expect(cache.shouldFilter('tool-a')).toBeNull();
    expect(cache.shouldFilter('tool-b')).toBe(false);
    expect(cache.shouldFilter('tool-d')).toBe(false);
  });

  it('refreshes LRU position on access', () => {
    const cache = new FilterCache({ maxEntries: 3 });
    cache.recordDecision('tool-a', true);
    cache.recordDecision('tool-b', false);
    cache.recordDecision('tool-c', true);

    // Access tool-a to refresh it
    cache.shouldFilter('tool-a');

    // Now add tool-d — tool-b should be evicted (oldest unreferenced)
    cache.recordDecision('tool-d', false);
    expect(cache.shouldFilter('tool-a')).toBe(true);
    expect(cache.shouldFilter('tool-b')).toBeNull();
  });

  it('expires entries after TTL', () => {
    const now = Date.now();
    vi.spyOn(Date, 'now').mockReturnValue(now);

    const cache = new FilterCache({ ttlMs: 1000 });
    cache.recordDecision('slack/search', true);
    expect(cache.shouldFilter('slack/search')).toBe(true);

    // Advance time past TTL
    vi.spyOn(Date, 'now').mockReturnValue(now + 1001);
    expect(cache.shouldFilter('slack/search')).toBeNull();
    // Entry should be removed
    expect(cache.size).toBe(0);
  });

  it('does not expire entries within TTL', () => {
    const now = Date.now();
    vi.spyOn(Date, 'now').mockReturnValue(now);

    const cache = new FilterCache({ ttlMs: 1000 });
    cache.recordDecision('slack/search', true);

    // Advance time within TTL
    vi.spyOn(Date, 'now').mockReturnValue(now + 999);
    expect(cache.shouldFilter('slack/search')).toBe(true);
  });

  it('clears all entries', () => {
    const cache = new FilterCache();
    cache.recordDecision('tool-a', true);
    cache.recordDecision('tool-b', false);
    expect(cache.size).toBe(2);

    cache.clear();
    expect(cache.size).toBe(0);
    expect(cache.shouldFilter('tool-a')).toBeNull();
  });

  it('uses default config values', () => {
    const cache = new FilterCache();
    // Should support the default number of entries without issue
    for (let i = 0; i < DEFAULT_FILTER_CACHE_CONFIG.maxEntries; i++) {
      cache.recordDecision(`tool-${i}`, true);
    }
    expect(cache.size).toBe(DEFAULT_FILTER_CACHE_CONFIG.maxEntries);

    // One more should trigger eviction
    cache.recordDecision('extra-tool', true);
    expect(cache.size).toBe(DEFAULT_FILTER_CACHE_CONFIG.maxEntries);
  });
});
