package keymutex

import (
	"testing"
	"time"

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

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

	locks := NewKeyMutex[string]()
	locks.Lock("same")
	acquired := make(chan struct{})
	done := make(chan struct{})
	go func() {
		defer close(done)
		locks.Lock("same")
		close(acquired)
		locks.Unlock("same")
	}()

	select {
	case <-acquired:
		t.Fatal("same key acquired before unlock")
	case <-time.After(50 * time.Millisecond):
	}

	locks.Unlock("same")
	require.Eventually(t, func() bool {
		select {
		case <-acquired:
			return true
		default:
			return false
		}
	}, time.Second, time.Millisecond)
	<-done
}

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

	locks := NewKeyMutex[int]()
	locks.Lock(1)
	acquired := make(chan struct{})
	go func() {
		locks.Lock(2)
		close(acquired)
		locks.Unlock(2)
	}()

	select {
	case <-acquired:
	case <-time.After(time.Second):
		t.Fatal("different key was blocked")
	}
	locks.Unlock(1)
}

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

	type key struct {
		kind string
		id   int
	}
	locks := NewKeyMutex[key]()
	entryKey := key{kind: "sandbox", id: 1}
	locks.Lock(entryKey)
	require.Len(t, locks.entries, 1)

	locks.Unlock(entryKey)
	require.Empty(t, locks.entries)
}

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

	locks := NewKeyMutex[string]()
	locks.Lock("locked")
	require.False(t, locks.TryLock("locked"))
	require.True(t, locks.TryLock("other"))
	locks.Unlock("other")
	locks.Unlock("locked")
	require.Empty(t, locks.entries)

	require.True(t, locks.TryLock("locked"))
	locks.Unlock("locked")
}

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

	locks := NewKeyMutex[string]()
	called := false
	locks.Do("key", func() {
		called = true
		require.False(t, locks.TryLock("key"))
	})
	require.True(t, called)
	require.Empty(t, locks.entries)
}

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

	locks := NewKeyMutex[string]()
	require.Panics(t, func() {
		locks.Do("key", func() {
			panic("failed")
		})
	})
	require.True(t, locks.TryLock("key"))
	locks.Unlock("key")
}
