package gocache

import (
	"fmt"
	"testing"
	"time"
)

func TestCache_StartJanitor(t *testing.T) {
	cache := NewCache()
	cache.SetWithTTL("1", "1", time.Nanosecond)
	if cacheSize := cache.Count(); cacheSize != 1 {
		t.Errorf("expected cacheSize to be 1, but was %d", cacheSize)
	}
	err := cache.StartJanitor()
	if err != nil {
		t.Fatal(err)
	}
	defer cache.StopJanitor()
	time.Sleep(JanitorMinShiftBackOff * 2)
	if cacheSize := cache.Count(); cacheSize != 0 {
		t.Errorf("expected cacheSize to be 0, but was %d", cacheSize)
	}
}

func TestCache_StartJanitorWhenAlreadyStarted(t *testing.T) {
	cache := NewCache()
	if err := cache.StartJanitor(); err != nil {
		t.Fatal(err)
	}
	if err := cache.StartJanitor(); err == nil {
		t.Fatal("expected StartJanitor to return an error, because the janitor is already started")
	}
	cache.StopJanitor()
}

func TestCache_StopJanitor(t *testing.T) {
	cache := NewCache()
	_ = cache.StartJanitor()
	if cache.stopJanitor == nil {
		t.Error("starting the janitor should've initialized cache.stopJanitor")
	}
	cache.StopJanitor()
	if cache.stopJanitor != nil {
		t.Error("stopping the janitor should've set cache.stopJanitor to nil")
	}
	// Check if stopping the janitor even though it's already stopped causes a panic
	cache.StopJanitor()
}

func TestJanitor(t *testing.T) {
	cache := NewCache().WithMaxSize(3 * JanitorMaxIterationsPerShift)
	defer cache.Clear()
	for i := 0; i < 3*JanitorMaxIterationsPerShift; i++ {
		if i < JanitorMaxIterationsPerShift && i%2 == 0 {
			cache.SetWithTTL(fmt.Sprintf("%d", i), "value", time.Millisecond)
		} else {
			cache.SetWithTTL(fmt.Sprintf("%d", i), "value", time.Hour)
		}
	}
	cacheSize := cache.Count()
	err := cache.StartJanitor()
	if err != nil {
		t.Fatal(err)
	}
	defer cache.StopJanitor()
	time.Sleep(JanitorMinShiftBackOff * 4)
	if cacheSize <= cache.Count() {
		t.Error("The janitor should be deleting expired cache entries")
	}
	cacheSize = cache.Count()
	time.Sleep(JanitorMinShiftBackOff * 4)
	if cacheSize <= cache.Count() {
		t.Error("The janitor should be deleting expired cache entries")
	}
	cacheSize = cache.Count()
	time.Sleep(JanitorMinShiftBackOff * 4)
	if cacheSize <= cache.Count() {
		t.Error("The janitor should be deleting expired cache entries")
	}
}

func TestJanitorIsLoopingProperly(t *testing.T) {
	cache := NewCache().WithMaxSize(JanitorMaxIterationsPerShift + 3)
	defer cache.Clear()
	for i := 0; i < JanitorMaxIterationsPerShift; i++ {
		cache.SetWithTTL(fmt.Sprintf("%d", i), "value", time.Hour)
	}
	cache.SetWithTTL("key-to-expire-1", "value", JanitorMinShiftBackOff*2)
	cache.SetWithTTL("key-to-expire-2", "value", JanitorMinShiftBackOff*2)
	cache.SetWithTTL("key-to-expire-3", "value", JanitorMinShiftBackOff*2)
	err := cache.StartJanitor()
	if err != nil {
		t.Fatal(err)
	}
	defer cache.StopJanitor()
	if cache.Count() != JanitorMaxIterationsPerShift+3 {
		t.Error("The janitor shouldn't have had enough time to remove anything from the cache yet", cache.Count())
	}
	const timeout = JanitorMinShiftBackOff * 20
	threeKeysExpiredWithinOneSecond := false
	for start := time.Now(); time.Since(start) < timeout; {
		if cache.Stats().ExpiredKeys == 3 {
			threeKeysExpiredWithinOneSecond = true
			break
		}
		time.Sleep(JanitorMinShiftBackOff)
	}
	if !threeKeysExpiredWithinOneSecond {
		t.Error("expected 3 keys to expire within 1 second")
	}
	if cache.Count() != JanitorMaxIterationsPerShift {
		t.Error("The janitor should've deleted 3 entries")
	}
}

func TestJanitorDoesNotThrowATantrumWhenThereIsNothingToClean(t *testing.T) {
	cache := NewCache()
	start := time.Now()
	_ = cache.StartJanitor()
	defer cache.StopJanitor()
	time.Sleep(JanitorMaxShiftBackOff * 3)
	// Technically, if the janitor doesn't backoff properly, the sleep above is likely to take more time than the sleep
	// below because it would be eating up the CPU.
	// This is a far-fetched test, but it's a good sanity check.
	if time.Since(start) > JanitorMaxShiftBackOff*4 {
		t.Error("The janitor should've backed off and prevented CPU usage from throttling the application")
	}
}
