package services

import (
	"context"
	"net/http"
	"net/http/httptest"
	"sync"
	"sync/atomic"
	"testing"
	"time"

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

// TestStreamService_StartProcessing_RetryOnFailure vérifie que le retry fonctionne quand le serveur échoue temporairement
// MOD-P1-RES-002: Test pour vérifier le mécanisme de retry
func TestStreamService_StartProcessing_RetryOnFailure(t *testing.T) {
	// Mock server qui échoue 2 fois puis réussit
	attemptCount := int32(0)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		attempt := atomic.AddInt32(&attemptCount, 1)
		if attempt <= 2 {
			// Échouer les 2 premières tentatives
			w.WriteHeader(http.StatusInternalServerError)
		} else {
			// Réussir à la 3ème tentative
			w.WriteHeader(http.StatusOK)
		}
	}))
	defer server.Close()

	logger := zap.NewNop()
	service := NewStreamService(server.URL, logger)

	trackID := uuid.New()
	err := service.StartProcessing(context.Background(), trackID, "/path/to/file")

	// Vérifier que la requête a finalement réussi
	assert.NoError(t, err, "Request should succeed after retries")
	assert.Equal(t, int32(3), attemptCount, "Should have made 3 attempts (2 failures + 1 success)")
}

// TestStreamService_StartProcessing_MaxRetriesExceeded vérifie que le service échoue après max retries
// MOD-P1-RES-002: Test pour vérifier que le retry s'arrête après maxRetries
func TestStreamService_StartProcessing_MaxRetriesExceeded(t *testing.T) {
	// Mock server qui échoue toujours
	attemptCount := int32(0)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&attemptCount, 1)
		w.WriteHeader(http.StatusInternalServerError)
	}))
	defer server.Close()

	logger := zap.NewNop()
	service := NewStreamService(server.URL, logger)

	trackID := uuid.New()
	err := service.StartProcessing(context.Background(), trackID, "/path/to/file")

	// Vérifier que la requête échoue après max retries
	assert.Error(t, err, "Request should fail after max retries")
	assert.Contains(t, err.Error(), "after 3 attempts", "Error should mention max retries")
	assert.Equal(t, int32(3), attemptCount, "Should have made exactly 3 attempts")
}

// TestStreamService_StartProcessing_RetryOnNetworkError vérifie que le retry fonctionne pour les erreurs réseau
// MOD-P1-RES-002: Test pour vérifier le retry sur erreurs réseau (client.Do error)
func TestStreamService_StartProcessing_RetryOnNetworkError(t *testing.T) {
	// Mock server qui ferme la connexion immédiatement (simule erreur réseau)
	attemptCount := int32(0)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		attempt := atomic.AddInt32(&attemptCount, 1)
		if attempt <= 2 {
			// Fermer la connexion immédiatement (simule erreur réseau)
			hj, ok := w.(http.Hijacker)
			if ok {
				conn, _, _ := hj.Hijack()
				conn.Close()
			}
			return
		}
		// Réussir à la 3ème tentative
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	logger := zap.NewNop()
	service := NewStreamService(server.URL, logger)

	trackID := uuid.New()
	err := service.StartProcessing(context.Background(), trackID, "/path/to/file")

	// Vérifier que la requête a finalement réussi après retries
	assert.NoError(t, err, "Request should succeed after retries on network error")
	assert.Equal(t, int32(3), attemptCount, "Should have made 3 attempts")
}

// TestStreamService_StartProcessing_ContextTimeout vérifie que le contexte annule correctement
// MOD-P1-RES-002: Test pour vérifier le respect du contexte timeout
func TestStreamService_StartProcessing_ContextTimeout(t *testing.T) {
	// Mock server qui prend trop de temps (plus que le timeout du contexte)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Attendre plus longtemps que le timeout du contexte
		time.Sleep(2 * time.Second)
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	logger := zap.NewNop()
	service := NewStreamService(server.URL, logger)

	// Créer un contexte avec un timeout très court (500ms)
	ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
	defer cancel()

	trackID := uuid.New()
	err := service.StartProcessing(ctx, trackID, "/path/to/file")

	// Vérifier que la requête échoue à cause du timeout du contexte
	assert.Error(t, err, "Request should fail due to context timeout")
	assert.Contains(t, err.Error(), "context", "Error should mention context")
}

// TestStreamService_StartProcessing_ContextCancelledDuringBackoff vérifie que le contexte annule pendant le backoff
// MOD-P1-RES-002: Test pour vérifier que le contexte est respecté pendant le backoff
func TestStreamService_StartProcessing_ContextCancelledDuringBackoff(t *testing.T) {
	// Mock server qui échoue toujours
	attemptCount := int32(0)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		atomic.AddInt32(&attemptCount, 1)
		w.WriteHeader(http.StatusInternalServerError)
	}))
	defer server.Close()

	logger := zap.NewNop()
	service := NewStreamService(server.URL, logger)

	// Créer un contexte qui sera annulé après le premier échec
	ctx, cancel := context.WithCancel(context.Background())

	// Annuler le contexte après un court délai (pendant le backoff)
	go func() {
		time.Sleep(100 * time.Millisecond)
		cancel()
	}()

	trackID := uuid.New()
	err := service.StartProcessing(ctx, trackID, "/path/to/file")

	// Vérifier que la requête échoue à cause de l'annulation du contexte
	assert.Error(t, err, "Request should fail due to context cancellation")
	assert.Contains(t, err.Error(), "context cancelled", "Error should mention context cancellation")
	// Le nombre de tentatives devrait être 1 (première tentative échoue, puis contexte annulé pendant backoff)
	assert.Equal(t, int32(1), attemptCount, "Should have made 1 attempt before context cancellation")
}

// TestStreamService_StartProcessing_BackoffTiming vérifie que le backoff exponentiel fonctionne
// MOD-P1-RES-002: Test pour vérifier le timing du backoff exponentiel
func TestStreamService_StartProcessing_BackoffTiming(t *testing.T) {
	// Mock server qui échoue 2 fois puis réussit
	attemptCount := int32(0)
	attemptTimes := make([]time.Time, 0, 3)
	var mu sync.Mutex

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		attemptTimes = append(attemptTimes, time.Now())
		mu.Unlock()

		attempt := atomic.AddInt32(&attemptCount, 1)
		if attempt <= 2 {
			w.WriteHeader(http.StatusInternalServerError)
		} else {
			w.WriteHeader(http.StatusOK)
		}
	}))
	defer server.Close()

	logger := zap.NewNop()
	service := NewStreamService(server.URL, logger)

	trackID := uuid.New()
	startTime := time.Now()
	err := service.StartProcessing(context.Background(), trackID, "/path/to/file")
	duration := time.Since(startTime)

	// Vérifier que la requête a réussi
	assert.NoError(t, err, "Request should succeed after retries")

	// Vérifier que le timing correspond au backoff exponentiel
	// Tentative 1: immédiate
	// Tentative 2: après 1s (backoff initial)
	// Tentative 3: après 2s (backoff * 2)
	// Total attendu: ~3s minimum (1s + 2s)
	assert.GreaterOrEqual(t, duration, 3*time.Second, "Total duration should be at least 3s (1s + 2s backoff)")
	assert.Less(t, duration, 5*time.Second, "Total duration should be less than 5s (with some margin)")

	// Vérifier que les tentatives sont espacées correctement
	mu.Lock()
	defer mu.Unlock()
	require.Len(t, attemptTimes, 3, "Should have made 3 attempts")
	if len(attemptTimes) >= 2 {
		// Intervalle entre tentative 1 et 2 devrait être ~1s
		interval1 := attemptTimes[1].Sub(attemptTimes[0])
		assert.GreaterOrEqual(t, interval1, 900*time.Millisecond, "Backoff between attempt 1 and 2 should be ~1s")
		assert.Less(t, interval1, 2*time.Second, "Backoff between attempt 1 and 2 should be ~1s")
	}
	if len(attemptTimes) >= 3 {
		// Intervalle entre tentative 2 et 3 devrait être ~2s
		interval2 := attemptTimes[2].Sub(attemptTimes[1])
		assert.GreaterOrEqual(t, interval2, 1800*time.Millisecond, "Backoff between attempt 2 and 3 should be ~2s")
		assert.Less(t, interval2, 3*time.Second, "Backoff between attempt 2 and 3 should be ~2s")
	}
}
