package services

import (
	"context"
	"os"
	"testing"
	"time"

	"github.com/redis/go-redis/v9"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// setupTestRedisClient crée un client Redis de test
// Utilise Redis en mémoire ou un Redis de test si disponible
func setupTestRedisClient(t *testing.T) *redis.Client {
	redisURL := os.Getenv("REDIS_TEST_URL")
	if redisURL == "" {
		redisURL = "redis://localhost:6379/15" // Utilise DB 15 pour les tests
	}

	opts, err := redis.ParseURL(redisURL)
	if err != nil {
		t.Skipf("Skipping test: failed to parse Redis URL: %v", err)
		return nil
	}

	client := redis.NewClient(opts)

	// Test de connexion
	ctx := context.Background()
	_, err = client.Ping(ctx).Result()
	if err != nil {
		t.Skipf("Skipping test: Redis not available: %v", err)
		return nil
	}

	// Nettoyer la base de données de test
	client.FlushDB(ctx)

	// Cleanup: Flush DB après les tests
	t.Cleanup(func() {
		client.FlushDB(ctx)
		client.Close()
	})

	return client
}

// setupTestTokenBlacklist crée un TokenBlacklist de test
func setupTestTokenBlacklist(t *testing.T) (*TokenBlacklist, *redis.Client) {
	client := setupTestRedisClient(t)
	if client == nil {
		t.Skip("Redis not available")
		return nil, nil
	}

	blacklist := NewTokenBlacklist(client)
	return blacklist, client
}

// T0174: Tests pour TokenBlacklist
func TestTokenBlacklist_Add(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token := "test_token_123"
	ttl := 1 * time.Hour

	err := blacklist.Add(ctx, token, ttl)
	assert.NoError(t, err)

	// Vérifier que le token est dans la blacklist
	isBlacklisted, err := blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted)
}

func TestTokenBlacklist_IsBlacklisted_True(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token := "test_token_456"
	ttl := 1 * time.Hour

	// Ajouter le token
	err := blacklist.Add(ctx, token, ttl)
	require.NoError(t, err)

	// Vérifier qu'il est blacklisté
	isBlacklisted, err := blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted)
}

func TestTokenBlacklist_IsBlacklisted_False(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token := "test_token_not_blacklisted"

	// Vérifier qu'un token non ajouté n'est pas blacklisté
	isBlacklisted, err := blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.False(t, isBlacklisted)
}

func TestTokenBlacklist_Expiration(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token := "test_token_expiration"
	ttl := 100 * time.Millisecond // TTL très court pour le test

	// Ajouter le token avec un TTL court
	err := blacklist.Add(ctx, token, ttl)
	require.NoError(t, err)

	// Vérifier qu'il est blacklisté immédiatement
	isBlacklisted, err := blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted)

	// Attendre que le TTL expire
	time.Sleep(150 * time.Millisecond)

	// Vérifier qu'il n'est plus blacklisté (expiré automatiquement)
	isBlacklisted, err = blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.False(t, isBlacklisted, "Token should be expired and removed from blacklist")
}

func TestTokenBlacklist_Remove(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token := "test_token_remove"
	ttl := 1 * time.Hour

	// Ajouter le token
	err := blacklist.Add(ctx, token, ttl)
	require.NoError(t, err)

	// Vérifier qu'il est blacklisté
	isBlacklisted, err := blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted)

	// Supprimer le token
	err = blacklist.Remove(ctx, token)
	assert.NoError(t, err)

	// Vérifier qu'il n'est plus blacklisté
	isBlacklisted, err = blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.False(t, isBlacklisted)
}

func TestTokenBlacklist_MultipleTokens(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token1 := "test_token_1"
	token2 := "test_token_2"
	token3 := "test_token_3"
	ttl := 1 * time.Hour

	// Ajouter plusieurs tokens
	err := blacklist.Add(ctx, token1, ttl)
	require.NoError(t, err)
	err = blacklist.Add(ctx, token2, ttl)
	require.NoError(t, err)

	// Vérifier que les tokens ajoutés sont blacklistés
	isBlacklisted1, err := blacklist.IsBlacklisted(ctx, token1)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted1)

	isBlacklisted2, err := blacklist.IsBlacklisted(ctx, token2)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted2)

	// Vérifier qu'un token non ajouté n'est pas blacklisté
	isBlacklisted3, err := blacklist.IsBlacklisted(ctx, token3)
	assert.NoError(t, err)
	assert.False(t, isBlacklisted3)
}

func TestTokenBlacklist_HashToken(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	token := "test_token_hash"

	// Le hash devrait être déterministe
	hash1 := blacklist.hashToken(token)
	hash2 := blacklist.hashToken(token)

	assert.Equal(t, hash1, hash2, "Hash should be deterministic")
	assert.NotEqual(t, token, hash1, "Hash should be different from original token")
	assert.Len(t, hash1, 64, "SHA256 hash should be 64 characters (hex)")
}

func TestTokenBlacklist_DifferentTokensDifferentHashes(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	token1 := "test_token_1"
	token2 := "test_token_2"

	hash1 := blacklist.hashToken(token1)
	hash2 := blacklist.hashToken(token2)

	assert.NotEqual(t, hash1, hash2, "Different tokens should have different hashes")
}

func TestTokenBlacklist_AddWithDifferentTTL(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token1 := "test_token_ttl_1"
	token2 := "test_token_ttl_2"
	token3 := "test_token_ttl_3"

	// Ajouter des tokens avec différents TTL
	err := blacklist.Add(ctx, token1, 1*time.Second)
	require.NoError(t, err)
	err = blacklist.Add(ctx, token2, 2*time.Second)
	require.NoError(t, err)
	err = blacklist.Add(ctx, token3, 500*time.Millisecond)
	require.NoError(t, err)

	// Tous devraient être blacklistés immédiatement
	isBlacklisted1, _ := blacklist.IsBlacklisted(ctx, token1)
	assert.True(t, isBlacklisted1)
	isBlacklisted2, _ := blacklist.IsBlacklisted(ctx, token2)
	assert.True(t, isBlacklisted2)
	isBlacklisted3, _ := blacklist.IsBlacklisted(ctx, token3)
	assert.True(t, isBlacklisted3)

	// Attendre que le premier expire
	time.Sleep(600 * time.Millisecond)
	isBlacklisted3, _ = blacklist.IsBlacklisted(ctx, token3)
	assert.False(t, isBlacklisted3, "Token3 should be expired")

	// Les autres devraient encore être là
	isBlacklisted1, _ = blacklist.IsBlacklisted(ctx, token1)
	assert.True(t, isBlacklisted1)
	isBlacklisted2, _ = blacklist.IsBlacklisted(ctx, token2)
	assert.True(t, isBlacklisted2)

	// Attendre que token1 expire
	time.Sleep(500 * time.Millisecond)
	isBlacklisted1, _ = blacklist.IsBlacklisted(ctx, token1)
	assert.False(t, isBlacklisted1, "Token1 should be expired")

	// Token2 devrait encore être là
	isBlacklisted2, _ = blacklist.IsBlacklisted(ctx, token2)
	assert.True(t, isBlacklisted2)

	// Attendre que token2 expire
	time.Sleep(1 * time.Second)
	isBlacklisted2, _ = blacklist.IsBlacklisted(ctx, token2)
	assert.False(t, isBlacklisted2, "Token2 should be expired")
}

func TestTokenBlacklist_AddTwice(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token := "test_token_add_twice"
	ttl := 1 * time.Hour

	// Ajouter le token deux fois
	err := blacklist.Add(ctx, token, ttl)
	require.NoError(t, err)
	err = blacklist.Add(ctx, token, ttl)
	require.NoError(t, err) // Ne devrait pas retourner d'erreur

	// Vérifier qu'il est toujours blacklisté
	isBlacklisted, err := blacklist.IsBlacklisted(ctx, token)
	assert.NoError(t, err)
	assert.True(t, isBlacklisted)
}

func TestTokenBlacklist_RemoveNonExistent(t *testing.T) {
	blacklist, _ := setupTestTokenBlacklist(t)
	if blacklist == nil {
		return
	}

	ctx := context.Background()
	token := "test_token_not_exists"

	// Supprimer un token qui n'existe pas ne devrait pas retourner d'erreur
	err := blacklist.Remove(ctx, token)
	assert.NoError(t, err)
}
