package cuckoo_test

import (
	"fmt"
	"maps"
	"os"
	"testing"

	"github.com/stretchr/testify/assert"
	go_fuzz_utils "github.com/trailofbits/go-fuzz-utils"

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

func offsetHash(seed uint32) cuckoo.Hash[uint32] {
	return func(x uint32) uint64 {
		v := uint64(x) ^ uint64(seed)
		v = (v ^ (v >> 30)) * 0xbf58476d1ce4e5b9
		v = (v ^ (v >> 27)) * 0x94d049bb133111eb
		return v ^ (v >> 31)
	}
}

type fuzzStep struct {
	drop       bool
	key, value uint32
}

type fuzzScenario struct {
	seedA, seedB           uint32
	capacity, growthFactor uint8
	steps                  []fuzzStep
}

func FuzzInsertLookup(f *testing.F) {
	f.Fuzz(func(t *testing.T, data []byte) {
		var scenario fuzzScenario
		assert := assert.New(t)

		if tp, err := go_fuzz_utils.NewTypeProvider(data); err != nil {
			return
		} else if err := tp.Fill(&scenario); err != nil {
			return
		}

		seedA, seedB := scenario.seedA, scenario.seedB
		growthFactor := max(2, int(scenario.growthFactor))
		capacity := int(scenario.capacity)

		// If they are the same number, the hashes will clash, always causing an
		// error.
		if seedA == seedB {
			t.Skip()
		}

		fmt.Fprintf(os.Stderr, "seedA=%d seedB=%d capacity=%d growthFactor=%d\n",
			seedA, seedB, capacity, growthFactor)

		actual := cuckoo.NewCustom[uint32, uint32](
			offsetHash(seedA),
			offsetHash(seedB),
			func(a, b uint32) bool { return a == b },
			cuckoo.Capacity(capacity),
			cuckoo.GrowthFactor(growthFactor),
		)

		expected := map[uint32]uint32{}

		for _, step := range scenario.steps {
			if step.drop {
				ok := actual.Drop(step.key)
				_, has := expected[step.key]
				assert.Equal(ok, has)

				delete(expected, step.key)

				_, ok = actual.Get(step.key)
				assert.False(ok)
			} else {
				err := actual.Put(step.key, step.value)
				assert.NoError(err)

				expected[step.key] = step.value

				found, ok := actual.Get(step.key)
				assert.True(ok)
				assert.Equal(step.value, found)
			}

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