package channeldb

import (
	"reflect"
	"testing"
)

// TestRejectCache checks the behavior of the rejectCache with respect to insertion,
// eviction, and removal of cache entries.
func TestRejectCache(t *testing.T) {
	const cacheSize = 100

	// Create a new reject cache with the configured max size.
	c := newRejectCache(cacheSize)

	// As a sanity check, assert that querying the empty cache does not
	// return an entry.
	_, ok := c.get(0)
	if ok {
		t.Fatalf("reject cache should be empty")
	}

	// Now, fill up the cache entirely.
	for i := uint64(0); i < cacheSize; i++ {
		c.insert(i, entryForInt(i))
	}

	// Assert that the cache has all of the entries just inserted, since no
	// eviction should occur until we try to surpass the max size.
	assertHasEntries(t, c, 0, cacheSize)

	// Now, insert a new element that causes the cache to evict an element.
	c.insert(cacheSize, entryForInt(cacheSize))

	// Assert that the cache has this last entry, as the cache should evict
	// some prior element and not the newly inserted one.
	assertHasEntries(t, c, cacheSize, cacheSize)

	// Iterate over all inserted elements and construct a set of the evicted
	// elements.
	evicted := make(map[uint64]struct{})
	for i := uint64(0); i < cacheSize+1; i++ {
		_, ok := c.get(i)
		if !ok {
			evicted[i] = struct{}{}
		}
	}

	// Assert that exactly one element has been evicted.
	numEvicted := len(evicted)
	if numEvicted != 1 {
		t.Fatalf("expected one evicted entry, got: %d", numEvicted)
	}

	// Remove the highest item which initially caused the eviction and
	// reinsert the element that was evicted prior.
	c.remove(cacheSize)
	for i := range evicted {
		c.insert(i, entryForInt(i))
	}

	// Since the removal created an extra slot, the last insertion should
	// not have caused an eviction and the entries for all channels in the
	// original set that filled the cache should be present.
	assertHasEntries(t, c, 0, cacheSize)

	// Finally, reinsert the existing set back into the cache and test that
	// the cache still has all the entries. If the randomized eviction were
	// happening on inserts for existing cache items, we expect this to fail
	// with high probability.
	for i := uint64(0); i < cacheSize; i++ {
		c.insert(i, entryForInt(i))
	}
	assertHasEntries(t, c, 0, cacheSize)

}

// assertHasEntries queries the reject cache for all channels in the range [start,
// end), asserting that they exist and their value matches the entry produced by
// entryForInt.
func assertHasEntries(t *testing.T, c *rejectCache, start, end uint64) {
	t.Helper()

	for i := start; i < end; i++ {
		entry, ok := c.get(i)
		if !ok {
			t.Fatalf("reject cache should contain chan %d", i)
		}

		expEntry := entryForInt(i)
		if !reflect.DeepEqual(entry, expEntry) {
			t.Fatalf("entry mismatch, want: %v, got: %v",
				expEntry, entry)
		}
	}
}

// entryForInt generates a unique rejectCacheEntry given an integer.
func entryForInt(i uint64) rejectCacheEntry {
	exists := i%2 == 0
	isZombie := i%3 == 0
	return rejectCacheEntry{
		upd1Time: int64(2 * i),
		upd2Time: int64(2*i + 1),
		flags:    packRejectFlags(exists, isZombie),
	}
}