package services

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/redis/go-redis/v9"
	"github.com/stretchr/testify/assert"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
	"go.uber.org/zap"
)

func setupTestAccountLockoutService(t *testing.T) (*AccountLockoutService, *redis.Client, func()) {
	redisURL := os.Getenv("REDIS_TEST_URL")
	if redisURL == "" {
		// Use testcontainers if Redis URL not provided
		ctx := context.Background()
		redisC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
			ContainerRequest: testcontainers.ContainerRequest{
				Image:        "redis:7-alpine",
				ExposedPorts: []string{"6379/tcp"},
				WaitingFor:   wait.ForLog("Ready to accept connections"),
			},
			Started: true,
		})
		if err != nil {
			t.Skipf("Skipping test: failed to start Redis container: %v", err)
			return nil, nil, func() {}
		}

		endpoint, err := redisC.Endpoint(ctx, "")
		if err != nil {
			t.Skipf("Skipping test: failed to get Redis endpoint: %v", err)
			return nil, nil, func() {}
		}

		client := redis.NewClient(&redis.Options{Addr: endpoint})
		logger := zap.NewNop()
		service := NewAccountLockoutService(client, logger)

		cleanup := func() {
			client.FlushDB(ctx)
			client.Close()
			redisC.Terminate(ctx)
		}

		return service, client, cleanup
	}

	// Use provided Redis URL
	opts, err := redis.ParseURL(redisURL)
	if err != nil {
		t.Skipf("Skipping test: failed to parse Redis URL: %v", err)
		return nil, nil, func() {}
	}

	client := redis.NewClient(opts)
	ctx := context.Background()
	_, err = client.Ping(ctx).Result()
	if err != nil {
		t.Skipf("Skipping test: Redis not available: %v", err)
		return nil, nil, func() {}
	}

	client.FlushDB(ctx)
	logger := zap.NewNop()
	service := NewAccountLockoutService(client, logger)

	cleanup := func() {
		client.FlushDB(ctx)
		client.Close()
	}

	return service, client, cleanup
}

func TestAccountLockoutService_NewAccountLockoutService(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	assert.NotNil(t, service)
	assert.NotNil(t, service.redisClient)
	assert.NotNil(t, service.logger)
	assert.Equal(t, 5, service.maxAttempts)
	assert.Equal(t, 30*time.Minute, service.lockoutDuration)
	assert.Equal(t, 15*time.Minute, service.windowDuration)
}

func TestAccountLockoutService_NewAccountLockoutServiceWithConfig(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	config := &AccountLockoutConfig{
		MaxAttempts:     10,
		LockoutDuration: 60 * time.Minute,
		WindowDuration:  30 * time.Minute,
	}

	newService := NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, config)

	assert.NotNil(t, newService)
	assert.Equal(t, 10, newService.maxAttempts)
	assert.Equal(t, 60*time.Minute, newService.lockoutDuration)
	assert.Equal(t, 30*time.Minute, newService.windowDuration)
}

func TestAccountLockoutService_NewAccountLockoutServiceWithConfig_NilConfig(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	newService := NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, nil)

	assert.NotNil(t, newService)
	assert.Equal(t, 5, newService.maxAttempts) // Default values
}

func TestAccountLockoutService_DefaultAccountLockoutConfig(t *testing.T) {
	config := DefaultAccountLockoutConfig()

	assert.NotNil(t, config)
	assert.Equal(t, 5, config.MaxAttempts)
	assert.Equal(t, 30*time.Minute, config.LockoutDuration)
	assert.Equal(t, 15*time.Minute, config.WindowDuration)
}

func TestAccountLockoutService_RecordFailedAttempt_Success(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	err := service.RecordFailedAttempt(ctx, email)
	assert.NoError(t, err)

	// Verify attempt was recorded
	count, err := service.GetFailedAttemptsCount(ctx, email)
	assert.NoError(t, err)
	assert.Equal(t, 1, count)
}

func TestAccountLockoutService_RecordFailedAttempt_MultipleAttempts(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	// Record multiple failed attempts
	for i := 0; i < 3; i++ {
		err := service.RecordFailedAttempt(ctx, email)
		assert.NoError(t, err)
	}

	// Verify count
	count, err := service.GetFailedAttemptsCount(ctx, email)
	assert.NoError(t, err)
	assert.Equal(t, 3, count)
}

func TestAccountLockoutService_RecordFailedAttempt_LocksAfterMaxAttempts(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	// Create service with lower max attempts for testing
	config := &AccountLockoutConfig{
		MaxAttempts:     3,
		LockoutDuration: 5 * time.Minute,
		WindowDuration:  15 * time.Minute,
	}
	service = NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, config)

	ctx := context.Background()
	email := "test@example.com"

	// Record attempts up to max
	for i := 0; i < 3; i++ {
		err := service.RecordFailedAttempt(ctx, email)
		assert.NoError(t, err)
	}

	// Verify account is locked
	locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.True(t, locked)
	assert.NotNil(t, lockedUntil)
	assert.True(t, lockedUntil.After(time.Now()))
}

func TestAccountLockoutService_RecordSuccessfulLogin_ResetsAttempts(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	// Record failed attempts
	for i := 0; i < 3; i++ {
		err := service.RecordFailedAttempt(ctx, email)
		assert.NoError(t, err)
	}

	// Record successful login
	err := service.RecordSuccessfulLogin(ctx, email)
	assert.NoError(t, err)

	// Verify attempts were reset
	count, err := service.GetFailedAttemptsCount(ctx, email)
	assert.NoError(t, err)
	assert.Equal(t, 0, count)
}

func TestAccountLockoutService_RecordSuccessfulLogin_UnlocksAccount(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	// Create service with lower max attempts
	config := &AccountLockoutConfig{
		MaxAttempts:     2,
		LockoutDuration: 5 * time.Minute,
		WindowDuration:  15 * time.Minute,
	}
	service = NewAccountLockoutServiceWithConfig(service.redisClient, service.logger, config)

	ctx := context.Background()
	email := "test@example.com"

	// Lock account
	for i := 0; i < 2; i++ {
		err := service.RecordFailedAttempt(ctx, email)
		assert.NoError(t, err)
	}

	// Verify locked
	locked, _, err := service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.True(t, locked)

	// Record successful login
	err = service.RecordSuccessfulLogin(ctx, email)
	assert.NoError(t, err)

	// Verify unlocked
	locked, _, err = service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.False(t, locked)
}

func TestAccountLockoutService_IsAccountLocked_NotLocked(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.False(t, locked)
	assert.Nil(t, lockedUntil)
}

func TestAccountLockoutService_IsAccountLocked_Locked(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	// Lock account
	err := service.LockAccount(ctx, email)
	assert.NoError(t, err)

	// Verify locked
	locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.True(t, locked)
	assert.NotNil(t, lockedUntil)
	assert.True(t, lockedUntil.After(time.Now()))
}

func TestAccountLockoutService_LockAccount_Success(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	err := service.LockAccount(ctx, email)
	assert.NoError(t, err)

	// Verify locked
	locked, _, err := service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.True(t, locked)
}

func TestAccountLockoutService_UnlockAccount_Success(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	// Lock account first
	err := service.LockAccount(ctx, email)
	assert.NoError(t, err)

	// Unlock account
	err = service.UnlockAccount(ctx, email)
	assert.NoError(t, err)

	// Verify unlocked
	locked, _, err := service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.False(t, locked)
}

func TestAccountLockoutService_UnlockAccount_ResetsAttempts(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	// Record failed attempts
	for i := 0; i < 3; i++ {
		err := service.RecordFailedAttempt(ctx, email)
		assert.NoError(t, err)
	}

	// Unlock account
	err := service.UnlockAccount(ctx, email)
	assert.NoError(t, err)

	// Verify attempts were reset
	count, err := service.GetFailedAttemptsCount(ctx, email)
	assert.NoError(t, err)
	assert.Equal(t, 0, count)
}

func TestAccountLockoutService_GetFailedAttemptsCount_Zero(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	count, err := service.GetFailedAttemptsCount(ctx, email)
	assert.NoError(t, err)
	assert.Equal(t, 0, count)
}

func TestAccountLockoutService_GetFailedAttemptsCount_WithAttempts(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	ctx := context.Background()
	email := "test@example.com"

	// Record failed attempts
	for i := 0; i < 5; i++ {
		err := service.RecordFailedAttempt(ctx, email)
		assert.NoError(t, err)
	}

	count, err := service.GetFailedAttemptsCount(ctx, email)
	assert.NoError(t, err)
	assert.Equal(t, 5, count)
}

func TestAccountLockoutService_getAttemptsKey(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	email := "test@example.com"
	key := service.getAttemptsKey(email)

	assert.Equal(t, "account_lockout:attempts:test@example.com", key)
}

func TestAccountLockoutService_getLockoutKey(t *testing.T) {
	service, _, cleanup := setupTestAccountLockoutService(t)
	if service == nil {
		return
	}
	defer cleanup()

	email := "test@example.com"
	key := service.getLockoutKey(email)

	assert.Equal(t, "account_lockout:locked:test@example.com", key)
}

func TestAccountLockoutService_RecordFailedAttempt_NoRedis(t *testing.T) {
	logger := zap.NewNop()
	service := NewAccountLockoutService(nil, logger)

	ctx := context.Background()
	email := "test@example.com"

	// Should not error even without Redis
	err := service.RecordFailedAttempt(ctx, email)
	assert.NoError(t, err)
}

func TestAccountLockoutService_IsAccountLocked_NoRedis(t *testing.T) {
	logger := zap.NewNop()
	service := NewAccountLockoutService(nil, logger)

	ctx := context.Background()
	email := "test@example.com"

	locked, lockedUntil, err := service.IsAccountLocked(ctx, email)
	assert.NoError(t, err)
	assert.False(t, locked)
	assert.Nil(t, lockedUntil)
}

func TestAccountLockoutService_AccountLockoutConfig_Fields(t *testing.T) {
	config := &AccountLockoutConfig{
		MaxAttempts:     10,
		LockoutDuration: 60 * time.Minute,
		WindowDuration:  30 * time.Minute,
	}

	assert.Equal(t, 10, config.MaxAttempts)
	assert.Equal(t, 60*time.Minute, config.LockoutDuration)
	assert.Equal(t, 30*time.Minute, config.WindowDuration)
}
