package services

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
	"veza-backend-api/internal/models"
)

func setupTestBatchUpdateDB(t *testing.T) (*TrackService, *gorm.DB, func()) {
	// Setup in-memory SQLite database
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)

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

	// Create test user
	user := &models.User{
		ID:       1,
		Username: "testuser",
		Email:    "test@example.com",
		IsActive: true,
	}
	err = db.Create(user).Error
	require.NoError(t, err)

	// Create another user
	user2 := &models.User{
		ID:       2,
		Username: "otheruser",
		Email:    "other@example.com",
		IsActive: true,
	}
	err = db.Create(user2).Error
	require.NoError(t, err)

	// Create logger
	logger := zap.NewNop()

	// Create service
	service := NewTrackService(db, logger, "test_uploads")

	// Cleanup function
	cleanup := func() {
		// SQLite in-memory database doesn't need explicit cleanup
	}

	return service, db, cleanup
}

func TestTrackService_BatchUpdateTracks_Success(t *testing.T) {
	service, db, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	// Create tracks
	track1 := &models.Track{
		UserID:   1,
		Title:    "Track 1",
		FilePath: "/path/to/track1.mp3",
		FileSize: 1024,
		Format:   "MP3",
		Duration: 180,
		IsPublic: false,
		Status:   models.TrackStatusCompleted,
	}
	track2 := &models.Track{
		UserID:   1,
		Title:    "Track 2",
		FilePath: "/path/to/track2.mp3",
		FileSize: 2048,
		Format:   "MP3",
		Duration: 240,
		IsPublic: false,
		Status:   models.TrackStatusCompleted,
	}
	err := db.Create(track1).Error
	require.NoError(t, err)
	err = db.Create(track2).Error
	require.NoError(t, err)

	// Batch update
	updates := map[string]interface{}{
		"is_public": true,
		"genre":     "Electronic",
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{track1.ID, track2.ID}, 1, updates)
	require.NoError(t, err)
	require.NotNil(t, result)

	// Verify results
	assert.Equal(t, 2, len(result.Updated))
	assert.Contains(t, result.Updated, track1.ID)
	assert.Contains(t, result.Updated, track2.ID)
	assert.Equal(t, 0, len(result.Failed))

	// Verify tracks are updated in database
	var updatedTrack1, updatedTrack2 models.Track
	db.First(&updatedTrack1, track1.ID)
	db.First(&updatedTrack2, track2.ID)
	assert.True(t, updatedTrack1.IsPublic)
	assert.True(t, updatedTrack2.IsPublic)
	assert.Equal(t, "Electronic", updatedTrack1.Genre)
	assert.Equal(t, "Electronic", updatedTrack2.Genre)
}

func TestTrackService_BatchUpdateTracks_EmptyList(t *testing.T) {
	service, _, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	updates := map[string]interface{}{
		"is_public": true,
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{}, 1, updates)
	require.NoError(t, err)
	require.NotNil(t, result)
	assert.Equal(t, 0, len(result.Updated))
	assert.Equal(t, 0, len(result.Failed))
}

func TestTrackService_BatchUpdateTracks_ExceedsMaxBatchSize(t *testing.T) {
	service, _, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	// Create a list with more than 100 tracks
	trackIDs := make([]int64, 101)
	for i := range trackIDs {
		trackIDs[i] = int64(i + 1)
	}

	updates := map[string]interface{}{
		"is_public": true,
	}
	result, err := service.BatchUpdateTracks(ctx, trackIDs, 1, updates)
	assert.Error(t, err)
	assert.Nil(t, result)
	assert.Contains(t, err.Error(), "batch size exceeds maximum")
}

func TestTrackService_BatchUpdateTracks_EmptyUpdates(t *testing.T) {
	service, _, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	result, err := service.BatchUpdateTracks(ctx, []int64{1, 2}, 1, map[string]interface{}{})
	assert.Error(t, err)
	assert.Nil(t, result)
	assert.Contains(t, err.Error(), "updates cannot be empty")
}

func TestTrackService_BatchUpdateTracks_NotFound(t *testing.T) {
	service, _, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	updates := map[string]interface{}{
		"is_public": true,
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{999, 1000}, 1, updates)
	require.NoError(t, err)
	require.NotNil(t, result)

	assert.Equal(t, 0, len(result.Updated))
	assert.Equal(t, 2, len(result.Failed))
	assert.Equal(t, int64(999), result.Failed[0].TrackID)
	assert.Equal(t, "track not found", result.Failed[0].Error)
}

func TestTrackService_BatchUpdateTracks_Forbidden(t *testing.T) {
	service, db, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	// Create track owned by user 1
	track1 := &models.Track{
		UserID:   1,
		Title:    "Track 1",
		FilePath: "/path/to/track1.mp3",
		FileSize: 1024,
		Format:   "MP3",
		Duration: 180,
		IsPublic: false,
		Status:   models.TrackStatusCompleted,
	}
	err := db.Create(track1).Error
	require.NoError(t, err)

	// Try to update as user 2
	updates := map[string]interface{}{
		"is_public": true,
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{track1.ID}, 2, updates)
	require.NoError(t, err)
	require.NotNil(t, result)

	assert.Equal(t, 0, len(result.Updated))
	assert.Equal(t, 1, len(result.Failed))
	assert.Equal(t, track1.ID, result.Failed[0].TrackID)
	assert.Contains(t, result.Failed[0].Error, "forbidden")

	// Verify track is not updated
	var updatedTrack models.Track
	db.First(&updatedTrack, track1.ID)
	assert.False(t, updatedTrack.IsPublic)
}

func TestTrackService_BatchUpdateTracks_PartialSuccess(t *testing.T) {
	service, db, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	// Create tracks
	track1 := &models.Track{
		UserID:   1,
		Title:    "Track 1",
		FilePath: "/path/to/track1.mp3",
		FileSize: 1024,
		Format:   "MP3",
		Duration: 180,
		IsPublic: false,
		Status:   models.TrackStatusCompleted,
	}
	track2 := &models.Track{
		UserID:   2, // Owned by different user
		Title:    "Track 2",
		FilePath: "/path/to/track2.mp3",
		FileSize: 2048,
		Format:   "MP3",
		Duration: 240,
		IsPublic: false,
		Status:   models.TrackStatusCompleted,
	}
	err := db.Create(track1).Error
	require.NoError(t, err)
	err = db.Create(track2).Error
	require.NoError(t, err)

	// Try to update both as user 1
	updates := map[string]interface{}{
		"is_public": true,
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{track1.ID, track2.ID}, 1, updates)
	require.NoError(t, err)
	require.NotNil(t, result)

	// Track1 should be updated, track2 should fail
	assert.Equal(t, 1, len(result.Updated))
	assert.Contains(t, result.Updated, track1.ID)
	assert.Equal(t, 1, len(result.Failed))
	assert.Equal(t, track2.ID, result.Failed[0].TrackID)
	assert.Contains(t, result.Failed[0].Error, "forbidden")

	// Verify track1 is updated, track2 is not
	var updatedTrack1, updatedTrack2 models.Track
	db.First(&updatedTrack1, track1.ID)
	db.First(&updatedTrack2, track2.ID)
	assert.True(t, updatedTrack1.IsPublic)
	assert.False(t, updatedTrack2.IsPublic)
}

func TestTrackService_BatchUpdateTracks_InvalidTitle(t *testing.T) {
	service, _, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	updates := map[string]interface{}{
		"title": "", // Empty title
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{1}, 1, updates)
	assert.Error(t, err)
	assert.Nil(t, result)
	assert.Contains(t, err.Error(), "title cannot be empty")
}

func TestTrackService_BatchUpdateTracks_InvalidYear(t *testing.T) {
	service, _, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	updates := map[string]interface{}{
		"year": 1800, // Year too old
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{1}, 1, updates)
	assert.Error(t, err)
	assert.Nil(t, result)
	assert.Contains(t, err.Error(), "year must be between")
}

func TestTrackService_BatchUpdateTracks_InvalidIsPublic(t *testing.T) {
	service, _, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	updates := map[string]interface{}{
		"is_public": "not a boolean", // Invalid type
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{1}, 1, updates)
	assert.Error(t, err)
	assert.Nil(t, result)
	assert.Contains(t, err.Error(), "invalid value for is_public")
}

func TestTrackService_BatchUpdateTracks_UnauthorizedField(t *testing.T) {
	service, db, cleanup := setupTestBatchUpdateDB(t)
	defer cleanup()

	ctx := context.Background()

	// Create track
	track := &models.Track{
		UserID:   1,
		Title:    "Track 1",
		FilePath: "/path/to/track1.mp3",
		FileSize: 1024,
		Format:   "MP3",
		Duration: 180,
		IsPublic: false,
		Status:   models.TrackStatusCompleted,
	}
	err := db.Create(track).Error
	require.NoError(t, err)

	// Try to update with unauthorized field (user_id should not be updatable)
	updates := map[string]interface{}{
		"is_public": true,
		"user_id":   999, // This should be ignored
	}
	result, err := service.BatchUpdateTracks(ctx, []int64{track.ID}, 1, updates)
	require.NoError(t, err)
	require.NotNil(t, result)

	// Should succeed with only is_public updated
	assert.Equal(t, 1, len(result.Updated))

	// Verify user_id is not changed
	var updatedTrack models.Track
	db.First(&updatedTrack, track.ID)
	assert.Equal(t, int64(1), updatedTrack.UserID)
	assert.True(t, updatedTrack.IsPublic)
}

