package services

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

// setupTestSessionServiceForT0204 crée un SessionService de test avec la table sessions
func setupTestSessionServiceForT0204(t *testing.T) (*SessionService, *gorm.DB, *database.Database) {
	gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
	require.NoError(t, err)

	err = gormDB.AutoMigrate(&models.User{})
	require.NoError(t, err)

	// Créer la table sessions
	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)

	user := &models.User{
		Email:    "test@example.com",
		Username: "testuser",
		Role:     "user",
		IsActive: true,
	}
	err = gormDB.Create(user).Error
	require.NoError(t, err)

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

	testDB := &database.Database{
		DB: sqlDB,
	}

	logger, _ := zap.NewDevelopment()
	service := NewSessionService(testDB, logger)

	return service, gormDB, testDB
}

// TestUpdateLastActivityIfNeeded_Debounce teste que le debounce fonctionne correctement
func TestUpdateLastActivityIfNeeded_Debounce(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0204(t)

	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session
	token := "test-token-debounce"
	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)

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

	// Attendre un peu
	time.Sleep(50 * time.Millisecond)

	// Première mise à jour (devrait mettre à jour)
	err = service.UpdateLastActivityIfNeeded(tokenHash, 100*time.Millisecond)
	assert.NoError(t, err)

	// Vérifier que last_activity a été mis à jour
	session, err = service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)
	assert.True(t, session.LastActivity.After(initialLastActivity), "First update should update last_activity")

	// Deuxième mise à jour immédiatement (devrait être ignorée par debounce)
	timeBeforeSecond := session.LastActivity
	err = service.UpdateLastActivityIfNeeded(tokenHash, 100*time.Millisecond)
	assert.NoError(t, err)

	// Vérifier que last_activity n'a pas changé (debounce)
	session, err = service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)
	assert.Equal(t, timeBeforeSecond.Unix(), session.LastActivity.Unix(), "Second update should be debounced")

	// Attendre plus que le debounce duration
	time.Sleep(150 * time.Millisecond)

	// Troisième mise à jour après le debounce (devrait mettre à jour)
	err = service.UpdateLastActivityIfNeeded(tokenHash, 100*time.Millisecond)
	assert.NoError(t, err)

	// Vérifier que last_activity a été mis à jour
	session, err = service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)
	assert.True(t, session.LastActivity.After(timeBeforeSecond), "Third update after debounce should update last_activity")
}

// TestUpdateLastActivityIfNeeded_ErrorHandling teste que les erreurs sont gérées silencieusement
func TestUpdateLastActivityIfNeeded_ErrorHandling(t *testing.T) {
	service, _, _ := setupTestSessionServiceForT0204(t)

	// Essayer de mettre à jour une session inexistante
	// L'erreur ne doit pas être retournée (gestion silencieuse)
	tokenHash := hashTokenForTest("non-existent-token")
	err := service.UpdateLastActivityIfNeeded(tokenHash, 5*time.Minute)
	assert.NoError(t, err, "Error should be handled silently")
}

// TestUpdateLastActivityIfNeeded_FirstUpdateAlwaysUpdates teste que la première mise à jour met toujours à jour
func TestUpdateLastActivityIfNeeded_FirstUpdateAlwaysUpdates(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0204(t)

	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer une session
	token := "test-token-first-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)

	tokenHash := hashTokenForTest(token)

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

	// Attendre un peu
	time.Sleep(50 * time.Millisecond)

	// Première mise à jour (devrait toujours mettre à jour)
	err = service.UpdateLastActivityIfNeeded(tokenHash, 5*time.Minute)
	assert.NoError(t, err)

	// Vérifier que last_activity a été mis à jour
	session, err = service.GetSessionWithBIGINT(tokenHash)
	require.NoError(t, err)
	assert.True(t, session.LastActivity.After(initialLastActivity), "First update should always update")
}

// TestUpdateLastActivityIfNeeded_MultipleTokens teste que le debounce fonctionne pour plusieurs tokens différents
func TestUpdateLastActivityIfNeeded_MultipleTokens(t *testing.T) {
	service, gormDB, _ := setupTestSessionServiceForT0204(t)

	var user models.User
	err := gormDB.First(&user).Error
	require.NoError(t, err)

	// Créer deux sessions
	token1 := "token-1"
	token2 := "token-2"
	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)

	tokenHash1 := hashTokenForTest(token1)
	tokenHash2 := hashTokenForTest(token2)

	// Mettre à jour token1
	err = service.UpdateLastActivityIfNeeded(tokenHash1, 100*time.Millisecond)
	assert.NoError(t, err)

	// Mettre à jour token2 immédiatement (devrait fonctionner car c'est un token différent)
	err = service.UpdateLastActivityIfNeeded(tokenHash2, 100*time.Millisecond)
	assert.NoError(t, err)

	// Vérifier que les deux sessions ont été mises à jour
	session1, err := service.GetSessionWithBIGINT(tokenHash1)
	require.NoError(t, err)
	session2, err := service.GetSessionWithBIGINT(tokenHash2)
	require.NoError(t, err)

	// Les deux devraient avoir été mises à jour (tokens différents)
	assert.True(t, time.Since(session1.LastActivity) < 1*time.Second, "Session1 should be updated")
	assert.True(t, time.Since(session2.LastActivity) < 1*time.Second, "Session2 should be updated")
}

// TestHashTokenForMiddleware teste que HashTokenForMiddleware retourne le bon hash
func TestHashTokenForMiddleware(t *testing.T) {
	service, _, _ := setupTestSessionServiceForT0204(t)

	token := "test-token-hash"
	hash1 := service.HashTokenForMiddleware(token)
	hash2 := service.HashTokenForMiddleware(token)

	// Le hash doit être consistant
	assert.Equal(t, hash1, hash2, "Hash should be consistent")

	// Le hash doit être différent pour un token différent
	token2 := "test-token-hash-2"
	hash3 := service.HashTokenForMiddleware(token2)
	assert.NotEqual(t, hash1, hash3, "Different tokens should have different hashes")

	// Le hash doit avoir une longueur raisonnable (SHA256 = 64 caractères hex)
	assert.Equal(t, 64, len(hash1), "SHA256 hash should be 64 characters")
}

