package go_kv

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

// openBenchDB opens a fresh database in b.TempDir() with default config.
// Returns the DB handle; caller must Close().
func openBenchDB(b *testing.B) *DB {
	b.Helper()
	db, err := Open(b.TempDir(), nil)
	if err != nil {
		b.Fatalf("Open: %v", err)
	}
	return db
}

// BenchmarkSinglePut measures single-threaded sequential Put throughput.
func BenchmarkSinglePut(b *testing.B) {
	db := openBenchDB(b)
	defer func() {
		if err := db.Close(); err != nil {
			b.Fatalf("Close: %v", err)
		}
	}()

	b.ReportAllocs()
	b.ResetTimer()

	i := 0
	for b.Loop() {
		key := fmt.Appendf(nil, "key-%08d", i)
		val := fmt.Appendf(nil, "value-%08d", i)
		if err := db.Put(key, val); err != nil {
			b.Fatalf("Put: %v", err)
		}
		i++
	}
}

// BenchmarkConcurrentPut measures multi-goroutine Put throughput.
// Uses b.N directly because b.Loop() cannot be called from spawned goroutines.
func BenchmarkConcurrentPut(b *testing.B) {
	for _, workers := range []int{2, 4, 8} {
		b.Run(fmt.Sprintf("workers=%d", workers), func(b *testing.B) {
			db := openBenchDB(b)
			defer func() {
				if err := db.Close(); err != nil {
					b.Fatalf("Close: %v", err)
				}
			}()

			b.ReportAllocs()
			b.ResetTimer()

			var wg sync.WaitGroup
			opsPerWorker := b.N / workers
			for w := range workers {
				wg.Add(1)
				go func(workerID int) {
					defer wg.Done()
					for i := range opsPerWorker {
						key := fmt.Appendf(nil, "key-%d-%08d", workerID, i)
						val := fmt.Appendf(nil, "value-%d-%08d", workerID, i)
						if err := db.Put(key, val); err != nil {
							b.Errorf("Put: %v", err)
							return
						}
					}
				}(w)
			}
			wg.Wait()
		})
	}
}

// BenchmarkGet measures Get latency after pre-loading 1000 keys.
func BenchmarkGet(b *testing.B) {
	const numKeys = 1000

	db := openBenchDB(b)
	defer func() {
		if err := db.Close(); err != nil {
			b.Fatalf("Close: %v", err)
		}
	}()

	// Pre-load keys.
	for i := range numKeys {
		key := fmt.Appendf(nil, "key-%08d", i)
		val := fmt.Appendf(nil, "value-%08d", i)
		if err := db.Put(key, val); err != nil {
			b.Fatalf("Put preload: %v", err)
		}
	}

	b.ReportAllocs()
	b.ResetTimer()

	i := 0
	for b.Loop() {
		key := fmt.Appendf(nil, "key-%08d", i%numKeys)
		res := db.Get(key)
		if !res.Found {
			b.Fatalf("Get key %s: not found", key)
		}
		i++
	}
}

// BenchmarkConcurrentGet measures parallel Get after pre-loading keys.
func BenchmarkConcurrentGet(b *testing.B) {
	const numKeys = 1000

	db := openBenchDB(b)
	defer func() {
		if err := db.Close(); err != nil {
			b.Fatalf("Close: %v", err)
		}
	}()

	// Pre-load keys.
	for i := range numKeys {
		key := fmt.Appendf(nil, "key-%08d", i)
		val := fmt.Appendf(nil, "value-%08d", i)
		if err := db.Put(key, val); err != nil {
			b.Fatalf("Put preload: %v", err)
		}
	}

	b.ReportAllocs()
	b.ResetTimer()

	b.RunParallel(func(pb *testing.PB) {
		i := 0
		for pb.Next() {
			key := fmt.Appendf(nil, "key-%08d", i%numKeys)
			res := db.Get(key)
			if !res.Found {
				b.Fatalf("Get key %s: not found", key)
			}
			i++
		}
	})
}

// BenchmarkMixedReadWrite measures a 50% Put + 50% Get workload mix.
func BenchmarkMixedReadWrite(b *testing.B) {
	const numKeys = 1000

	db := openBenchDB(b)
	defer func() {
		if err := db.Close(); err != nil {
			b.Fatalf("Close: %v", err)
		}
	}()

	// Pre-load half the keys so Gets are not all misses.
	for i := range numKeys / 2 {
		key := fmt.Appendf(nil, "key-%08d", i)
		val := fmt.Appendf(nil, "value-%08d", i)
		if err := db.Put(key, val); err != nil {
			b.Fatalf("Put preload: %v", err)
		}
	}

	b.ReportAllocs()
	b.ResetTimer()

	i := 0
	for b.Loop() {
		key := fmt.Appendf(nil, "key-%08d", i%numKeys)
		val := fmt.Appendf(nil, "value-%08d", i%numKeys)
		if i%2 == 0 {
			if err := db.Put(key, val); err != nil {
				b.Fatalf("Put: %v", err)
			}
		} else {
			_ = db.Get(key)
		}
		i++
	}
}

// BenchmarkWALRecovery measures recovery (Open) time for different data sizes.
func BenchmarkWALRecovery(b *testing.B) {
	for _, nKeys := range []int{100, 1000, 10000} {
		b.Run(fmt.Sprintf("keys=%d", nKeys), func(b *testing.B) {
			dir := b.TempDir()

			// Write N keys to populate WAL segments.
			func() {
				db, err := Open(dir, nil)
				if err != nil {
					b.Fatalf("Open for write: %v", err)
				}
				defer db.Close()

				for i := range nKeys {
					key := fmt.Appendf(nil, "key-%08d", i)
					val := fmt.Appendf(nil, "value-%08d", i)
					if err := db.Put(key, val); err != nil {
						b.Fatalf("Put: %v", err)
					}
				}
			}()

			// Now benchmark only the recovery (Open) phase.
			b.ReportAllocs()
			b.ResetTimer()

			for b.Loop() {
				db, err := Open(dir, nil)
				if err != nil {
					b.Fatalf("Open recovery: %v", err)
				}
				if err := db.Close(); err != nil {
					b.Fatalf("Close: %v", err)
				}
			}
		})
	}
}
