//nolint:errcheck
package cache_test

import (
	"context"
	"strconv"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/android-sms-gateway/server/pkg/cache"
)

func TestMemoryCache_ConcurrentReads(t *testing.T) {
	cache := cache.NewMemory(0)

	ctx := context.Background()
	key := "test-key"
	value := "test-value"

	// Set initial value
	err := cache.Set(ctx, key, value)
	if err != nil {
		t.Fatalf("Set failed: %v", err)
	}

	const numGoroutines = 100
	var wg sync.WaitGroup

	// Launch multiple concurrent reads
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()

			retrieved, err := cache.Get(ctx, key)
			if err != nil {
				t.Errorf("Get failed: %v", err)
				return
			}

			if retrieved != value {
				t.Errorf("Expected %s, got %s", value, retrieved)
			}
		}()
	}

	wg.Wait()
}

func TestMemoryCache_ConcurrentWrites(t *testing.T) {
	cache := cache.NewMemory(0)

	ctx := context.Background()
	const numKeys = 100
	const numGoroutines = 10
	var wg sync.WaitGroup

	// Launch multiple concurrent writes
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(goroutineID int) {
			defer wg.Done()

			for j := 0; j < numKeys/numGoroutines; j++ {
				key := "key-" + strconv.Itoa(goroutineID) + "-" + strconv.Itoa(j)
				value := "value-" + strconv.Itoa(goroutineID) + "-" + strconv.Itoa(j)

				err := cache.Set(ctx, key, value)
				if err != nil {
					t.Errorf("Set failed for key %s: %v", key, err)
				}
			}
		}(i)
	}

	wg.Wait()

	// Verify all keys were set correctly
	for i := 0; i < numGoroutines; i++ {
		for j := 0; j < numKeys/numGoroutines; j++ {
			key := "key-" + strconv.Itoa(i) + "-" + strconv.Itoa(j)
			expectedValue := "value-" + strconv.Itoa(i) + "-" + strconv.Itoa(j)

			retrieved, err := cache.Get(ctx, key)
			if err != nil {
				t.Errorf("Get failed for key %s: %v", key, err)
				continue
			}

			if retrieved != expectedValue {
				t.Errorf("Expected %s, got %s for key %s", expectedValue, retrieved, key)
			}
		}
	}
}

func TestMemoryCache_ConcurrentReadWrite(t *testing.T) {
	c := cache.NewMemory(0)

	ctx := context.Background()
	const numOperations = 1000
	const numReaders = 5
	const numWriters = 2
	var wg sync.WaitGroup
	var readCount, writeCount atomic.Int64

	// Launch concurrent readers
	for range numReaders {
		wg.Add(1)
		go func() {
			defer wg.Done()

			for range numOperations / numReaders {
				key := "shared-key"
				_, err := c.Get(ctx, key)
				if err != nil && err != cache.ErrKeyNotFound {
					t.Errorf("Get failed: %v", err)
				} else if err == nil {
					readCount.Add(1)
				}
			}
		}()
	}

	// Launch concurrent writers
	for range numWriters {
		wg.Add(1)
		go func() {
			defer wg.Done()

			for j := range numOperations / numWriters {
				key := "shared-key"
				value := "value-" + strconv.Itoa(j)

				err := c.Set(ctx, key, value)
				if err != nil {
					t.Errorf("Set failed: %v", err)
				} else {
					writeCount.Add(1)
				}
			}
		}()
	}

	wg.Wait()
	t.Logf("Completed %d successful reads and %d writes", readCount.Load(), writeCount.Load())
}

func TestMemoryCache_ConcurrentSetAndGetAndDelete(t *testing.T) {
	cache := cache.NewMemory(0)

	ctx := context.Background()
	const numOperations = 500
	const numGoroutines = 10
	var wg sync.WaitGroup

	// Launch goroutines that perform Set, Get, and Delete operations
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(goroutineID int) {
			defer wg.Done()

			for j := 0; j < numOperations/numGoroutines; j++ {
				key := "key-" + strconv.Itoa(goroutineID) + "-" + strconv.Itoa(j)
				value := "value-" + strconv.Itoa(goroutineID) + "-" + strconv.Itoa(j)

				// Set
				err := cache.Set(ctx, key, value)
				if err != nil {
					t.Errorf("Set failed: %v", err)
					continue
				}

				// Get
				retrieved, err := cache.Get(ctx, key)
				if err != nil {
					t.Errorf("Get failed: %v", err)
					continue
				}

				if retrieved != value {
					t.Errorf("Expected %s, got %s", value, retrieved)
				}

				// Delete
				err = cache.Delete(ctx, key)
				if err != nil {
					t.Errorf("Delete failed: %v", err)
				}
			}
		}(i)
	}

	wg.Wait()
}

func TestMemoryCache_ConcurrentSetOrFail(t *testing.T) {
	c := cache.NewMemory(0)

	ctx := context.Background()
	const numGoroutines = 10
	const attemptsPerGoroutine = 100
	var wg sync.WaitGroup

	// Launch goroutines that try to SetOrFail the same key
	key := "contentious-key"
	value := "initial-value"

	var successCount atomic.Int32
	var existsCount atomic.Int32

	for range numGoroutines {
		wg.Add(1)
		go func() {
			defer wg.Done()

			for range attemptsPerGoroutine {
				err := c.SetOrFail(ctx, key, value)
				switch err {
				case nil:
					successCount.Add(1)
				case cache.ErrKeyExists:
					existsCount.Add(1)
				default:
					t.Errorf("SetOrFail failed: %v", err)
				}
			}
		}()
	}

	wg.Wait()

	// Only one goroutine should succeed, all others should get ErrKeyExists
	if c := successCount.Load(); c != 1 {
		t.Errorf("Expected 1 successful SetOrFail, got %d", c)
	}

	expectedExistsCount := (numGoroutines * attemptsPerGoroutine) - 1
	if c := int(existsCount.Load()); c != expectedExistsCount {
		t.Errorf("Expected %d ErrKeyExists, got %d", expectedExistsCount, c)
	}
}

func TestMemoryCache_ConcurrentDrain(t *testing.T) {
	c := cache.NewMemory(0)

	ctx := context.Background()
	const numItems = 100
	const numGoroutines = 5
	var wg sync.WaitGroup
	var drainResults sync.Map

	// Pre-populate cache with items
	for i := range numItems {
		key := "item-" + strconv.Itoa(i)
		value := "value-" + strconv.Itoa(i)

		err := c.Set(ctx, key, value)
		if err != nil {
			t.Fatalf("Set failed for item %d: %v", i, err)
		}
	}

	// Launch concurrent drain operations
	for i := range numGoroutines {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()

			items, err := c.Drain(ctx)
			if err != nil {
				t.Errorf("Drain failed: %v", err)
			}
			drainResults.Store(id, items)
		}(i)
	}

	wg.Wait()

	// Verify that items were drained (at least one goroutine should have gotten items)
	totalDrained := 0
	drainResults.Range(func(key, value any) bool {
		items := value.(map[string]string)
		totalDrained += len(items)
		return true
	})

	if totalDrained != numItems {
		t.Errorf("Expected %d total items drained, got %d", numItems, totalDrained)
	}

	// Cache should be empty after all drain operations
	for i := range numItems {
		key := "item-" + strconv.Itoa(i)
		_, err := c.Get(ctx, key)
		if err != cache.ErrKeyNotFound {
			t.Errorf("Expected ErrKeyNotFound for key %s after drain, got %v", key, err)
		}
	}
}

func TestMemoryCache_ConcurrentCleanup(t *testing.T) {
	c := cache.NewMemory(0)

	ctx := context.Background()
	const numItems = 50
	const numGoroutines = 5
	var wg sync.WaitGroup

	// Pre-populate cache with items that will expire quickly
	for i := range numItems {
		key := "item-" + strconv.Itoa(i)
		value := "value-" + strconv.Itoa(i)

		err := c.Set(ctx, key, value, cache.WithTTL(10*time.Millisecond))
		if err != nil {
			t.Fatalf("Set failed for item %d: %v", i, err)
		}
	}

	// Wait for items to expire before launching cleanup operations
	time.Sleep(15 * time.Millisecond)

	// Launch concurrent cleanup operations
	for range numGoroutines {
		wg.Add(1)
		go func() {
			defer wg.Done()

			err := c.Cleanup(ctx)
			if err != nil {
				t.Errorf("Cleanup failed: %v", err)
			}
		}()
	}

	wg.Wait()

	// All items should be expired and removed
	for i := range numItems {
		key := "item-" + strconv.Itoa(i)
		_, err := c.Get(ctx, key)
		if err != cache.ErrKeyNotFound {
			t.Errorf("Expected ErrKeyNotFound for key %s, got %v", key, err)
		}
	}
}

func TestMemoryCache_ConcurrentGetAndDelete(t *testing.T) {
	c := cache.NewMemory(0)

	ctx := context.Background()
	const numGoroutines = 10
	const attemptsPerGoroutine = 50
	var wg sync.WaitGroup

	// Pre-populate cache with items
	for i := 0; i < numGoroutines*attemptsPerGoroutine; i++ {
		key := "item-" + strconv.Itoa(i)
		value := "value-" + strconv.Itoa(i)

		err := c.Set(ctx, key, value)
		if err != nil {
			t.Fatalf("Set failed for item %d: %v", i, err)
		}
	}

	// Launch goroutines that perform GetAndDelete operations
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(goroutineID int) {
			defer wg.Done()

			for j := 0; j < attemptsPerGoroutine; j++ {
				key := "item-" + strconv.Itoa(goroutineID*attemptsPerGoroutine+j)

				_, err := c.GetAndDelete(ctx, key)
				if err != nil && err != cache.ErrKeyNotFound {
					t.Errorf("GetAndDelete failed: %v", err)
				}
			}
		}(i)
	}

	wg.Wait()

	// All items should be deleted
	for i := 0; i < numGoroutines*attemptsPerGoroutine; i++ {
		key := "item-" + strconv.Itoa(i)
		_, err := c.Get(ctx, key)
		if err != cache.ErrKeyNotFound {
			t.Errorf("Expected ErrKeyNotFound for key %s after GetAndDelete, got %v", key, err)
		}
	}
}

func TestMemoryCache_RaceConditionDetection(t *testing.T) {
	// This test is specifically designed to detect race conditions
	// by running many operations concurrently with the race detector enabled

	cache := cache.NewMemory(0)

	ctx := context.Background()
	const duration = 2 * time.Second
	const numGoroutines = 20
	var wg sync.WaitGroup

	start := time.Now()

	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(goroutineID int) {
			defer wg.Done()

			for time.Since(start) < duration {
				key := "race-key-" + strconv.Itoa(goroutineID)
				value := "race-value-" + strconv.Itoa(goroutineID) + "-" + strconv.FormatInt(time.Now().UnixNano(), 10)

				// Randomly choose operation
				switch time.Now().UnixNano() % 4 {
				case 0:
					cache.Set(ctx, key, value)
				case 1:
					cache.Get(ctx, key)
				case 2:
					cache.Delete(ctx, key)
				case 3:
					cache.GetAndDelete(ctx, key)
				}
			}
		}(i)
	}

	wg.Wait()
}
