package handlers

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

	"veza-backend-api/internal/models"

	"veza-backend-api/internal/services" // Needed for search params

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

// MockPlaylistService is a mock implementation of PlaylistServiceInterface
type MockPlaylistService struct {
	mock.Mock
}

func (m *MockPlaylistService) CreatePlaylist(ctx context.Context, userID uuid.UUID, title, description string, isPublic bool) (*models.Playlist, error) {
	args := m.Called(ctx, userID, title, description, isPublic)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Playlist), args.Error(1)
}

func (m *MockPlaylistService) GetPlaylists(ctx context.Context, currentUserID *uuid.UUID, filterUserID *uuid.UUID, page, limit int) ([]*models.Playlist, int64, error) {
	args := m.Called(ctx, currentUserID, filterUserID, page, limit)
	if args.Get(0) == nil {
		return nil, 0, args.Error(2)
	}
	return args.Get(0).([]*models.Playlist), args.Get(1).(int64), args.Error(2)
}

func (m *MockPlaylistService) GetPlaylist(ctx context.Context, id uuid.UUID, currentUserID *uuid.UUID) (*models.Playlist, error) {
	args := m.Called(ctx, id, currentUserID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Playlist), args.Error(1)
}

func (m *MockPlaylistService) UpdatePlaylist(ctx context.Context, id uuid.UUID, userID uuid.UUID, title, description *string, isPublic *bool) (*models.Playlist, error) {
	args := m.Called(ctx, id, userID, title, description, isPublic)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.Playlist), args.Error(1)
}

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

func (m *MockPlaylistService) AddTrack(ctx context.Context, playlistID, trackID, userID uuid.UUID) error {
	args := m.Called(ctx, playlistID, trackID, userID)
	return args.Error(0)
}

func (m *MockPlaylistService) RemoveTrack(ctx context.Context, playlistID, trackID, userID uuid.UUID) error {
	args := m.Called(ctx, playlistID, trackID, userID)
	return args.Error(0)
}

func (m *MockPlaylistService) ReorderTracks(ctx context.Context, playlistID, userID uuid.UUID, trackIDs []uuid.UUID) error {
	args := m.Called(ctx, playlistID, userID, trackIDs)
	return args.Error(0)
}

func (m *MockPlaylistService) AddCollaborator(ctx context.Context, playlistID, userID, collaboratorUserID uuid.UUID, permission models.PlaylistPermission) (*models.PlaylistCollaborator, error) {
	args := m.Called(ctx, playlistID, userID, collaboratorUserID, permission)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.PlaylistCollaborator), args.Error(1)
}

func (m *MockPlaylistService) RemoveCollaborator(ctx context.Context, playlistID, userID, collaboratorUserID uuid.UUID) error {
	args := m.Called(ctx, playlistID, userID, collaboratorUserID)
	return args.Error(0)
}

func (m *MockPlaylistService) UpdateCollaboratorPermission(ctx context.Context, playlistID, userID, collaboratorUserID uuid.UUID, permission models.PlaylistPermission) error {
	args := m.Called(ctx, playlistID, userID, collaboratorUserID, permission)
	return args.Error(0)
}

func (m *MockPlaylistService) GetCollaborators(ctx context.Context, playlistID, userID uuid.UUID) ([]*models.PlaylistCollaborator, error) {
	args := m.Called(ctx, playlistID, userID)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).([]*models.PlaylistCollaborator), args.Error(1)
}

func (m *MockPlaylistService) CreateShareLink(ctx context.Context, playlistID, userID uuid.UUID, expiresAt *time.Time) (*models.PlaylistShareLink, error) {
	args := m.Called(ctx, playlistID, userID, expiresAt)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.PlaylistShareLink), args.Error(1)
}

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

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

func (m *MockPlaylistService) CheckPermission(ctx context.Context, playlistID, userID uuid.UUID, permission models.PlaylistPermission) (bool, error) {
	args := m.Called(ctx, playlistID, userID, permission)
	return args.Bool(0), args.Error(1)
}

func (m *MockPlaylistService) SearchPlaylists(ctx context.Context, params services.SearchPlaylistsParams) ([]*models.Playlist, int64, error) {
	args := m.Called(ctx, params)
	if args.Get(0) == nil {
		return nil, 0, args.Error(2)
	}
	return args.Get(0).([]*models.Playlist), args.Get(1).(int64), args.Error(2)
}

func setupPlaylistTestRouter(mockService *MockPlaylistService) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()

	logger := zap.NewNop()
	// Use the generic new handler with interface
	handler := NewPlaylistHandlerWithInterface(mockService, nil, logger) // db is nil as we use mock service

	api := router.Group("/api/v1")
	api.Use(func(c *gin.Context) {
		// Mock auth middleware manually for simplicity
		userIDStr := c.GetHeader("X-User-ID")
		if userIDStr != "" {
			uid, err := uuid.Parse(userIDStr)
			if err == nil {
				// Inject user_id into context as middleware would
				c.Set("user_id", uid)
			}
		}
		c.Next()
	})
	{
		api.GET("/playlists", handler.GetPlaylists)
		api.POST("/playlists", handler.CreatePlaylist)
		api.GET("/playlists/:id", handler.GetPlaylist)
		api.PUT("/playlists/:id", handler.UpdatePlaylist)
		api.DELETE("/playlists/:id", handler.DeletePlaylist)
	}

	return router
}

func TestPlaylistHandler_GetPlaylists_Success(t *testing.T) {
	mockService := new(MockPlaylistService)
	router := setupPlaylistTestRouter(mockService)

	userID := uuid.New()

	expectedPlaylists := []*models.Playlist{
		{ID: uuid.New(), Title: "List 1", UserID: userID},
		{ID: uuid.New(), Title: "List 2", UserID: userID},
	}

	// Expect GetPlaylists call
	mockService.On("GetPlaylists", mock.Anything, mock.MatchedBy(func(u *uuid.UUID) bool {
		return u != nil && *u == userID
	}), mock.Anything, 1, 20).Return(expectedPlaylists, int64(2), nil)

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

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

	var response map[string]interface{}
	json.Unmarshal(w.Body.Bytes(), &response)

	data := response["data"].(map[string]interface{})
	assert.Equal(t, float64(2), data["total"])

	mockService.AssertExpectations(t)
}

func TestPlaylistHandler_CreatePlaylist_Success(t *testing.T) {
	mockService := new(MockPlaylistService)
	router := setupPlaylistTestRouter(mockService)

	userID := uuid.New()
	reqBody := CreatePlaylistRequest{
		Title:       "New Playlist",
		Description: "Desc",
		IsPublic:    true,
	}

	createdPlaylist := &models.Playlist{
		ID:          uuid.New(),
		UserID:      userID,
		Title:       reqBody.Title,
		Description: reqBody.Description,
		IsPublic:    reqBody.IsPublic,
	}

	mockService.On("CreatePlaylist", mock.Anything, userID, reqBody.Title, reqBody.Description, reqBody.IsPublic).Return(createdPlaylist, nil)

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("POST", "/api/v1/playlists", 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.Equal(t, http.StatusCreated, w.Code)
	mockService.AssertExpectations(t)
}

func TestPlaylistHandler_GetPlaylist_NotFound(t *testing.T) {
	mockService := new(MockPlaylistService)
	router := setupPlaylistTestRouter(mockService)

	userID := uuid.New() // Authenticated user
	playlistID := uuid.New()

	// Error returned by service when not found or access denied
	mockService.On("GetPlaylist", mock.Anything, playlistID, mock.MatchedBy(func(u *uuid.UUID) bool {
		return u != nil && *u == userID
	})).Return(nil, services.ErrPlaylistNotFound)

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

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

func TestPlaylistHandler_DeletePlaylist_Success(t *testing.T) {
	mockService := new(MockPlaylistService)
	router := setupPlaylistTestRouter(mockService)

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

	mockService.On("DeletePlaylist", mock.Anything, playlistID, userID).Return(nil)

	req, _ := http.NewRequest("DELETE", "/api/v1/playlists/"+playlistID.String(), nil)
	req.Header.Set("X-User-ID", userID.String())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaylistHandler_DeletePlaylist_Forbidden(t *testing.T) {
	mockService := new(MockPlaylistService)
	router := setupPlaylistTestRouter(mockService)

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

	mockService.On("DeletePlaylist", mock.Anything, playlistID, userID).Return(services.ErrAccessDenied)

	req, _ := http.NewRequest("DELETE", "/api/v1/playlists/"+playlistID.String(), nil)
	req.Header.Set("X-User-ID", userID.String())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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

func TestPlaylistHandler_UpdatePlaylist_Success(t *testing.T) {
	mockService := new(MockPlaylistService)
	router := setupPlaylistTestRouter(mockService)

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

	newTitle := "Updated Title"
	reqBody := UpdatePlaylistRequest{
		Title: &newTitle,
	}

	updatedPlaylist := &models.Playlist{
		ID:    playlistID,
		Title: newTitle,
	}

	mockService.On("UpdatePlaylist", mock.Anything, playlistID, userID,
		mock.MatchedBy(func(s *string) bool { return s != nil && *s == "Updated Title" }),
		(*string)(nil), (*bool)(nil)).Return(updatedPlaylist, nil)

	body, _ := json.Marshal(reqBody)
	req, _ := http.NewRequest("PUT", "/api/v1/playlists/"+playlistID.String(), 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.Equal(t, http.StatusOK, w.Code)
	mockService.AssertExpectations(t)
}

func TestPlaylistHandler_UpdatePlaylist_ValidationError(t *testing.T) {
	mockService := new(MockPlaylistService)
	router := setupPlaylistTestRouter(mockService)

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

	// Title too long or empty if it was required, but here just malformed request maybe?
	// Let's send invalid json
	req, _ := http.NewRequest("PUT", "/api/v1/playlists/"+playlistID.String(), bytes.NewBuffer([]byte("{invalid")))
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-User-ID", userID.String())
	w := httptest.NewRecorder()
	router.ServeHTTP(w, req)

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