// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later

package common

import (
	"math/rand"
	"sync"
	"sync/atomic"
	"testing"
	"time"

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

func TestMutexMap_BasicLockUnlock(t *testing.T) {
	mm := &MutexMap{}

	unlock := mm.Lock("test-key")
	unlock()

	// Should be able to lock again
	unlock2 := mm.Lock("test-key")
	unlock2()
}

func TestMutexMap_ConcurrentSameKey(t *testing.T) {
	mm := &MutexMap{}
	var anotherLockActive atomic.Bool
	var firstError atomic.Value
	var wg sync.WaitGroup

	wg.Add(10)
	for range 10 {
		go func() {
			unlock := mm.Lock("shared-key")

			// should *not* find that another goroutine has put `true` into here.
			swapped := anotherLockActive.CompareAndSwap(false, true)
			if !swapped {
				firstError.CompareAndSwap(nil, "anotherLockActive was true!")
			}
			time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond) // jitter the goroutines to ensure no serial execution
			anotherLockActive.Store(false)

			unlock()

			wg.Done()
		}()
	}

	wg.Wait()

	if err := firstError.Load(); err != nil {
		t.Fatal(err)
	}
}

func TestMutexMap_DifferentKeys(t *testing.T) {
	mm := &MutexMap{}
	done := make(chan bool, 1)

	go func() {
		// If these somehow referred to the same underlying `sync.Mutex`, because `sync.Mutex` is not re-entrant this would
		// never complete.
		unlock1 := mm.Lock("test-key-1")
		unlock2 := mm.Lock("test-key-2")
		unlock3 := mm.Lock("test-key-3")
		unlock1()
		unlock2()
		unlock3()
		done <- true
	}()

	select {
	case <-done:
		// Success
	case <-time.After(1 * time.Second): // early timeout so that we don't wait for t.Deadline()
		t.Fatal("test incomplete after timeout, indicating a locking bug")
	}
}

func TestMutexMap_SimpleCleanup(t *testing.T) {
	mm := &MutexMap{}
	unlock1 := mm.Lock("test-key-1")

	mm.mu.Lock()
	assert.Len(t, mm.mutexMap, 1)
	mm.mu.Unlock()

	unlock1()

	mm.mu.Lock()
	assert.Empty(t, mm.mutexMap)
	mm.mu.Unlock()
}

func TestMutexMap_ConcurrentCleanup(t *testing.T) {
	mm := &MutexMap{}
	var foundRefGreaterThanOne atomic.Bool
	var wg sync.WaitGroup

	wg.Add(10)
	for range 10 {
		go func() {
			unlock := mm.Lock("shared-key")

			time.Sleep(time.Duration(rand.Intn(20)) * time.Millisecond) // jitter the goroutines to ensure no serial execution

			mm.mu.Lock()
			rcMutex := mm.mutexMap["shared-key"]
			if rcMutex.refCount > 1 {
				foundRefGreaterThanOne.Store(true)
			}
			mm.mu.Unlock()

			unlock()
			wg.Done()
		}()
	}

	wg.Wait()

	assert.True(t, foundRefGreaterThanOne.Load(), "expected to find a refCount > 1")

	mm.mu.Lock()
	assert.Empty(t, mm.mutexMap)
	mm.mu.Unlock()
}

func TestMutexMap_UnlockTwice(t *testing.T) {
	mm := &MutexMap{}
	assert.Panics(t, func() {
		unlock := mm.Lock("test")
		unlock()
		unlock()
	})
}
