//go:build integration || analytics
// +build integration analytics

package analytics

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"veza-backend-api/internal/database"
	"veza-backend-api/internal/handlers"
	"veza-backend-api/internal/models"
	"veza-backend-api/internal/services"
	"veza-backend-api/internal/workers"

	"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/sqlite"
	"gorm.io/gorm"
)

// setupAnalyticsTestRouter crée un router de test avec les services nécessaires pour les tests d'analytics
func setupAnalyticsTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, *database.Database, *services.AnalyticsService, *workers.JobWorker, func()) {
	gin.SetMode(gin.TestMode)
	logger := zaptest.NewLogger(t)

	// Setup in-memory SQLite database
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)
	db.Exec("PRAGMA foreign_keys = ON")

	// Auto-migrate models
	err = db.AutoMigrate(
		&models.User{},
		&models.Track{},
		&models.TrackPlay{},
		&workers.Job{},
	)
	require.NoError(t, err)

	// Get underlying sql.DB from GORM for raw SQL queries
	sqlDB, err := db.DB()
	require.NoError(t, err)

	dbWrapper := &database.Database{
		DB:     sqlDB,
		GormDB: db,
		Logger: logger,
	}

	// Setup services
	analyticsService := services.NewAnalyticsService(db, logger)
	jobService := services.NewJobService(logger)
	jobWorker := workers.NewJobWorker(db, jobService, logger, 100, 1, 3, nil)
	jobService.SetJobEnqueuer(jobWorker) // Connect JobService to JobWorker

	// Setup handlers
	analyticsHandler := handlers.NewAnalyticsHandler(analyticsService, logger)
	analyticsHandler.SetJobWorker(jobWorker)

	// Create router
	router := gin.New()

	// Mock auth middleware - set user_id from header if present
	router.Use(func(c *gin.Context) {
		if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" {
			if userID, err := uuid.Parse(userIDStr); err == nil {
				c.Set("user_id", userID)
			}
		}
		c.Next()
	})

	// Setup analytics routes
	analytics := router.Group("/api/v1/analytics")
	{
		analytics.POST("/events", analyticsHandler.RecordEvent)
		analytics.GET("/tracks/:id", analyticsHandler.GetTrackAnalyticsDashboard)
	}

	cleanup := func() {
		// Database will be closed automatically
	}

	return router, db, dbWrapper, analyticsService, jobWorker, cleanup
}

// createTestUser crée un utilisateur de test
func createTestUser(t *testing.T, db *gorm.DB, email, username string) *models.User {
	user := &models.User{
		ID:           uuid.New(),
		Username:     username,
		Email:        email,
		PasswordHash: "hashed_password",
		Slug:         username,
		IsActive:     true,
		IsVerified:   true,
	}
	err := db.Create(user).Error
	require.NoError(t, err)
	return user
}

// createTestTrack crée un track de test
func createTestTrack(t *testing.T, db *gorm.DB, userID uuid.UUID, title string, duration int) *models.Track {
	track := &models.Track{
		ID:       uuid.New(),
		UserID:   userID,
		Title:    title,
		FilePath: fmt.Sprintf("/tracks/%s.mp3", uuid.New().String()),
		FileSize: 5 * 1024 * 1024,
		Format:   "MP3",
		Duration: duration,
		IsPublic: true,
		Status:   models.TrackStatusCompleted,
	}
	err := db.Create(track).Error
	require.NoError(t, err)
	return track
}

// createTestTrackPlay crée un play de test
func createTestTrackPlay(t *testing.T, db *gorm.DB, trackID uuid.UUID, userID *uuid.UUID, duration int, playedAt time.Time) *models.TrackPlay {
	play := &models.TrackPlay{
		ID:        uuid.New(),
		TrackID:   trackID,
		UserID:    userID,
		Duration:  duration,
		PlayedAt:  playedAt,
		Device:    "Test Device",
		IPAddress: "127.0.0.1",
	}
	err := db.Create(play).Error
	require.NoError(t, err)
	return play
}

// TestAnalytics_RecordEvent teste l'enregistrement d'un événement analytics personnalisé
func TestAnalytics_RecordEvent(t *testing.T) {
	router, db, _, _, _, cleanup := setupAnalyticsTestRouter(t)
	defer cleanup()

	user := createTestUser(t, db, "test@example.com", "testuser")

	t.Run("Record event with authenticated user", func(t *testing.T) {
		payload := map[string]interface{}{
			"action": "button_click",
			"page":   "home",
		}
		body, _ := json.Marshal(handlers.RecordEventRequest{
			EventName: "user_interaction",
			Payload:   payload,
		})

		req := httptest.NewRequest(http.MethodPost, "/api/v1/analytics/events", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)

		var response handlers.APIResponse
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.True(t, response.Success)

		// Verify job was created
		var job workers.Job
		err = db.Where("type = ?", "analytics").First(&job).Error
		assert.NoError(t, err)
		assert.Equal(t, "analytics", job.Type)
		assert.Equal(t, "pending", job.Status)
	})

	t.Run("Record event without authentication", func(t *testing.T) {
		payload := map[string]interface{}{
			"action": "page_view",
		}
		body, _ := json.Marshal(handlers.RecordEventRequest{
			EventName: "page_view",
			Payload:   payload,
		})

		req := httptest.NewRequest(http.MethodPost, "/api/v1/analytics/events", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)

		var response handlers.APIResponse
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.True(t, response.Success)

		// Verify job was created (anonymous event)
		var job workers.Job
		err = db.Where("type = ?", "analytics").Order("created_at DESC").First(&job).Error
		assert.NoError(t, err)
	})

	t.Run("Record event with invalid request", func(t *testing.T) {
		// Missing event_name
		body, _ := json.Marshal(map[string]interface{}{
			"payload": map[string]interface{}{"key": "value"},
		})

		req := httptest.NewRequest(http.MethodPost, "/api/v1/analytics/events", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
	})

	t.Run("Record event with empty event_name", func(t *testing.T) {
		body, _ := json.Marshal(handlers.RecordEventRequest{
			EventName: "",
			Payload:   map[string]interface{}{"key": "value"},
		})

		req := httptest.NewRequest(http.MethodPost, "/api/v1/analytics/events", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
	})

	t.Run("Record event with very long event_name", func(t *testing.T) {
		longName := make([]byte, 101)
		for i := range longName {
			longName[i] = 'a'
		}
		body, _ := json.Marshal(handlers.RecordEventRequest{
			EventName: string(longName),
			Payload:   map[string]interface{}{"key": "value"},
		})

		req := httptest.NewRequest(http.MethodPost, "/api/v1/analytics/events", bytes.NewBuffer(body))
		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
	})
}

// TestAnalytics_GetTrackAnalyticsDashboard teste la récupération du dashboard d'analytics pour un track
func TestAnalytics_GetTrackAnalyticsDashboard(t *testing.T) {
	router, db, _, _, _, cleanup := setupAnalyticsTestRouter(t)
	defer cleanup()

	user := createTestUser(t, db, "test@example.com", "testuser")
	track := createTestTrack(t, db, user.ID, "Test Track", 180)

	t.Run("Get dashboard for track with plays", func(t *testing.T) {
		// Create multiple plays
		now := time.Now()
		userID := user.ID
		createTestTrackPlay(t, db, track.ID, &userID, 120, now.Add(-2*24*time.Hour))
		createTestTrackPlay(t, db, track.ID, &userID, 150, now.Add(-1*24*time.Hour))
		createTestTrackPlay(t, db, track.ID, nil, 100, now)
		createTestTrackPlay(t, db, track.ID, nil, 180, now) // Completed play (90%+)

		req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/analytics/tracks/%s", track.ID.String()), nil)
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)

		var response handlers.APIResponse
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.True(t, response.Success)

		// Verify dashboard structure
		dataBytes, _ := json.Marshal(response.Data)
		var data map[string]interface{}
		err = json.Unmarshal(dataBytes, &data)
		require.NoError(t, err)

		dashboard, ok := data["dashboard"].(map[string]interface{})
		require.True(t, ok)

		// Verify track_id
		assert.Equal(t, track.ID.String(), dashboard["track_id"])

		// Verify stats
		stats, ok := dashboard["stats"].(map[string]interface{})
		require.True(t, ok)
		assert.Greater(t, stats["total_plays"], float64(0))
		assert.GreaterOrEqual(t, stats["unique_listeners"], float64(0))
		assert.GreaterOrEqual(t, stats["average_duration"], float64(0))
		assert.GreaterOrEqual(t, stats["completion_rate"], float64(0))

		// Verify plays_over_time
		playsOverTime, ok := dashboard["plays_over_time"].([]interface{})
		require.True(t, ok)
		assert.GreaterOrEqual(t, len(playsOverTime), 0)

		// Verify period
		period, ok := dashboard["period"].(map[string]interface{})
		require.True(t, ok)
		assert.NotEmpty(t, period["start_date"])
		assert.NotEmpty(t, period["end_date"])
		assert.Equal(t, float64(30), period["days"])
	})

	t.Run("Get dashboard for track without plays", func(t *testing.T) {
		newTrack := createTestTrack(t, db, user.ID, "New Track", 180)

		req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/analytics/tracks/%s", newTrack.ID.String()), nil)
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusOK, w.Code)

		var response handlers.APIResponse
		err := json.Unmarshal(w.Body.Bytes(), &response)
		require.NoError(t, err)
		assert.True(t, response.Success)

		// Verify dashboard structure
		dataBytes, _ := json.Marshal(response.Data)
		var data map[string]interface{}
		err = json.Unmarshal(dataBytes, &data)
		require.NoError(t, err)

		dashboard, ok := data["dashboard"].(map[string]interface{})
		require.True(t, ok)

		// Verify stats are zero
		stats, ok := dashboard["stats"].(map[string]interface{})
		require.True(t, ok)
		assert.Equal(t, float64(0), stats["total_plays"])
		assert.Equal(t, float64(0), stats["unique_listeners"])
		assert.Equal(t, float64(0), stats["average_duration"])
		assert.Equal(t, float64(0), stats["completion_rate"])
	})

	t.Run("Get dashboard for non-existent track", func(t *testing.T) {
		nonExistentID := uuid.New()

		req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/analytics/tracks/%s", nonExistentID.String()), nil)
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusNotFound, w.Code)
	})

	t.Run("Get dashboard with invalid track ID", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/api/v1/analytics/tracks/invalid-uuid", nil)
		req.Header.Set("X-User-ID", user.ID.String())
		w := httptest.NewRecorder()

		router.ServeHTTP(w, req)

		assert.Equal(t, http.StatusBadRequest, w.Code)
	})
}

// TestAnalytics_RecordEvent_JobWorkerMissing teste le cas où le JobWorker n'est pas disponible
func TestAnalytics_RecordEvent_JobWorkerMissing(t *testing.T) {
	gin.SetMode(gin.TestMode)
	logger := zaptest.NewLogger(t)

	// Setup in-memory SQLite database
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)
	db.Exec("PRAGMA foreign_keys = ON")

	// Auto-migrate models
	err = db.AutoMigrate(
		&models.User{},
		&models.Track{},
	)
	require.NoError(t, err)

	// Setup services
	analyticsService := services.NewAnalyticsService(db, logger)

	// Setup handler WITHOUT JobWorker
	analyticsHandler := handlers.NewAnalyticsHandler(analyticsService, logger)
	// Don't set JobWorker

	// Create router
	router := gin.New()
	router.Use(func(c *gin.Context) {
		if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" {
			if userID, err := uuid.Parse(userIDStr); err == nil {
				c.Set("user_id", userID)
			}
		}
		c.Next()
	})

	analytics := router.Group("/api/v1/analytics")
	{
		analytics.POST("/events", analyticsHandler.RecordEvent)
	}

	user := createTestUser(t, db, "test@example.com", "testuser")

	payload := map[string]interface{}{
		"action": "button_click",
	}
	body, _ := json.Marshal(handlers.RecordEventRequest{
		EventName: "user_interaction",
		Payload:   payload,
	})

	req := httptest.NewRequest(http.MethodPost, "/api/v1/analytics/events", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-User-ID", user.ID.String())
	w := httptest.NewRecorder()

	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusInternalServerError, w.Code)

	var response handlers.APIResponse
	err = json.Unmarshal(w.Body.Bytes(), &response)
	require.NoError(t, err)
	assert.False(t, response.Success)
}

// TestAnalytics_GetTrackAnalyticsDashboard_PlaysOverTime teste que les données temporelles sont correctement formatées
func TestAnalytics_GetTrackAnalyticsDashboard_PlaysOverTime(t *testing.T) {
	router, db, _, _, _, cleanup := setupAnalyticsTestRouter(t)
	defer cleanup()

	user := createTestUser(t, db, "test@example.com", "testuser")
	track := createTestTrack(t, db, user.ID, "Test Track", 180)

	// Create plays at different times
	now := time.Now()
	userID := user.ID
	createTestTrackPlay(t, db, track.ID, &userID, 120, now.Add(-5*24*time.Hour))
	createTestTrackPlay(t, db, track.ID, &userID, 150, now.Add(-3*24*time.Hour))
	createTestTrackPlay(t, db, track.ID, nil, 100, now.Add(-1*24*time.Hour))
	createTestTrackPlay(t, db, track.ID, nil, 180, now)

	req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/analytics/tracks/%s", track.ID.String()), nil)
	req.Header.Set("X-User-ID", user.ID.String())
	w := httptest.NewRecorder()

	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusOK, w.Code)

	var response handlers.APIResponse
	err := json.Unmarshal(w.Body.Bytes(), &response)
	require.NoError(t, err)
	assert.True(t, response.Success)

	// Verify plays_over_time structure
	dataBytes, _ := json.Marshal(response.Data)
	var data map[string]interface{}
	err = json.Unmarshal(dataBytes, &data)
	require.NoError(t, err)

	dashboard, _ := data["dashboard"].(map[string]interface{})
	playsOverTime, ok := dashboard["plays_over_time"].([]interface{})
	require.True(t, ok)

	// Verify that plays_over_time contains valid data points
	if len(playsOverTime) > 0 {
		point, ok := playsOverTime[0].(map[string]interface{})
		require.True(t, ok)
		assert.Contains(t, point, "date")
		assert.Contains(t, point, "count")
	}
}
