package locking

import (
	"testing"
	"time"

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

func TestLocalLockBackend(t *testing.T) {
	backend := NewLocalLockBackend()

	token1, err := backend.Acquire("test-lock", 30*time.Second)
	assert.NoError(t, err)
	assert.NotEmpty(t, token1)

	// Try to acquire again (should fail)
	_, err = backend.Acquire("test-lock", 30*time.Second)
	assert.Error(t, err)

	// Release
	err = backend.Release("test-lock", token1)
	assert.NoError(t, err)

	// Acquire again (should succeed)
	_, err = backend.Acquire("test-lock", 30*time.Second)
	assert.NoError(t, err)
}

func TestLocalLockReleaseWrongToken(t *testing.T) {
	backend := NewLocalLockBackend()

	_, _ = backend.Acquire("test-lock", 30*time.Second)
	err := backend.Release("test-lock", "wrong-token")
	assert.Error(t, err)

	// Lock should still be held
	locked, _ := backend.IsLocked("test-lock")
	assert.True(t, locked)
}

func TestLocalLockIsLocked(t *testing.T) {
	backend := NewLocalLockBackend()

	locked, _ := backend.IsLocked("test-lock")
	assert.False(t, locked)

	backend.Acquire("test-lock", 30*time.Second)
	locked, _ = backend.IsLocked("test-lock")
	assert.True(t, locked)
}

func TestDistributedLockAcquire(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("test-lock", backend, 30*time.Second)

	err := lock.Acquire(5 * time.Second)
	assert.NoError(t, err)
	assert.True(t, lock.IsAcquired())
}

func TestDistributedLockRelease(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("test-lock", backend, 30*time.Second)

	lock.Acquire(5 * time.Second)
	err := lock.Release()
	assert.NoError(t, err)
	assert.False(t, lock.IsAcquired())
}

func TestDistributedLockTimeout(t *testing.T) {
	backend := NewLocalLockBackend()
	lock1 := NewDistributedLock("test-lock", backend, 30*time.Second)
	lock2 := NewDistributedLock("test-lock", backend, 30*time.Second)

	lock1.Acquire(5 * time.Second)

	// Try to acquire with very short timeout
	start := time.Now()
	err := lock2.Acquire(100 * time.Millisecond)
	elapsed := time.Since(start)

	assert.Error(t, err)
	assert.Greater(t, elapsed, 50*time.Millisecond)
}

func TestDistributedLockGetAcquiredAt(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("test-lock", backend, 30*time.Second)

	lock.Acquire(5 * time.Second)
	acquiredAt := lock.GetAcquiredAt()

	assert.NotZero(t, acquiredAt)
	assert.True(t, acquiredAt.Before(time.Now()))
}

func TestDistributedLockGetHoldDuration(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("test-lock", backend, 30*time.Second)

	lock.Acquire(5 * time.Second)
	time.Sleep(100 * time.Millisecond)
	duration := lock.GetHoldDuration()

	assert.Greater(t, duration, 50*time.Millisecond)
	assert.Less(t, duration, 200*time.Millisecond)
}

func TestDistributedLockRenew(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("test-lock", backend, 30*time.Second)

	lock.Acquire(5 * time.Second)
	err := lock.Renew()
	assert.NoError(t, err)
	assert.True(t, lock.IsAcquired())
}

func TestLockManagerAcquire(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	err := manager.AcquireLock("lock-1", 5*time.Second)
	assert.NoError(t, err)

	stats := manager.GetLockStats()
	assert.Equal(t, 1, stats.TotalAcquisitions)
	assert.Equal(t, 1, stats.ActiveLocks)
}

func TestLockManagerRelease(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	manager.AcquireLock("lock-1", 5*time.Second)
	err := manager.ReleaseLock("lock-1")
	assert.NoError(t, err)

	stats := manager.GetLockStats()
	assert.Equal(t, 1, stats.TotalReleases)
	assert.Equal(t, 0, stats.ActiveLocks)
}

func TestLockManagerMultipleLocks(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	for i := 0; i < 5; i++ {
		key := "lock-" + string(rune(48+i))
		err := manager.AcquireLock(key, 5*time.Second)
		assert.NoError(t, err)
	}

	stats := manager.GetLockStats()
	assert.Equal(t, 5, stats.ActiveLocks)

	activeLocks := manager.GetActiveLocks()
	assert.Equal(t, 5, len(activeLocks))
}

func TestLockManagerReleaseAll(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	for i := 0; i < 5; i++ {
		key := "lock-" + string(rune(48+i))
		manager.AcquireLock(key, 5*time.Second)
	}

	assert.Equal(t, 5, manager.GetLockStats().ActiveLocks)

	manager.ReleaseAll()

	assert.Equal(t, 0, manager.GetLockStats().ActiveLocks)
	assert.Equal(t, 0, len(manager.GetActiveLocks()))
}

func TestLockManagerRenew(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	manager.AcquireLock("lock-1", 5*time.Second)
	err := manager.RenewLock("lock-1")
	assert.NoError(t, err)
}

func TestLockManagerGetStats(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	manager.AcquireLock("lock-1", 5*time.Second)
	manager.AcquireLock("lock-2", 5*time.Second)

	manager.ReleaseLock("lock-1")

	stats := manager.GetLockStats()
	assert.Equal(t, 2, stats.TotalAcquisitions)
	assert.Equal(t, 1, stats.TotalReleases)
	assert.Equal(t, 1, stats.ActiveLocks)
}

func TestLockManagerFailedAcquisition(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)
	lock1 := NewDistributedLock("lock-1", backend, 30*time.Second)

	// Acquire from outside manager
	lock1.Acquire(5 * time.Second)

	// Try to acquire from manager
	err := manager.AcquireLock("lock-1", 100*time.Millisecond)
	assert.Error(t, err)

	stats := manager.GetLockStats()
	assert.Equal(t, 1, stats.FailedAcquisitions)
}

func TestDistributedLockDifferentKeys(t *testing.T) {
	backend := NewLocalLockBackend()
	lock1 := NewDistributedLock("lock-1", backend, 30*time.Second)
	lock2 := NewDistributedLock("lock-2", backend, 30*time.Second)

	lock1.Acquire(5 * time.Second)
	// lock2 should acquire without blocking
	err := lock2.Acquire(100 * time.Millisecond)
	assert.NoError(t, err)

	assert.True(t, lock1.IsAcquired())
	assert.True(t, lock2.IsAcquired())
}

func TestLockManagerDuplicateAcquisition(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	manager.AcquireLock("lock-1", 5*time.Second)
	err := manager.AcquireLock("lock-1", 5*time.Second)
	assert.Error(t, err)
}

func TestLockManagerReleaseMissing(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	err := manager.ReleaseLock("nonexistent")
	assert.Error(t, err)
}

func TestDistributedLockReleaseNotAcquired(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("test-lock", backend, 30*time.Second)

	err := lock.Release()
	assert.Error(t, err)
}

func TestConcurrentLockAcquisition(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("shared-lock", backend, 30*time.Second)

	acquired := false
	lock.Acquire(5 * time.Second)

	// Simulate another goroutine trying to acquire
	go func() {
		lock2 := NewDistributedLock("shared-lock", backend, 30*time.Second)
		err := lock2.Acquire(100 * time.Millisecond)
		if err == nil {
			acquired = true
		}
	}()

	time.Sleep(200 * time.Millisecond)
	assert.False(t, acquired)
}

func TestDefaultLockTTL(t *testing.T) {
	backend := NewLocalLockBackend()
	lock := NewDistributedLock("test-lock", backend, 0)

	assert.Equal(t, 30*time.Second, lock.ttl)
}

func TestDefaultLockManagerTTL(t *testing.T) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 0)

	assert.Equal(t, 30*time.Second, manager.lockTTL)
}

func BenchmarkLockAcquisition(b *testing.B) {
	backend := NewLocalLockBackend()

	for i := 0; i < b.N; i++ {
		lock := NewDistributedLock("test-lock", backend, 30*time.Second)
		lock.Acquire(5 * time.Second)
		lock.Release()
	}
}

func BenchmarkLockManagerAcquisition(b *testing.B) {
	backend := NewLocalLockBackend()
	manager := NewLockManager(backend, 30*time.Second)

	for i := 0; i < b.N; i++ {
		key := "lock-" + string(rune(48+i%100))
		manager.AcquireLock(key, 5*time.Second)
		manager.ReleaseLock(key)
	}
}
