package dataext

import (
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"math/rand"
	"strconv"
	"testing"
)

func init() {
	rand.Seed(0)
}

func TestResultCache1(t *testing.T) {
	cache := NewLRUMap[string, string](8)
	verifyLRUList(cache, t)

	key := randomKey()
	val := randomVal()

	if cache.Size() != 0 {
		t.Errorf("cache size expected == 0, actual == %v", cache.Size())
	}

	if _, ok := cache.TryGet(key); ok {
		t.Errorf("empty cache TryGet returned value")
	}
	verifyLRUList(cache, t)

	cache.Put(key, val)
	verifyLRUList(cache, t)

	if cache.Size() != 1 {
		t.Errorf("cache size expected == 1, actual == %v", cache.Size())
	}

	cacheval, ok := cache.TryGet(key)
	verifyLRUList(cache, t)
	if !ok {
		t.Errorf("cache TryGet returned no value")
	}
	if cacheval != val {
		t.Errorf("cache TryGet returned different value (%+v <> %+v)", cacheval, val)
	}

	if _, ok := cache.TryGet(randomKey()); ok {
		t.Errorf("cache TryGet returned a value for non-existant key")
	}
	verifyLRUList(cache, t)
}

func TestResultCache2(t *testing.T) {
	cache := NewLRUMap[string, string](8)
	verifyLRUList(cache, t)

	key1 := "key1"
	val1 := randomVal()
	cache.Put(key1, val1)
	verifyLRUList(cache, t)

	key2 := "key2"
	val2 := randomVal()
	cache.Put(key2, val2)
	verifyLRUList(cache, t)

	key3 := "key3"
	val3 := randomVal()
	cache.Put(key3, val3)
	verifyLRUList(cache, t)

	key4 := "key4"
	val4 := randomVal()
	cache.Put(key4, val4)
	verifyLRUList(cache, t)

	if _, ok := cache.TryGet(key1); !ok {
		t.Errorf("cache TryGet returned no value")
	}
	verifyLRUList(cache, t)
	if _, ok := cache.TryGet(key2); !ok {
		t.Errorf("cache TryGet returned no value")
	}
	verifyLRUList(cache, t)
	if _, ok := cache.TryGet(key3); !ok {
		t.Errorf("cache TryGet returned no value")
	}
	verifyLRUList(cache, t)
	if _, ok := cache.TryGet(key4); !ok {
		t.Errorf("cache TryGet returned no value")
	}
	verifyLRUList(cache, t)
	if _, ok := cache.TryGet(randomKey()); ok {
		t.Errorf("cache TryGet returned a value for non-existant key")
	}
	verifyLRUList(cache, t)
	if cache.Size() != 4 {
		t.Errorf("cache size expected == 4, actual == %v", cache.Size())
	}
	verifyLRUList(cache, t)

	cache.Put(key4, val4) // same key again
	verifyLRUList(cache, t)

	if cache.Size() != 4 {
		t.Errorf("cache size expected == 4, actual == %v", cache.Size())
	}

	cache.Put(randomKey(), randomVal())
	verifyLRUList(cache, t)
	cache.Put(randomKey(), randomVal())
	verifyLRUList(cache, t)
	cache.Put(randomKey(), randomVal())
	verifyLRUList(cache, t)
	cache.Put(randomKey(), randomVal())
	verifyLRUList(cache, t)

	if cache.Size() != 8 {
		t.Errorf("cache size expected == 8, actual == %v", cache.Size())
	}

	cache.Put(randomKey(), randomVal()) // drops key1
	verifyLRUList(cache, t)

	if cache.Size() != 8 {
		t.Errorf("cache size expected == 8, actual == %v", cache.Size())
	}

	if _, ok := cache.TryGet(key1); ok {
		t.Errorf("[key1] should be dropped from cache")
	}
	verifyLRUList(cache, t)
	if _, ok := cache.TryGet(key2); !ok { // moves key2 to most-recently used
		t.Errorf("[key2] should still be in cache")
	}
	verifyLRUList(cache, t)

	cache.Put(randomKey(), randomVal()) // drops key3
	verifyLRUList(cache, t)

	if cache.Size() != 8 {
		t.Errorf("cache size expected == 8, actual == %v", cache.Size())
	}

	if _, ok := cache.TryGet(key3); ok {
		t.Errorf("[key3] should be dropped from cache")
	}
	if _, ok := cache.TryGet(key2); !ok {
		t.Errorf("[key2] should still be in cache")
	}
}

func TestResultCache3(t *testing.T) {
	cache := NewLRUMap[string, string](8)
	verifyLRUList(cache, t)

	key1 := "key1"
	val1 := randomVal()
	val2 := randomVal()

	cache.Put(key1, val1)
	verifyLRUList(cache, t)

	if val, ok := cache.TryGet(key1); !ok || val != val1 {
		t.Errorf("Value in cache should be [val1]")
	}

	cache.Put(key1, val2)
	verifyLRUList(cache, t)

	if val, ok := cache.TryGet(key1); !ok || val != val2 {
		t.Errorf("Value in cache should be [val2]")
	}
}

// does a basic consistency check over the internal cache representation
func verifyLRUList[TKey comparable, TData any](cache *LRUMap[TKey, TData], t *testing.T) {
	size := 0

	tailFound := false
	headFound := false
	curr := cache.lfuHead
	for curr != nil {
		size++

		if curr.parent == nil {
			headFound = true
			if curr != cache.lfuHead {
				t.Errorf("head != lfuHead")
				return
			}
		}

		if curr.child == nil {
			tailFound = true
			if curr != cache.lfuTail {
				t.Errorf("tail != lfuTail")
				return
			}
		}

		if curr.child != nil {
			if curr.child.parent != curr {
				t.Errorf("error in child <-> parent link")
				return
			}
		}

		if curr.parent != nil {
			if curr.parent.child != curr {
				t.Errorf("error in parent <-> child link")
				return
			}
		}

		curr = curr.child
	}

	if cache.Size() > 0 && cache.lfuHead == nil {
		t.Errorf("no head in cache")
	}
	if cache.Size() > 0 && cache.lfuTail == nil {
		t.Errorf("no tail in cache")
	}

	if cache.Size() == 0 && cache.lfuHead != nil {
		t.Errorf("dangling head in cache")
	}
	if cache.Size() == 0 && cache.lfuTail != nil {
		t.Errorf("dangling tail in cache")
	}

	if cache.Size() > 0 && !headFound {
		t.Errorf("head not found")
	}

	if cache.Size() > 0 && !tailFound {
		t.Errorf("tail not found")
	}

	if size != cache.Size() {
		t.Errorf("error size mismatch (%v <> %v)", size, cache.Size())
	}

	if cache.Size() > cache.maxsize {
		t.Errorf("too many items: %v", cache.Size())
	}
}

func randomKey() string {
	return strconv.FormatInt(rand.Int63(), 16)
}

func randomVal() string {
	v, err := langext.NewHexUUID()
	if err != nil {
		panic(err)
	}
	return v
}
