package database

import (
	"fmt"
	"os"
	"strconv"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

// setupPoolTestDB crée une connexion de test à la base de données pour les tests de pool
// Nécessite une base de données PostgreSQL en cours d'exécution
func setupPoolTestDB(t *testing.T) *gorm.DB {
	// Récupérer les variables d'environnement ou utiliser des valeurs par défaut
	host := getEnv("DB_HOST", "localhost")
	port := getEnvInt("DB_PORT", 5432)
	user := getEnv("DB_USER", "veza")
	password := getEnv("DB_PASSWORD", "veza_password")
	dbname := getEnv("DB_NAME", "veza_db")

	dsn := buildDSN(host, port, user, password, dbname)

	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		t.Skipf("Skipping test: cannot connect to database: %v", err)
		return nil
	}

	// Configurer le pool de connexions pour les tests
	sqlDB, err := db.DB()
	if err != nil {
		t.Skipf("Skipping test: cannot get underlying sql.DB: %v", err)
		return nil
	}

	sqlDB.SetMaxOpenConns(5) // Moins de connexions pour les tests
	sqlDB.SetMaxIdleConns(2)
	sqlDB.SetConnMaxLifetime(1 * time.Minute)
	sqlDB.SetConnMaxIdleTime(30 * time.Second)

	// Tester la connexion
	if err := sqlDB.Ping(); err != nil {
		t.Skipf("Skipping test: cannot ping database: %v", err)
		return nil
	}

	return db
}

// Helper functions
func getEnv(key, defaultValue string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
	value := os.Getenv(key)
	if value != "" {
		if intValue, err := strconv.Atoi(value); err == nil {
			return intValue
		}
	}
	return defaultValue
}

func buildDSN(host string, port int, user, password, dbname string) string {
	return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable",
		host, user, password, dbname, port)
}

func TestNewDB(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	host := getEnv("DB_HOST", "localhost")
	port := getEnvInt("DB_PORT", 5432)
	user := getEnv("DB_USER", "veza")
	password := getEnv("DB_PASSWORD", "veza_password")
	dbname := getEnv("DB_NAME", "veza_db")

	// Test de création de connexion
	db, err := NewDB(host, port, user, password, dbname)
	if err != nil {
		t.Skipf("Skipping test: cannot connect to database: %v", err)
		return
	}
	require.NotNil(t, db)
	defer CloseDB(db)

	// Vérifier que la connexion fonctionne
	sqlDB, err := db.DB()
	require.NoError(t, err)
	require.NotNil(t, sqlDB)

	// Vérifier les paramètres du pool
	stats := sqlDB.Stats()
	assert.Equal(t, 25, stats.MaxOpenConnections, "MaxOpenConns should be 25")
}

func TestNewDB_InvalidCredentials(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	// Test avec des credentials invalides
	_, err := NewDB("localhost", 5432, "invalid_user", "invalid_password", "invalid_db")
	require.Error(t, err)
	assert.Contains(t, err.Error(), "failed to open database")
}

func TestCloseDB(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	db := setupPoolTestDB(t)
	if db == nil {
		return
	}

	// Fermer la connexion
	err := CloseDB(db)
	assert.NoError(t, err)

	// Vérifier que la connexion est fermée
	sqlDB, err := db.DB()
	require.NoError(t, err)

	err = sqlDB.Ping()
	assert.Error(t, err, "Connection should be closed")
}

func TestCloseDB_NilDB(t *testing.T) {
	// Test avec une DB nil
	err := CloseDB(nil)
	assert.NoError(t, err, "Closing nil DB should not return error")
}

func TestGetPoolStats(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	db := setupPoolTestDB(t)
	if db == nil {
		return
	}
	defer CloseDB(db)

	stats, err := GetPoolStats(db)
	require.NoError(t, err)
	require.NotNil(t, stats)

	// Vérifier que les statistiques contiennent des informations valides
	assert.GreaterOrEqual(t, stats.MaxOpenConnections, 0)
	assert.GreaterOrEqual(t, stats.OpenConnections, 0)
	assert.GreaterOrEqual(t, stats.InUse, 0)
	assert.GreaterOrEqual(t, stats.Idle, 0)
}

func TestGetPoolStats_NilDB(t *testing.T) {
	_, err := GetPoolStats(nil)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "database connection is nil")
}

func TestIsConnectionHealthy(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	db := setupPoolTestDB(t)
	if db == nil {
		return
	}
	defer CloseDB(db)

	// Test avec un timeout suffisant
	err := IsConnectionHealthy(db, 5*time.Second)
	assert.NoError(t, err, "Healthy connection should not return error")
}

func TestIsConnectionHealthy_Timeout(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	db := setupPoolTestDB(t)
	if db == nil {
		return
	}
	defer CloseDB(db)

	// Test avec un timeout très court (devrait timeout)
	// Note: Ce test peut être flaky, mais il vérifie le comportement de timeout
	err := IsConnectionHealthy(db, 1*time.Nanosecond)
	// Le timeout peut ne pas se produire si la connexion est très rapide
	// Donc on accepte soit une erreur de timeout, soit pas d'erreur
	if err != nil {
		assert.Contains(t, err.Error(), "timeout")
	}
}

func TestIsConnectionHealthy_NilDB(t *testing.T) {
	err := IsConnectionHealthy(nil, 5*time.Second)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "database connection is nil")
}

func TestDBPool_ConnectionPooling(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	db := setupPoolTestDB(t)
	if db == nil {
		return
	}
	defer CloseDB(db)

	sqlDB, err := db.DB()
	require.NoError(t, err)

	// Vérifier les paramètres du pool
	stats := sqlDB.Stats()
	_ = stats.OpenConnections // Vérification que le pool fonctionne

	// Simuler plusieurs requêtes pour utiliser le pool
	for i := 0; i < 10; i++ {
		var result int
		err := sqlDB.QueryRow("SELECT 1").Scan(&result)
		require.NoError(t, err)
		assert.Equal(t, 1, result)
	}

	// Vérifier que les connexions sont réutilisées (le nombre ne devrait pas augmenter significativement)
	stats = sqlDB.Stats()
	// Le nombre de connexions ouvertes ne devrait pas dépasser MaxOpenConns
	assert.LessOrEqual(t, stats.OpenConnections, 25, "Open connections should not exceed MaxOpenConns")
}

func TestDBPool_MaxConnections(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping integration test in short mode")
	}

	db := setupPoolTestDB(t)
	if db == nil {
		return
	}
	defer CloseDB(db)

	sqlDB, err := db.DB()
	require.NoError(t, err)

	// Vérifier que MaxOpenConns est configuré
	stats := sqlDB.Stats()
	assert.Equal(t, 25, stats.MaxOpenConnections, "MaxOpenConns should be 25")
}

// Test de performance: vérifier que le pool peut gérer 100+ connexions simultanées
func TestDBPool_Performance(t *testing.T) {
	if testing.Short() {
		t.Skip("Skipping performance test in short mode")
	}

	db := setupPoolTestDB(t)
	if db == nil {
		return
	}
	defer CloseDB(db)

	sqlDB, err := db.DB()
	require.NoError(t, err)

	// Simuler 100 requêtes simultanées
	const numRequests = 100
	results := make(chan error, numRequests)

	for i := 0; i < numRequests; i++ {
		go func() {
			var result int
			err := sqlDB.QueryRow("SELECT $1", 1).Scan(&result)
			results <- err
		}()
	}

	// Collecter tous les résultats
	var errors int
	for i := 0; i < numRequests; i++ {
		if err := <-results; err != nil {
			errors++
		}
	}

	// Toutes les requêtes devraient réussir
	assert.Equal(t, 0, errors, "All requests should succeed")

	// Vérifier les statistiques du pool
	stats := sqlDB.Stats()
	assert.LessOrEqual(t, stats.OpenConnections, stats.MaxOpenConnections,
		"Open connections should not exceed MaxOpenConns")
}
