package recovery

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

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

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

	fn := func() error {
		attempts++
		if attempts < 2 {
			return errors.New("temporary error")
		}
		return nil
	}

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

	strategy := NewRetryRecoveryStrategy(fn, config, logger)

	assert.True(t, strategy.CanRecover(errors.New("timeout")))
	assert.False(t, strategy.CanRecover(nil))

	err := strategy.Recover(ctx, errors.New("temporary error"))
	assert.NoError(t, err)
	assert.Equal(t, 2, attempts)
}

func TestFallbackRecoveryStrategy(t *testing.T) {
	ctx := context.Background()
	logger, _ := zap.NewDevelopment()
	fallbackCalled := false

	fallbackFn := func() error {
		fallbackCalled = true
		return nil
	}

	strategy := NewFallbackRecoveryStrategy(fallbackFn, logger)

	assert.True(t, strategy.CanRecover(errors.New("any error")))

	err := strategy.Recover(ctx, errors.New("original error"))
	assert.NoError(t, err)
	assert.True(t, fallbackCalled)
}

func TestCircuitBreakerRecoveryStrategy(t *testing.T) {
	// Utiliser un contexte avec timeout court pour éviter d'attendre 30s
	ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
	defer cancel()

	logger, _ := zap.NewDevelopment()
	circuitOpen := true

	checkFn := func() bool {
		return circuitOpen
	}

	strategy := NewCircuitBreakerRecoveryStrategy(checkFn, logger)

	assert.True(t, strategy.CanRecover(errors.New("circuit breaker error")))

	// Simuler la fermeture du circuit breaker après un court délai
	go func() {
		time.Sleep(50 * time.Millisecond)
		circuitOpen = false
	}()

	err := strategy.Recover(ctx, errors.New("circuit breaker open"))
	// Le timeout du contexte devrait se produire avant le timeout de la stratégie
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "timeout")
}

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

	retryFn := func() error {
		attempts++
		if attempts < 2 {
			return errors.New("temporary error")
		}
		return nil
	}

	config := &RetryConfig{
		MaxAttempts:  3,
		InitialDelay: 10 * time.Millisecond,
		RetryableFunc: func(err error) bool {
			return true // Toujours retryable pour ce test
		},
	}

	retryStrategy := NewRetryRecoveryStrategy(retryFn, config, logger)
	composite := NewCompositeRecoveryStrategy([]ErrorRecoveryStrategy{retryStrategy}, logger)

	// Utiliser une erreur qui est détectée comme retryable par IsRetryableError
	testErr := errors.New("timeout error")
	assert.True(t, composite.CanRecover(testErr))

	err := composite.Recover(ctx, testErr)
	assert.NoError(t, err)
	assert.Equal(t, 2, attempts)
}

func TestRecoverWithStrategies(t *testing.T) {
	ctx := context.Background()
	logger, _ := zap.NewDevelopment()
	fallbackCalled := false

	retryFn := func() error {
		return errors.New("retry failed")
	}

	retryStrategy := NewRetryRecoveryStrategy(retryFn, nil, logger)
	fallbackStrategy := NewFallbackRecoveryStrategy(func() error {
		fallbackCalled = true
		return nil
	}, logger)

	strategies := []ErrorRecoveryStrategy{retryStrategy, fallbackStrategy}

	err := RecoverWithStrategies(ctx, errors.New("original error"), strategies, logger)
	assert.NoError(t, err)
	assert.True(t, fallbackCalled)
}

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

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

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

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