package services import ( "context" "github.com/google/uuid" "math" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" ) func setupTestPlaybackABTestServiceDB(t *testing.T) (*gorm.DB, *PlaybackABTestService) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") err = db.AutoMigrate(&models.User{}, &models.Track{}, &models.PlaybackAnalytics{}) require.NoError(t, err) logger := zaptest.NewLogger(t) service := NewPlaybackABTestService(db, logger) return db, service } func TestNewPlaybackABTestService(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) service := NewPlaybackABTestService(db, logger) assert.NotNil(t, service) assert.Equal(t, db, service.db) assert.NotNil(t, service.logger) } func TestNewPlaybackABTestService_NilLogger(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) service := NewPlaybackABTestService(db, nil) assert.NotNil(t, service) assert.NotNil(t, service.logger) } func TestPlaybackABTestService_CompareVariants_EmptyVariantNames(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) ctx := context.Background() filterA := VariantFilter{} filterB := VariantFilter{} result, err := service.CompareVariants(ctx, "", "B", filterA, filterB) assert.Error(t, err) assert.Contains(t, err.Error(), "variant names cannot be empty") assert.Nil(t, result) result, err = service.CompareVariants(ctx, "A", "", filterA, filterB) assert.Error(t, err) assert.Contains(t, err.Error(), "variant names cannot be empty") assert.Nil(t, result) } func TestPlaybackABTestService_CompareVariants_NoData(t *testing.T) { db, service := setupTestPlaybackABTestServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) filterA := VariantFilter{TrackID: &trackID} filterB := VariantFilter{TrackID: &trackID} result, err := service.CompareVariants(ctx, "A", "B", filterA, filterB) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, "A", result.VariantA.VariantName) assert.Equal(t, "B", result.VariantB.VariantName) assert.Equal(t, int64(0), result.VariantA.TotalSessions) assert.Equal(t, int64(0), result.VariantB.TotalSessions) assert.NotNil(t, result.Significance) } func TestPlaybackABTestService_CompareVariants_WithData(t *testing.T) { db, service := setupTestPlaybackABTestServiceDB(t) ctx := context.Background() // Créer users et track user1ID := uuid.New() user2ID := uuid.New() trackID := uuid.New() user1 := &models.User{ID: user1ID, Username: "user1", Slug: "user1", Email: "user1@example.com", IsActive: true} user2 := &models.User{ID: user2ID, Username: "user2", Slug: "user2", Email: "user2@example.com", IsActive: true} db.Create(user1) db.Create(user2) track := &models.Track{ ID: trackID, UserID: user1ID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) now := time.Now() // Variant A: High completion for i := 0; i < 10; i++ { db.Create(&models.PlaybackAnalytics{ TrackID: trackID, UserID: user1ID, PlayTime: 180, PauseCount: 0, SeekCount: 0, CompletionRate: 100.0, StartedAt: now, CreatedAt: now, }) } // Variant B: Lower completion for i := 0; i < 10; i++ { db.Create(&models.PlaybackAnalytics{ TrackID: trackID, UserID: user2ID, PlayTime: 90, PauseCount: 2, SeekCount: 1, CompletionRate: 50.0, StartedAt: now, CreatedAt: now, }) } filterA := VariantFilter{TrackID: &trackID, UserIDs: []uuid.UUID{user1ID}} filterB := VariantFilter{TrackID: &trackID, UserIDs: []uuid.UUID{user2ID}} result, err := service.CompareVariants(ctx, "A", "B", filterA, filterB) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, "A", result.VariantA.VariantName) assert.Equal(t, "B", result.VariantB.VariantName) assert.Equal(t, int64(10), result.VariantA.TotalSessions) assert.Equal(t, int64(10), result.VariantB.TotalSessions) assert.Equal(t, 100.0, result.VariantA.AverageCompletion) assert.Equal(t, 50.0, result.VariantB.AverageCompletion) assert.NotNil(t, result.Significance) assert.NotNil(t, result.Difference) assert.NotNil(t, result.PercentageChange) } func TestPlaybackABTestService_CalculateVariantStats(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) analytics := []models.PlaybackAnalytics{ {PlayTime: 180, PauseCount: 0, SeekCount: 0, CompletionRate: 100.0}, {PlayTime: 180, PauseCount: 1, SeekCount: 0, CompletionRate: 95.0}, {PlayTime: 90, PauseCount: 2, SeekCount: 1, CompletionRate: 50.0}, } stats := service.calculateVariantStats("TestVariant", analytics) assert.NotNil(t, stats) assert.Equal(t, "TestVariant", stats.VariantName) assert.Equal(t, int64(3), stats.TotalSessions) assert.InDelta(t, 150.0, stats.AveragePlayTime, 0.1) // (180 + 180 + 90) / 3 assert.InDelta(t, 81.67, stats.AverageCompletion, 0.1) // (100 + 95 + 50) / 3 assert.Equal(t, 1.0, stats.AveragePauses) // (0 + 1 + 2) / 3 assert.InDelta(t, 0.33, stats.AverageSeeks, 0.1) // (0 + 0 + 1) / 3 } func TestPlaybackABTestService_CalculateVariantStats_Empty(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) analytics := []models.PlaybackAnalytics{} stats := service.calculateVariantStats("EmptyVariant", analytics) assert.NotNil(t, stats) assert.Equal(t, "EmptyVariant", stats.VariantName) assert.Equal(t, int64(0), stats.TotalSessions) } func TestPlaybackABTestService_CalculateStatisticalSignificance(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) // Variant A: High completion (tous à 100%) analyticsA := []models.PlaybackAnalytics{ {CompletionRate: 100.0}, {CompletionRate: 100.0}, {CompletionRate: 100.0}, {CompletionRate: 100.0}, {CompletionRate: 100.0}, } // Variant B: Lower completion (tous à 50%) analyticsB := []models.PlaybackAnalytics{ {CompletionRate: 50.0}, {CompletionRate: 50.0}, {CompletionRate: 50.0}, {CompletionRate: 50.0}, {CompletionRate: 50.0}, } significance := service.calculateStatisticalSignificance(analyticsA, analyticsB) assert.NotNil(t, significance) assert.GreaterOrEqual(t, significance.PValue, 0.0) assert.LessOrEqual(t, significance.PValue, 1.0) assert.Greater(t, significance.ConfidenceLevel, 0.0) // EffectSize peut être 0 si les écarts-types sont 0 (toutes les valeurs identiques) // Dans ce cas, on vérifie juste qu'il n'est pas NaN assert.False(t, math.IsNaN(significance.EffectSize)) assert.False(t, math.IsInf(significance.EffectSize, 0)) } func TestPlaybackABTestService_CalculateStatisticalSignificance_Empty(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) analyticsA := []models.PlaybackAnalytics{} analyticsB := []models.PlaybackAnalytics{} significance := service.calculateStatisticalSignificance(analyticsA, analyticsB) assert.NotNil(t, significance) assert.Equal(t, 1.0, significance.PValue) assert.False(t, significance.IsSignificant) } func TestPlaybackABTestService_CalculateMeanAndStdDev(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) data := []float64{10.0, 20.0, 30.0, 40.0, 50.0} mean, stdDev := service.calculateMeanAndStdDev(data) assert.Equal(t, 30.0, mean) assert.Greater(t, stdDev, 0.0) } func TestPlaybackABTestService_CalculateMeanAndStdDev_Empty(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) data := []float64{} mean, stdDev := service.calculateMeanAndStdDev(data) assert.Equal(t, 0.0, mean) assert.Equal(t, 0.0, stdDev) } func TestPlaybackABTestService_DetermineWinner(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) statsA := &VariantStats{CompletionRate: 80.0} statsB := &VariantStats{CompletionRate: 90.0} significance := &StatisticalSignificance{IsSignificant: true, PValue: 0.01} winner := service.determineWinner(statsA, statsB, significance) assert.Equal(t, "B", winner) // Test avec A gagnant statsA2 := &VariantStats{CompletionRate: 90.0} statsB2 := &VariantStats{CompletionRate: 80.0} winner2 := service.determineWinner(statsA2, statsB2, significance) assert.Equal(t, "A", winner2) // Test non significatif significance2 := &StatisticalSignificance{IsSignificant: false, PValue: 0.1} winner3 := service.determineWinner(statsA, statsB, significance2) assert.Equal(t, "inconclusive", winner3) } func TestPlaybackABTestService_GenerateRecommendation(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) statsA := &VariantStats{CompletionRate: 80.0} statsB := &VariantStats{CompletionRate: 90.0} significance := &StatisticalSignificance{IsSignificant: true, PValue: 0.01} recommendation := service.generateRecommendation(statsA, statsB, significance) assert.NotEmpty(t, recommendation) assert.Contains(t, recommendation, "variant B") assert.Contains(t, recommendation, "significativement meilleur") // Test avec A gagnant statsA2 := &VariantStats{CompletionRate: 90.0} statsB2 := &VariantStats{CompletionRate: 80.0} recommendation2 := service.generateRecommendation(statsA2, statsB2, significance) assert.Contains(t, recommendation2, "variant A") // Test non significatif significance2 := &StatisticalSignificance{IsSignificant: false, PValue: 0.1} recommendation3 := service.generateRecommendation(statsA, statsB, significance2) assert.Contains(t, recommendation3, "pas statistiquement significatifs") } func TestPlaybackABTestService_GetAnalyticsForVariant(t *testing.T) { db, service := setupTestPlaybackABTestServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 180, PauseCount: 0, SeekCount: 0, CompletionRate: 100.0, StartedAt: now, CreatedAt: now, } db.Create(analytics) filter := VariantFilter{TrackID: &trackID} result, err := service.getAnalyticsForVariant(ctx, filter) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, 1, len(result)) } func TestPlaybackABTestService_GetAnalyticsForVariant_WithDateFilter(t *testing.T) { db, service := setupTestPlaybackABTestServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) now := time.Now() yesterday := now.AddDate(0, 0, -1) tomorrow := now.AddDate(0, 0, 1) // Analytics créé aujourd'hui analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 180, PauseCount: 0, SeekCount: 0, CompletionRate: 100.0, StartedAt: now, CreatedAt: now, } db.Create(analytics) // Filtrer par date (hier à demain) - devrait inclure l'analytics filter := VariantFilter{ TrackID: &trackID, StartDate: &yesterday, EndDate: &tomorrow, } result, err := service.getAnalyticsForVariant(ctx, filter) require.NoError(t, err) assert.Equal(t, 1, len(result)) // Filtrer par date (avant-hier à hier) - ne devrait pas inclure l'analytics dayBeforeYesterday := now.AddDate(0, 0, -2) filter2 := VariantFilter{ TrackID: &trackID, StartDate: &dayBeforeYesterday, EndDate: &yesterday, } result2, err := service.getAnalyticsForVariant(ctx, filter2) require.NoError(t, err) assert.Equal(t, 0, len(result2)) } func TestPlaybackABTestService_GetAnalyticsForVariant_WithUserFilter(t *testing.T) { db, service := setupTestPlaybackABTestServiceDB(t) ctx := context.Background() // Créer users et track user1ID := uuid.New() user2ID := uuid.New() trackID := uuid.New() user1 := &models.User{ID: user1ID, Username: "user1", Slug: "user1", Email: "user1@example.com", IsActive: true} user2 := &models.User{ID: user2ID, Username: "user2", Slug: "user2", Email: "user2@example.com", IsActive: true} db.Create(user1) db.Create(user2) track := &models.Track{ ID: trackID, UserID: user1ID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) now := time.Now() analytics1 := &models.PlaybackAnalytics{ TrackID: trackID, UserID: user1ID, PlayTime: 180, CompletionRate: 100.0, StartedAt: now, CreatedAt: now, } analytics2 := &models.PlaybackAnalytics{ TrackID: trackID, UserID: user2ID, PlayTime: 90, CompletionRate: 50.0, StartedAt: now, CreatedAt: now, } db.Create(analytics1) db.Create(analytics2) // Filtrer par user 1 seulement filter := VariantFilter{ TrackID: &trackID, UserIDs: []uuid.UUID{user1ID}, } result, err := service.getAnalyticsForVariant(ctx, filter) require.NoError(t, err) assert.Equal(t, 1, len(result)) assert.Equal(t, user1ID, result[0].UserID) } func TestPlaybackABTestService_SafePercentageChange(t *testing.T) { _, service := setupTestPlaybackABTestServiceDB(t) // Test normal result := service.safePercentageChange(100.0, 120.0) assert.Equal(t, 20.0, result) // Test avec base zéro et courant non-zéro result2 := service.safePercentageChange(0.0, 100.0) assert.True(t, math.IsInf(result2, 1)) // Test avec base zéro et courant zéro result3 := service.safePercentageChange(0.0, 0.0) assert.Equal(t, 0.0, result3) // Test négatif result4 := service.safePercentageChange(100.0, 80.0) assert.Equal(t, -20.0, result4) } func TestPlaybackABTestService_CompareVariants_WithDateRange(t *testing.T) { db, service := setupTestPlaybackABTestServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) now := time.Now() weekAgo := now.AddDate(0, 0, -7) threeDaysAgo := now.AddDate(0, 0, -3) // Analytics pour variant A (il y a une semaine) analyticsA := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 180, CompletionRate: 100.0, StartedAt: weekAgo, CreatedAt: weekAgo, } db.Create(analyticsA) // Analytics pour variant B (il y a 3 jours) analyticsB := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 90, CompletionRate: 50.0, StartedAt: threeDaysAgo, CreatedAt: threeDaysAgo, } db.Create(analyticsB) // Filtrer variant A par période (il y a 8 jours à 6 jours) eightDaysAgo := now.AddDate(0, 0, -8) sixDaysAgo := now.AddDate(0, 0, -6) filterA := VariantFilter{ TrackID: &trackID, StartDate: &eightDaysAgo, EndDate: &sixDaysAgo, } // Filtrer variant B par période (il y a 4 jours à 2 jours) fourDaysAgo := now.AddDate(0, 0, -4) twoDaysAgo := now.AddDate(0, 0, -2) filterB := VariantFilter{ TrackID: &trackID, StartDate: &fourDaysAgo, EndDate: &twoDaysAgo, } result, err := service.CompareVariants(ctx, "A", "B", filterA, filterB) require.NoError(t, err) assert.NotNil(t, result) assert.Equal(t, int64(1), result.VariantA.TotalSessions) assert.Equal(t, int64(1), result.VariantB.TotalSessions) }