package services

import (
	"crypto/sha256"
	"encoding/hex"
	"github.com/google/uuid"
	"testing"
	"time"

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

// setupTestSessionServiceForT0202 crée un SessionService de test avec la table sessions (BIGINT user_id)
func setupTestSessionServiceForT0202(t *testing.T) (*SessionService, *gorm.DB, *database.Database) {
	// 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 sessions manuellement (selon migration T0201)
	err = gormDB.Exec(`
		CREATE TABLE sessions (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
			token_hash TEXT NOT NULL UNIQUE,
			ip_address TEXT,
			user_agent TEXT,
			expires_at TIMESTAMP NOT NULL,
			last_activity TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
			created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
		)
	`).Error
	require.NoError(t, err, "Failed to create sessions table")

	// Créer les index
	err = gormDB.Exec("CREATE INDEX idx_sessions_user_id ON sessions(user_id)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_sessions_token_hash ON sessions(token_hash)").Error
	require.NoError(t, err)
	err = gormDB.Exec("CREATE INDEX idx_sessions_expires_at ON sessions(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
	testDB := &database.Database{
		DB: sqlDB,
	}

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

	return service, gormDB, testDB
}

// hashToken helper pour les tests
func hashTokenForTest(token string) string {
	hash := sha256.Sum256([]byte(token))
	return hex.EncodeToString(hash[:])
}

// TestSessionService_CreateSessionForT0202_Success teste la création d'une session
func TestSessionService_CreateSessionForT0202_Success(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session
	token := "test-token-123"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	assert.NoError(t, err, "Should create session successfully")

	// Vérifier que la session a été créée
	tokenHash := hashTokenForTest(token)
	var count int64
	err = gormDB.Raw("SELECT COUNT(*) FROM sessions WHERE token_hash = ?", tokenHash).Scan(&count).Error
	require.NoError(t, err)
	assert.Equal(t, int64(1), count, "Session should be created")
}

// TestSessionService_CreateSessionForT0202_InvalidUserID teste avec un user_id invalide
func TestSessionService_CreateSessionForT0202_InvalidUserID(t *testing.T) {
	service, _, _ := setupTestSessionServiceForT0202(t)

	token := "test-token-123"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	// Essayer de créer une session avec un user_id inexistant
	err := service.CreateSessionWithBIGINT(99999, token, ipAddress, userAgent, expiresAt)
	assert.Error(t, err, "Should fail with invalid user_id")
}

// TestSessionService_GetSession_Success teste la récupération d'une session
func TestSessionService_GetSession_Success(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session
	token := "test-token-456"
	ipAddress := "192.168.1.2"
	userAgent := "Chrome"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	// Récupérer la session
	tokenHash := hashTokenForTest(token)
	session, err := service.GetSessionWithBIGINT(tokenHash)
	assert.NoError(t, err, "Should get session successfully")
	assert.NotNil(t, session)
	assert.Equal(t, user.ID, session.UserID)
	assert.Equal(t, tokenHash, session.TokenHash)
	assert.Equal(t, ipAddress, session.IPAddress)
	assert.Equal(t, userAgent, session.UserAgent)
}

// TestSessionService_GetSession_NotFound teste la récupération d'une session inexistante
func TestSessionService_GetSession_NotFound(t *testing.T) {
	service, _, _ := setupTestSessionServiceForT0202(t)

	// Essayer de récupérer une session inexistante
	tokenHash := hashTokenForTest("non-existent-token")
	session, err := service.GetSessionWithBIGINT(tokenHash)
	assert.Error(t, err, "Should return error for non-existent session")
	assert.Nil(t, session)
	assert.Contains(t, err.Error(), "session not found")
}

// TestSessionService_GetSession_Expired teste la récupération d'une session expirée
func TestSessionService_GetSession_Expired(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session expirée directement dans la DB
	token := "expired-token"
	tokenHash := hashTokenForTest(token)
	expiredTime := time.Now().Add(-1 * time.Hour) // Expirée il y a 1 heure

	err = gormDB.Exec(`
		INSERT INTO sessions (user_id, token_hash, ip_address, user_agent, expires_at, last_activity, created_at)
		VALUES (?, ?, ?, ?, ?, ?, ?)
	`, user.ID, tokenHash, "192.168.1.1", "Mozilla/5.0", expiredTime, time.Now(), time.Now()).Error
	require.NoError(t, err)

	// Essayer de récupérer la session expirée
	session, err := service.GetSessionWithBIGINT(tokenHash)
	assert.Error(t, err, "Should return error for expired session")
	assert.Nil(t, session)
}

// TestSessionService_UpdateLastActivity_Success teste la mise à jour de last_activity
func TestSessionService_UpdateLastActivity_Success(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session
	token := "test-token-update"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	// Récupérer la session initiale pour obtenir last_activity
	tokenHash := hashTokenForTest(token)
	sessionBefore, err := service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)
	initialLastActivity := sessionBefore.LastActivity

	// Attendre un peu pour s'assurer que le temps change
	time.Sleep(100 * time.Millisecond)

	// Mettre à jour last_activity
	err = service.UpdateLastActivity(tokenHash)
	assert.NoError(t, err, "Should update last_activity successfully")

	// Vérifier que last_activity a été mis à jour
	sessionAfter, err := service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)
	assert.True(t, sessionAfter.LastActivity.After(initialLastActivity), "Last activity should be updated")
}

// TestSessionService_UpdateLastActivity_NotFound teste la mise à jour d'une session inexistante
func TestSessionService_UpdateLastActivity_NotFound(t *testing.T) {
	service, _, _ := setupTestSessionServiceForT0202(t)

	// Essayer de mettre à jour une session inexistante
	tokenHash := hashTokenForTest("non-existent-token")
	err := service.UpdateLastActivity(tokenHash)
	assert.Error(t, err, "Should return error for non-existent session")
	assert.Contains(t, err.Error(), "session not found")
}

// TestSessionService_DeleteSession_Success teste la suppression d'une session
func TestSessionService_DeleteSession_Success(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session
	token := "test-token-delete"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	// Vérifier que la session existe
	tokenHash := hashTokenForTest(token)
	session, err := service.GetSessionWithBIGINT(tokenHash)
	assert.NoError(t, err)
	assert.NotNil(t, session)

	// Supprimer la session
	err = service.DeleteSession(tokenHash)
	assert.NoError(t, err, "Should delete session successfully")

	// Vérifier que la session a été supprimée
	session, err = service.GetSessionWithBIGINT(tokenHash)
	assert.Error(t, err, "Session should not exist after deletion")
	assert.Nil(t, session)
}

// TestSessionService_DeleteSession_NotFound teste la suppression d'une session inexistante
func TestSessionService_DeleteSession_NotFound(t *testing.T) {
	service, _, _ := setupTestSessionServiceForT0202(t)

	// Essayer de supprimer une session inexistante
	tokenHash := hashTokenForTest("non-existent-token")
	err := service.DeleteSession(tokenHash)
	assert.Error(t, err, "Should return error for non-existent session")
	assert.Contains(t, err.Error(), "session not found")
}

// TestSessionService_DeleteAllUserSessions_Success teste la suppression de toutes les sessions d'un utilisateur
func TestSessionService_DeleteAllUserSessions_Success(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer plusieurs sessions
	token1 := "token-1"
	token2 := "token-2"
	token3 := "token-3"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token1, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)
	err = service.CreateSessionWithBIGINT(user.ID, token2, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)
	err = service.CreateSessionWithBIGINT(user.ID, token3, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	// Vérifier que les sessions existent
	var count int64
	err = gormDB.Raw("SELECT COUNT(*) FROM sessions WHERE user_id = ?", user.ID).Scan(&count).Error
	require.NoError(t, err)
	assert.Equal(t, int64(3), count, "Should have 3 sessions")

	// Supprimer toutes les sessions
	err = service.DeleteAllUserSessions(user.ID)
	assert.NoError(t, err, "Should delete all user sessions successfully")

	// Vérifier que toutes les sessions ont été supprimées
	err = gormDB.Raw("SELECT COUNT(*) FROM sessions WHERE user_id = ?", user.ID).Scan(&count).Error
	require.NoError(t, err)
	assert.Equal(t, int64(0), count, "All sessions should be deleted")
}

// TestSessionService_DeleteAllUserSessions_NoSessions teste la suppression quand il n'y a pas de sessions
func TestSessionService_DeleteAllUserSessions_NoSessions(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Supprimer toutes les sessions (il n'y en a pas)
	err = service.DeleteAllUserSessions(user.ID)
	assert.NoError(t, err, "Should not error when no sessions exist")
}

// TestSessionService_DeleteAllUserSessions_MultipleUsers teste que seul l'utilisateur spécifié est affecté
func TestSessionService_DeleteAllUserSessions_MultipleUsers(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(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)

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

	// Créer des sessions pour les deux utilisateurs
	token1 := "token-user1"
	token2 := "token-user2"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user1.ID, token1, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)
	err = service.CreateSessionWithBIGINT(user2.ID, token2, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	// Supprimer toutes les sessions de user1
	err = service.DeleteAllUserSessions(user1.ID)
	assert.NoError(t, err)

	// Vérifier que seule la session de user1 a été supprimée
	var count1, count2 int64
	err = gormDB.Raw("SELECT COUNT(*) FROM sessions WHERE user_id = ?", user1.ID).Scan(&count1).Error
	require.NoError(t, err)
	err = gormDB.Raw("SELECT COUNT(*) FROM sessions WHERE user_id = ?", user2.ID).Scan(&count2).Error
	require.NoError(t, err)
	assert.Equal(t, int64(0), count1, "User1 sessions should be deleted")
	assert.Equal(t, int64(1), count2, "User2 session should still exist")
}

// TestSessionService_CreateSession_UniqueTokenHash teste que le token_hash doit être unique
func TestSessionService_CreateSession_UniqueTokenHash(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une première session
	token := "duplicate-token"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	// Essayer de créer une deuxième session avec le même token
	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	assert.Error(t, err, "Should fail with duplicate token_hash")
}

// TestSessionService_GetSession_AllFields teste que tous les champs sont correctement récupérés
func TestSessionService_GetSession_AllFields(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session avec tous les champs
	token := "test-token-all-fields"
	ipAddress := "192.168.1.100"
	userAgent := "Custom User Agent/1.0"
	expiresAt := time.Now().Add(48 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	// Récupérer la session
	tokenHash := hashTokenForTest(token)
	session, err := service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)

	// Vérifier tous les champs
	assert.NotZero(t, session.ID, "ID should be set")
	assert.Equal(t, user.ID, session.UserID, "UserID should match")
	assert.Equal(t, tokenHash, session.TokenHash, "TokenHash should match")
	assert.Equal(t, ipAddress, session.IPAddress, "IPAddress should match")
	assert.Equal(t, userAgent, session.UserAgent, "UserAgent should match")
	assert.False(t, session.ExpiresAt.IsZero(), "ExpiresAt should be set")
	assert.False(t, session.LastActivity.IsZero(), "LastActivity should be set")
	assert.False(t, session.CreatedAt.IsZero(), "CreatedAt should be set")
}

// TestSessionService_UpdateLastActivity_MultipleUpdates teste plusieurs mises à jour
func TestSessionService_UpdateLastActivity_MultipleUpdates(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0202(t)

	// Récupérer l'utilisateur
	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session
	token := "test-token-multiple-updates"
	ipAddress := "192.168.1.1"
	userAgent := "Mozilla/5.0"
	expiresAt := time.Now().Add(24 * time.Hour)

	err = service.CreateSessionWithBIGINT(user.ID, token, ipAddress, userAgent, expiresAt)
	require.NoError(t, err)

	tokenHash := hashTokenForTest(token)

	// Mettre à jour plusieurs fois
	err = service.UpdateLastActivity(tokenHash)
	assert.NoError(t, err)

	time.Sleep(50 * time.Millisecond)

	err = service.UpdateLastActivity(tokenHash)
	assert.NoError(t, err)

	time.Sleep(50 * time.Millisecond)

	err = service.UpdateLastActivity(tokenHash)
	assert.NoError(t, err)

	// Vérifier que la session existe toujours et que last_activity a été mis à jour
	session, err := service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)
	assert.NotNil(t, session)
}
