package transactions

import (
	"context"
	"testing"

	"veza-backend-api/internal/database"
	"veza-backend-api/internal/models"
	"veza-backend-api/internal/services"
	"veza-backend-api/internal/testutils"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap/zaptest"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

// setupTestDB crée une DB de test avec testcontainers
func setupTestDB(t *testing.T) *gorm.DB {
	ctx := context.Background()
	dsn, err := testutils.GetTestContainerDB(ctx)
	require.NoError(t, err, "Failed to setup test database")

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	require.NoError(t, err, "Failed to open database connection")

	// Note: Les migrations SQL sont déjà exécutées lors du démarrage du conteneur PostgreSQL
	// via postgres.WithInitScripts dans internal/testutils/setup.go
	// AutoMigrate n'est pas nécessaire et peut causer des erreurs avec les vues existantes

	return db
}

// cleanupTestDB nettoie la DB entre les tests
func cleanupTestDB(_ *testing.T, db *gorm.DB) {
	// Supprimer toutes les données
	db.Exec("TRUNCATE TABLE user_roles CASCADE")
	db.Exec("TRUNCATE TABLE users CASCADE")
	db.Exec("TRUNCATE TABLE roles CASCADE")
}

// createTestUser crée un utilisateur de test
func createTestUser(t *testing.T, db *gorm.DB) *models.User {
	user := &models.User{
		Username:     "testuser_" + uuid.New().String()[:8],
		Slug:         "testuser_" + uuid.New().String()[:8], // Unique slug
		Email:        "test_" + uuid.New().String()[:8] + "@example.com",
		PasswordHash: "$2a$10$examplehash",
		IsActive:     true,
		IsVerified:   true,
	}
	err := db.Create(user).Error
	require.NoError(t, err)
	return user
}

// createTestRole crée un rôle de test
func createTestRole(t *testing.T, db *gorm.DB) *models.Role {
	role := &models.Role{
		Name:        "test_role_" + uuid.New().String()[:8],
		Description: "Test role for transaction tests",
	}
	err := db.Create(role).Error
	require.NoError(t, err)
	return role
}

// TestAssignRoleToUser_Success vérifie que l'assignation fonctionne correctement
func TestAssignRoleToUser_Success(t *testing.T) {
	db := setupTestDB(t)
	defer cleanupTestDB(t, db)

	logger := zaptest.NewLogger(t)
	// Initialize RBAC service
	dbWrapper := &database.Database{GormDB: db}
	rbacService := services.NewRBACService(dbWrapper, logger)

	user := createTestUser(t, db)
	role := createTestRole(t, db)

	// Assigner le rôle
	err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
	require.NoError(t, err, "AssignRoleToUser should succeed")

	// Vérifier que l'assignation existe
	var count int64
	db.Model(&models.UserRole{}).
		Where("user_id = ? AND role_id = ?", user.ID, role.ID).
		Count(&count)
	assert.Equal(t, int64(1), count, "UserRole should be created")
}

// TestAssignRoleToUser_RollbackOnUserNotFound vérifie le rollback si l'utilisateur n'existe pas
func TestAssignRoleToUser_RollbackOnUserNotFound(t *testing.T) {
	db := setupTestDB(t)
	defer cleanupTestDB(t, db)

	logger := zaptest.NewLogger(t)
	rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)

	role := createTestRole(t, db)
	fakeUserID := uuid.New()

	// Tenter d'assigner le rôle à un utilisateur inexistant
	err := rbacService.AssignRoleToUser(context.Background(), fakeUserID, role.ID)
	require.Error(t, err, "AssignRoleToUser should fail")
	assert.Contains(t, err.Error(), "user not found", "Error should mention user not found")

	// Vérifier qu'aucune assignation n'a été créée
	var count int64
	db.Model(&models.UserRole{}).Count(&count)
	assert.Equal(t, int64(0), count, "No UserRole should be created on error")
}

// TestAssignRoleToUser_RollbackOnRoleNotFound vérifie le rollback si le rôle n'existe pas
func TestAssignRoleToUser_RollbackOnRoleNotFound(t *testing.T) {
	db := setupTestDB(t)
	defer cleanupTestDB(t, db)

	logger := zaptest.NewLogger(t)
	rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)

	user := createTestUser(t, db)
	fakeRoleID := uuid.New()

	// Tenter d'assigner un rôle inexistant
	err := rbacService.AssignRoleToUser(context.Background(), user.ID, fakeRoleID)
	require.Error(t, err, "AssignRoleToUser should fail")
	assert.Contains(t, err.Error(), "role not found", "Error should mention role not found")

	// Vérifier qu'aucune assignation n'a été créée
	var count int64
	db.Model(&models.UserRole{}).Count(&count)
	assert.Equal(t, int64(0), count, "No UserRole should be created on error")
}

// TestAssignRoleToUser_RollbackOnDuplicate vérifie le rollback si le rôle est déjà assigné
func TestAssignRoleToUser_RollbackOnDuplicate(t *testing.T) {
	db := setupTestDB(t)
	defer cleanupTestDB(t, db)

	logger := zaptest.NewLogger(t)
	rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)

	user := createTestUser(t, db)
	role := createTestRole(t, db)

	// Première assignation (succès)
	err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
	require.NoError(t, err, "First assignment should succeed")

	// Deuxième assignation (doublon)
	err = rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
	require.Error(t, err, "Second assignment should fail")
	assert.Contains(t, err.Error(), "role already assigned", "Error should mention duplicate")

	// Vérifier qu'il n'y a qu'une seule assignation
	var count int64
	db.Model(&models.UserRole{}).
		Where("user_id = ? AND role_id = ?", user.ID, role.ID).
		Count(&count)
	assert.Equal(t, int64(1), count, "Should have exactly one UserRole")
}

// TestAssignRoleToUser_Concurrency vérifie qu'il n'y a pas de race condition
func TestAssignRoleToUser_Concurrency(t *testing.T) {
	db := setupTestDB(t)
	defer cleanupTestDB(t, db)

	logger := zaptest.NewLogger(t)
	rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)

	user := createTestUser(t, db)
	role := createTestRole(t, db)

	// Lancer 10 goroutines qui tentent d'assigner le même rôle simultanément
	results := make(chan error, 10)
	for i := 0; i < 10; i++ {
		go func() {
			err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
			results <- err
		}()
	}

	// Collecter les résultats
	successCount := 0
	errorCount := 0
	for i := 0; i < 10; i++ {
		err := <-results
		if err == nil {
			successCount++
		} else {
			errorCount++
			assert.Contains(t, err.Error(), "role already assigned", "Error should be about duplicate")
		}
	}

	// Une seule assignation devrait réussir
	assert.Equal(t, 1, successCount, "Only one assignment should succeed")
	assert.Equal(t, 9, errorCount, "Nine assignments should fail due to duplicate")

	// Vérifier qu'il n'y a qu'une seule assignation en DB
	var count int64
	db.Model(&models.UserRole{}).
		Where("user_id = ? AND role_id = ?", user.ID, role.ID).
		Count(&count)
	assert.Equal(t, int64(1), count, "Should have exactly one UserRole despite concurrent attempts")
}

// TestAssignRoleToUser_Atomicity vérifie l'atomicité complète de la transaction
func TestAssignRoleToUser_Atomicity(t *testing.T) {
	db := setupTestDB(t)
	defer cleanupTestDB(t, db)

	logger := zaptest.NewLogger(t)
	rbacService := services.NewRBACService(&database.Database{GormDB: db}, logger)

	user := createTestUser(t, db)
	role := createTestRole(t, db)

	// Supprimer le rôle juste avant l'assignation pour forcer une erreur
	// (simulation d'une erreur au milieu de la transaction)
	// Note: Dans une vraie transaction, cela ne devrait pas arriver car FOR UPDATE verrouille
	// Mais on peut tester en supprimant le rôle après le début de la transaction

	// Créer un hook GORM pour simuler une erreur
	// Pour ce test, on va simplement vérifier que si le rôle est supprimé
	// entre la vérification et l'INSERT, la contrainte FK bloque l'insertion

	// Assigner le rôle normalement d'abord
	err := rbacService.AssignRoleToUser(context.Background(), user.ID, role.ID)
	require.NoError(t, err)

	// Supprimer le rôle
	db.Delete(role)

	// Tenter d'assigner à un autre utilisateur (devrait échouer car le rôle n'existe plus)
	user2 := createTestUser(t, db)
	err = rbacService.AssignRoleToUser(context.Background(), user2.ID, role.ID)
	require.Error(t, err, "Should fail because role was deleted")
	assert.Contains(t, err.Error(), "role not found", "Error should mention role not found")
}
