package redis_test

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"os"
	"strconv"
	"strings"
	"testing"

	"github.com/redis/go-redis/v9"
)

func init() {
	// Initialize RedisVersion from environment variable for regular Go tests
	// (Ginkgo tests initialize this in BeforeSuite)
	if version := os.Getenv("REDIS_VERSION"); version != "" {
		if v, err := strconv.ParseFloat(strings.Trim(version, "\""), 64); err == nil && v > 0 {
			RedisVersion = v
		}
	}
}

// skipBeforeRedisVersion checks if Redis version is below the specified version and skips the test if so
func skipBeforeRedisVersion(t *testing.T, version float64, msg string) {
	t.Helper()
	if RedisVersion < version {
		t.Skipf("Skipping test: Redis version %.1f < %.1f: %s", RedisVersion, version, msg)
	}
}

// TestTLSCertificateAuthentication tests that Redis automatically authenticates
// a user based on the CN field in the client's TLS certificate.
//
// This test requires:
// 1. Redis 8.6+ configured with: tls-auth-clients-user CN
// 2. A client certificate with CN matching the Redis ACL username
// 3. The Docker image generates testcertuser.{crt,key} when TLS_CLIENT_CNS=testcertuser
//
// The test flow:
// 1. Create a Redis ACL user with a specific username (testcertuser)
// 2. Load the pre-generated client certificate with that username in the CN field
// 3. Connect using TLS with that certificate
// 4. Verify that Redis automatically authenticates as that user (no AUTH command needed)
func TestTLSCertificateAuthentication(t *testing.T) {
	skipBeforeRedisVersion(t, 8.6, "tls-auth-clients-user CN requires Redis 8.6+")

	ctx := context.Background()
	testUsername := "testcertuser"
	tlsCertDir := "dockers/standalone/tls"

	// Step 1: Create a non-TLS client to set up the ACL user
	setupClient := redis.NewClient(&redis.Options{
		Addr: "localhost:6379", // Non-TLS port
	})
	defer setupClient.Close()

	// Verify connection
	if err := setupClient.Ping(ctx).Err(); err != nil {
		t.Fatalf("Redis not available: %v", err)
	}

	// Clean up any existing test user
	setupClient.ACLDelUser(ctx, testUsername)

	// Step 2: Create ACL user with specific permissions
	// The user can read/write keys but has limited command access
	err := setupClient.ACLSetUser(ctx,
		testUsername,
		"on",          // Enable the user
		"nopass",      // No password required (will use cert auth)
		"~*",          // Can access all keys
		"+get",        // Allow GET command
		"+set",        // Allow SET command
		"+ping",       // Allow PING command
		"+acl|whoami", // Allow ACL WHOAMI command
	).Err()
	if err != nil {
		t.Fatalf("Failed to create ACL user: %v", err)
	}
	defer setupClient.ACLDelUser(ctx, testUsername) // Cleanup

	// Verify user was created
	users, err := setupClient.ACLUsers(ctx).Result()
	if err != nil {
		t.Fatalf("Failed to list ACL users: %v", err)
	}
	t.Logf("ACL users: %v", users)

	// Step 3: Load CA certificate for server verification
	caCertPEM, err := os.ReadFile(tlsCertDir + "/ca.crt")
	if err != nil {
		t.Fatalf("CA cert not found: %v", err)
	}

	// Step 4: Load the pre-generated client certificate with CN=testcertuser
	// This certificate is generated by the Docker image when TLS_CLIENT_CNS=testcertuser
	clientCert, err := tls.LoadX509KeyPair(
		tlsCertDir+"/"+testUsername+".crt",
		tlsCertDir+"/"+testUsername+".key",
	)
	if err != nil {
		t.Fatalf("Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)", err, testUsername)
	}

	// Step 5: Create TLS config with the client certificate
	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCertPEM)

	tlsConfig := &tls.Config{
		RootCAs:            caCertPool,
		Certificates:       []tls.Certificate{clientCert},
		ServerName:         "localhost",
		InsecureSkipVerify: true, // Using self-signed certs
	}

	// Step 6: Connect with TLS using the certificate
	// NOTE: This test requires Redis to be configured with:
	//   tls-auth-clients-user CN
	// Without this config, the certificate CN won't be used for authentication
	tlsClient := redis.NewClient(&redis.Options{
		Addr:      "localhost:6666", // TLS port
		TLSConfig: tlsConfig,
		// NO Username/Password - authentication should happen via certificate!
	})
	defer tlsClient.Close()

	// Step 7: Verify we're authenticated as the correct user
	whoami, err := tlsClient.ACLWhoAmI(ctx).Result()
	if err != nil {
		t.Fatalf("ACL WHOAMI failed: %v (this test requires Redis to be configured with: tls-auth-clients-user CN)", err)
	}

	if whoami != testUsername {
		t.Fatalf("Expected to be authenticated as %q, but got %q. Ensure Redis is configured with: tls-auth-clients-user CN", testUsername, whoami)
	}
	t.Logf("✅ Successfully authenticated as %q using certificate CN", whoami)

	// Step 8: Test that we can execute allowed commands
	err = tlsClient.Set(ctx, "test_cert_auth_key", "test_value", 0).Err()
	if err != nil {
		t.Fatalf("SET command failed (should be allowed): %v", err)
	}

	val, err := tlsClient.Get(ctx, "test_cert_auth_key").Result()
	if err != nil {
		t.Fatalf("GET command failed (should be allowed): %v", err)
	}
	if val != "test_value" {
		t.Errorf("Expected 'test_value', got %q", val)
	}

	// Step 9: Test that we CANNOT execute disallowed commands
	// The user doesn't have +del permission, so this should fail
	err = tlsClient.Del(ctx, "test_cert_auth_key").Err()
	if err == nil {
		t.Error("DEL command succeeded but should have failed (user doesn't have +del permission)")
	} else {
		t.Logf("✅ DEL command correctly denied: %v", err)
	}

	// Cleanup
	setupClient.Del(ctx, "test_cert_auth_key")

	t.Log("✅ TLS certificate authentication test passed")
}

// TestTLSCertificateAuthenticationNoUser tests that when a certificate CN
// doesn't match any existing ACL user, Redis falls back to the default user.
//
// This test:
// 1. Ensures the testcertuser ACL user does NOT exist
// 2. Connects with a certificate that has CN=testcertuser
// 3. Verifies that Redis authenticates as "default" (fallback behavior)
func TestTLSCertificateAuthenticationNoUser(t *testing.T) {
	skipBeforeRedisVersion(t, 8.6, "tls-auth-clients-user CN requires Redis 8.6+")

	ctx := context.Background()
	testUsername := "testcertuser"
	tlsCertDir := "dockers/standalone/tls"

	// Step 1: Create a non-TLS client to ensure the user does NOT exist
	setupClient := redis.NewClient(&redis.Options{
		Addr: "localhost:6379", // Non-TLS port
	})
	defer setupClient.Close()

	// Verify connection
	if err := setupClient.Ping(ctx).Err(); err != nil {
		t.Fatalf("Redis not available: %v", err)
	}

	// Delete the test user if it exists - we want to test fallback behavior
	setupClient.ACLDelUser(ctx, testUsername)

	// Verify user does not exist
	users, err := setupClient.ACLUsers(ctx).Result()
	if err != nil {
		t.Fatalf("Failed to list ACL users: %v", err)
	}
	for _, u := range users {
		if u == testUsername {
			t.Fatalf("User %q should not exist for this test", testUsername)
		}
	}
	t.Logf("ACL users (should not contain %s): %v", testUsername, users)

	// Step 2: Load CA certificate for server verification
	caCertPEM, err := os.ReadFile(tlsCertDir + "/ca.crt")
	if err != nil {
		t.Fatalf("CA cert not found: %v", err)
	}

	// Step 3: Load the client certificate with CN=testcertuser
	// Even though the user doesn't exist, we still use this certificate
	clientCert, err := tls.LoadX509KeyPair(
		tlsCertDir+"/"+testUsername+".crt",
		tlsCertDir+"/"+testUsername+".key",
	)
	if err != nil {
		t.Fatalf("Client certificate not found: %v (ensure TLS_CLIENT_CNS=%s is set)", err, testUsername)
	}

	// Step 4: Create TLS config with the client certificate
	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCertPEM)

	tlsConfig := &tls.Config{
		RootCAs:            caCertPool,
		Certificates:       []tls.Certificate{clientCert},
		ServerName:         "localhost",
		InsecureSkipVerify: true, // Using self-signed certs
	}

	// Step 5: Connect with TLS using the certificate
	tlsClient := redis.NewClient(&redis.Options{
		Addr:      "localhost:6666", // TLS port
		TLSConfig: tlsConfig,
	})
	defer tlsClient.Close()

	// Step 6: Verify we're authenticated as "default" (fallback)
	whoami, err := tlsClient.ACLWhoAmI(ctx).Result()
	if err != nil {
		t.Fatalf("ACL WHOAMI failed: %v (Redis may not be configured for certificate-based authentication)", err)
	}

	// When the CN user doesn't exist, Redis should fall back to "default"
	if whoami != "default" {
		t.Fatalf("Expected to be authenticated as %q (fallback), but got %q", "default", whoami)
	}
	t.Logf("✅ Correctly fell back to %q user (CN user %q does not exist)", whoami, testUsername)

	// Step 7: Verify we can execute commands as default user
	err = tlsClient.Set(ctx, "test_cert_auth_fallback_key", "fallback_value", 0).Err()
	if err != nil {
		t.Fatalf("SET command failed: %v", err)
	}

	val, err := tlsClient.Get(ctx, "test_cert_auth_fallback_key").Result()
	if err != nil {
		t.Fatalf("GET command failed: %v", err)
	}
	if val != "fallback_value" {
		t.Errorf("Expected 'fallback_value', got %q", val)
	}

	// Cleanup
	tlsClient.Del(ctx, "test_cert_auth_fallback_key")

	t.Log("✅ TLS certificate authentication fallback test passed")
}
