package engine

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

func TestGenerateCaching(t *testing.T) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		t.Fatalf("NewDefaultGenerator failed: %v", err)
	}
	hash := "abcdef123456789"
	size := 64.0

	// Generate icon first time
	icon1, err := generator.Generate(context.Background(), hash, size)
	if err != nil {
		t.Fatalf("First generate failed: %v", err)
	}

	// Check cache size
	if generator.GetCacheSize() != 1 {
		t.Errorf("Expected cache size 1, got %d", generator.GetCacheSize())
	}

	// Generate same icon again
	icon2, err := generator.Generate(context.Background(), hash, size)
	if err != nil {
		t.Fatalf("Second generate failed: %v", err)
	}

	// Should be the same instance from cache
	if icon1 != icon2 {
		t.Error("Second generate did not return cached instance")
	}

	// Cache size should still be 1
	if generator.GetCacheSize() != 1 {
		t.Errorf("Expected cache size 1 after second generate, got %d", generator.GetCacheSize())
	}
}

func TestClearCache(t *testing.T) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		t.Fatalf("NewDefaultGenerator failed: %v", err)
	}
	hash := "abcdef123456789"
	size := 64.0

	// Generate an icon to populate cache
	_, err = generator.Generate(context.Background(), hash, size)
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	// Verify cache has content
	if generator.GetCacheSize() == 0 {
		t.Error("Cache should not be empty after generate")
	}

	// Clear cache
	generator.ClearCache()

	// Verify cache is empty
	if generator.GetCacheSize() != 0 {
		t.Errorf("Expected cache size 0 after clear, got %d", generator.GetCacheSize())
	}
}

func TestSetConfig(t *testing.T) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		t.Fatalf("NewDefaultGenerator failed: %v", err)
	}
	hash := "abcdef123456789"
	size := 64.0

	// Generate an icon to populate cache
	_, err = generator.Generate(context.Background(), hash, size)
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	// Verify cache has content
	if generator.GetCacheSize() == 0 {
		t.Error("Cache should not be empty after generate")
	}

	// Set new config
	newConfig := DefaultColorConfig()
	newConfig.IconPadding = 0.1
	generator.SetConfig(newConfig)

	// Verify config was updated
	if generator.GetConfig().IconPadding != 0.1 {
		t.Errorf("Expected icon padding 0.1, got %f", generator.GetConfig().IconPadding)
	}

	// Verify cache was cleared
	if generator.GetCacheSize() != 0 {
		t.Errorf("Expected cache size 0 after config change, got %d", generator.GetCacheSize())
	}
}

func TestLRUCacheEviction(t *testing.T) {
	// Create generator with small cache for testing eviction
	config := GeneratorConfig{
		ColorConfig: DefaultColorConfig(),
		CacheSize:   2, // Small cache to test eviction
	}

	generator, err := NewGeneratorWithConfig(config)
	if err != nil {
		t.Fatalf("NewGeneratorWithConfig failed: %v", err)
	}

	// Generate 3 different icons to trigger eviction
	hashes := []string{
		"abcdef1234567890abcdef1234567890abcdef12",
		"123456789abcdef0123456789abcdef0123456789",
		"fedcba0987654321fedcba0987654321fedcba09",
	}
	size := 64.0

	icons := make([]*Icon, len(hashes))
	for i, hash := range hashes {
		var icon *Icon
		icon, err = generator.Generate(context.Background(), hash, size)
		if err != nil {
			t.Fatalf("Generate failed for hash %s: %v", hash, err)
		}
		icons[i] = icon
	}

	// Cache should only contain 2 items (the last 2)
	if generator.GetCacheSize() != 2 {
		t.Errorf("Expected cache size 2 after eviction, got %d", generator.GetCacheSize())
	}

	// The first icon should have been evicted, so generating it again should create a new instance
	icon1Again, err := generator.Generate(context.Background(), hashes[0], size)
	if err != nil {
		t.Fatalf("Generate failed for evicted hash: %v", err)
	}

	// This should be a different instance since the first was evicted
	if icons[0] == icon1Again {
		t.Error("First icon was not evicted from cache as expected")
	}

	// The last icon should still be cached
	icon3Again, err := generator.Generate(context.Background(), hashes[2], size)
	if err != nil {
		t.Fatalf("Generate failed for cached hash: %v", err)
	}

	// This should be the same instance
	if icons[2] != icon3Again {
		t.Error("Last icon was evicted from cache unexpectedly")
	}
}

func TestCacheMetrics(t *testing.T) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		t.Fatalf("NewDefaultGenerator failed: %v", err)
	}

	// Initially, metrics should be zero
	hits, misses := generator.GetCacheMetrics()
	if hits != 0 || misses != 0 {
		t.Errorf("Expected initial metrics (0, 0), got (%d, %d)", hits, misses)
	}

	hash := "abcdef1234567890abcdef1234567890abcdef12"
	size := 64.0

	// First generation should be a cache miss
	_, err = generator.Generate(context.Background(), hash, size)
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	hits, misses = generator.GetCacheMetrics()
	if hits != 0 || misses != 1 {
		t.Errorf("Expected metrics (0, 1) after first generate, got (%d, %d)", hits, misses)
	}

	// Second generation should be a cache hit
	_, err = generator.Generate(context.Background(), hash, size)
	if err != nil {
		t.Fatalf("Second generate failed: %v", err)
	}

	hits, misses = generator.GetCacheMetrics()
	if hits != 1 || misses != 1 {
		t.Errorf("Expected metrics (1, 1) after cache hit, got (%d, %d)", hits, misses)
	}

	// Generate different icon should be another miss
	_, err = generator.Generate(context.Background(), "1234567890abcdef1234567890abcdef12345678", size)
	if err != nil {
		t.Fatalf("Generate with different hash failed: %v", err)
	}

	hits, misses = generator.GetCacheMetrics()
	if hits != 1 || misses != 2 {
		t.Errorf("Expected metrics (1, 2) after different hash, got (%d, %d)", hits, misses)
	}

	// Clear cache should reset metrics
	generator.ClearCache()
	hits, misses = generator.GetCacheMetrics()
	if hits != 0 || misses != 0 {
		t.Errorf("Expected metrics (0, 0) after cache clear, got (%d, %d)", hits, misses)
	}
}

func TestSetGeneratorConfig(t *testing.T) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		t.Fatalf("NewDefaultGenerator failed: %v", err)
	}

	// Generate an icon to populate cache
	hash := "abcdef1234567890abcdef1234567890abcdef12"
	_, err = generator.Generate(context.Background(), hash, 64.0)
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	// Verify cache has content
	if generator.GetCacheSize() == 0 {
		t.Error("Cache should not be empty after generate")
	}

	// Update configuration with different cache size
	newConfig := GeneratorConfig{
		ColorConfig: DefaultColorConfig(),
		CacheSize:   500,
	}
	newConfig.ColorConfig.IconPadding = 0.15

	err = generator.SetGeneratorConfig(newConfig)
	if err != nil {
		t.Fatalf("SetGeneratorConfig failed: %v", err)
	}

	// Verify configuration was updated
	currentConfig := generator.GetGeneratorConfig()
	if currentConfig.CacheSize != 500 {
		t.Errorf("Expected cache size 500, got %d", currentConfig.CacheSize)
	}

	if currentConfig.ColorConfig.IconPadding != 0.15 {
		t.Errorf("Expected icon padding 0.15, got %f", currentConfig.ColorConfig.IconPadding)
	}

	// Verify cache was cleared due to config change
	if generator.GetCacheSize() != 0 {
		t.Errorf("Expected cache size 0 after config change, got %d", generator.GetCacheSize())
	}

	// Verify cache capacity is updated
	if generator.GetCacheCapacity() != 500 {
		t.Errorf("Expected cache capacity 500, got %d", generator.GetCacheCapacity())
	}
}

func TestSetGeneratorConfigSameSize(t *testing.T) {
	config := GeneratorConfig{
		ColorConfig: DefaultColorConfig(),
		CacheSize:   1000, // Same as default
	}

	generator, err := NewGeneratorWithConfig(config)
	if err != nil {
		t.Fatalf("NewGeneratorWithConfig failed: %v", err)
	}

	// Generate an icon to populate cache
	hash := "abcdef1234567890abcdef1234567890abcdef12"
	_, err = generator.Generate(context.Background(), hash, 64.0)
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	// Update configuration with same cache size but different color config
	newConfig := config
	newConfig.ColorConfig.IconPadding = 0.2

	err = generator.SetGeneratorConfig(newConfig)
	if err != nil {
		t.Fatalf("SetGeneratorConfig failed: %v", err)
	}

	// Cache should be cleared even with same cache size
	if generator.GetCacheSize() != 0 {
		t.Errorf("Expected cache size 0 after config change, got %d", generator.GetCacheSize())
	}
}

func TestSetGeneratorConfigInvalidCacheSize(t *testing.T) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		t.Fatalf("NewDefaultGenerator failed: %v", err)
	}

	tests := []struct {
		name      string
		cacheSize int
	}{
		{"Zero cache size", 0},
		{"Negative cache size", -1},
		{"Very negative cache size", -100},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			config := GeneratorConfig{
				ColorConfig: DefaultColorConfig(),
				CacheSize:   test.cacheSize,
			}

			err := generator.SetGeneratorConfig(config)
			if err == nil {
				t.Errorf("Expected error for cache size %d, but got none", test.cacheSize)
			}
		})
	}
}

func TestConcurrentCacheAccess(t *testing.T) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		t.Fatalf("NewDefaultGenerator failed: %v", err)
	}

	const numGoroutines = 10
	const numGenerations = 5

	hash := "abcdef1234567890abcdef1234567890abcdef12"
	size := 64.0

	var wg sync.WaitGroup
	wg.Add(numGoroutines)

	// Launch multiple goroutines that generate the same icon concurrently
	for i := 0; i < numGoroutines; i++ {
		go func() {
			defer wg.Done()
			for j := 0; j < numGenerations; j++ {
				_, err := generator.Generate(context.Background(), hash, size)
				if err != nil {
					t.Errorf("Concurrent generate failed: %v", err)
					return
				}
			}
		}()
	}

	wg.Wait()

	// Cache should only contain one item since all goroutines generated the same icon
	if generator.GetCacheSize() != 1 {
		t.Errorf("Expected cache size 1 after concurrent access, got %d", generator.GetCacheSize())
	}

	// Total cache operations should be recorded correctly (allow tolerance for singleflight deduplication)
	hits, misses := generator.GetCacheMetrics()
	totalOperations := hits + misses
	expectedOperations := int64(numGoroutines * numGenerations)

	// Singleflight can significantly reduce counted operations when many goroutines
	// request the same key simultaneously - they share the result from one generation.
	// Allow for up to 50% reduction due to deduplication in highly concurrent scenarios.
	minExpected := expectedOperations / 2
	if totalOperations < minExpected || totalOperations > expectedOperations {
		t.Errorf("Expected %d-%d total cache operations, got %d (hits: %d, misses: %d)",
			minExpected, expectedOperations, totalOperations, hits, misses)
	}

	// There should be at least one miss (the first generation)
	if misses < 1 {
		t.Errorf("Expected at least 1 cache miss, got %d", misses)
	}
}

func BenchmarkCacheKey(b *testing.B) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		b.Fatalf("NewDefaultGenerator failed: %v", err)
	}

	hash := "abcdef1234567890abcdef1234567890abcdef12"
	size := 64.0

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_ = generator.cacheKey(hash, size)
	}
}

func BenchmarkLRUCacheHit(b *testing.B) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		b.Fatalf("NewDefaultGenerator failed: %v", err)
	}

	hash := "abcdef1234567890abcdef1234567890abcdef12"
	size := 64.0

	// Pre-populate cache
	_, err = generator.Generate(context.Background(), hash, size)
	if err != nil {
		b.Fatalf("Pre-populate failed: %v", err)
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, err := generator.Generate(context.Background(), hash, size)
		if err != nil {
			b.Fatalf("Generate failed: %v", err)
		}
	}
}

func BenchmarkLRUCacheMiss(b *testing.B) {
	generator, err := NewDefaultGenerator()
	if err != nil {
		b.Fatalf("NewDefaultGenerator failed: %v", err)
	}

	size := 64.0

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// Use different hash each time to ensure cache miss
		hash := fmt.Sprintf("%040x", i)
		_, err := generator.GenerateWithoutCache(context.Background(), hash, size)
		if err != nil {
			b.Fatalf("Generate failed: %v", err)
		}
	}
}
