package workers

import (
	"context"
	"testing"
	"time"

	"github.com/google/uuid"

	"veza-backend-api/internal/models"
	"veza-backend-api/internal/services"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func setupTestDBForWorker(t *testing.T) *gorm.DB {
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)

	// Migrer les modèles nécessaires
	err = db.AutoMigrate(
		&models.Track{},
		&models.PlaybackAnalytics{},
	)
	require.NoError(t, err)

	return db
}

func TestNewPlaybackAnalyticsWorker(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		1000,
		3,
		3,
		100,
		5*time.Second,
	)

	assert.NotNil(t, worker)
	assert.Equal(t, db, worker.db)
	assert.Equal(t, analyticsService, worker.analyticsService)
	assert.Equal(t, 1000, cap(worker.queue))
	assert.Equal(t, 3, worker.processingWorkers)
	assert.Equal(t, 3, worker.maxRetries)
	assert.Equal(t, 100, worker.batchSize)
	assert.Equal(t, 5*time.Second, worker.batchTimeout)
	assert.False(t, worker.IsRunning())
}

func TestNewPlaybackAnalyticsWorker_DefaultValues(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		0, // Utiliser les valeurs par défaut
		0,
		0,
		0,
		0,
	)

	assert.NotNil(t, worker)
	assert.Equal(t, 1000, cap(worker.queue))
	assert.Equal(t, 3, worker.processingWorkers)
	assert.Equal(t, 3, worker.maxRetries)
	assert.Equal(t, 100, worker.batchSize)
	assert.Equal(t, 5*time.Second, worker.batchTimeout)
}

func TestPlaybackAnalyticsWorker_Enqueue(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		10,
		1,
		3,
		5,
		1*time.Second,
	)

	analytics := &models.PlaybackAnalytics{
		TrackID:        uuid.New(),
		UserID:         uuid.New(),
		PlayTime:       180,
		PauseCount:     2,
		SeekCount:      1,
		CompletionRate: 75.0,
		StartedAt:      time.Now(),
	}

	// Enqueue un job
	err := worker.Enqueue(analytics, 1)
	require.NoError(t, err)

	assert.Equal(t, 1, worker.GetQueueSize())

	// Enqueue avec analytics nil devrait échouer
	err = worker.Enqueue(nil, 1)
	assert.Error(t, err)
}

func TestPlaybackAnalyticsWorker_EnqueueBatch(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		100,
		1,
		3,
		10,
		1*time.Second,
	)

	// Créer plusieurs analytics
	analyticsList := make([]*models.PlaybackAnalytics, 5)
	for i := 0; i < 5; i++ {
		analyticsList[i] = &models.PlaybackAnalytics{
			TrackID:        uuid.New(),
			UserID:         uuid.New(),
			PlayTime:       180,
			CompletionRate: 75.0,
			StartedAt:      time.Now(),
		}
	}

	// Enqueue le batch
	err := worker.EnqueueBatch(analyticsList, 1)
	require.NoError(t, err)

	assert.Equal(t, 5, worker.GetQueueSize())

	// Enqueue avec liste vide devrait échouer
	err = worker.EnqueueBatch([]*models.PlaybackAnalytics{}, 1)
	assert.Error(t, err)
}

func TestPlaybackAnalyticsWorker_StartStop(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		10,
		2,
		3,
		5,
		100*time.Millisecond,
	)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Démarrer le worker
	worker.Start(ctx)

	// Attendre un peu pour que les workers démarrent
	time.Sleep(50 * time.Millisecond)

	assert.True(t, worker.IsRunning())

	// Arrêter le worker
	worker.Stop()

	// Attendre un peu pour que les workers s'arrêtent
	time.Sleep(100 * time.Millisecond)

	// Le worker devrait être arrêté
	assert.False(t, worker.IsRunning())
}

func TestPlaybackAnalyticsWorker_ProcessBatch(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	// Créer un track
	trackID := uuid.New()
	track := &models.Track{
		ID:       trackID,
		Title:    "Test Track",
		Duration: 180,
	}
	require.NoError(t, db.Create(track).Error)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		10,
		1,
		3,
		5,
		100*time.Millisecond,
	)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Démarrer le worker
	worker.Start(ctx)
	defer worker.Stop()

	// Créer et enqueue des analytics
	analyticsList := make([]*models.PlaybackAnalytics, 3)
	for i := 0; i < 3; i++ {
		analyticsList[i] = &models.PlaybackAnalytics{
			TrackID:        trackID,
			UserID:         uuid.New(),
			PlayTime:       180,
			CompletionRate: 100.0,
			StartedAt:      time.Now(),
		}
		require.NoError(t, worker.Enqueue(analyticsList[i], 1))
	}

	// Attendre que le batch soit traité
	time.Sleep(500 * time.Millisecond)

	// Vérifier que les analytics ont été enregistrés
	var count int64
	require.NoError(t, db.Model(&models.PlaybackAnalytics{}).Count(&count).Error)
	assert.GreaterOrEqual(t, count, int64(3))
}

func TestPlaybackAnalyticsWorker_CollectBatch(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		10,
		1,
		3,
		5,
		200*time.Millisecond,
	)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Enqueue quelques jobs
	for i := 0; i < 3; i++ {
		analytics := &models.PlaybackAnalytics{
			TrackID:        uuid.New(),
			UserID:         uuid.New(),
			PlayTime:       180,
			CompletionRate: 75.0,
			StartedAt:      time.Now(),
		}
		require.NoError(t, worker.Enqueue(analytics, 1))
	}

	// Collecter un batch
	batch := worker.collectBatch(ctx, 0)

	assert.GreaterOrEqual(t, len(batch.Jobs), 3)
	assert.NotZero(t, batch.CreatedAt)
}

func TestPlaybackAnalyticsWorker_CollectBatch_Timeout(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		10,
		1,
		3,
		10,                   // Batch size plus grand que le nombre de jobs
		100*time.Millisecond, // Timeout court
	)

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	// Enqueue un seul job
	analytics := &models.PlaybackAnalytics{
		TrackID:        uuid.New(),
		UserID:         uuid.New(),
		PlayTime:       180,
		CompletionRate: 75.0,
		StartedAt:      time.Now(),
	}
	require.NoError(t, worker.Enqueue(analytics, 1))

	// Collecter un batch (devrait timeout et retourner le job)
	batch := worker.collectBatch(ctx, 0)

	assert.Equal(t, 1, len(batch.Jobs))
}

func TestPlaybackAnalyticsWorker_GetStats(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		100,
		3,
		5,
		50,
		10*time.Second,
	)

	stats := worker.GetStats()

	assert.False(t, stats.Running)
	assert.Equal(t, 0, stats.QueueSize)
	assert.Equal(t, 3, stats.Workers)
	assert.Equal(t, 5, stats.MaxRetries)
	assert.Equal(t, 50, stats.BatchSize)
	assert.Equal(t, 10*time.Second, stats.BatchTimeout)

	// Enqueue quelques jobs
	for i := 0; i < 5; i++ {
		analytics := &models.PlaybackAnalytics{
			TrackID:        uuid.New(),
			UserID:         uuid.New(),
			PlayTime:       180,
			CompletionRate: 75.0,
			StartedAt:      time.Now(),
		}
		require.NoError(t, worker.Enqueue(analytics, 1))
	}

	stats = worker.GetStats()
	assert.Equal(t, 5, stats.QueueSize)
}

func TestPlaybackAnalyticsWorker_RetryFailedJobs(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		10,
		1,
		3,
		5,
		100*time.Millisecond,
	)

	// Créer des jobs avec retries
	jobs := make([]AnalyticsJob, 3)
	for i := 0; i < 3; i++ {
		jobs[i] = AnalyticsJob{
			ID: uuid.New(),
			Analytics: &models.PlaybackAnalytics{
				TrackID:        uuid.New(),
				UserID:         uuid.New(),
				PlayTime:       180,
				CompletionRate: 75.0,
				StartedAt:      time.Now(),
			},
			Retries: i, // Premier job: 0 retries, deuxième: 1, troisième: 2
		}
	}

	ctx := context.Background()

	// Retry les jobs (le troisième devrait être drop car retries >= maxRetries)
	worker.retryFailedJobs(ctx, jobs, assert.AnError, 0)

	// Vérifier que les jobs ont été re-enqueued (sauf celui qui a dépassé maxRetries)
	// Le troisième job a 2 retries, donc après incrémentation il aura 3, ce qui est >= maxRetries (3)
	// Donc seulement les 2 premiers devraient être re-enqueued
	// time.AfterFunc est asynchrone, donc attendre suffisamment longtemps pour que les jobs soient re-enqueued
	// Le délai est job.Retries * 1 seconde, donc max 2 secondes pour le deuxième job
	time.Sleep(2500 * time.Millisecond)

	// La queue devrait contenir au moins les 2 premiers jobs
	assert.GreaterOrEqual(t, worker.GetQueueSize(), 2)
}

func TestPlaybackAnalyticsWorker_QueueFull(t *testing.T) {
	db := setupTestDBForWorker(t)
	logger := zap.NewNop()
	analyticsService := services.NewPlaybackAnalyticsService(db, logger)

	// Créer un worker avec une queue très petite
	worker := NewPlaybackAnalyticsWorker(
		db,
		analyticsService,
		logger,
		2, // Queue de taille 2
		1,
		3,
		5,
		1*time.Second,
	)

	// Remplir la queue
	for i := 0; i < 2; i++ {
		analytics := &models.PlaybackAnalytics{
			TrackID:        uuid.New(),
			UserID:         uuid.New(),
			PlayTime:       180,
			CompletionRate: 75.0,
			StartedAt:      time.Now(),
		}
		require.NoError(t, worker.Enqueue(analytics, 1))
	}

	// Essayer d'enqueue un autre job (devrait échouer car queue pleine)
	analytics := &models.PlaybackAnalytics{
		TrackID:        uuid.New(),
		UserID:         uuid.New(),
		PlayTime:       180,
		CompletionRate: 75.0,
		StartedAt:      time.Now(),
	}
	err := worker.Enqueue(analytics, 1)
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "queue is full")
}
