package services

import (
	"database/sql"
	"testing"
	"time"
	"unsafe"

	"github.com/google/uuid"
	"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/database"
	"veza-backend-api/internal/models"
)

// setupTestPasswordResetService crée un PasswordResetService de test avec une base de données en mémoire
func setupTestPasswordResetService(t *testing.T) (*PasswordResetService, *database.Database, *gorm.DB) {
	// Créer une base de données GORM en mémoire
	gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err, "Failed to open test database")

	// Auto-migrate pour créer la table users
	err = gormDB.AutoMigrate(&models.User{})
	require.NoError(t, err, "Failed to migrate users table")

	// Créer la table password_reset_tokens manuellement
	err = gormDB.Exec(`
		CREATE TABLE password_reset_tokens (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
			token TEXT NOT NULL UNIQUE,
			expires_at TIMESTAMP NOT NULL,
			used INTEGER NOT NULL DEFAULT 0,
			created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
		)
	`).Error
	require.NoError(t, err, "Failed to create password_reset_tokens table")

	// Créer les index
	err = gormDB.Exec("CREATE INDEX idx_password_reset_tokens_token ON password_reset_tokens(token)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_password_reset_tokens_user_id ON password_reset_tokens(user_id)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_password_reset_tokens_expires_at ON password_reset_tokens(expires_at)").Error
	require.NoError(t, err)

	// Créer un utilisateur de test
	user := &models.User{
		Email:    "test@example.com",
		Username: "testuser",
		Role:     "user",
		IsActive: true,
	}
	err = gormDB.Create(user).Error
	require.NoError(t, err, "Failed to create test user")

	// Obtenir le sql.DB depuis GORM
	sqlDB, err := gormDB.DB()
	require.NoError(t, err, "Failed to get sql.DB from GORM")

	// Créer un Database wrapper en utilisant la même approche que createTestDatabase
	// database.Database embeds *sql.DB, donc on utilise une structure temporaire avec le même layout
	type tempDB struct {
		*sql.DB
		gormDB interface{}
		config interface{}
		logger interface{}
	}
	temp := &tempDB{DB: sqlDB}
	testDB := (*database.Database)(unsafe.Pointer(temp))

	// Créer le logger
	logger, _ := zap.NewDevelopment()

	// Créer le service
	service := NewPasswordResetService(testDB, logger)

	return service, testDB, gormDB
}

// TestPasswordResetService_GenerateToken teste la génération de token
func TestPasswordResetService_GenerateToken(t *testing.T) {
	service, _, _ := setupTestPasswordResetService(t)

	// Générer un token
	token, err := service.GenerateToken()

	assert.NoError(t, err)
	assert.NotEmpty(t, token)
	assert.Greater(t, len(token), 20, "Token should be at least 20 characters")
}

// TestPasswordResetService_GenerateToken_Unique teste que les tokens générés sont uniques
func TestPasswordResetService_GenerateToken_Unique(t *testing.T) {
	service, _, _ := setupTestPasswordResetService(t)

	// Générer plusieurs tokens
	token1, err1 := service.GenerateToken()
	token2, err2 := service.GenerateToken()
	token3, err3 := service.GenerateToken()

	assert.NoError(t, err1)
	assert.NoError(t, err2)
	assert.NoError(t, err3)

	// Vérifier que les tokens sont différents
	assert.NotEqual(t, token1, token2)
	assert.NotEqual(t, token2, token3)
	assert.NotEqual(t, token1, token3)
}

// TestPasswordResetService_StoreToken teste le stockage d'un token
func TestPasswordResetService_StoreToken(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Générer et stocker un token
	token, err := service.GenerateToken()
	require.NoError(t, err)

	err = service.StoreToken(user.ID, token)
	assert.NoError(t, err)

	// Vérifier que le token a été stocké
	var count int64
	err = gormDB.Raw("SELECT COUNT(*) FROM password_reset_tokens WHERE token = ? AND user_id = ?", token, user.ID).Scan(&count).Error
	require.NoError(t, err)
	assert.Equal(t, int64(1), count, "Token should be stored")
}

// TestPasswordResetService_StoreToken_Expiration teste que le token a une expiration de 1h
func TestPasswordResetService_StoreToken_Expiration(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Générer et stocker un token
	token, err := service.GenerateToken()
	require.NoError(t, err)

	err = service.StoreToken(user.ID, token)
	require.NoError(t, err)

	// Vérifier l'expiration
	var expiresAt time.Time
	err = gormDB.Raw("SELECT expires_at FROM password_reset_tokens WHERE token = ?", token).Scan(&expiresAt).Error
	require.NoError(t, err)

	// L'expiration devrait être environ 1h dans le futur (avec une marge de 5 secondes)
	expectedExpiry := time.Now().Add(1 * time.Hour)
	assert.WithinDuration(t, expectedExpiry, expiresAt, 5*time.Second, "Token should expire in 1 hour")
}

// TestPasswordResetService_VerifyToken_Valid teste la vérification d'un token valide
func TestPasswordResetService_VerifyToken_Valid(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Générer et stocker un token
	token, err := service.GenerateToken()
	require.NoError(t, err)

	err = service.StoreToken(user.ID, token)
	require.NoError(t, err)

	// Vérifier le token
	userID, err := service.VerifyToken(token)

	assert.NoError(t, err)
	assert.Equal(t, user.ID, userID, "User ID should match")
}

// TestPasswordResetService_VerifyToken_Invalid teste la vérification d'un token invalide
func TestPasswordResetService_VerifyToken_Invalid(t *testing.T) {
	service, _, _ := setupTestPasswordResetService(t)

	// Tenter de vérifier un token inexistant
	userID, err := service.VerifyToken("invalid-token-123")

	assert.Error(t, err)
	assert.Equal(t, uuid.Nil, userID)
	assert.Contains(t, err.Error(), "invalid token")
}

// TestPasswordResetService_VerifyToken_Expired teste la vérification d'un token expiré
func TestPasswordResetService_VerifyToken_Expired(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Créer un token expiré manuellement
	expiredTime := time.Now().Add(-2 * time.Hour)
	token := "expired-token-123"
	err = gormDB.Exec(`
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
		VALUES (?, ?, ?, ?, ?)
	`, user.ID, token, expiredTime, false, time.Now().Add(-3*time.Hour)).Error
	require.NoError(t, err)

	// Tenter de vérifier le token expiré
	userID, err := service.VerifyToken(token)

	assert.Error(t, err)
	assert.Equal(t, uuid.Nil, userID)
	assert.Contains(t, err.Error(), "expired")
}

// TestPasswordResetService_VerifyToken_AlreadyUsed teste la vérification d'un token déjà utilisé
func TestPasswordResetService_VerifyToken_AlreadyUsed(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Créer un token déjà utilisé
	expiresAt := time.Now().Add(1 * time.Hour)
	token := "used-token-123"
	err = gormDB.Exec(`
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
		VALUES (?, ?, ?, ?, ?)
	`, user.ID, token, expiresAt, true, time.Now()).Error
	require.NoError(t, err)

	// Tenter de vérifier le token utilisé
	userID, err := service.VerifyToken(token)

	assert.Error(t, err)
	assert.Equal(t, uuid.Nil, userID)
	assert.Contains(t, err.Error(), "already used")
}

// TestPasswordResetService_MarkTokenAsUsed teste le marquage d'un token comme utilisé
func TestPasswordResetService_MarkTokenAsUsed(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Générer et stocker un token
	token, err := service.GenerateToken()
	require.NoError(t, err)

	err = service.StoreToken(user.ID, token)
	require.NoError(t, err)

	// Marquer le token comme utilisé
	err = service.MarkTokenAsUsed(token)
	assert.NoError(t, err)

	// Vérifier que le token est marqué comme utilisé
	var used bool
	err = gormDB.Raw("SELECT used FROM password_reset_tokens WHERE token = ?", token).Scan(&used).Error
	require.NoError(t, err)
	assert.True(t, used, "Token should be marked as used")
}

// TestPasswordResetService_MarkTokenAsUsed_InvalidToken teste le marquage d'un token inexistant
func TestPasswordResetService_MarkTokenAsUsed_InvalidToken(t *testing.T) {
	service, _, _ := setupTestPasswordResetService(t)

	// Tenter de marquer un token inexistant comme utilisé
	err := service.MarkTokenAsUsed("non-existent-token")

	assert.Error(t, err)
	assert.Contains(t, err.Error(), "token not found")
}

// TestPasswordResetService_InvalidateOldTokens teste l'invalidation des anciens tokens
func TestPasswordResetService_InvalidateOldTokens(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Créer plusieurs tokens non utilisés
	expiresAt := time.Now().Add(1 * time.Hour)
	token1 := "old-token-1"
	token2 := "old-token-2"
	token3 := "old-token-3"

	err = gormDB.Exec(`
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
		VALUES (?, ?, ?, ?, ?)
	`, user.ID, token1, expiresAt, false, time.Now()).Error
	require.NoError(t, err)

	err = gormDB.Exec(`
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
		VALUES (?, ?, ?, ?, ?)
	`, user.ID, token2, expiresAt, false, time.Now()).Error
	require.NoError(t, err)

	err = gormDB.Exec(`
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
		VALUES (?, ?, ?, ?, ?)
	`, user.ID, token3, expiresAt, false, time.Now()).Error
	require.NoError(t, err)

	// Invalider les anciens tokens
	err = service.InvalidateOldTokens(user.ID)
	assert.NoError(t, err)

	// Vérifier que tous les tokens sont marqués comme utilisés
	var count int64
	err = gormDB.Raw("SELECT COUNT(*) FROM password_reset_tokens WHERE user_id = ? AND used = FALSE", user.ID).Scan(&count).Error
	require.NoError(t, err)
	assert.Equal(t, int64(0), count, "All tokens should be invalidated")
}

// TestPasswordResetService_InvalidateOldTokens_OnlyUnused teste que seuls les tokens non utilisés sont invalidés
func TestPasswordResetService_InvalidateOldTokens_OnlyUnused(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Créer un token utilisé et un token non utilisé
	expiresAt := time.Now().Add(1 * time.Hour)
	tokenUsed := "used-token"
	tokenUnused := "unused-token"

	err = gormDB.Exec(`
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
		VALUES (?, ?, ?, ?, ?)
	`, user.ID, tokenUsed, expiresAt, true, time.Now()).Error
	require.NoError(t, err)

	err = gormDB.Exec(`
		INSERT INTO password_reset_tokens (user_id, token, expires_at, used, created_at)
		VALUES (?, ?, ?, ?, ?)
	`, user.ID, tokenUnused, expiresAt, false, time.Now()).Error
	require.NoError(t, err)

	// Invalider les anciens tokens
	err = service.InvalidateOldTokens(user.ID)
	assert.NoError(t, err)

	// Vérifier que le token utilisé reste utilisé et l'autre est invalidé
	var used1, used2 bool
	err = gormDB.Raw("SELECT used FROM password_reset_tokens WHERE token = ?", tokenUsed).Scan(&used1).Error
	require.NoError(t, err)
	err = gormDB.Raw("SELECT used FROM password_reset_tokens WHERE token = ?", tokenUnused).Scan(&used2).Error
	require.NoError(t, err)

	assert.True(t, used1, "Used token should remain used")
	assert.True(t, used2, "Unused token should be invalidated")
}

// TestPasswordResetService_StoreToken_Duplicate teste qu'on ne peut pas stocker deux tokens identiques
func TestPasswordResetService_StoreToken_Duplicate(t *testing.T) {
	service, _, gormDB := setupTestPasswordResetService(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Stocker un token
	token := "duplicate-token"
	err = service.StoreToken(user.ID, token)
	require.NoError(t, err)

	// Tenter de stocker le même token à nouveau
	err = service.StoreToken(user.ID, token)
	assert.Error(t, err, "Should not be able to store duplicate token")
}
