//go:build integration
// +build integration

package integration

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"veza-backend-api/internal/core/track"
	"veza-backend-api/internal/models"
	"veza-backend-api/internal/services"
	"veza-backend-api/internal/testutils"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

// TestUploadAsyncPollingStatus teste le flux complet upload async avec polling status
// P1: Test d'intégration pour valider upload async + polling status
// Utilise testcontainers pour PostgreSQL et Redis (environnement reproductible)
func TestUploadAsyncPollingStatus(t *testing.T) {
	ctx := context.Background()
	gin.SetMode(gin.TestMode)

	// Setup PostgreSQL via testcontainers
	dsn, err := testutils.GetTestContainerDB(ctx)
	if err != nil {
		t.Skipf("Skipping test: PostgreSQL testcontainer not available: %v", err)
		return
	}

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	require.NoError(t, err)

	// Les migrations sont déjà appliquées par testcontainers
	// Vérifier que les tables existent
	var count int64
	err = db.Table("users").Count(&count).Error
	if err != nil {
		t.Fatalf("Database not properly initialized: %v", err)
	}

	// Créer utilisateur de test
	userID := uuid.New()
	user := &models.User{
		ID:       userID,
		Email:    "test@example.com",
		Username: "testuser",
		IsActive: true,
	}
	require.NoError(t, db.Create(user).Error)

	// Setup logger
	logger := zaptest.NewLogger(t)

	// Setup services
	uploadDir := t.TempDir()
	trackService := track.NewTrackService(db, logger, uploadDir)

	// UploadValidator nécessite UploadConfig
	uploadConfig := &services.UploadConfig{
		ClamAVEnabled: false, // Désactivé pour test
	}
	uploadValidator, err := services.NewUploadValidator(uploadConfig, logger)
	require.NoError(t, err)

	// TrackHandler nécessite plusieurs services - créer avec signatures correctes
	trackUploadService := services.NewTrackUploadService(db, logger)

	// Setup Redis via testcontainers
	redisClient, err := testutils.GetTestRedisClient(ctx)
	if err != nil {
		t.Skipf("Skipping test: Redis testcontainer not available: %v", err)
		return
	}

	// TrackChunkService nécessite uploadDir, redis, logger
	chunkService := services.NewTrackChunkService(uploadDir, redisClient, logger)

	likeService := services.NewTrackLikeService(db, logger)
	streamService := services.NewStreamService("", logger) // empty string pour test

	trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService)
	trackHandler.SetUploadValidator(uploadValidator)

	// Setup router
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})

	api := router.Group("/api/v1")
	{
		api.POST("/tracks", trackHandler.UploadTrack)
		api.GET("/tracks/:id/status", trackHandler.GetUploadStatus)
	}

	// Étape 1: Créer un fichier de test audio minimal valide (WAV)
	// WAV est plus simple à créer qu'un MP3 valide et http.DetectContentType le détecte comme "audio/wave"
	testFile := filepath.Join(t.TempDir(), "test_audio.wav")
	testFileName := "test_audio.wav"

	// Créer un fichier WAV minimal valide (44 bytes header + quelques samples)
	// RIFF header (12 bytes)
	wavHeader := []byte("RIFF")
	wavHeader = append(wavHeader, []byte{0x24, 0x00, 0x00, 0x00}...) // File size - 8
	wavHeader = append(wavHeader, []byte("WAVE")...)

	// fmt chunk (24 bytes)
	wavHeader = append(wavHeader, []byte("fmt ")...)
	wavHeader = append(wavHeader, []byte{0x10, 0x00, 0x00, 0x00}...) // fmt chunk size
	wavHeader = append(wavHeader, []byte{0x01, 0x00}...)             // Audio format (PCM)
	wavHeader = append(wavHeader, []byte{0x01, 0x00}...)             // Num channels
	wavHeader = append(wavHeader, []byte{0x44, 0xAC, 0x00, 0x00}...) // Sample rate (44100)
	wavHeader = append(wavHeader, []byte{0x88, 0x58, 0x01, 0x00}...) // Byte rate
	wavHeader = append(wavHeader, []byte{0x02, 0x00}...)             // Block align
	wavHeader = append(wavHeader, []byte{0x10, 0x00}...)             // Bits per sample

	// data chunk (8 bytes header + data)
	wavHeader = append(wavHeader, []byte("data")...)
	wavHeader = append(wavHeader, []byte{0x04, 0x00, 0x00, 0x00}...) // Data size
	wavHeader = append(wavHeader, []byte{0x00, 0x00, 0x00, 0x00}...) // Sample data (silence)

	require.NoError(t, os.WriteFile(testFile, wavHeader, 0644))

	// Étape 2: Upload fichier (POST /api/v1/tracks)
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// Ajouter champs
	writer.WriteField("title", "Test Track")
	writer.WriteField("artist", "Test Artist")
	writer.WriteField("file_type", "audio")
	writer.WriteField("duration", "180") // Duration > 0 requis par contrainte DB

	// Ajouter fichier
	fileWriter, err := writer.CreateFormFile("file", testFileName)
	require.NoError(t, err)
	file, err := os.Open(testFile)
	require.NoError(t, err)
	_, err = io.Copy(fileWriter, file)
	require.NoError(t, err)
	file.Close()
	writer.Close()

	req := httptest.NewRequest("POST", "/api/v1/tracks", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	w := httptest.NewRecorder()

	router.ServeHTTP(w, req)

	// Vérifier réponse 202 Accepted
	if w.Code != http.StatusAccepted {
		t.Logf("Unexpected status code: %d", w.Code)
		t.Logf("Response body: %s", w.Body.String())
	}
	assert.Equal(t, http.StatusAccepted, w.Code, "Should return 202 Accepted - Response: %s", w.Body.String())

	var uploadResp map[string]interface{}
	err = json.Unmarshal(w.Body.Bytes(), &uploadResp)
	require.NoError(t, err, "Response should be valid JSON: %s", w.Body.String())

	assert.True(t, uploadResp["success"].(bool), "Response should have success=true")
	data, ok := uploadResp["data"].(map[string]interface{})
	require.True(t, ok, "Response should have data object: %v", uploadResp)

	trackIDInterface, ok := data["track_id"]
	require.True(t, ok, "Data should have track_id: %v", data)
	trackIDStr, ok := trackIDInterface.(string)
	require.True(t, ok, "track_id should be a string: %v (type: %T)", trackIDInterface, trackIDInterface)
	trackID, err := uuid.Parse(trackIDStr)
	require.NoError(t, err)

	// Vérifier Location header
	location := w.Header().Get("Location")
	assert.Contains(t, location, "/api/v1/tracks/")
	assert.Contains(t, location, "/status")

	// Vérifier status initial (dans la réponse 202, c'est directement dans data)
	initialStatusInterface, ok := data["status"]
	require.True(t, ok, "Data should have status: %v", data)
	initialStatus, ok := initialStatusInterface.(string)
	require.True(t, ok, "Status should be a string: %v", initialStatusInterface)
	assert.Contains(t, []string{"uploading", "processing"}, initialStatus, "Initial status should be uploading or processing")

	// Étape 3: Polling status (GET /api/v1/tracks/:id/status)
	// Le fichier est copié en arrière-plan, on doit attendre la transition
	maxAttempts := 30                  // Augmenter pour laisser le temps à la copie async
	interval := 200 * time.Millisecond // 200ms pour laisser le temps à la goroutine
	finalStatus := ""

	for i := 0; i < maxAttempts; i++ {
		time.Sleep(interval)

		req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/tracks/%s/status", trackID.String()), nil)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code, "Status endpoint should return 200")

		var statusResp map[string]interface{}
		err := json.Unmarshal(w.Body.Bytes(), &statusResp)
		require.NoError(t, err)

		assert.True(t, statusResp["success"].(bool))
		statusData, ok := statusResp["data"].(map[string]interface{})
		require.True(t, ok, "Response should have data object: %v", statusResp)

		// GetUploadStatus retourne data.progress avec status, progress, message, etc.
		progressData, ok := statusData["progress"].(map[string]interface{})
		require.True(t, ok, "Data should have progress object: %v", statusData)

		statusInterface, ok := progressData["status"]
		require.True(t, ok, "Progress should have status: %v", progressData)
		finalStatus, ok = statusInterface.(string)
		require.True(t, ok, "Status should be a string: %v (type: %T)", statusInterface, statusInterface)

		t.Logf("Poll attempt %d: status=%s", i+1, finalStatus)

		// Si status est completed ou failed, arrêter
		if finalStatus == "completed" || finalStatus == "failed" {
			break
		}
	}

	// Vérifier status final
	assert.Contains(t, []string{"completed", "processing", "failed"}, finalStatus, "Final status should be completed, processing, or failed")

	// Si completed, vérifier que le fichier existe
	if finalStatus == "completed" {
		// Vérifier en DB que le track est créé
		var track models.Track
		err := db.Where("id = ?", trackID).First(&track).Error
		require.NoError(t, err)
		assert.Equal(t, models.TrackStatusCompleted, track.Status)

		// Vérifier que le fichier existe (si path stocké)
		if track.FilePath != "" {
			_, err := os.Stat(track.FilePath)
			assert.NoError(t, err, "Track file should exist")
		}
	}
}

// TestUploadAsyncPollingStatus_Transitions teste les transitions de status
// P1: Vérifie que les transitions sont cohérentes
func TestUploadAsyncPollingStatus_Transitions(t *testing.T) {
	ctx := context.Background()
	gin.SetMode(gin.TestMode)

	// Setup PostgreSQL via testcontainers
	dsn, err := testutils.GetTestContainerDB(ctx)
	if err != nil {
		t.Skipf("Skipping test: PostgreSQL testcontainer not available: %v", err)
		return
	}

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	require.NoError(t, err)

	// Utiliser un email unique pour éviter les conflits entre tests
	// Username doit respecter chk_users_username_format (pas de tirets, seulement alphanum + underscore)
	userID := uuid.New()
	userIDShort := userID.String()[:8]
	// Remplacer les tirets par des underscores pour respecter la contrainte
	usernameSafe := fmt.Sprintf("test_%s", userIDShort)
	user := &models.User{
		ID:       userID,
		Email:    fmt.Sprintf("test_transitions_%s@example.com", userIDShort),
		Username: usernameSafe,
		IsActive: true,
	}
	require.NoError(t, db.Create(user).Error)

	logger := zaptest.NewLogger(t)
	uploadDir := t.TempDir()
	trackService := track.NewTrackService(db, logger, uploadDir)

	uploadConfig := &services.UploadConfig{ClamAVEnabled: false}
	uploadValidator, err := services.NewUploadValidator(uploadConfig, logger)
	require.NoError(t, err)

	// Setup Redis via testcontainers
	redisClient, err := testutils.GetTestRedisClient(ctx)
	if err != nil {
		t.Skipf("Skipping test: Redis testcontainer not available: %v", err)
		return
	}

	trackUploadService := services.NewTrackUploadService(db, logger)
	chunkService := services.NewTrackChunkService(uploadDir, redisClient, logger)
	likeService := services.NewTrackLikeService(db, logger)
	streamService := services.NewStreamService("", logger)

	trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService)
	trackHandler.SetUploadValidator(uploadValidator)

	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})
	router.POST("/api/v1/tracks", trackHandler.UploadTrack)
	router.GET("/api/v1/tracks/:id/status", trackHandler.GetUploadStatus)

	// Créer fichier de test
	testFile := filepath.Join(t.TempDir(), "test.mp3")
	require.NoError(t, os.WriteFile(testFile, []byte("test content"), 0644))

	// Upload
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	writer.WriteField("title", "Test")
	writer.WriteField("artist", "Test")
	writer.WriteField("file_type", "audio")
	fileWriter, _ := writer.CreateFormFile("file", "test.mp3")
	file, _ := os.Open(testFile)
	io.Copy(fileWriter, file)
	file.Close()
	writer.Close()

	req := httptest.NewRequest("POST", "/api/v1/tracks", body)
	req.Header.Set("Content-Type", writer.FormDataContentType())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	require.Equal(t, http.StatusAccepted, w.Code)
	var resp map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &resp)
	trackID := resp["data"].(map[string]interface{})["track_id"].(string)

	// Polling avec vérification transitions
	seenStatuses := make(map[string]bool)
	maxAttempts := 30 // Augmenter pour laisser le temps aux transitions

	for i := 0; i < maxAttempts; i++ {
		time.Sleep(200 * time.Millisecond)

		req := httptest.NewRequest("GET", fmt.Sprintf("/api/v1/tracks/%s/status", trackID), nil)
		w := httptest.NewRecorder()
		router.ServeHTTP(w, req)

		var statusResp map[string]interface{}
		json.Unmarshal(w.Body.Bytes(), &statusResp)
		status := statusResp["data"].(map[string]interface{})["status"].(string)
		seenStatuses[status] = true

		if status == "completed" || status == "failed" {
			break
		}
	}

	// Vérifier que les transitions sont logiques
	// uploading -> processing -> completed (ou failed)
	t.Logf("Status vus: %v", seenStatuses)
	assert.True(t, len(seenStatuses) > 0, "Au moins un status doit être vu")
}
