package cache

import (
	"errors"
	"fmt"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("SimpleCache", func() {
	var (
		cache SimpleCache[string, string]
	)

	BeforeEach(func() {
		cache = NewSimpleCache[string, string]()
	})

	Describe("Add and Get", func() {
		It("should add and retrieve a value", func() {
			err := cache.Add("key", "value")
			Expect(err).NotTo(HaveOccurred())

			value, err := cache.Get("key")
			Expect(err).NotTo(HaveOccurred())
			Expect(value).To(Equal("value"))
		})
	})

	Describe("AddWithTTL and Get", func() {
		It("should add a value with TTL and retrieve it", func() {
			err := cache.AddWithTTL("key", "value", 1*time.Minute)
			Expect(err).NotTo(HaveOccurred())

			value, err := cache.Get("key")
			Expect(err).NotTo(HaveOccurred())
			Expect(value).To(Equal("value"))
		})

		It("should not retrieve a value after its TTL has expired", func() {
			err := cache.AddWithTTL("key", "value", 10*time.Millisecond)
			Expect(err).NotTo(HaveOccurred())

			time.Sleep(50 * time.Millisecond)

			_, err = cache.Get("key")
			Expect(err).To(HaveOccurred())
		})
	})

	Describe("GetWithLoader", func() {
		It("should retrieve a value using the loader function", func() {
			loader := func(key string) (string, time.Duration, error) {
				return fmt.Sprintf("%s=value", key), 1 * time.Minute, nil
			}

			value, err := cache.GetWithLoader("key", loader)
			Expect(err).NotTo(HaveOccurred())
			Expect(value).To(Equal("key=value"))
		})

		It("should return the error returned by the loader function", func() {
			loader := func(key string) (string, time.Duration, error) {
				return "", 0, errors.New("some error")
			}

			_, err := cache.GetWithLoader("key", loader)
			Expect(err).To(HaveOccurred())
		})
	})

	Describe("Keys and Values", func() {
		It("should return all keys and all values", func() {
			err := cache.Add("key1", "value1")
			Expect(err).NotTo(HaveOccurred())

			err = cache.Add("key2", "value2")
			Expect(err).NotTo(HaveOccurred())

			keys := cache.Keys()
			Expect(keys).To(ConsistOf("key1", "key2"))

			values := cache.Values()
			Expect(values).To(ConsistOf("value1", "value2"))
		})

		Context("when there are expired items in the cache", func() {
			It("should not return expired items", func() {
				Expect(cache.Add("key0", "value0")).To(Succeed())
				for i := 1; i <= 3; i++ {
					err := cache.AddWithTTL(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i), 10*time.Millisecond)
					Expect(err).NotTo(HaveOccurred())
				}

				time.Sleep(50 * time.Millisecond)

				Expect(cache.Keys()).To(ConsistOf("key0"))
				Expect(cache.Values()).To(ConsistOf("value0"))
			})
		})
	})

	Describe("Options", func() {
		Context("when size limit is set", func() {
			BeforeEach(func() {
				cache = NewSimpleCache[string, string](Options{
					SizeLimit: 2,
				})
			})

			It("should drop the oldest item when the size limit is reached", func() {
				for i := 1; i <= 3; i++ {
					err := cache.Add(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
					Expect(err).NotTo(HaveOccurred())
				}

				Expect(cache.Keys()).To(ConsistOf("key2", "key3"))
			})
		})

		Context("when default TTL is set", func() {
			BeforeEach(func() {
				cache = NewSimpleCache[string, string](Options{
					DefaultTTL: 10 * time.Millisecond,
				})
			})

			It("should expire items after the default TTL", func() {
				Expect(cache.AddWithTTL("key0", "value0", 1*time.Minute)).To(Succeed())
				for i := 1; i <= 3; i++ {
					err := cache.Add(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
					Expect(err).NotTo(HaveOccurred())
				}

				time.Sleep(50 * time.Millisecond)

				for i := 1; i <= 3; i++ {
					_, err := cache.Get(fmt.Sprintf("key%d", i))
					Expect(err).To(HaveOccurred())
				}
				Expect(cache.Get("key0")).To(Equal("value0"))
			})
		})

		Describe("OnExpiration", func() {
			It("should call callback when item expires", func() {
				cache = NewSimpleCache[string, string]()
				expired := make(chan struct{})
				cache.OnExpiration(func(k, v string) { close(expired) })
				Expect(cache.AddWithTTL("key", "value", 10*time.Millisecond)).To(Succeed())
				select {
				case <-expired:
				case <-time.After(100 * time.Millisecond):
					Fail("expiration callback not called")
				}
			})
		})
	})
})
