package semaphore

// Based on https://github.com/golang/sync/blob/master/semaphore/semaphore_test.go

import (
	"context"
	"math/rand"
	"runtime"
	"sync"
	"testing"
	"time"

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

const maxSleep = 1 * time.Millisecond

func HammerDynamic(sem *Dynamic, loops int) {
	for i := 0; i < loops; i++ {
		sem.Acquire(context.Background())
		time.Sleep(time.Duration(rand.Int63n(int64(maxSleep/time.Nanosecond))) * time.Nanosecond)
		sem.Release()
	}
}

// TestDynamic hammers the semaphore from all available cores to ensure we don't
// hit a panic or race detector notice something wonky.
func TestDynamic(t *testing.T) {
	t.Parallel()

	n := runtime.GOMAXPROCS(0)
	loops := 10000 / n
	sem := NewDynamic(int64(n))
	var wg sync.WaitGroup
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func() {
			defer wg.Done()
			HammerDynamic(sem, loops)
		}()
	}
	wg.Wait()
}

func TestDynamicPanic(t *testing.T) {
	t.Parallel()

	defer func() {
		if recover() == nil {
			t.Fatal("release of an unacquired dynamic semaphore did not panic")
		}
	}()
	w := NewDynamic(1)
	w.Release()
}

func checkAcquire(t *testing.T, sem *Dynamic, wantAcquire bool) {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer cancel()
	err := sem.Acquire(ctx)
	if wantAcquire {
		require.NoErrorf(t, err, "failed to acquire when we should have")
	} else {
		require.Error(t, err, "failed to block when should be full")
	}
}

func TestDynamicAcquire(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	sem := NewDynamic(2)

	// Consume one slot [free: 1]
	sem.Acquire(ctx)
	// Should be able to consume another [free: 0]
	checkAcquire(t, sem, true)
	// Should fail to consume another [free: 0]
	checkAcquire(t, sem, false)

	// Release 2
	sem.Release()
	sem.Release()

	// Should be able to consume another [free: 1]
	checkAcquire(t, sem, true)
	// Should be able to consume another [free: 0]
	checkAcquire(t, sem, true)
	// Should fail to consume another [free: 0]
	checkAcquire(t, sem, false)

	// Now expand the semaphore and we should be able to acquire again [free: 2]
	sem.SetSize(4)

	// Should be able to consume another [free: 1]
	checkAcquire(t, sem, true)
	// Should be able to consume another [free: 0]
	checkAcquire(t, sem, true)
	// Should fail to consume another [free: 0]
	checkAcquire(t, sem, false)

	// Shrinking it should work [free: 0]
	sem.SetSize(3)

	// Should fail to consume another [free: 0]
	checkAcquire(t, sem, false)

	// Release one [free: 0] (3 slots used are release, size only 3)
	sem.Release()

	// Should fail to consume another [free: 0]
	checkAcquire(t, sem, false)

	sem.Release()

	// Should be able to consume another [free: 1]
	checkAcquire(t, sem, true)
}
