// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package limiter

import (
	"context"
	"sync"
	"sync/atomic"
	"testing"
	"time"

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

func TestSessionLimiter(t *testing.T) {
	lim := NewSessionLimiter()

	ctx, cancel := context.WithCancel(context.Background())
	t.Cleanup(cancel)
	go lim.Run(ctx)

	// doneCh is used to shut the goroutines down at the end of the test.
	doneCh := make(chan struct{})
	t.Cleanup(func() { close(doneCh) })

	// Start 10 sessions, and increment the counter when they are terminated.
	var (
		terminations uint32
		wg           sync.WaitGroup
	)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			sess, err := lim.BeginSession()
			require.NoError(t, err)
			defer sess.End()

			wg.Done()

			select {
			case <-sess.Terminated():
				atomic.AddUint32(&terminations, 1)
			case <-doneCh:
			}
		}()
	}

	// Wait for all the sessions to begin.
	wg.Wait()

	// Lowering max sessions to 5 should result in 5 sessions being terminated.
	lim.SetMaxSessions(5)
	require.Eventually(t, func() bool {
		return atomic.LoadUint32(&terminations) == 5
	}, 2*time.Second, 50*time.Millisecond)

	// Attempting to start a new session should fail immediately.
	_, err := lim.BeginSession()
	require.Equal(t, ErrCapacityReached, err)

	// Raising MaxSessions should make room for a new session.
	lim.SetMaxSessions(6)
	sess, err := lim.BeginSession()
	require.NoError(t, err)

	// ...but trying to start another new one should fail
	_, err = lim.BeginSession()
	require.Equal(t, ErrCapacityReached, err)

	// ...until another session ends.
	sess.End()
	_, err = lim.BeginSession()
	require.NoError(t, err)

	// Calling End twice is a no-op.
	sess.End()
	_, err = lim.BeginSession()
	require.Equal(t, ErrCapacityReached, err)
}
