package cuckoo_test

import (
	"errors"
	"maps"
	"math/rand/v2"
	"testing"

	"github.com/stretchr/testify/assert"

	"git.maximhutz.com/tools/go-cuckoo"
)

func TestNewTable(t *testing.T) {
	assert := assert.New(t)

	table := cuckoo.New[int, bool]()

	assert.NotNil(table)
	assert.Zero(table.Size())
}

func TestAddItem(t *testing.T) {
	assert := assert.New(t)
	key, value := 0, true
	table := cuckoo.New[int, bool]()

	err := table.Put(key, value)

	assert.NoError(err)
	assert.Equal(1, table.Size())
	assert.True(table.Has(key))
}

func TestPutOverwrite(t *testing.T) {
	assert := assert.New(t)
	key, value, newValue := 0, 1, 2
	table := cuckoo.New[int, int]()
	(table.Put(key, value))

	err := table.Put(key, newValue)

	assert.NoError(err)
	assert.Equal(1, table.Size())
	assert.True(table.Has(key))
	found, _ := table.Get(key)
	assert.Equal(newValue, found)
}

func TestSameHash(t *testing.T) {
	assert := assert.New(t)
	hash := func(int) uint64 { return 0 }
	table := cuckoo.NewCustom[int, bool](hash, hash, cuckoo.DefaultEqualFunc[int])

	errA := table.Put(0, true)
	errB := table.Put(1, true)
	errC := table.Put(2, true)

	assert.NoError(errA)
	assert.NoError(errB)
	assert.ErrorContains(errC, "bad hash")
}

func TestStartingCapacity(t *testing.T) {
	assert := assert.New(t)
	table := cuckoo.New[int, bool](cuckoo.Capacity(64))

	assert.Equal(uint64(128), table.TotalCapacity())
}

func TestResizeCapacity(t *testing.T) {
	assert := assert.New(t)
	table := cuckoo.New[int, bool](
		cuckoo.Capacity(8),
		cuckoo.GrowthFactor(2),
	)

	for table.TotalCapacity() == 16 {
		err := table.Put(rand.Int(), true)
		assert.NoError(err)
	}

	assert.Equal(uint64(32), table.TotalCapacity())
}

func TestPutMany(t *testing.T) {
	assert := assert.New(t)
	expected, actual := map[int]bool{}, cuckoo.New[int, bool]()

	for i := range 1_000 {
		expected[i] = true
		err := actual.Put(i, true)

		assert.NoError(err)
	}

	assert.Equal(maps.Collect(actual.Entries()), expected)
	assert.Equal(len(expected), actual.Size())
}

func TestGetMany(t *testing.T) {
	assert := assert.New(t)
	table := cuckoo.New[int, bool]()

	for i := range 1_000 {
		err := table.Put(i, true)
		assert.NoError(err)
	}

	for i := range 2_000 {
		value, ok := table.Get(i)
		if i < 1_000 {
			assert.True(ok)
			assert.Equal(value, true)
		} else {
			assert.False(ok)
		}
	}
}

func TestDropExistingItem(t *testing.T) {
	assert := assert.New(t)
	key, value := 0, true
	table := cuckoo.New[int, bool]()
	(table.Put(key, value))

	had := table.Drop(key)

	assert.True(had)
	assert.Equal(0, table.Size())
	assert.False(table.Has(key))
}

func TestDropNoItem(t *testing.T) {
	assert := assert.New(t)
	key := 0
	table := cuckoo.New[int, bool]()

	had := table.Drop(key)

	assert.False(had)
	assert.Equal(0, table.Size())
	assert.False(table.Has(key))
}

func TestDropItemCapacity(t *testing.T) {
	assert := assert.New(t)
	key := 0
	table := cuckoo.New[int, bool](
		cuckoo.Capacity(64),
		cuckoo.GrowthFactor(2),
	)

	startingCapacity := table.TotalCapacity()
	table.Drop(key)
	endingCapacity := table.TotalCapacity()

	assert.Equal(0, table.Size())
	assert.Equal(uint64(128), startingCapacity)
	assert.Equal(uint64(64), endingCapacity)
}

func TestPutNoCapacity(t *testing.T) {
	assert := assert.New(t)
	key, value := 0, true
	table := cuckoo.New[int, bool](
		cuckoo.Capacity(0),
	)

	err := table.Put(key, value)

	assert.NoError(err)
	assert.Equal(1, table.Size())
	assert.True(table.Has(key))
}

func TestBadHashCapacity(t *testing.T) {
	assert := assert.New(t)
	table := cuckoo.NewCustom[int, bool](
		func(int) uint64 { return 0 },
		func(int) uint64 { return 0 },
		func(a, b int) bool { return a == b },
		cuckoo.Capacity(20),
	)

	err1 := table.Put(0, true)
	err2 := table.Put(1, true)
	err3 := table.Put(2, true)

	assert.NoError(err1)
	assert.NoError(err2)
	assert.Error(err3)

	assert.Equal(uint64(80), table.TotalCapacity())
}

func TestDropResizeCapacity(t *testing.T) {
	assert := assert.New(t)
	table := cuckoo.New[int, bool](
		cuckoo.Capacity(10),
	)

	err1 := table.Put(0, true)
	err2 := table.Put(1, true)
	table.Drop(1)

	assert.NoError(errors.Join(err1, err2))
	assert.Equal(uint64(20), table.TotalCapacity())
}

func TestNewTableBy(t *testing.T) {
	type User struct {
		_    func()
		id   string
		name string
	}

	assert := assert.New(t)
	table := cuckoo.NewBy[User, bool](func(u User) string { return u.id })

	err := table.Put(User{nil, "1", "Robert"}, true)

	assert.NoError(err)
	assert.Equal(1, table.Size())
	assert.True(table.Has(User{nil, "1", "Robbie"}))
}
