package cache

import (
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var (
	called      = 0
	errSentinel = errors.New("an error")
	errCached   = errors.New("a cached error")
)

func setup(t *testing.T) (*Cache, CreateFunc) {
	called = 0
	create := func(path string) (any, bool, error) {
		assert.Equal(t, 0, called)
		called++
		switch path {
		case "/":
			return "/", true, nil
		case "/file.txt":
			return "/file.txt", true, errCached
		case "/error":
			return nil, false, errSentinel
		case "/err":
			return nil, false, errSentinel
		}
		panic(fmt.Sprintf("Unknown path %q", path))
	}
	c := New()
	return c, create
}

func TestGet(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, len(c.cache))

	f, err := c.Get("/", create)
	require.NoError(t, err)

	assert.Equal(t, 1, len(c.cache))

	f2, err := c.Get("/", create)
	require.NoError(t, err)

	assert.Equal(t, f, f2)
}

func TestGetFile(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, len(c.cache))

	f, err := c.Get("/file.txt", create)
	require.Equal(t, errCached, err)

	assert.Equal(t, 1, len(c.cache))

	f2, err := c.Get("/file.txt", create)
	require.Equal(t, errCached, err)

	assert.Equal(t, f, f2)
}

func TestGetError(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, len(c.cache))

	f, err := c.Get("/error", create)
	require.Equal(t, errSentinel, err)
	require.Equal(t, nil, f)

	assert.Equal(t, 0, len(c.cache))
}

func TestPutErr(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, len(c.cache))

	c.PutErr("/alien", "slime", errSentinel)

	assert.Equal(t, 1, len(c.cache))

	fNew, err := c.Get("/alien", create)
	require.Equal(t, errSentinel, err)
	require.Equal(t, "slime", fNew)

	assert.Equal(t, 1, len(c.cache))
}

func TestPut(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, len(c.cache))

	c.Put("/alien", "slime")

	assert.Equal(t, 1, len(c.cache))

	fNew, err := c.Get("/alien", create)
	require.NoError(t, err)
	require.Equal(t, "slime", fNew)

	assert.Equal(t, 1, len(c.cache))
}

func TestCacheExpire(t *testing.T) {
	c, create := setup(t)

	c.SetExpireInterval(time.Millisecond)
	assert.Equal(t, false, c.expireRunning)

	_, err := c.Get("/", create)
	require.NoError(t, err)

	c.mu.Lock()
	entry := c.cache["/"]
	assert.Equal(t, 1, len(c.cache))
	c.mu.Unlock()

	c.cacheExpire()

	c.mu.Lock()
	assert.Equal(t, 1, len(c.cache))
	entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second)
	assert.Equal(t, true, c.expireRunning)
	c.mu.Unlock()

	time.Sleep(250 * time.Millisecond)

	c.mu.Lock()
	assert.Equal(t, false, c.expireRunning)
	assert.Equal(t, 0, len(c.cache))
	c.mu.Unlock()
}

func TestCacheNoExpire(t *testing.T) {
	c, create := setup(t)

	assert.False(t, c.noCache())

	c.SetExpireDuration(0)
	assert.Equal(t, false, c.expireRunning)

	assert.True(t, c.noCache())

	f, err := c.Get("/", create)
	require.NoError(t, err)
	require.NotNil(t, f)

	c.mu.Lock()
	assert.Equal(t, 0, len(c.cache))
	c.mu.Unlock()

	c.Put("/alien", "slime")

	c.mu.Lock()
	assert.Equal(t, 0, len(c.cache))
	c.mu.Unlock()
}

func TestCachePin(t *testing.T) {
	c, create := setup(t)

	_, err := c.Get("/", create)
	require.NoError(t, err)

	// Pin a nonexistent item to show nothing happens
	c.Pin("notfound")

	c.mu.Lock()
	entry := c.cache["/"]
	assert.Equal(t, 1, len(c.cache))
	c.mu.Unlock()

	c.cacheExpire()

	c.mu.Lock()
	assert.Equal(t, 1, len(c.cache))
	c.mu.Unlock()

	// Pin the entry and check it does not get expired
	c.Pin("/")

	// Reset last used to make the item expirable
	c.mu.Lock()
	entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second)
	c.mu.Unlock()

	c.cacheExpire()

	c.mu.Lock()
	assert.Equal(t, 1, len(c.cache))
	c.mu.Unlock()

	// Unpin the entry and check it does get expired now
	c.Unpin("/")

	// Reset last used
	c.mu.Lock()
	entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second)
	c.mu.Unlock()

	c.cacheExpire()

	c.mu.Lock()
	assert.Equal(t, 0, len(c.cache))
	c.mu.Unlock()
}

func TestClear(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, len(c.cache))

	_, err := c.Get("/", create)
	require.NoError(t, err)

	assert.Equal(t, 1, len(c.cache))

	c.Clear()

	assert.Equal(t, 0, len(c.cache))
}

func TestEntries(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, c.Entries())

	_, err := c.Get("/", create)
	require.NoError(t, err)

	assert.Equal(t, 1, c.Entries())

	c.Clear()

	assert.Equal(t, 0, c.Entries())
}

func TestGetMaybe(t *testing.T) {
	c, create := setup(t)

	value, found := c.GetMaybe("/")
	assert.Equal(t, false, found)
	assert.Nil(t, value)

	f, err := c.Get("/", create)
	require.NoError(t, err)

	value, found = c.GetMaybe("/")
	assert.Equal(t, true, found)
	assert.Equal(t, f, value)

	c.Clear()

	value, found = c.GetMaybe("/")
	assert.Equal(t, false, found)
	assert.Nil(t, value)
}

func TestDelete(t *testing.T) {
	c, create := setup(t)

	assert.Equal(t, 0, len(c.cache))

	_, err := c.Get("/", create)
	require.NoError(t, err)

	assert.Equal(t, 1, len(c.cache))

	assert.Equal(t, false, c.Delete("notfound"))
	assert.Equal(t, 1, len(c.cache))

	assert.Equal(t, true, c.Delete("/"))
	assert.Equal(t, 0, len(c.cache))

	assert.Equal(t, false, c.Delete("/"))
	assert.Equal(t, 0, len(c.cache))
}

func TestDeletePrefix(t *testing.T) {
	create := func(path string) (any, bool, error) {
		return path, true, nil
	}
	c := New()

	_, err := c.Get("remote:path", create)
	require.NoError(t, err)
	_, err = c.Get("remote:path2", create)
	require.NoError(t, err)
	_, err = c.Get("remote:", create)
	require.NoError(t, err)
	_, err = c.Get("remote", create)
	require.NoError(t, err)

	assert.Equal(t, 4, len(c.cache))

	assert.Equal(t, 3, c.DeletePrefix("remote:"))
	assert.Equal(t, 1, len(c.cache))

	assert.Equal(t, 1, c.DeletePrefix(""))
	assert.Equal(t, 0, len(c.cache))

	assert.Equal(t, 0, c.DeletePrefix(""))
	assert.Equal(t, 0, len(c.cache))
}

func TestCacheRename(t *testing.T) {
	c := New()
	create := func(path string) (any, bool, error) {
		return path, true, nil
	}

	existing1, err := c.Get("existing1", create)
	require.NoError(t, err)
	_, err = c.Get("existing2", create)
	require.NoError(t, err)

	assert.Equal(t, 2, c.Entries())

	// rename to nonexistent
	value, found := c.Rename("existing1", "EXISTING1")
	assert.Equal(t, true, found)
	assert.Equal(t, existing1, value)

	assert.Equal(t, 2, c.Entries())

	// rename to existent and check existing value is returned
	value, found = c.Rename("existing2", "EXISTING1")
	assert.Equal(t, true, found)
	assert.Equal(t, existing1, value)

	assert.Equal(t, 1, c.Entries())

	// rename nonexistent
	value, found = c.Rename("notfound", "NOTFOUND")
	assert.Equal(t, false, found)
	assert.Nil(t, value)

	assert.Equal(t, 1, c.Entries())
}

func TestCacheFinalize(t *testing.T) {
	c := New()
	numCalled := 0
	c.SetFinalizer(func(v any) {
		numCalled++
	})
	create := func(path string) (any, bool, error) {
		return path, true, nil
	}
	_, _ = c.Get("ok", create)
	assert.Equal(t, 0, numCalled)
	c.Clear()
	assert.Equal(t, 1, numCalled)

	_, _ = c.Get("ok", create)
	c.Delete("ok")
	assert.Equal(t, 2, numCalled)

	_, _ = c.Get("ok", create)
	c.DeletePrefix("ok")
	assert.Equal(t, 3, numCalled)

	_, _ = c.Get("old", create)
	_, _ = c.Get("new", create)
	c.Rename("old", "new")
	assert.Equal(t, 4, numCalled)

	c.expireDuration = 1 * time.Millisecond
	_, _ = c.Get("ok", create)
	time.Sleep(2 * time.Millisecond)
	c.cacheExpire() // "ok" and "new" fall out of cache
	assert.Equal(t, 6, numCalled)
}
