package retry

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

	"AutoSSO/internal/pkg/httpclient"

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

func TestDefaultConfig(t *testing.T) {
	assert.Equal(t, 3, DefaultConfig.MaxRetries)
	assert.Equal(t, 100*time.Millisecond, DefaultConfig.InitialBackoff)
	assert.Equal(t, 10*time.Second, DefaultConfig.MaxBackoff)
	assert.Equal(t, 2.0, DefaultConfig.Multiplier)
	assert.True(t, DefaultConfig.Jitter)
}

func TestDo_Success(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     3,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	err := Do(ctx, cfg, func() error {
		callCount++
		return nil
	})

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

func TestDo_RetryableError_EventualSuccess(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     3,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	err := Do(ctx, cfg, func() error {
		callCount++
		if callCount < 3 {
			return NewRetryableError(errors.New("temporary error"))
		}
		return nil
	})

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

func TestDo_NonRetryableError(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     3,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	expectedErr := errors.New("permanent error")
	err := Do(ctx, cfg, func() error {
		callCount++
		return expectedErr
	})

	assert.ErrorIs(t, err, expectedErr)
	assert.Equal(t, 1, callCount) // Should not retry non-retryable errors
}

func TestDo_MaxRetriesExceeded(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     2,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	err := Do(ctx, cfg, func() error {
		callCount++
		return NewRetryableError(errors.New("always fails"))
	})

	assert.Error(t, err)
	assert.True(t, IsMaxRetriesExceeded(err))
	assert.Equal(t, 3, callCount) // Initial + 2 retries
}

func TestDo_ContextCancellation(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	cfg := Config{
		MaxRetries:     5,
		InitialBackoff: 100 * time.Millisecond,
		MaxBackoff:     1 * time.Second,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	go func() {
		time.Sleep(50 * time.Millisecond)
		cancel()
	}()

	err := Do(ctx, cfg, func() error {
		callCount++
		return NewRetryableError(errors.New("temporary error"))
	})

	assert.Error(t, err)
	assert.True(t, errors.Is(err, context.Canceled))
}

func TestDo_ContextTimeout(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer cancel()

	cfg := Config{
		MaxRetries:     5,
		InitialBackoff: 100 * time.Millisecond,
		MaxBackoff:     1 * time.Second,
		Multiplier:     2.0,
		Jitter:         false,
	}

	err := Do(ctx, cfg, func() error {
		return NewRetryableError(errors.New("temporary error"))
	})

	assert.Error(t, err)
	assert.True(t, errors.Is(err, context.DeadlineExceeded))
}

func TestDo_HTTPClientRetryableError(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     2,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	// Use httpclient's timeout error which is retryable
	err := Do(ctx, cfg, func() error {
		callCount++
		if callCount < 3 {
			return httpclient.ErrTimeout
		}
		return nil
	})

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

func TestDo_HTTPError5xx(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     2,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	err := Do(ctx, cfg, func() error {
		callCount++
		if callCount < 3 {
			return &httpclient.HTTPError{StatusCode: 503, Status: "Service Unavailable"}
		}
		return nil
	})

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

func TestDo_HTTPError4xx_NotRetryable(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     3,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	err := Do(ctx, cfg, func() error {
		callCount++
		return &httpclient.HTTPError{StatusCode: 404, Status: "Not Found"}
	})

	assert.Error(t, err)
	assert.Equal(t, 1, callCount) // Should not retry 4xx errors (except 429)
}

func TestDo_HTTPError429_Retryable(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     2,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	err := Do(ctx, cfg, func() error {
		callCount++
		if callCount < 3 {
			return &httpclient.HTTPError{StatusCode: 429, Status: "Too Many Requests"}
		}
		return nil
	})

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

func TestDo_RetryableErrorWithDelay(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     2,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	start := time.Now()
	err := Do(ctx, cfg, func() error {
		callCount++
		if callCount < 2 {
			return NewRetryableErrorWithDelay(errors.New("rate limited"), 50*time.Millisecond)
		}
		return nil
	})

	elapsed := time.Since(start)
	assert.NoError(t, err)
	assert.Equal(t, 2, callCount)
	// Should have waited at least 50ms due to RetryAfter
	assert.GreaterOrEqual(t, elapsed, 50*time.Millisecond)
}

func TestDo_ZeroRetries(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     0,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	err := Do(ctx, cfg, func() error {
		callCount++
		return NewRetryableError(errors.New("temporary error"))
	})

	assert.Error(t, err)
	assert.True(t, IsMaxRetriesExceeded(err))
	assert.Equal(t, 1, callCount) // Only initial attempt, no retries
}

func TestDoWithResult_Success(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     3,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	result, err := DoWithResult(ctx, cfg, func() (string, error) {
		return "success", nil
	})

	assert.NoError(t, err)
	assert.Equal(t, "success", result)
}

func TestDoWithResult_RetryableError_EventualSuccess(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     3,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	result, err := DoWithResult(ctx, cfg, func() (int, error) {
		callCount++
		if callCount < 3 {
			return 0, NewRetryableError(errors.New("temporary error"))
		}
		return 42, nil
	})

	assert.NoError(t, err)
	assert.Equal(t, 42, result)
	assert.Equal(t, 3, callCount)
}

func TestDoWithResult_MaxRetriesExceeded(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     2,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	callCount := 0
	result, err := DoWithResult(ctx, cfg, func() (string, error) {
		callCount++
		return "", NewRetryableError(errors.New("always fails"))
	})

	assert.Error(t, err)
	assert.True(t, IsMaxRetriesExceeded(err))
	assert.Equal(t, "", result)
	assert.Equal(t, 3, callCount)
}

func TestIsRetryable(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		expected bool
	}{
		{
			name:     "nil error",
			err:      nil,
			expected: false,
		},
		{
			name:     "RetryableError",
			err:      NewRetryableError(errors.New("test")),
			expected: true,
		},
		{
			name:     "RetryableError with delay",
			err:      NewRetryableErrorWithDelay(errors.New("test"), time.Second),
			expected: true,
		},
		{
			name:     "httpclient timeout",
			err:      httpclient.ErrTimeout,
			expected: true,
		},
		{
			name:     "httpclient connection refused",
			err:      httpclient.ErrConnectionRefused,
			expected: true,
		},
		{
			name:     "HTTP 500 error",
			err:      &httpclient.HTTPError{StatusCode: 500},
			expected: true,
		},
		{
			name:     "HTTP 503 error",
			err:      &httpclient.HTTPError{StatusCode: 503},
			expected: true,
		},
		{
			name:     "HTTP 429 error",
			err:      &httpclient.HTTPError{StatusCode: 429},
			expected: true,
		},
		{
			name:     "HTTP 400 error",
			err:      &httpclient.HTTPError{StatusCode: 400},
			expected: false,
		},
		{
			name:     "HTTP 404 error",
			err:      &httpclient.HTTPError{StatusCode: 404},
			expected: false,
		},
		{
			name:     "regular error",
			err:      errors.New("regular error"),
			expected: false,
		},
	}

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

func TestRetryableError_Error(t *testing.T) {
	err := NewRetryableError(errors.New("test error"))
	assert.Equal(t, "retryable error: test error", err.Error())

	errWithDelay := NewRetryableErrorWithDelay(errors.New("test error"), 5*time.Second)
	assert.Equal(t, "retryable error (retry after 5s): test error", errWithDelay.Error())
}

func TestRetryableError_Unwrap(t *testing.T) {
	innerErr := errors.New("inner error")
	err := NewRetryableError(innerErr)

	assert.True(t, errors.Is(err, innerErr))
}

func TestMaxRetriesExceededError_Error(t *testing.T) {
	innerErr := errors.New("inner error")
	err := &MaxRetriesExceededError{Attempts: 4, LastErr: innerErr}

	assert.Equal(t, "max retries exceeded after 4 attempts: inner error", err.Error())
}

func TestMaxRetriesExceededError_Unwrap(t *testing.T) {
	innerErr := errors.New("inner error")
	err := &MaxRetriesExceededError{Attempts: 4, LastErr: innerErr}

	assert.True(t, errors.Is(err, innerErr))
}

func TestCalculateBackoff_NoJitter(t *testing.T) {
	base := 100 * time.Millisecond
	result := calculateBackoff(base, false)
	assert.Equal(t, base, result)
}

func TestCalculateBackoff_WithJitter(t *testing.T) {
	base := 100 * time.Millisecond

	// Run multiple times to verify jitter produces different values
	results := make(map[time.Duration]bool)
	for i := 0; i < 100; i++ {
		result := calculateBackoff(base, true)
		results[result] = true

		// Verify result is within expected range [0.5x, 1.5x)
		assert.GreaterOrEqual(t, result, 50*time.Millisecond)
		assert.Less(t, result, 150*time.Millisecond)
	}

	// With 100 iterations, we should have multiple different values
	assert.Greater(t, len(results), 1, "Jitter should produce varying backoff values")
}

func TestExponentialBackoff(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     4,
		InitialBackoff: 10 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	var backoffTimes []time.Duration
	var lastTime time.Time

	callCount := 0
	_ = Do(ctx, cfg, func() error {
		callCount++
		now := time.Now()
		if !lastTime.IsZero() {
			backoffTimes = append(backoffTimes, now.Sub(lastTime))
		}
		lastTime = now
		return NewRetryableError(errors.New("always fails"))
	})

	require.Len(t, backoffTimes, 4) // 4 retries = 4 backoff periods

	// Verify exponential growth (with some tolerance for timing)
	// Expected: 10ms, 20ms, 40ms, 80ms
	tolerance := 5 * time.Millisecond
	assert.InDelta(t, 10*time.Millisecond, backoffTimes[0], float64(tolerance))
	assert.InDelta(t, 20*time.Millisecond, backoffTimes[1], float64(tolerance))
	assert.InDelta(t, 40*time.Millisecond, backoffTimes[2], float64(tolerance))
	assert.InDelta(t, 80*time.Millisecond, backoffTimes[3], float64(tolerance))
}

func TestMaxBackoffCap(t *testing.T) {
	ctx := context.Background()
	cfg := Config{
		MaxRetries:     5,
		InitialBackoff: 50 * time.Millisecond,
		MaxBackoff:     100 * time.Millisecond,
		Multiplier:     2.0,
		Jitter:         false,
	}

	var backoffTimes []time.Duration
	var lastTime time.Time

	callCount := 0
	_ = Do(ctx, cfg, func() error {
		callCount++
		now := time.Now()
		if !lastTime.IsZero() {
			backoffTimes = append(backoffTimes, now.Sub(lastTime))
		}
		lastTime = now
		return NewRetryableError(errors.New("always fails"))
	})

	require.Len(t, backoffTimes, 5)

	// Verify backoff is capped at MaxBackoff
	// Expected: 50ms, 100ms (capped), 100ms (capped), 100ms (capped), 100ms (capped)
	tolerance := 10 * time.Millisecond
	assert.InDelta(t, 50*time.Millisecond, backoffTimes[0], float64(tolerance))
	for i := 1; i < len(backoffTimes); i++ {
		assert.InDelta(t, 100*time.Millisecond, backoffTimes[i], float64(tolerance))
	}
}
