package cutil

import (
	"math/rand"
	"runtime"
	"testing"
	"time"
	"unsafe"

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

func TestPtrGuard(t *testing.T) {
	t.Run("storeAndRelease", func(t *testing.T) {
		s := "string"
		goPtr := (unsafe.Pointer)(&s)
		cPtr := Malloc(PtrSize)
		defer Free(cPtr)
		pg := NewPtrGuard(cPtr, goPtr)
		assert.Equal(t, *(*unsafe.Pointer)(cPtr), goPtr)
		pg.Release()
		assert.Zero(t, *(*unsafe.Pointer)(cPtr))
	})

	t.Run("multiRelease", func(t *testing.T) {
		s := "string"
		goPtr := (unsafe.Pointer)(&s)
		cPtr := Malloc(PtrSize)
		defer Free(cPtr)
		pg := NewPtrGuard(cPtr, goPtr)
		assert.Equal(t, *(*unsafe.Pointer)(cPtr), goPtr)
		pg.Release()
		pg.Release()
		pg.Release()
		pg.Release()
		assert.Zero(t, *(*unsafe.Pointer)(cPtr))
	})

	t.Run("uintptrescapesTest", func(t *testing.T) {
		// This test assures that the special //go:uintptrescapes comment before
		// the storeUntilRelease() function works as intended, that is the
		// garbage collector doesn't touch the object referenced by the uintptr
		// until the function returns after Release() is called. The test will
		// fail if the //go:uintptrescapes comment is disabled (removed) or
		// stops working in future versions of go.
		var pgDone, uDone bool
		var goPtr = func(b *bool) unsafe.Pointer {
			s := "ok"
			runtime.SetFinalizer(&s, func(p *string) { *b = true })
			return unsafe.Pointer(&s)
		}
		cPtr := Malloc(PtrSize)
		defer Free(cPtr)
		pg := NewPtrGuard(cPtr, goPtr(&pgDone))
		u := uintptr(goPtr(&uDone))
		runtime.GC()
		assert.Eventually(t, func() bool { return uDone },
			time.Second, 10*time.Millisecond)
		assert.False(t, pgDone)
		pg.Release()
		runtime.GC()
		assert.Eventually(t, func() bool { return pgDone },
			time.Second, 10*time.Millisecond)
		assert.NotZero(t, u) // avoid "unused" error
	})

	t.Run("stressTest", func(t *testing.T) {
		// Because the default thread limit of the Go runtime is 10000, creating
		// 20000 parallel PtrGuards asserts, that Go routines of PtrGuards don't
		// create threads.
		const N = 20000  // Number of parallel PtrGuards
		const M = 100000 // Number of loops
		var ptrGuards [N]*PtrGuard
		cPtrArr := (*[N]CPtr)(unsafe.Pointer(Malloc(N * PtrSize)))
		defer Free(CPtr(&cPtrArr[0]))
		toggle := func(i int) {
			if ptrGuards[i] == nil {
				goPtr := unsafe.Pointer(&(struct{ byte }{42}))
				cPtrPtr := CPtr(&cPtrArr[i])
				ptrGuards[i] = NewPtrGuard(cPtrPtr, goPtr)
				assert.Equal(t, (unsafe.Pointer)(cPtrArr[i]), goPtr)
			} else {
				ptrGuards[i].Release()
				ptrGuards[i] = nil
				assert.Zero(t, cPtrArr[i])
			}
		}
		for i := range ptrGuards {
			toggle(i)
		}
		for n := 0; n < M; n++ {
			i := rand.Intn(N)
			toggle(i)
		}
		for i := range ptrGuards {
			if ptrGuards[i] != nil {
				ptrGuards[i].Release()
				ptrGuards[i] = nil
			}
		}
		for i := uintptr(0); i < N; i++ {
			assert.Zero(t, cPtrArr[i])
		}
	})
}
