package services

import (
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"testing"
	"time"
	"unsafe"

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

	"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"
)

// setupTestEmailVerificationService crée un EmailVerificationService de test avec une base de données en mémoire
func setupTestEmailVerificationService(t *testing.T) (*EmailVerificationService, *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 email_verification_tokens manuellement avec le schéma complet
	// Correspond à migrations/010_auth_and_users.sql
	err = gormDB.Exec(`
		CREATE TABLE email_verification_tokens (
			id TEXT PRIMARY KEY,
			user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
			token TEXT NOT NULL UNIQUE,
			token_hash TEXT NOT NULL,
			email TEXT NOT NULL,
			verified INTEGER NOT NULL DEFAULT 0,
			used INTEGER NOT NULL DEFAULT 0,
			verified_at TIMESTAMP,
			expires_at TIMESTAMP NOT NULL,
			created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
		)
	`).Error
	require.NoError(t, err, "Failed to create email_verification_tokens table")

	// Créer les index
	err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_token ON email_verification_tokens(token)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_user_id ON email_verification_tokens(user_id)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_token_hash ON email_verification_tokens(token_hash)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_email ON email_verification_tokens(email)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_email_verification_tokens_expires_at ON email_verification_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 *sql.DB depuis GORM
	sqlDB, err := gormDB.DB()
	require.NoError(t, err, "Failed to get sql.DB from GORM")

	// Créer un database.Database de test
	// database.Database embeds *sql.DB directement, donc on crée une structure vide puis on assigne
	// Pour les tests, on crée un Database avec sqlDB embedé
	// Note: On ne peut pas initialiser un type embedé dans un struct literal, donc on utilise une approche alternative
	// On crée un Database avec les champs nécessaires
	testDB := &database.Database{}
	// On assigne sqlDB via un type composite qui contient *sql.DB
	// Mais comme *sql.DB est embedé, on doit utiliser une approche différente
	// Solution: créer un Database temporaire pour obtenir la structure, puis copier sqlDB
	// Ou utiliser une fonction helper qui crée un Database avec sqlDB

	// Pour les tests, on va créer un Database minimal en utilisant reflection ou une fonction helper
	// Mais la solution la plus simple: utiliser directement sqlDB dans les tests via un wrapper
	// Créons un Database avec sqlDB assigné manuellement via une fonction helper de test
	testDB = createTestDatabase(sqlDB)

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

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

	return service, testDB, gormDB
}

// createTestDatabase crée un database.Database de test avec un *sql.DB
// database.Database embeds *sql.DB, donc on utilise une structure temporaire avec le même layout
func createTestDatabase(sqlDB *sql.DB) *database.Database {
	// Créer une structure temporaire avec le même layout que database.Database
	// database.Database a *sql.DB en premier (embedé), puis GormDB, config, logger
	type tempDB struct {
		*sql.DB
		gormDB *gorm.DB
		config interface{}
		logger interface{}
	}

	// Créer la structure temporaire avec sqlDB
	temp := &tempDB{
		DB: sqlDB,
	}

	// Convertir en database.Database en utilisant unsafe.Pointer
	// Note: Cette conversion est sûre car les deux structures ont *sql.DB en premier
	// et on n'utilise que les méthodes de *sql.DB dans les tests
	return (*database.Database)(unsafe.Pointer(temp))
}

func TestEmailVerificationService_GenerateToken(t *testing.T) {
	logger, _ := zap.NewDevelopment()
	service := &EmailVerificationService{
		db:     nil, // Pas besoin pour GenerateToken
		logger: logger,
	}

	token, err := service.GenerateToken()
	assert.NoError(t, err)
	assert.NotEmpty(t, token)
	assert.GreaterOrEqual(t, len(token), 32) // base64 URL encoding de 32 bytes donne ~43 caractères
}

func TestEmailVerificationService_GenerateToken_Unique(t *testing.T) {
	logger, _ := zap.NewDevelopment()
	service := &EmailVerificationService{
		db:     nil,
		logger: logger,
	}

	token1, err1 := service.GenerateToken()
	require.NoError(t, err1)

	token2, err2 := service.GenerateToken()
	require.NoError(t, err2)

	assert.NotEqual(t, token1, token2, "Tokens should be unique")
}

func TestEmailVerificationService_StoreToken(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	token, err := service.GenerateToken()
	require.NoError(t, err)

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

	// Vérifier que le token a été stocké
	var count int64
	sqlDB, _ := gormDB.DB()
	err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND token = ?", user.ID, token).Scan(&count)
	assert.NoError(t, err)
	assert.Equal(t, int64(1), count)
}

func TestEmailVerificationService_StoreToken_Expiration(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	token, err := service.GenerateToken()
	require.NoError(t, err)

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

	// Vérifier que l'expiration est dans 24h (avec une marge de ±1 minute)
	var expiresAt time.Time
	sqlDB, _ := gormDB.DB()
	err = sqlDB.QueryRow("SELECT expires_at FROM email_verification_tokens WHERE token = ?", token).Scan(&expiresAt)
	assert.NoError(t, err)

	expectedExpiration := time.Now().Add(24 * time.Hour)
	diff := expiresAt.Sub(expectedExpiration)
	assert.True(t, diff < time.Minute && diff > -time.Minute, "Expiration should be approximately 24h from now")
}

func TestEmailVerificationService_VerifyToken_ValidToken(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	token, err := service.GenerateToken()
	require.NoError(t, err)

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

	userID, err := service.VerifyToken(token)
	assert.NoError(t, err)
	assert.Equal(t, user.ID, userID)

	// Vérifier que le token a été marqué comme utilisé
	var used bool
	sqlDB, _ := gormDB.DB()
	err = sqlDB.QueryRow("SELECT used FROM email_verification_tokens WHERE token = ?", token).Scan(&used)
	assert.NoError(t, err)
	assert.True(t, used)
}

func TestEmailVerificationService_VerifyToken_InvalidToken(t *testing.T) {
	service, _, _ := setupTestEmailVerificationService(t)

	invalidToken := "invalid-token-123"

	userID, err := service.VerifyToken(invalidToken)
	assert.Error(t, err)
	assert.Equal(t, uuid.Nil, userID)
	assert.Contains(t, err.Error(), "invalid token")
}

// hashToken helper pour hasher le token (même logique que dans le service)
func hashTokenForTest(token string) string {
	hash := sha256.Sum256([]byte(token))
	return hex.EncodeToString(hash[:])
}

func TestEmailVerificationService_VerifyToken_ExpiredToken(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	token, err := service.GenerateToken()
	require.NoError(t, err)
	tokenHash := hashTokenForTest(token)

	// Insérer un token expiré directement
	sqlDB, _ := gormDB.DB()
	expiredAt := time.Now().Add(-1 * time.Hour) // Expiré il y a 1 heure
	_, err = sqlDB.Exec(
		"INSERT INTO email_verification_tokens (user_id, email, token, token_hash, expires_at, used) VALUES (?, ?, ?, ?, ?, 0)",
		user.ID, user.Email, token, tokenHash, expiredAt,
	)
	require.NoError(t, err)

	userID, err := service.VerifyToken(token)
	assert.Error(t, err)
	assert.Equal(t, uuid.Nil, userID)
	assert.Contains(t, err.Error(), "token expired")
}

func TestEmailVerificationService_VerifyToken_AlreadyUsed(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	token, err := service.GenerateToken()
	require.NoError(t, err)
	tokenHash := hashTokenForTest(token)

	// Insérer un token déjà utilisé
	sqlDB, _ := gormDB.DB()
	expiresAt := time.Now().Add(24 * time.Hour)
	_, err = sqlDB.Exec(
		"INSERT INTO email_verification_tokens (user_id, email, token, token_hash, expires_at, used) VALUES (?, ?, ?, ?, ?, 1)",
		user.ID, user.Email, token, tokenHash, expiresAt,
	)
	require.NoError(t, err)

	userID, err := service.VerifyToken(token)
	assert.Error(t, err)
	assert.Equal(t, uuid.Nil, userID)
	assert.Contains(t, err.Error(), "token already used")
}

func TestEmailVerificationService_VerifyToken_CannotReuse(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	token, err := service.GenerateToken()
	require.NoError(t, err)

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

	// Première vérification - devrait réussir
	userID, err := service.VerifyToken(token)
	assert.NoError(t, err)
	assert.Equal(t, user.ID, userID)

	// Deuxième vérification - devrait échouer car déjà utilisé
	userID2, err2 := service.VerifyToken(token)
	assert.Error(t, err2)
	assert.Equal(t, uuid.Nil, userID2)
	assert.Contains(t, err2.Error(), "token already used")
}

func TestEmailVerificationService_InvalidateOldTokens(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Créer plusieurs tokens pour le même utilisateur
	token1, err := service.GenerateToken()
	require.NoError(t, err)
	err = service.StoreToken(user.ID, user.Email, token1)
	require.NoError(t, err)

	token2, err := service.GenerateToken()
	require.NoError(t, err)
	err = service.StoreToken(user.ID, user.Email, token2)
	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
	sqlDB, _ := gormDB.DB()
	var count int
	err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND used = 0", user.ID).Scan(&count)
	assert.NoError(t, err)
	assert.Equal(t, 0, count, "All tokens should be invalidated")
}

func TestEmailVerificationService_InvalidateOldTokens_NoTokens(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	var user models.User
	err := gormDB.Where("email = ?", "test@example.com").First(&user).Error
	require.NoError(t, err)

	// Invalider les tokens pour un utilisateur sans tokens
	err = service.InvalidateOldTokens(user.ID)
	assert.NoError(t, err) // Ne devrait pas retourner d'erreur même s'il n'y a pas de tokens
}

func TestEmailVerificationService_InvalidateOldTokens_MultipleUsers(t *testing.T) {
	service, _, gormDB := setupTestEmailVerificationService(t)

	// Créer un deuxième utilisateur
	user2 := &models.User{
		Email:    "user2@example.com",
		Username: "user2",
		Role:     "user",
		IsActive: true,
	}
	err := gormDB.Create(user2).Error
	require.NoError(t, err)

	var user1 models.User
	err = gormDB.Where("email = ?", "test@example.com").First(&user1).Error
	require.NoError(t, err)

	// Créer des tokens pour les deux utilisateurs
	token1, err := service.GenerateToken()
	require.NoError(t, err)
	err = service.StoreToken(user1.ID, user1.Email, token1)
	require.NoError(t, err)

	token2, err := service.GenerateToken()
	require.NoError(t, err)
	err = service.StoreToken(user2.ID, user2.Email, token2)
	require.NoError(t, err)

	// Invalider uniquement les tokens de user1
	err = service.InvalidateOldTokens(user1.ID)
	assert.NoError(t, err)

	// Vérifier que seul le token de user1 est invalidé
	sqlDB, _ := gormDB.DB()
	var count1 int
	err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND used = 0", user1.ID).Scan(&count1)
	assert.NoError(t, err)
	assert.Equal(t, 0, count1, "User1 tokens should be invalidated")

	var count2 int
	err = sqlDB.QueryRow("SELECT COUNT(*) FROM email_verification_tokens WHERE user_id = ? AND used = 0", user2.ID).Scan(&count2)
	assert.NoError(t, err)
	assert.Equal(t, 1, count2, "User2 tokens should not be invalidated")
}
