package lock

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	mysql2 "github.com/tidbcloud/cluster-service-ng/pkg/storage/mysql"
)

func TestLock_AcquireAndRelease(t *testing.T) {
	gormDB := dbConn.Load()
	require.NotNil(t, gormDB, "database connection not initialized")

	provider, _, _ := mysql2.NewDBProvider(gormDB)
	lockRepo := NewLockRepo(provider)

	ctx := context.Background()
	unlock, err := lockRepo.Lock(ctx, &entity.Lock{Key: "test_lock_basic", TimeoutSecs: 5})
	require.NoError(t, err)

	// Lock should be held.
	used, err := lockRepo.IsUsed(ctx, &entity.Lock{Key: "test_lock_basic"})
	require.NoError(t, err)
	assert.True(t, used)

	// Release.
	err = unlock()
	require.NoError(t, err)

	// Lock should be free.
	free, err := lockRepo.IsFree(ctx, &entity.Lock{Key: "test_lock_basic"})
	require.NoError(t, err)
	assert.True(t, free)
}

// TestLock_ReleaseSameConnection verifies GET_LOCK and RELEASE_LOCK use the same
// connection, which is the core bug this fix addresses.
// We acquire the lock, do some unrelated work (sleep), then release.
// If the connection is not pinned, RELEASE_LOCK would fail.
func TestLock_ReleaseSameConnection(t *testing.T) {
	gormDB := dbConn.Load()
	require.NotNil(t, gormDB, "database connection not initialized")

	provider, _, _ := mysql2.NewDBProvider(gormDB)
	lockRepo := NewLockRepo(provider)

	ctx := context.Background()
	unlock, err := lockRepo.Lock(ctx, &entity.Lock{Key: "test_lock_conn", TimeoutSecs: 5})
	require.NoError(t, err)

	// Simulate a long-running cron job by sleeping and doing other DB work.
	time.Sleep(2 * time.Second)

	// Do some unrelated queries to exercise the connection pool.
	for i := 0; i < 20; i++ {
		var result int
		gormDB.Raw("SELECT 1").Scan(&result)
	}

	// Release should succeed — same connection must be used.
	err = unlock()
	require.NoError(t, err, "RELEASE_LOCK failed: connection was not pinned")
}

// TestLock_MutualExclusion verifies two callers cannot hold the same lock simultaneously.
func TestLock_MutualExclusion(t *testing.T) {
	gormDB := dbConn.Load()
	require.NotNil(t, gormDB, "database connection not initialized")

	provider, _, _ := mysql2.NewDBProvider(gormDB)
	lockRepo := NewLockRepo(provider)

	ctx := context.Background()
	const lockKey = "test_lock_mutex"

	// First caller acquires the lock and holds it.
	unlock1, err := lockRepo.Lock(ctx, &entity.Lock{Key: lockKey, TimeoutSecs: 10})
	require.NoError(t, err)

	// Second caller tries to acquire with timeout=0 — should fail immediately.
	_, err2 := lockRepo.Lock(ctx, &entity.Lock{Key: lockKey, TimeoutSecs: 0})
	require.Error(t, err2, "second lock should fail while first is held")

	// Release first lock.
	require.NoError(t, unlock1())

	// Now second caller can acquire.
	unlock3, err := lockRepo.Lock(ctx, &entity.Lock{Key: lockKey, TimeoutSecs: 5})
	require.NoError(t, err)
	require.NoError(t, unlock3())
}

// TestLock_ConcurrentDifferentKeys verifies different keys can be locked concurrently.
func TestLock_ConcurrentDifferentKeys(t *testing.T) {
	gormDB := dbConn.Load()
	require.NotNil(t, gormDB, "database connection not initialized")

	provider, _, _ := mysql2.NewDBProvider(gormDB)
	lockRepo := NewLockRepo(provider)

	ctx := context.Background()
	const n = 5
	var wg sync.WaitGroup

	for i := 0; i < n; i++ {
		wg.Add(1)
		go func(idx int) {
			defer wg.Done()
			key := "test_lock_concurrent_" + time.Now().Format("150405") + "_" + string(rune('a'+idx))
			unlock, err := lockRepo.Lock(ctx, &entity.Lock{Key: key, TimeoutSecs: 10})
			assert.NoError(t, err)
			if err == nil {
				time.Sleep(100 * time.Millisecond)
				assert.NoError(t, unlock())
			}
		}(i)
	}

	wg.Wait()
}

// TestLock_RepeatedLockUnlock verifies the lock can be acquired and released multiple times.
func TestLock_RepeatedLockUnlock(t *testing.T) {
	gormDB := dbConn.Load()
	require.NotNil(t, gormDB, "database connection not initialized")

	provider, _, _ := mysql2.NewDBProvider(gormDB)
	lockRepo := NewLockRepo(provider)

	ctx := context.Background()
	const lockKey = "test_lock_repeat"

	for i := 0; i < 10; i++ {
		unlock, err := lockRepo.Lock(ctx, &entity.Lock{Key: lockKey, TimeoutSecs: 5})
		require.NoError(t, err, "iteration %d: lock failed", i)
		require.NoError(t, unlock(), "iteration %d: unlock failed", i)
	}
}
