package recovery

import (
	"context"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"go.uber.org/zap"
)

func TestRetry_Success(t *testing.T) {
	ctx := context.Background()
	attempts := 0

	err := Retry(ctx, func() error {
		attempts++
		return nil
	}, nil)

	assert.NoError(t, err)
	assert.Equal(t, 1, attempts)
}

func TestRetry_SuccessAfterRetries(t *testing.T) {
	ctx := context.Background()
	attempts := 0

	config := &RetryConfig{
		MaxAttempts:  3,
		InitialDelay: 10 * time.Millisecond,
		MaxDelay:     100 * time.Millisecond,
		Multiplier:   2.0,
	}

	err := Retry(ctx, func() error {
		attempts++
		if attempts < 3 {
			return errors.New("temporary error")
		}
		return nil
	}, config)

	assert.NoError(t, err)
	assert.Equal(t, 3, attempts)
}

func TestRetry_MaxAttemptsReached(t *testing.T) {
	ctx := context.Background()
	attempts := 0

	config := &RetryConfig{
		MaxAttempts:  3,
		InitialDelay: 10 * time.Millisecond,
		MaxDelay:     100 * time.Millisecond,
	}

	err := Retry(ctx, func() error {
		attempts++
		return errors.New("persistent error")
	}, config)

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "max attempts")
	assert.Equal(t, 3, attempts)
}

func TestRetry_ContextCancellation(t *testing.T) {
	// Utiliser un contexte avec timeout pour garantir l'annulation pendant le délai d'attente
	// Le timeout doit être suffisant pour que fn() soit appelé au moins une fois,
	// mais pas trop long pour que le contexte soit annulé pendant le délai d'attente
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
	defer cancel()

	attempts := 0

	config := &RetryConfig{
		MaxAttempts:  5,                      // Réduire le nombre de tentatives pour garantir que le contexte soit annulé à temps
		InitialDelay: 200 * time.Millisecond, // Délai plus long que le timeout du contexte pour garantir l'annulation
		RetryableFunc: func(err error) bool {
			return true // Toujours retryable pour ce test
		},
	}

	err := Retry(ctx, func() error {
		attempts++
		// Ajouter un petit délai pour ralentir le test et garantir que le contexte soit annulé pendant l'attente
		time.Sleep(5 * time.Millisecond)
		return errors.New("temporary error")
	}, config)

	assert.Error(t, err)
	// L'erreur peut être "context cancelled" ou "context cancelled during retry"
	assert.True(t,
		strings.Contains(err.Error(), "context cancelled") ||
			strings.Contains(err.Error(), "context cancelled during retry"),
		"Error should contain 'context cancelled': %s", err.Error())
	assert.Greater(t, attempts, 0) // Devrait avoir fait au moins un appel
}

func TestRetry_NonRetryableError(t *testing.T) {
	ctx := context.Background()
	attempts := 0

	permanentErr := errors.New("permanent error")
	config := &RetryConfig{
		MaxAttempts: 3,
		RetryableFunc: func(err error) bool {
			return err != permanentErr
		},
	}

	err := Retry(ctx, func() error {
		attempts++
		return permanentErr
	}, config)

	assert.Error(t, err)
	assert.Equal(t, 1, attempts) // Ne devrait retryer qu'une fois
}

func TestRetryWithResult_Success(t *testing.T) {
	ctx := context.Background()
	attempts := 0

	result, err := RetryWithResult(ctx, func() (int, error) {
		attempts++
		return 42, nil
	}, nil)

	assert.NoError(t, err)
	assert.Equal(t, 42, result)
	assert.Equal(t, 1, attempts)
}

func TestRetryWithResult_Retry(t *testing.T) {
	ctx := context.Background()
	attempts := 0

	config := &RetryConfig{
		MaxAttempts:  3,
		InitialDelay: 10 * time.Millisecond,
	}

	result, err := RetryWithResult(ctx, func() (int, error) {
		attempts++
		if attempts < 2 {
			return 0, errors.New("temporary error")
		}
		return 100, nil
	}, config)

	assert.NoError(t, err)
	assert.Equal(t, 100, result)
	assert.Equal(t, 2, attempts)
}

func TestRetryWithLogger(t *testing.T) {
	ctx := context.Background()
	logger, _ := zap.NewDevelopment()
	attempts := 0

	config := &RetryConfig{
		MaxAttempts:  3,
		InitialDelay: 10 * time.Millisecond,
	}

	err := RetryWithLogger(ctx, logger, func() error {
		attempts++
		if attempts < 2 {
			return errors.New("temporary error")
		}
		return nil
	}, config)

	assert.NoError(t, err)
	assert.Equal(t, 2, attempts)
}

func TestIsRetryableError(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		expected bool
	}{
		{"timeout", errors.New("timeout"), true},
		{"connection refused", errors.New("connection refused"), true},
		{"server error", errors.New("server error 500"), true},
		{"permanent", errors.New("invalid input"), false},
		{"nil", nil, false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := IsRetryableError(tt.err)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestCalculateDelay(t *testing.T) {
	config := &RetryConfig{
		InitialDelay: 100 * time.Millisecond,
		MaxDelay:     1 * time.Second,
		Multiplier:   2.0,
		Jitter:       false,
	}

	// Test exponential backoff
	delay1 := calculateDelay(1, config)
	assert.Equal(t, 100*time.Millisecond, delay1)

	delay2 := calculateDelay(2, config)
	assert.Equal(t, 200*time.Millisecond, delay2)

	delay3 := calculateDelay(3, config)
	assert.Equal(t, 400*time.Millisecond, delay3)

	// Test max delay
	delay10 := calculateDelay(10, config)
	assert.LessOrEqual(t, delay10, config.MaxDelay)
}

func TestDefaultRetryConfig(t *testing.T) {
	config := DefaultRetryConfig()
	assert.NotNil(t, config)
	assert.Equal(t, 3, config.MaxAttempts)
	assert.Equal(t, 100*time.Millisecond, config.InitialDelay)
	assert.Equal(t, 5*time.Second, config.MaxDelay)
	assert.Equal(t, 2.0, config.Multiplier)
	assert.True(t, config.Jitter)
}
