package handlers

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

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"go.uber.org/zap/zaptest"

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

// MockPlaybackAnalyticsServiceForHandler is a mock implementation of PlaybackAnalyticsService
type MockPlaybackAnalyticsServiceForHandler struct {
	mock.Mock
}

func (m *MockPlaybackAnalyticsServiceForHandler) RecordPlayback(ctx context.Context, analytics *models.PlaybackAnalytics) error {
	args := m.Called(ctx, analytics)
	return args.Error(0)
}

func (m *MockPlaybackAnalyticsServiceForHandler) GetTrackStats(ctx context.Context, trackID uuid.UUID) (*services.PlaybackStats, error) {
	args := m.Called(ctx, trackID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*services.PlaybackStats), args.Error(1)
}

func (m *MockPlaybackAnalyticsServiceForHandler) GetSessionsByDateRange(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time) ([]models.PlaybackAnalytics, error) {
	args := m.Called(ctx, trackID, startDate, endDate)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]models.PlaybackAnalytics), args.Error(1)
}

// MockPlaybackAnalyticsRateLimiter is a mock implementation of PlaybackAnalyticsRateLimiter
type MockPlaybackAnalyticsRateLimiter struct {
	mock.Mock
}

func (m *MockPlaybackAnalyticsRateLimiter) CheckRateLimit(ctx context.Context, userID uuid.UUID) (*services.RateLimitResult, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*services.RateLimitResult), args.Error(1)
}

func (m *MockPlaybackAnalyticsRateLimiter) RecordRequest(ctx context.Context, userID uuid.UUID) error {
	args := m.Called(ctx, userID)
	return args.Error(0)
}

func (m *MockPlaybackAnalyticsRateLimiter) GetQuotaInfo(ctx context.Context, userID uuid.UUID) (map[string]interface{}, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(map[string]interface{}), args.Error(1)
}

// MockPlaybackHeatmapService is a mock implementation of PlaybackHeatmapService
type MockPlaybackHeatmapService struct {
	mock.Mock
}

func (m *MockPlaybackHeatmapService) GenerateHeatmap(ctx context.Context, trackID uuid.UUID, segmentSize int) (*services.HeatmapData, error) {
	args := m.Called(ctx, trackID, segmentSize)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*services.HeatmapData), args.Error(1)
}

func setupTestPlaybackAnalyticsHandler(t *testing.T) (*PlaybackAnalyticsHandler, *MockPlaybackAnalyticsServiceForHandler, *MockPlaybackAnalyticsRateLimiter, *MockPlaybackHeatmapService) {
	logger := zaptest.NewLogger(t)
	mockAnalyticsService := new(MockPlaybackAnalyticsServiceForHandler)
	mockRateLimiter := new(MockPlaybackAnalyticsRateLimiter)
	mockHeatmapService := new(MockPlaybackHeatmapService)

	handler := &PlaybackAnalyticsHandler{
		analyticsService: mockAnalyticsService,
		heatmapService:   mockHeatmapService,
		rateLimiter:      mockRateLimiter,
		commonHandler:    NewCommonHandler(logger),
	}

	return handler, mockAnalyticsService, mockRateLimiter, mockHeatmapService
}

func TestNewPlaybackAnalyticsHandler(t *testing.T) {
	logger := zaptest.NewLogger(t)
	mockService := new(MockPlaybackAnalyticsServiceForHandler)
	handler := NewPlaybackAnalyticsHandlerWithInterface(mockService, logger)

	assert.NotNil(t, handler)
	assert.Equal(t, mockService, handler.analyticsService)
	assert.Nil(t, handler.heatmapService)
	assert.Nil(t, handler.rateLimiter)
}

func TestPlaybackAnalyticsHandler_RecordAnalytics_Success(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, mockService, mockRateLimiter, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	userID := uuid.New()
	startedAt := time.Now().Add(-time.Minute)

	reqBody := RecordAnalyticsRequest{
		PlayTime:   100,
		PauseCount: 2,
		SeekCount:  1,
		StartedAt:  startedAt,
	}

	// Rate limiter allows the request
	mockRateLimiter.On("CheckRateLimit", mock.Anything, userID).Return(&services.RateLimitResult{
		Allowed:   true,
		Remaining: 59,
	}, nil)
	mockRateLimiter.On("RecordRequest", mock.Anything, userID).Return(nil)

	// Analytics service records successfully
	mockService.On("RecordPlayback", mock.Anything, mock.AnythingOfType("*models.PlaybackAnalytics")).Return(nil)

	router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics)

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})

	router.ServeHTTP(w, req)

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

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

	mockService.AssertExpectations(t)
	mockRateLimiter.AssertExpectations(t)
}

func TestPlaybackAnalyticsHandler_RecordAnalytics_Unauthorized(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()

	router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics)

	req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_RecordAnalytics_InvalidTrackID(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t)

	userID := uuid.New()

	router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics)

	req, _ := http.NewRequest("POST", "/tracks/invalid-id/playback/analytics", nil)
	w := httptest.NewRecorder()

	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})

	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_RecordAnalytics_RateLimitExceeded(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, mockRateLimiter, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	userID := uuid.New()
	startedAt := time.Now().Add(-time.Minute)

	reqBody := RecordAnalyticsRequest{
		PlayTime:   100,
		PauseCount: 2,
		SeekCount:  1,
		StartedAt:  startedAt,
	}

	// Rate limiter rejects the request
	mockRateLimiter.On("CheckRateLimit", mock.Anything, userID).Return(&services.RateLimitResult{
		Allowed:    false,
		Reason:     "quota_exceeded",
		RetryAfter: 60 * time.Second,
		QuotaUsed:  10000,
		QuotaLimit: 10000,
	}, nil)

	router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics)

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})

	router.ServeHTTP(w, req)

	assert.Equal(t, http.StatusTooManyRequests, w.Code)
	mockRateLimiter.AssertExpectations(t)
}

func TestPlaybackAnalyticsHandler_RecordAnalytics_InvalidRequest(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	userID := uuid.New()

	// Invalid request: negative play_time
	reqBody := map[string]interface{}{
		"play_time":  -1,
		"started_at": time.Now().Format(time.RFC3339),
	}

	router.POST("/tracks/:id/playback/analytics", handler.RecordAnalytics)

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/tracks/"+trackID.String()+"/playback/analytics", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()

	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})

	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetQuotaInfo_Success(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, mockRateLimiter, _ := setupTestPlaybackAnalyticsHandler(t)

	userID := uuid.New()
	quotaInfo := map[string]interface{}{
		"quota_used":  5000,
		"quota_limit": 10000,
		"remaining":   5000,
	}

	mockRateLimiter.On("GetQuotaInfo", mock.Anything, userID).Return(quotaInfo, nil)

	router.GET("/playback/analytics/quota", handler.GetQuotaInfo)

	req, _ := http.NewRequest("GET", "/playback/analytics/quota", nil)
	w := httptest.NewRecorder()

	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})

	router.ServeHTTP(w, req)

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

	var apiResponse APIResponse
	err := json.Unmarshal(w.Body.Bytes(), &apiResponse)
	assert.NoError(t, err)
	assert.True(t, apiResponse.Success)
	mockRateLimiter.AssertExpectations(t)
}

func TestPlaybackAnalyticsHandler_GetQuotaInfo_RateLimiterNotEnabled(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	logger := zaptest.NewLogger(t)
	mockService := new(MockPlaybackAnalyticsServiceForHandler)
	handler := &PlaybackAnalyticsHandler{
		analyticsService: mockService,
		heatmapService:   nil,
		rateLimiter:      nil, // Rate limiter not enabled
		commonHandler:    NewCommonHandler(logger),
	}

	userID := uuid.New()

	router.GET("/playback/analytics/quota", handler.GetQuotaInfo)

	req, _ := http.NewRequest("GET", "/playback/analytics/quota", nil)
	w := httptest.NewRecorder()

	router.Use(func(c *gin.Context) {
		c.Set("user_id", userID)
		c.Next()
	})

	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetDashboard_Success(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	stats := &services.PlaybackStats{
		TotalSessions:     100,
		TotalPlayTime:     5000,
		AveragePlayTime:   50.0,
		AverageCompletion: 75.0,
		CompletionRate:    80.0,
	}

	mockService.On("GetTrackStats", mock.Anything, trackID).Return(stats, nil)
	mockService.On("GetSessionsByDateRange", mock.Anything, trackID, mock.Anything, mock.Anything).Return([]models.PlaybackAnalytics{}, nil).Times(4)

	router.GET("/tracks/:id/playback/dashboard", handler.GetDashboard)

	req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/dashboard", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

	var apiResponse APIResponse
	err := json.Unmarshal(w.Body.Bytes(), &apiResponse)
	assert.NoError(t, err)
	assert.True(t, apiResponse.Success)
	mockService.AssertExpectations(t)
}

func TestPlaybackAnalyticsHandler_GetDashboard_InvalidTrackID(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t)

	router.GET("/tracks/:id/playback/dashboard", handler.GetDashboard)

	req, _ := http.NewRequest("GET", "/tracks/invalid-id/playback/dashboard", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetDashboard_TrackNotFound(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	mockService.On("GetTrackStats", mock.Anything, trackID).Return(nil, errors.New("track not found"))

	router.GET("/tracks/:id/playback/dashboard", handler.GetDashboard)

	req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/dashboard", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetSummary_Success(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	stats := &services.PlaybackStats{
		TotalSessions:   100,
		AveragePlayTime: 50.0,
		CompletionRate:  80.0,
	}

	mockService.On("GetTrackStats", mock.Anything, trackID).Return(stats, nil)

	router.GET("/tracks/:id/playback/summary", handler.GetSummary)

	req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/summary", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

	var apiResponse APIResponse
	err := json.Unmarshal(w.Body.Bytes(), &apiResponse)
	assert.NoError(t, err)
	assert.True(t, apiResponse.Success)
	mockService.AssertExpectations(t)
}

func TestPlaybackAnalyticsHandler_GetSummary_InvalidTrackID(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t)

	router.GET("/tracks/:id/playback/summary", handler.GetSummary)

	req, _ := http.NewRequest("GET", "/tracks/invalid-id/playback/summary", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetSummary_TrackNotFound(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, mockService, _, _ := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	mockService.On("GetTrackStats", mock.Anything, trackID).Return(nil, errors.New("track not found"))

	router.GET("/tracks/:id/playback/summary", handler.GetSummary)

	req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/summary", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetHeatmap_Success(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, mockHeatmapService := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	heatmapData := &services.HeatmapData{
		TrackID:       trackID,
		TrackDuration: 300,
		SegmentSize:   5,
		TotalSessions: 100,
		Segments:      []services.HeatmapSegment{},
		MaxIntensity:  1.0,
		GeneratedAt:   time.Now(),
	}

	mockHeatmapService.On("GenerateHeatmap", mock.Anything, trackID, 5).Return(heatmapData, nil)

	router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap)

	req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/heatmap?segment_size=5", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

	var apiResponse APIResponse
	err := json.Unmarshal(w.Body.Bytes(), &apiResponse)
	assert.NoError(t, err)
	assert.True(t, apiResponse.Success)
	mockHeatmapService.AssertExpectations(t)
}

func TestPlaybackAnalyticsHandler_GetHeatmap_HeatmapServiceNotAvailable(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	logger := zaptest.NewLogger(t)
	mockService := new(MockPlaybackAnalyticsServiceForHandler)
	handler := &PlaybackAnalyticsHandler{
		analyticsService: mockService,
		heatmapService:   nil, // Heatmap service not available
		rateLimiter:      nil,
		commonHandler:    NewCommonHandler(logger),
	}

	trackID := uuid.New()

	router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap)

	req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/heatmap", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetHeatmap_InvalidTrackID(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, _ := setupTestPlaybackAnalyticsHandler(t)

	router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap)

	req, _ := http.NewRequest("GET", "/tracks/invalid-id/playback/heatmap", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaybackAnalyticsHandler_GetHeatmap_TrackNotFound(t *testing.T) {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	handler, _, _, mockHeatmapService := setupTestPlaybackAnalyticsHandler(t)

	trackID := uuid.New()
	mockHeatmapService.On("GenerateHeatmap", mock.Anything, trackID, 5).Return(nil, errors.New("track not found"))

	router.GET("/tracks/:id/playback/heatmap", handler.GetHeatmap)

	req, _ := http.NewRequest("GET", "/tracks/"+trackID.String()+"/playback/heatmap", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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