package lrucache

import (
	"fmt"
	"sync"
	"testing"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/cgroup"
)

func TestCache(t *testing.T) {
	sizeMaxBytes := 64 * 1024
	// Multiply sizeMaxBytes by the square of available CPU cores
	// in order to get proper distribution of sizes between cache shards.
	// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/2204
	cpus := cgroup.AvailableCPUs()
	sizeMaxBytes *= cpus * cpus
	getMaxSize := func() int {
		return sizeMaxBytes
	}
	c := NewCache(getMaxSize)
	defer c.MustStop()
	if n := c.SizeBytes(); n != 0 {
		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, 0)
	}
	if n := c.SizeMaxBytes(); n != sizeMaxBytes {
		t.Fatalf("unexpected SizeMaxBytes(); got %d; want %d", n, sizeMaxBytes)
	}
	k := "foobar"
	var e testEntry
	entrySize := e.SizeBytes()
	// Put a single entry into cache
	c.PutEntry(k, &e)
	if n := c.Len(); n != 1 {
		t.Fatalf("unexpected number of items in the cache; got %d; want %d", n, 1)
	}
	if n := c.SizeBytes(); n != entrySize {
		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, entrySize)
	}
	if n := c.Requests(); n != 0 {
		t.Fatalf("unexpected number of requests; got %d; want %d", n, 0)
	}
	if n := c.Misses(); n != 0 {
		t.Fatalf("unexpected number of misses; got %d; want %d", n, 0)
	}
	// Obtain this entry from the cache
	if e1 := c.GetEntry(k); e1 != &e {
		t.Fatalf("unexpected entry obtained; got %v; want %v", e1, &e)
	}
	if n := c.Requests(); n != 1 {
		t.Fatalf("unexpected number of requests; got %d; want %d", n, 1)
	}
	if n := c.Misses(); n != 0 {
		t.Fatalf("unexpected number of misses; got %d; want %d", n, 0)
	}
	// Obtain non-existing entry from the cache
	if e1 := c.GetEntry("non-existing-key"); e1 != nil {
		t.Fatalf("unexpected non-nil block obtained for non-existing key: %v", e1)
	}
	if n := c.Requests(); n != 2 {
		t.Fatalf("unexpected number of requests; got %d; want %d", n, 2)
	}
	if n := c.Misses(); n != 1 {
		t.Fatalf("unexpected number of misses; got %d; want %d", n, 1)
	}
	// Store the entry again.
	c.PutEntry(k, &e)
	if n := c.SizeBytes(); n != entrySize {
		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, entrySize)
	}
	if e1 := c.GetEntry(k); e1 != &e {
		t.Fatalf("unexpected entry obtained; got %v; want %v", e1, &e)
	}
	if n := c.Requests(); n != 3 {
		t.Fatalf("unexpected number of requests; got %d; want %d", n, 3)
	}
	if n := c.Misses(); n != 1 {
		t.Fatalf("unexpected number of misses; got %d; want %d", n, 1)
	}

	// Manually clean the cache. The entry shouldn't be deleted because it was recently accessed.
	c.cleanByTimeout()
	if n := c.SizeBytes(); n != entrySize {
		t.Fatalf("unexpected SizeBytes(); got %d; want %d", n, entrySize)
	}
}

func TestCacheConcurrentAccess(_ *testing.T) {
	const sizeMaxBytes = 16 * 1024 * 1024
	getMaxSize := func() int {
		return sizeMaxBytes
	}
	c := NewCache(getMaxSize)
	defer c.MustStop()

	workers := 5
	var wg sync.WaitGroup
	wg.Add(workers)
	for i := 0; i < workers; i++ {
		go func(worker int) {
			defer wg.Done()
			testCacheSetGet(c, worker)
		}(i)
	}
	wg.Wait()
}

func testCacheSetGet(c *Cache, worker int) {
	for i := 0; i < 1000; i++ {
		e := testEntry{}
		k := fmt.Sprintf("key_%d_%d", worker, i)
		c.PutEntry(k, &e)
		if e1 := c.GetEntry(k); e1 != &e {
			panic(fmt.Errorf("unexpected entry obtained; got %v; want %v", e1, &e))
		}
		if e1 := c.GetEntry("non-existing-key"); e1 != nil {
			panic(fmt.Errorf("unexpected non-nil entry obtained: %v", e1))
		}
	}
}

type testEntry struct{}

func (tb *testEntry) SizeBytes() int {
	return 42
}