package cache

import (
	"context"
	"fmt"
	"sync"
	"testing"
	"time"
)

func TestCacheBasicOperations(t *testing.T) {
	ctx := context.Background()
	config := DefaultConfig()
	config.DefaultTTL = 100 * time.Millisecond
	config.CleanupInterval = 50 * time.Millisecond
	cache := New(config)
	defer cache.Close()

	// Test Set and Get
	cache.Set(ctx, "key1", "value1")
	if val, ok := cache.Get(ctx, "key1"); !ok || val != "value1" {
		t.Errorf("Expected 'value1', got %v, exists: %v", val, ok)
	}

	// Test SetWithTTL
	cache.SetWithTTL(ctx, "key2", "value2", 200*time.Millisecond)
	if val, ok := cache.Get(ctx, "key2"); !ok || val != "value2" {
		t.Errorf("Expected 'value2', got %v, exists: %v", val, ok)
	}

	// Test Delete
	cache.Delete(ctx, "key1")
	if _, ok := cache.Get(ctx, "key1"); ok {
		t.Errorf("Key 'key1' should have been deleted")
	}

	// Test automatic expiration
	time.Sleep(150 * time.Millisecond)
	if _, ok := cache.Get(ctx, "key1"); ok {
		t.Errorf("Key 'key1' should have expired")
	}
	// key2 should still be valid (200ms TTL)
	if _, ok := cache.Get(ctx, "key2"); !ok {
		t.Errorf("Key 'key2' should still be valid")
	}

	// Wait for key2 to expire
	time.Sleep(100 * time.Millisecond)
	if _, ok := cache.Get(ctx, "key2"); ok {
		t.Errorf("Key 'key2' should have expired")
	}

	// Test Clear
	cache.Set(ctx, "key3", "value3")
	cache.Clear(ctx)
	if _, ok := cache.Get(ctx, "key3"); ok {
		t.Errorf("Cache should be empty after Clear()")
	}
}

func TestCacheEviction(t *testing.T) {
	ctx := context.Background()
	config := DefaultConfig()
	config.MaxItems = 5
	cache := New(config)
	defer cache.Close()

	// Add 5 items (max capacity)
	for i := 0; i < 5; i++ {
		key := fmt.Sprintf("key%d", i)
		cache.Set(ctx, key, i)
	}

	// Verify all 5 items are in the cache
	for i := 0; i < 5; i++ {
		key := fmt.Sprintf("key%d", i)
		if _, ok := cache.Get(ctx, key); !ok {
			t.Errorf("Key '%s' should be in the cache", key)
		}
	}

	// Add 2 more items to trigger eviction
	cache.Set(ctx, "keyA", "valueA")
	cache.Set(ctx, "keyB", "valueB")

	// Verify size is still within limits
	if cache.Size() > int64(config.MaxItems) {
		t.Errorf("Cache size %d exceeds limit %d", cache.Size(), config.MaxItems)
	}

	// Some of the original keys should have been evicted
	evictedCount := 0
	for i := 0; i < 5; i++ {
		key := fmt.Sprintf("key%d", i)
		if _, ok := cache.Get(ctx, key); !ok {
			evictedCount++
		}
	}

	if evictedCount == 0 {
		t.Errorf("No keys were evicted despite exceeding max items")
	}

	// The newer keys should still be present
	if _, ok := cache.Get(ctx, "keyA"); !ok {
		t.Errorf("Key 'keyA' should be in the cache")
	}
	if _, ok := cache.Get(ctx, "keyB"); !ok {
		t.Errorf("Key 'keyB' should be in the cache")
	}
}

func TestCacheConcurrency(t *testing.T) {
	ctx := context.Background()
	cache := NewDefault()
	defer cache.Close()

	const goroutines = 10
	const operationsPerGoroutine = 100

	var wg sync.WaitGroup
	wg.Add(goroutines)

	for i := 0; i < goroutines; i++ {
		go func(id int) {
			defer wg.Done()

			baseKey := fmt.Sprintf("worker%d-", id)

			// Set operations
			for j := 0; j < operationsPerGoroutine; j++ {
				key := fmt.Sprintf("%skey%d", baseKey, j)
				value := fmt.Sprintf("value%d-%d", id, j)
				cache.Set(ctx, key, value)
			}

			// Get operations
			for j := 0; j < operationsPerGoroutine; j++ {
				key := fmt.Sprintf("%skey%d", baseKey, j)
				val, ok := cache.Get(ctx, key)
				if !ok {
					t.Errorf("Key '%s' should exist in cache", key)
					continue
				}
				expected := fmt.Sprintf("value%d-%d", id, j)
				if val != expected {
					t.Errorf("For key '%s', expected '%s', got '%s'", key, expected, val)
				}
			}

			// Delete half the keys
			for j := 0; j < operationsPerGoroutine/2; j++ {
				key := fmt.Sprintf("%skey%d", baseKey, j)
				cache.Delete(ctx, key)
			}
		}(i)
	}

	wg.Wait()

	// Verify size and deletion
	var totalKeysExpected int64 = goroutines * operationsPerGoroutine / 2
	if cache.Size() != totalKeysExpected {
		t.Errorf("Expected cache size to be %d, got %d", totalKeysExpected, cache.Size())
	}
}

func TestEvictionCallback(t *testing.T) {
	ctx := context.Background()
	evicted := make(map[string]interface{})
	evictedMu := sync.Mutex{}

	config := DefaultConfig()
	config.DefaultTTL = 50 * time.Millisecond
	config.CleanupInterval = 25 * time.Millisecond
	config.OnEviction = func(key string, value interface{}) {
		evictedMu.Lock()
		evicted[key] = value
		evictedMu.Unlock()
	}

	cache := New(config)
	defer cache.Close()

	// Add items
	cache.Set(ctx, "key1", "value1")
	cache.Set(ctx, "key2", "value2")

	// Manually delete
	cache.Delete(ctx, "key1")

	// Verify manual deletion triggered callback
	time.Sleep(10 * time.Millisecond) // Small delay to ensure callback processed
	evictedMu.Lock()
	if evicted["key1"] != "value1" {
		t.Errorf("Eviction callback not triggered for manual deletion")
	}
	evictedMu.Unlock()

	// Wait for automatic expiration
	time.Sleep(60 * time.Millisecond)

	// Verify TTL expiration triggered callback
	evictedMu.Lock()
	if evicted["key2"] != "value2" {
		t.Errorf("Eviction callback not triggered for TTL expiration")
	}
	evictedMu.Unlock()
}
