package handlers

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

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

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

// MockAnalyticsService mocks AnalyticsService
type MockAnalyticsService struct {
	mock.Mock
}

func (m *MockAnalyticsService) RecordPlay(ctx context.Context, trackID uuid.UUID, userID *uuid.UUID, duration int, device, ipAddress string) error {
	args := m.Called(ctx, trackID, userID, duration, device, ipAddress)
	return args.Error(0)
}

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

func (m *MockAnalyticsService) GetTopTracks(ctx context.Context, limit int, startDate, endDate *time.Time) ([]services.TopTrack, error) {
	args := m.Called(ctx, limit, startDate, endDate)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]services.TopTrack), args.Error(1)
}

func (m *MockAnalyticsService) GetPlaysOverTime(ctx context.Context, trackID uuid.UUID, startDate, endDate time.Time, interval string) ([]services.PlayTimePoint, error) {
	args := m.Called(ctx, trackID, startDate, endDate, interval)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]services.PlayTimePoint), args.Error(1)
}

func (m *MockAnalyticsService) GetUserStats(ctx context.Context, userID uuid.UUID) (*types.UserStats, error) {
	args := m.Called(ctx, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*types.UserStats), args.Error(1)
}

// MockAnalyticsJobWorker mocks JobWorker for analytics
type MockAnalyticsJobWorker struct {
	mock.Mock
}

func (m *MockAnalyticsJobWorker) EnqueueAnalyticsJob(eventName string, userID *uuid.UUID, payload map[string]interface{}) {
	m.Called(eventName, userID, payload)
}

func setupTestAnalyticsRouter(mockService *MockAnalyticsService, mockJobWorker *MockAnalyticsJobWorker) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	logger := zap.NewNop()
	handler := NewAnalyticsHandlerWithInterface(mockService, logger)
	if mockJobWorker != nil {
		handler.SetJobWorker(mockJobWorker)
	}

	api := router.Group("/api/v1/analytics")
	api.Use(func(c *gin.Context) {
		userIDStr := c.GetHeader("X-User-ID")
		if userIDStr != "" {
			uid, err := uuid.Parse(userIDStr)
			if err == nil {
				c.Set("user_id", uid)
			}
		}
		c.Next()
	})
	{
		api.POST("/tracks/:id/play", handler.RecordPlay)
		api.GET("/tracks/:id/stats", handler.GetTrackStats)
		api.GET("/tracks/top", handler.GetTopTracks)
		api.GET("/tracks/:id/plays-over-time", handler.GetPlaysOverTime)
		api.GET("/users/:id/stats", handler.GetUserStats)
		api.GET("/tracks/:id", handler.GetTrackAnalyticsDashboard)
		api.POST("/events", handler.RecordEvent)
	}

	return router
}

func TestAnalyticsHandler_RecordPlay_Success(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	trackID := uuid.New()
	userID := uuid.New()
	reqBody := RecordPlayRequest{
		Duration: 120,
		Device:   "iPhone",
	}

	mockService.On("RecordPlay", mock.Anything, trackID, &userID, 120, "iPhone", mock.Anything).Return(nil)

	body, _ := json.Marshal(reqBody)

	// Execute
	req, _ := http.NewRequest("POST", "/api/v1/analytics/tracks/"+trackID.String()+"/play", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-User-ID", userID.String())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	mockService.AssertExpectations(t)
}

func TestAnalyticsHandler_RecordPlay_InvalidTrackID(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	reqBody := RecordPlayRequest{
		Duration: 120,
	}

	body, _ := json.Marshal(reqBody)

	// Execute - Invalid UUID
	req, _ := http.NewRequest("POST", "/api/v1/analytics/tracks/invalid-id/play", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusBadRequest, w.Code)
	mockService.AssertNotCalled(t, "RecordPlay")
}

func TestAnalyticsHandler_RecordPlay_Anonymous(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	trackID := uuid.New()
	reqBody := RecordPlayRequest{
		Duration: 120,
	}

	mockService.On("RecordPlay", mock.Anything, trackID, (*uuid.UUID)(nil), 120, mock.Anything, mock.Anything).Return(nil)

	body, _ := json.Marshal(reqBody)

	// Execute - No X-User-ID header (anonymous)
	req, _ := http.NewRequest("POST", "/api/v1/analytics/tracks/"+trackID.String()+"/play", bytes.NewBuffer(body))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	mockService.AssertExpectations(t)
}

func TestAnalyticsHandler_GetTrackStats_Success(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	trackID := uuid.New()
	expectedStats := &types.TrackStats{
		TotalPlays:      100,
		UniqueListeners: 50,
		AverageDuration: 120,
		CompletionRate:  0.8,
	}

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

	// Execute
	req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/stats", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	var response map[string]interface{}
	err := json.Unmarshal(w.Body.Bytes(), &response)
	assert.NoError(t, err)
	assert.True(t, response["success"].(bool))
	mockService.AssertExpectations(t)
}

func TestAnalyticsHandler_GetTrackStats_TrackNotFound(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	trackID := uuid.New()

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

	// Execute
	req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/stats", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusInternalServerError, w.Code)
	mockService.AssertExpectations(t)
}

func TestAnalyticsHandler_GetTopTracks_Success(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	expectedTracks := []services.TopTrack{
		{TrackID: uuid.New(), TotalPlays: 100},
		{TrackID: uuid.New(), TotalPlays: 50},
	}

	mockService.On("GetTopTracks", mock.Anything, 10, (*time.Time)(nil), (*time.Time)(nil)).Return(expectedTracks, nil)

	// Execute
	req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/top?limit=10", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	mockService.AssertExpectations(t)
}

func TestAnalyticsHandler_GetTopTracks_InvalidLimit(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	// Execute - Limit too high
	req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/top?limit=200", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusBadRequest, w.Code)
	mockService.AssertNotCalled(t, "GetTopTracks")
}

func TestAnalyticsHandler_GetPlaysOverTime_Success(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	trackID := uuid.New()
	expectedPoints := []services.PlayTimePoint{
		{Date: time.Now(), Count: 10},
	}

	mockService.On("GetPlaysOverTime", mock.Anything, trackID, mock.Anything, mock.Anything, "day").Return(expectedPoints, nil)

	// Execute
	req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/plays-over-time?interval=day", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	mockService.AssertExpectations(t)
}

func TestAnalyticsHandler_GetPlaysOverTime_InvalidInterval(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	trackID := uuid.New()

	// Execute - Invalid interval
	req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String()+"/plays-over-time?interval=invalid", nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusBadRequest, w.Code)
	mockService.AssertNotCalled(t, "GetPlaysOverTime")
}

func TestAnalyticsHandler_GetUserStats_Success(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	userID := uuid.New()
	expectedStats := &types.UserStats{
		TotalPlays:    50,
		UniqueTracks:  10,
		TotalDuration: 3600,
	}

	mockService.On("GetUserStats", mock.Anything, userID).Return(expectedStats, nil)

	// Execute
	req, _ := http.NewRequest("GET", "/api/v1/analytics/users/"+userID.String()+"/stats", nil)
	req.Header.Set("X-User-ID", userID.String())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	mockService.AssertExpectations(t)
}

func TestAnalyticsHandler_GetUserStats_Forbidden(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

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

	// Execute - Trying to access another user's stats
	req, _ := http.NewRequest("GET", "/api/v1/analytics/users/"+userID.String()+"/stats", nil)
	req.Header.Set("X-User-ID", otherUserID.String())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusForbidden, w.Code)
	mockService.AssertNotCalled(t, "GetUserStats")
}

func TestAnalyticsHandler_RecordEvent_Success(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	userID := uuid.New()
	reqBody := RecordEventRequest{
		EventName: "track_liked",
		Payload:   map[string]interface{}{"track_id": uuid.New().String()},
	}

	mockJobWorker.On("EnqueueAnalyticsJob", "track_liked", &userID, reqBody.Payload).Return()

	body, _ := json.Marshal(reqBody)

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

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	mockJobWorker.AssertExpectations(t)
}

func TestAnalyticsHandler_RecordEvent_NoJobWorker(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	router := setupTestAnalyticsRouter(mockService, nil) // No job worker

	userID := uuid.New()
	reqBody := RecordEventRequest{
		EventName: "track_liked",
		Payload:   map[string]interface{}{"track_id": uuid.New().String()},
	}

	body, _ := json.Marshal(reqBody)

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

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

func TestAnalyticsHandler_GetTrackAnalyticsDashboard_Success(t *testing.T) {
	// Setup
	mockService := new(MockAnalyticsService)
	mockJobWorker := new(MockAnalyticsJobWorker)
	router := setupTestAnalyticsRouter(mockService, mockJobWorker)

	trackID := uuid.New()
	expectedStats := &types.TrackStats{
		TotalPlays:      100,
		UniqueListeners: 50,
		AverageDuration: 120,
		CompletionRate:  0.8,
	}
	expectedPoints := []services.PlayTimePoint{
		{Date: time.Now(), Count: 10},
	}

	mockService.On("GetTrackStats", mock.Anything, trackID).Return(expectedStats, nil)
	mockService.On("GetPlaysOverTime", mock.Anything, trackID, mock.Anything, mock.Anything, "day").Return(expectedPoints, nil)

	// Execute
	req, _ := http.NewRequest("GET", "/api/v1/analytics/tracks/"+trackID.String(), nil)
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

	// Assert
	assert.Equal(t, http.StatusOK, w.Code)
	mockService.AssertExpectations(t)
}
