package services

import (
	"bytes"
	"context"
	"errors"
	"mime/multipart"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"

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

// ========== MOCK REPOSITORY ==========

type MockUserRepository struct {
	mock.Mock
}

func (m *MockUserRepository) GetByID(id string) (*models.User, error) {
	args := m.Called(id)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.User), args.Error(1)
}

func (m *MockUserRepository) GetByEmail(email string) (*models.User, error) {
	args := m.Called(email)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.User), args.Error(1)
}

func (m *MockUserRepository) GetByUsername(username string) (*models.User, error) {
	args := m.Called(username)
	if args.Get(0) == nil {
		return nil, args.Error(1)
	}
	return args.Get(0).(*models.User), args.Error(1)
}

func (m *MockUserRepository) Create(user *models.User) error {
	args := m.Called(user)
	return args.Error(0)
}

func (m *MockUserRepository) Update(user *models.User) error {
	args := m.Called(user)
	return args.Error(0)
}

func (m *MockUserRepository) Delete(id string) error {
	args := m.Called(id)
	return args.Error(0)
}

// ========== TEST DATABASE SETUP ==========

func setupUserTestDB(t *testing.T) *gorm.DB {
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	if err != nil {
		t.Fatalf("Failed to connect to test database: %v", err)
	}

	// Auto-migrate les modèles nécessaires
	err = db.AutoMigrate(
		&models.User{},
		&models.UserSettings{},
		&models.UserProfile{},
	)
	if err != nil {
		t.Fatalf("Failed to migrate: %v", err)
	}

	return db
}

// ========== TESTS ==========

func TestUserService_GetProfile_Success(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	user := &models.User{
		ID:        userID,
		Username:  "testuser",
		FirstName: "Test",
		LastName:  "User",
		Email:     "test@example.com",
		CreatedAt: time.Now(),
		IsPublic:  true,
	}

	mockRepo.On("GetByID", userID.String()).Return(user, nil)

	// Execute
	profile, err := service.GetProfile(userID, &userID)

	// Assert
	assert.NoError(t, err)
	assert.NotNil(t, profile)
	assert.Equal(t, userID, profile.ID)
	assert.Equal(t, "testuser", profile.Username)
	mockRepo.AssertExpectations(t)
}

func TestUserService_GetProfile_NotFound(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()

	mockRepo.On("GetByID", userID.String()).Return(nil, errors.New("not found"))

	// Execute
	profile, err := service.GetProfile(userID, &userID)

	// Assert
	assert.Error(t, err)
	assert.Nil(t, profile)
}

func TestUserService_GetProfile_Private(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	otherID := uuid.New()
	bio := "Secret bio"
	user := &models.User{
		ID:       userID,
		Username: "privateuser",
		IsPublic: false,
		Bio:      bio,
	}

	mockRepo.On("GetByID", userID.String()).Return(user, nil)

	// Execute as another user
	profile, err := service.GetProfile(userID, &otherID)

	// Assert
	assert.NoError(t, err)
	assert.NotNil(t, profile)
	assert.Nil(t, profile.Bio, "Bio should be nil for private profile viewed by other")
}

func TestUserService_GetProfileByUsername_Success(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	username := "testuser"
	user := &models.User{
		ID:       userID,
		Username: username,
		IsPublic: true,
	}

	mockRepo.On("GetByUsername", username).Return(user, nil)
	mockRepo.On("GetByID", userID.String()).Return(user, nil)

	// Execute
	profile, err := service.GetProfileByUsername(username, &userID)

	// Assert
	assert.NoError(t, err)
	assert.Equal(t, username, profile.Username)
}

func TestUserService_UpdateProfile_Success(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	user := &models.User{
		ID:       userID,
		Username: "oldname",
		Bio:      "old bio",
	}

	newName := "newname"
	newBio := "new bio"
	req := types.UpdateProfileRequest{
		Username: &newName,
		Bio:      &newBio,
	}

	mockRepo.On("GetByID", userID.String()).Return(user, nil)
	mockRepo.On("Update", mock.MatchedBy(func(u *models.User) bool {
		return u.Username == "newname" && u.Bio == "new bio"
	})).Return(nil)

	// Execute
	profile, err := service.UpdateProfile(userID, req)

	// Assert
	assert.NoError(t, err)
	assert.Equal(t, "new name", profile.Username)
	assert.Equal(t, "new bio", *profile.Bio)
}

func TestUserService_UpdateProfile_WithSocialLinks_Success(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	user := &models.User{
		ID: userID,
	}

	socialLinks := map[string]interface{}{
		"twitter": "https://twitter.com/test",
	}
	req := types.UpdateProfileRequest{
		SocialLinks: socialLinks,
	}

	mockRepo.On("GetByID", userID.String()).Return(user, nil)
	mockRepo.On("Update", mock.MatchedBy(func(u *models.User) bool {
		return u.SocialLinks != "" && strings.Contains(u.SocialLinks, "twitter")
	})).Return(nil)

	// Execute
	profile, err := service.UpdateProfile(userID, req)

	// Assert
	assert.NoError(t, err)
	assert.NotNil(t, profile.SocialLinks)
	assert.Equal(t, "https://twitter.com/test", profile.SocialLinks["twitter"])
	mockRepo.AssertExpectations(t)
}

func TestUserService_GetUserSettings_Success(t *testing.T) {
	// Setup with DB
	db := setupUserTestDB(t)
	mockRepo := new(MockUserRepository)
	service := NewUserServiceWithDB(mockRepo, db)

	userID := uuid.New()

	// Create settings in DB
	settings := models.UserSettings{
		UserID:             userID,
		EmailNotifications: true,
	}
	db.Create(&settings)

	// Create profile in DB
	profile := models.UserProfile{
		UserID:   userID,
		Language: "fr",
		Timezone: "Europe/Paris",
	}
	db.Create(&profile)

	// Execute
	resp, err := service.GetUserSettings(userID)

	// Assert
	assert.NoError(t, err)
	assert.True(t, resp.Notifications.Email)
	assert.Equal(t, "fr", resp.Preferences.Language)
	assert.Equal(t, "Europe/Paris", resp.Preferences.Timezone)
}

func TestUserService_UpdateUserSettings_Success(t *testing.T) {
	// Setup with DB
	db := setupUserTestDB(t)
	mockRepo := new(MockUserRepository)
	service := NewUserServiceWithDB(mockRepo, db)

	userID := uuid.New()

	// Initial state
	settings := models.UserSettings{UserID: userID, EmailNotifications: false}
	db.Create(&settings)
	profile := models.UserProfile{UserID: userID, Language: "en"}
	db.Create(&profile)

	// Request
	newLang := "es"
	req := types.UpdateSettingsRequest{
		Notifications: &types.NotificationSettings{Email: true},
		Preferences:   &types.PreferenceSettings{Language: newLang},
	}

	// Execute
	err := service.UpdateUserSettings(userID, &req)

	// Assert
	assert.NoError(t, err)

	// Verify DB
	var dbSettings models.UserSettings
	db.First(&dbSettings, "user_id = ?", userID)
	assert.True(t, dbSettings.EmailNotifications)

	var dbProfile models.UserProfile
	db.First(&dbProfile, "user_id = ?", userID)
	assert.Equal(t, "es", dbProfile.Language)
}

func TestUserService_DeleteUser_Success(t *testing.T) {
	// Setup
	db := setupUserTestDB(t)
	mockRepo := new(MockUserRepository)
	service := NewUserServiceWithDB(mockRepo, db)

	userID := uuid.New()
	user := &models.User{ID: userID, IsActive: true}

	// Create user in DB for soft delete check (if needed by service implementation detail)
	db.Create(user)

	mockRepo.On("GetByID", userID.String()).Return(user, nil)
	mockRepo.On("Delete", userID.String()).Return(nil)

	// Execute
	ctx := context.Background()
	err := service.DeleteUser(ctx, userID)

	// Assert
	assert.NoError(t, err)

	// Verify IsActive updated in DB (as per implementation)
	var dbUser models.User
	db.First(&dbUser, "id = ?", userID)
	assert.False(t, dbUser.IsActive)

	mockRepo.AssertExpectations(t)
}

func TestUserService_UploadAvatar(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	// Use temp dir for uploads
	tmpDir, err := os.MkdirTemp("", "avatar_test")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)
	service.SetUploadDir(tmpDir)

	userID := uuid.New()
	content := []byte("fake image content")
	fileHeader := createMultipartFileHeader(t, "avatar.png", content, "image/png")

	// Execute
	url, err := service.UploadAvatar(userID, fileHeader)

	// Assert
	assert.NoError(t, err)
	assert.Contains(t, url, "/uploads/avatars/")
	assert.Contains(t, url, ".png")
}

func TestUserService_UpdateAvatarURL(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	user := &models.User{ID: userID}
	newAvatar := "/uploads/avatars/new.png"

	mockRepo.On("GetByID", userID.String()).Return(user, nil)
	mockRepo.On("Update", mock.MatchedBy(func(u *models.User) bool {
		return u.Avatar == newAvatar
	})).Return(nil)

	// Execute
	err := service.UpdateAvatarURL(userID, newAvatar)

	// Assert
	assert.NoError(t, err)
	mockRepo.AssertExpectations(t)
}

func TestUserService_ValidateUsername(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	otherID := uuid.New()
	currentUsername := "current"
	newUsername := "newname"
	takenUsername := "taken"

	user := &models.User{
		ID:       userID,
		Username: currentUsername,
	}

	mockRepo.On("GetByID", userID.String()).Return(user, nil)
	// Case 1: Username available
	mockRepo.On("GetByUsername", newUsername).Return(nil, gorm.ErrRecordNotFound)

	// Case 2: Username taken
	otherUser := &models.User{ID: otherID, Username: takenUsername}
	mockRepo.On("GetByUsername", takenUsername).Return(otherUser, nil)

	// Execute Case 1
	err := service.ValidateUsername(userID, newUsername)
	assert.NoError(t, err)

	// Execute Case 2
	err = service.ValidateUsername(userID, takenUsername)
	assert.Error(t, err)
	assert.Equal(t, "username already taken", err.Error())

	// Case 3: Rate limit
	// (Skipping rate limit test complexity for now as logic is standard time check)
}

func TestUserService_CalculateProfileCompletion(t *testing.T) {
	// Setup
	mockRepo := new(MockUserRepository)
	service := NewUserService(mockRepo)

	userID := uuid.New()
	avatar := "avatar.png"
	bio := "bio"
	socialLinks := `{"twitter": "https://twitter.com/test"}`
	user := &models.User{
		ID:          userID,
		Username:    "complete",
		FirstName:   "John",
		LastName:    "Doe",
		Bio:         bio,
		Avatar:      avatar,
		IsPublic:    true,
		SocialLinks: socialLinks,
	}

	mockRepo.On("GetByID", userID.String()).Return(user, nil)

	// Execute
	completion, err := service.CalculateProfileCompletion(userID)

	// Assert
	assert.NoError(t, err)
	assert.Equal(t, 100, completion.Percentage)
	assert.Empty(t, completion.Missing)
}

// Helper
func createMultipartFileHeader(t *testing.T, filename string, content []byte, contentType string) *multipart.FileHeader {
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", filename)
	if err != nil {
		t.Fatal(err)
	}
	_, err = part.Write(content)
	if err != nil {
		t.Fatal(err)
	}
	err = writer.Close()
	if err != nil {
		t.Fatal(err)
	}

	reader := multipart.NewReader(body, writer.Boundary())
	form, err := reader.ReadForm(1024 * 1024)
	if err != nil {
		t.Fatal(err)
	}

	headers := form.File["file"]
	if len(headers) == 0 {
		t.Fatal("no file header")
	}
	headers[0].Header.Set("Content-Type", contentType)

	return headers[0]
}
