package e2e

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"sync"
	"testing"
	"time"

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

// debugE2E returns true if E2E_DEBUG environment variable is set to "true"
// Use this to control verbose debug logging in e2e tests
func debugE2E() bool {
	return os.Getenv("E2E_DEBUG") == "true"
}

// isPortUnavailableErrorString checks if the error string indicates a port unavailable error
func isPortUnavailableErrorString(errStr string) bool {
	return strings.Contains(errStr, "port_unavailable") || strings.Contains(errStr, "Unavailable or invalid port")
}

// DatabaseEndpoint represents a single database endpoint configuration
type DatabaseEndpoint struct {
	Addr                               []string `json:"addr"`
	AddrType                           string   `json:"addr_type"`
	DNSName                            string   `json:"dns_name"`
	OSSClusterAPIPreferredEndpointType string   `json:"oss_cluster_api_preferred_endpoint_type"`
	OSSClusterAPIPreferredIPType       string   `json:"oss_cluster_api_preferred_ip_type"`
	Port                               int      `json:"port"`
	ProxyPolicy                        string   `json:"proxy_policy"`
	UID                                string   `json:"uid"`
}

// EnvDatabaseConfig represents the configuration for a single database
type EnvDatabaseConfig struct {
	BdbID                interface{}        `json:"bdb_id,omitempty"`
	Username             string             `json:"username,omitempty"`
	Password             string             `json:"password,omitempty"`
	TLS                  bool               `json:"tls"`
	CertificatesLocation string             `json:"certificatesLocation,omitempty"`
	RawEndpoints         []DatabaseEndpoint `json:"raw_endpoints,omitempty"`
	Endpoints            []string           `json:"endpoints"`
	OSSCluster           bool               `json:"oss_cluster,omitempty"`
}

// EnvDatabasesConfig represents the complete configuration file structure
type EnvDatabasesConfig map[string]EnvDatabaseConfig

// EnvConfig represents environment configuration for test scenarios
type EnvConfig struct {
	RedisEndpointsConfigPath string
	FaultInjectorURL         string
}

// RedisConnectionConfig represents Redis connection parameters
type RedisConnectionConfig struct {
	Host                 string
	Port                 int
	Username             string
	Password             string
	TLS                  bool
	BdbID                int
	CertificatesLocation string
	Endpoints            []string
	IsClusterMode        bool // Force cluster mode even with single endpoint
}

// GetEnvConfig reads environment variables required for the test scenario
func GetEnvConfig() (*EnvConfig, error) {
	redisConfigPath := os.Getenv("REDIS_ENDPOINTS_CONFIG_PATH")
	if redisConfigPath == "" {
		return nil, fmt.Errorf("REDIS_ENDPOINTS_CONFIG_PATH environment variable must be set")
	}

	faultInjectorURL := os.Getenv("FAULT_INJECTION_API_URL")
	if faultInjectorURL == "" {
		// Default to localhost if not set
		faultInjectorURL = "http://localhost:8080"
	}

	return &EnvConfig{
		RedisEndpointsConfigPath: redisConfigPath,
		FaultInjectorURL:         faultInjectorURL,
	}, nil
}

// GetDatabaseConfigFromEnv reads database configuration from a file
func GetDatabaseConfigFromEnv(filePath string) (EnvDatabasesConfig, error) {
	fileContent, err := os.ReadFile(filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read database config from %s: %w", filePath, err)
	}

	var config EnvDatabasesConfig
	if err := json.Unmarshal(fileContent, &config); err != nil {
		return nil, fmt.Errorf("failed to parse database config from %s: %w", filePath, err)
	}

	return config, nil
}

// GetDatabaseConfig gets Redis connection parameters for a specific database
func GetDatabaseConfig(databasesConfig EnvDatabasesConfig, databaseName string) (*RedisConnectionConfig, error) {
	var dbConfig EnvDatabaseConfig
	var exists bool

	if databaseName == "" {
		// Get the first database if no name is provided
		for _, config := range databasesConfig {
			dbConfig = config
			exists = true
			break
		}
	} else {
		dbConfig, exists = databasesConfig[databaseName]
	}

	if !exists {
		return nil, fmt.Errorf("database %s not found in configuration", databaseName)
	}

	// Parse connection details from endpoints or raw_endpoints
	var host string
	var port int

	if len(dbConfig.RawEndpoints) > 0 {
		// Use raw_endpoints if available (for more complex configurations)
		endpoint := dbConfig.RawEndpoints[0] // Use the first endpoint
		host = endpoint.DNSName
		port = endpoint.Port
	} else if len(dbConfig.Endpoints) > 0 {
		// Parse from endpoints URLs
		endpointURL, err := url.Parse(dbConfig.Endpoints[0])
		if err != nil {
			return nil, fmt.Errorf("failed to parse endpoint URL %s: %w", dbConfig.Endpoints[0], err)
		}

		host = endpointURL.Hostname()
		portStr := endpointURL.Port()
		if portStr == "" {
			// Default ports based on scheme
			switch endpointURL.Scheme {
			case "redis":
				port = 6379
			case "rediss":
				port = 6380
			default:
				port = 6379
			}
		} else {
			port, err = strconv.Atoi(portStr)
			if err != nil {
				return nil, fmt.Errorf("invalid port in endpoint URL %s: %w", dbConfig.Endpoints[0], err)
			}
		}

		// Override TLS setting based on scheme if not explicitly set
		if endpointURL.Scheme == "rediss" {
			dbConfig.TLS = true
		}
	} else {
		return nil, fmt.Errorf("no endpoints found in database configuration")
	}

	var bdbId int
	switch (dbConfig.BdbID).(type) {
	case int:
		bdbId = dbConfig.BdbID.(int)
	case float64:
		bdbId = int(dbConfig.BdbID.(float64))
	case string:
		bdbId, _ = strconv.Atoi(dbConfig.BdbID.(string))
	}

	return &RedisConnectionConfig{
		Host:                 host,
		Port:                 port,
		Username:             dbConfig.Username,
		Password:             dbConfig.Password,
		TLS:                  dbConfig.TLS,
		BdbID:                bdbId,
		CertificatesLocation: dbConfig.CertificatesLocation,
		Endpoints:            dbConfig.Endpoints,
	}, nil
}

// ConvertEnvDatabaseConfigToRedisConnectionConfig converts EnvDatabaseConfig to RedisConnectionConfig
func ConvertEnvDatabaseConfigToRedisConnectionConfig(dbConfig EnvDatabaseConfig) (*RedisConnectionConfig, error) {
	// Parse connection details from endpoints or raw_endpoints
	var host string
	var port int

	if len(dbConfig.RawEndpoints) > 0 {
		// Use raw_endpoints if available (for more complex configurations)
		endpoint := dbConfig.RawEndpoints[0] // Use the first endpoint
		host = endpoint.DNSName
		port = endpoint.Port
	} else if len(dbConfig.Endpoints) > 0 {
		// Parse from endpoints URLs
		endpointURL, err := url.Parse(dbConfig.Endpoints[0])
		if err != nil {
			return nil, fmt.Errorf("failed to parse endpoint URL %s: %w", dbConfig.Endpoints[0], err)
		}

		host = endpointURL.Hostname()
		portStr := endpointURL.Port()
		if portStr == "" {
			// Default ports based on scheme
			switch endpointURL.Scheme {
			case "redis":
				port = 6379
			case "rediss":
				port = 6380
			default:
				port = 6379
			}
		} else {
			port, err = strconv.Atoi(portStr)
			if err != nil {
				return nil, fmt.Errorf("invalid port in endpoint URL %s: %w", dbConfig.Endpoints[0], err)
			}
		}

		// Override TLS setting based on scheme if not explicitly set
		if endpointURL.Scheme == "rediss" {
			dbConfig.TLS = true
		}
	} else {
		return nil, fmt.Errorf("no endpoints found in database configuration")
	}

	var bdbId int
	switch dbConfig.BdbID.(type) {
	case int:
		bdbId = dbConfig.BdbID.(int)
	case float64:
		bdbId = int(dbConfig.BdbID.(float64))
	case string:
		bdbId, _ = strconv.Atoi(dbConfig.BdbID.(string))
	}

	return &RedisConnectionConfig{
		Host:                 host,
		Port:                 port,
		Username:             dbConfig.Username,
		Password:             dbConfig.Password,
		TLS:                  dbConfig.TLS,
		BdbID:                bdbId,
		CertificatesLocation: dbConfig.CertificatesLocation,
		Endpoints:            dbConfig.Endpoints,
		IsClusterMode:        dbConfig.OSSCluster,
	}, nil
}

// ClientFactory manages Redis client creation and lifecycle
type ClientFactory struct {
	config  *RedisConnectionConfig
	clients map[string]redis.UniversalClient
	mutex   sync.RWMutex
}

// NewClientFactory creates a new client factory with the specified configuration
func NewClientFactory(config *RedisConnectionConfig) *ClientFactory {
	return &ClientFactory{
		config:  config,
		clients: make(map[string]redis.UniversalClient),
	}
}

// CreateClientOptions represents options for creating Redis clients
type CreateClientOptions struct {
	Protocol                   int
	MaintNotificationsConfig   *maintnotifications.Config
	MaxRetries                 int
	PoolSize                   int
	MinIdleConns               int
	MaxActiveConns             int
	ClientName                 string
	DB                         int
	ReadTimeout                time.Duration
	WriteTimeout               time.Duration
	ClusterStateReloadInterval time.Duration // For cluster clients, interval for automatic state reloads
}

// DefaultCreateClientOptions returns default options for creating Redis clients
func DefaultCreateClientOptions() *CreateClientOptions {
	return &CreateClientOptions{
		Protocol: 3, // RESP3 by default for push notifications
		MaintNotificationsConfig: &maintnotifications.Config{
			Mode:           maintnotifications.ModeEnabled,
			HandoffTimeout: 30 * time.Second,
			RelaxedTimeout: 10 * time.Second,
			MaxWorkers:     20,
		},
		MaxRetries:     3,
		PoolSize:       10,
		MinIdleConns:   10,
		MaxActiveConns: 10,
	}
}

func (cf *ClientFactory) PrintPoolStats(t *testing.T) {
	cf.mutex.RLock()
	defer cf.mutex.RUnlock()

	for key, client := range cf.clients {
		stats := client.PoolStats()
		t.Logf("Pool stats for client %s: %+v", key, stats)
	}
}

// Create creates a new Redis client with the specified options and connects it
func (cf *ClientFactory) Create(key string, options *CreateClientOptions) (redis.UniversalClient, error) {
	if options == nil {
		options = DefaultCreateClientOptions()
	}

	cf.mutex.Lock()
	defer cf.mutex.Unlock()

	// Check if client already exists
	if client, exists := cf.clients[key]; exists {
		return client, nil
	}

	var client redis.UniversalClient
	var opts interface{}

	// Determine if this is a cluster configuration
	if len(cf.config.Endpoints) > 1 || cf.isClusterEndpoint() {
		// Create cluster client
		clusterOptions := &redis.ClusterOptions{
			Addrs:                      cf.getAddresses(),
			Username:                   cf.config.Username,
			Password:                   cf.config.Password,
			Protocol:                   options.Protocol,
			MaintNotificationsConfig:   options.MaintNotificationsConfig,
			MaxRetries:                 options.MaxRetries,
			PoolSize:                   options.PoolSize,
			MinIdleConns:               options.MinIdleConns,
			MaxActiveConns:             options.MaxActiveConns,
			ClientName:                 options.ClientName,
			ClusterStateReloadInterval: 10 * time.Minute,
		}

		if options.ReadTimeout > 0 {
			clusterOptions.ReadTimeout = options.ReadTimeout
		}
		if options.WriteTimeout > 0 {
			clusterOptions.WriteTimeout = options.WriteTimeout
		}
		if options.ClusterStateReloadInterval > 0 {
			clusterOptions.ClusterStateReloadInterval = options.ClusterStateReloadInterval
		}

		if cf.config.TLS {
			clusterOptions.TLSConfig = &tls.Config{
				InsecureSkipVerify: true, // For testing purposes
			}
		}

		opts = clusterOptions
		client = redis.NewClusterClient(clusterOptions)
	} else {
		// Create single client
		clientOptions := &redis.Options{
			Addr:                     fmt.Sprintf("%s:%d", cf.config.Host, cf.config.Port),
			Username:                 cf.config.Username,
			Password:                 cf.config.Password,
			DB:                       options.DB,
			Protocol:                 options.Protocol,
			MaintNotificationsConfig: options.MaintNotificationsConfig,
			MaxRetries:               options.MaxRetries,
			PoolSize:                 options.PoolSize,
			MinIdleConns:             options.MinIdleConns,
			MaxActiveConns:           options.MaxActiveConns,
			ClientName:               options.ClientName,
		}

		if options.ReadTimeout > 0 {
			clientOptions.ReadTimeout = options.ReadTimeout
		}
		if options.WriteTimeout > 0 {
			clientOptions.WriteTimeout = options.WriteTimeout
		}

		if cf.config.TLS {
			clientOptions.TLSConfig = &tls.Config{
				InsecureSkipVerify: true, // For testing purposes
			}
		}

		opts = clientOptions
		client = redis.NewClient(clientOptions)
	}

	if err := client.Ping(context.Background()).Err(); err != nil {
		return nil, fmt.Errorf("failed to connect to Redis: %w\nOptions: %+v", err, opts)
	}

	// Store the client
	cf.clients[key] = client

	return client, nil
}

// Get retrieves an existing client by key or the first one if no key is provided
func (cf *ClientFactory) Get(key string) redis.UniversalClient {
	cf.mutex.RLock()
	defer cf.mutex.RUnlock()

	if key != "" {
		return cf.clients[key]
	}

	// Return the first client if no key is provided
	for _, client := range cf.clients {
		return client
	}

	return nil
}

// GetAll returns all created clients
func (cf *ClientFactory) GetAll() map[string]redis.UniversalClient {
	cf.mutex.RLock()
	defer cf.mutex.RUnlock()

	result := make(map[string]redis.UniversalClient)
	for key, client := range cf.clients {
		result[key] = client
	}

	return result
}

// DestroyAll closes and removes all created clients
func (cf *ClientFactory) DestroyAll() error {
	cf.mutex.Lock()
	defer cf.mutex.Unlock()

	var lastErr error
	for key, client := range cf.clients {
		if err := client.Close(); err != nil {
			lastErr = err
		}
		delete(cf.clients, key)
	}

	return lastErr
}

// Destroy closes and removes a specific client
func (cf *ClientFactory) Destroy(key string) error {
	cf.mutex.Lock()
	defer cf.mutex.Unlock()

	client, exists := cf.clients[key]
	if !exists {
		return fmt.Errorf("client %s not found", key)
	}

	err := client.Close()
	delete(cf.clients, key)
	return err
}

// GetConfig returns the connection configuration
func (cf *ClientFactory) GetConfig() *RedisConnectionConfig {
	return cf.config
}

// Helper methods

// isClusterEndpoint determines if the configuration represents a cluster
func (cf *ClientFactory) isClusterEndpoint() bool {
	// Check if cluster mode is explicitly set
	if cf.config.IsClusterMode {
		return true
	}

	// Check if any endpoint contains cluster-related keywords
	for _, endpoint := range cf.config.Endpoints {
		if strings.Contains(strings.ToLower(endpoint), "cluster") {
			return true
		}
	}

	// Check if we have multiple raw endpoints
	if len(cf.config.Endpoints) > 1 {
		return true
	}

	return false
}

// getAddresses returns a list of addresses for cluster configuration
func (cf *ClientFactory) getAddresses() []string {
	if len(cf.config.Endpoints) > 0 {
		addresses := make([]string, 0, len(cf.config.Endpoints))
		for _, endpoint := range cf.config.Endpoints {
			if parsedURL, err := url.Parse(endpoint); err == nil {
				addr := parsedURL.Host
				if addr != "" {
					addresses = append(addresses, addr)
				}
			}
		}
		if len(addresses) > 0 {
			return addresses
		}
	}

	// Fallback to single address
	return []string{fmt.Sprintf("%s:%d", cf.config.Host, cf.config.Port)}
}

// Utility functions for common test scenarios

// CreateTestClientFactory creates a client factory from environment configuration
func CreateTestClientFactory(databaseName string) (*ClientFactory, error) {
	envConfig, err := GetEnvConfig()
	if err != nil {
		return nil, fmt.Errorf("failed to get environment config: %w", err)
	}

	databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
	if err != nil {
		return nil, fmt.Errorf("failed to get database config: %w", err)
	}

	dbConfig, err := GetDatabaseConfig(databasesConfig, databaseName)
	if err != nil {
		return nil, fmt.Errorf("failed to get database config for %s: %w", databaseName, err)
	}

	return NewClientFactory(dbConfig), nil
}

// CreateTestClientFactoryWithBdbID creates a client factory using a specific bdb_id
// This is useful when you've created a fresh database and want to connect to it
func CreateTestClientFactoryWithBdbID(databaseName string, bdbID int) (*ClientFactory, error) {
	envConfig, err := GetEnvConfig()
	if err != nil {
		return nil, fmt.Errorf("failed to get environment config: %w", err)
	}

	databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
	if err != nil {
		return nil, fmt.Errorf("failed to get database config: %w", err)
	}

	dbConfig, err := GetDatabaseConfig(databasesConfig, databaseName)
	if err != nil {
		return nil, fmt.Errorf("failed to get database config for %s: %w", databaseName, err)
	}

	// Override the bdb_id with the newly created database ID
	dbConfig.BdbID = bdbID

	return NewClientFactory(dbConfig), nil
}

// CreateTestFaultInjector creates a fault injector client from environment configuration
// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it automatically starts a proxy fault injector server
//
// Deprecated: Use CreateTestFaultInjectorWithCleanup instead for proper cleanup
func CreateTestFaultInjector() (*FaultInjectorClient, error) {
	client, _, err := CreateTestFaultInjectorWithCleanup()
	return client, err
}

// CreateTestFaultInjectorWithCleanup creates a fault injector client and returns a cleanup function
//
// Decision logic based on environment:
// 1. If REDIS_ENDPOINTS_CONFIG_PATH is set -> use real fault injector from FAULT_INJECTION_API_URL
// 2. If REDIS_ENDPOINTS_CONFIG_PATH is NOT set -> use Docker fault injector at http://localhost:15000
//
// Both the Docker proxy and fault injector should already be running (started via Docker Compose)
// This function does NOT start any services - it only connects to existing ones
//
// Usage:
//
//	client, cleanup, err := CreateTestFaultInjectorWithCleanup()
//	if err != nil { ... }
//	defer cleanup()
func CreateTestFaultInjectorWithCleanup() (*FaultInjectorClient, func(), error) {
	// Try to get environment config
	envConfig, err := GetEnvConfig()

	// If environment config fails, use Docker fault injector
	// Note: GetEnvConfig() only fails if REDIS_ENDPOINTS_CONFIG_PATH is not set
	if err != nil {
		// Use Docker fault injector at http://localhost:15000 (updated to avoid macOS Control Center conflict)
		// The fault injector should already be running via docker-compose
		faultInjectorURL := "http://localhost:15000"

		// Check if fault injector is accessible
		client := &http.Client{Timeout: 2 * time.Second}
		resp, err := client.Get(faultInjectorURL + "/actions")
		if err != nil {
			return nil, nil, fmt.Errorf("Docker fault injector not accessible at %s (did you run 'make docker.e2e.start'?): %w", faultInjectorURL, err)
		}
		resp.Body.Close()

		fmt.Printf("✓ Using Docker fault injector at %s\n", faultInjectorURL)

		// Return client with no-op cleanup (Docker manages the fault injector lifecycle)
		noopCleanup := func() {}
		return NewFaultInjectorClient(faultInjectorURL), noopCleanup, nil
	}

	// Using real fault injector - no cleanup needed
	fmt.Printf("✓ Using real fault injector at %s\n", envConfig.FaultInjectorURL)
	noopCleanup := func() {}
	return NewFaultInjectorClient(envConfig.FaultInjectorURL), noopCleanup, nil
}

// GetAvailableDatabases returns a list of available database names from the configuration
func GetAvailableDatabases(configPath string) ([]string, error) {
	databasesConfig, err := GetDatabaseConfigFromEnv(configPath)
	if err != nil {
		return nil, err
	}

	databases := make([]string, 0, len(databasesConfig))
	for name := range databasesConfig {
		databases = append(databases, name)
	}

	return databases, nil
}

// ConvertEnvDatabaseConfigToFaultInjectorConfig converts EnvDatabaseConfig to fault injector DatabaseConfig
// This creates an OSS Cluster database by default for backward compatibility.
// Use ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode for explicit control over cluster mode.
func ConvertEnvDatabaseConfigToFaultInjectorConfig(envConfig EnvDatabaseConfig, name string) (DatabaseConfig, error) {
	return ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envConfig, name, true)
}

// ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode converts EnvDatabaseConfig to fault injector DatabaseConfig
// with explicit control over whether to create a cluster or standalone database.
// Parameters:
//   - envConfig: The environment database configuration
//   - name: The name for the new database
//   - clusterMode: If true, creates an OSS Cluster database (ClusterClient). If false, creates a standalone database (Client).
func ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envConfig EnvDatabaseConfig, name string, clusterMode bool) (DatabaseConfig, error) {
	var port int

	// Extract port and DNS name from raw_endpoints or endpoints
	if len(envConfig.RawEndpoints) > 0 {
		endpoint := envConfig.RawEndpoints[0]
		port = endpoint.Port
	} else if len(envConfig.Endpoints) > 0 {
		endpointURL, err := url.Parse(envConfig.Endpoints[0])
		if err != nil {
			return DatabaseConfig{}, fmt.Errorf("failed to parse endpoint URL: %w", err)
		}
		portStr := endpointURL.Port()
		if portStr != "" {
			port, err = strconv.Atoi(portStr)
			if err != nil {
				return DatabaseConfig{}, fmt.Errorf("invalid port: %w", err)
			}
		} else {
			port = 6379 * 2 // default*2
		}
	} else {
		return DatabaseConfig{}, fmt.Errorf("no endpoints found in configuration")
	}

	randomPortOffset := 1 + rand.Intn(5) + rand.Intn(10) + rand.Intn(10) + rand.Intn(10) // Random port offset to avoid conflicts

	var dbConfig DatabaseConfig

	if clusterMode {
		// Build OSS Cluster database config for ClusterClient support
		// Supports cluster-specific features like SMIGRATING notifications
		dbConfig = DatabaseConfig{
			Name:           name,
			Port:           port + randomPortOffset,
			MemorySize:     268435456, // 256MB default
			Replication:    true,
			EvictionPolicy: "noeviction",
			ProxyPolicy:    "all-master-shards", // OSS Cluster API requires all-master-shards
			AutoUpgrade:    true,
			Sharding:       true,
			ShardsCount:    2,
			ShardKeyRegex: []ShardKeyRegexPattern{
				{Regex: ".*\\{(?<tag>.*)\\}.*"},
				{Regex: "(?<tag>.*)"},
			},
			ShardsPlacement: "sparse", // Use sparse placement to distribute shards across nodes (required for slot-shuffle)
			ModuleList: []DatabaseModule{
				{ModuleArgs: "", ModuleName: "ReJSON"},
				{ModuleArgs: "", ModuleName: "search"},
				{ModuleArgs: "", ModuleName: "timeseries"},
				{ModuleArgs: "", ModuleName: "bf"},
			},
			OSSCluster: true, // Enable OSS Cluster API for ClusterClient support
		}

		// If we have raw_endpoints with cluster info, configure for cluster
		if len(envConfig.RawEndpoints) > 0 {
			endpoint := envConfig.RawEndpoints[0]

			// Check if this is a cluster configuration
			if endpoint.ProxyPolicy != "" && endpoint.ProxyPolicy != "single" {
				dbConfig.OSSCluster = true
				dbConfig.Sharding = true
				dbConfig.ShardsCount = 3 // default for cluster
				dbConfig.ProxyPolicy = endpoint.ProxyPolicy
				dbConfig.Replication = true
			}

			// Note: We ignore endpoint.OSSClusterAPIPreferredIPType here because
			// e2e tests run from outside the cluster network and need external IPs
		}

		// For OSS Cluster databases, always use external IPs since e2e tests
		// run from outside the cluster network and cannot reach internal IPs
		dbConfig.OSSClusterAPIPreferredIPType = "external"
	} else {
		// Build standalone database config for regular Client support
		// Supports MOVING, MIGRATING, MIGRATED, FAILING_OVER, FAILED_OVER notifications
		dbConfig = DatabaseConfig{
			Name:           name,
			Port:           port + randomPortOffset,
			MemorySize:     268435456, // 256MB default
			Replication:    true,      // Enable replication for failover support
			EvictionPolicy: "noeviction",
			ProxyPolicy:    "single", // Single proxy for standalone database
			AutoUpgrade:    true,
			Sharding:       false, // No sharding for standalone
			ShardsCount:    1,     // Single shard
			ModuleList: []DatabaseModule{
				{ModuleArgs: "", ModuleName: "ReJSON"},
				{ModuleArgs: "", ModuleName: "search"},
				{ModuleArgs: "", ModuleName: "timeseries"},
				{ModuleArgs: "", ModuleName: "bf"},
			},
			OSSCluster: false, // Disable OSS Cluster API for standalone Client support
		}
	}

	return dbConfig, nil
}

// TestDatabaseManager manages database lifecycle for tests
type TestDatabaseManager struct {
	faultInjector *FaultInjectorClient
	clusterIndex  int
	createdBdbID  int
	dbConfig      DatabaseConfig
	t             *testing.T
}

// NewTestDatabaseManager creates a new test database manager
func NewTestDatabaseManager(t *testing.T, faultInjector *FaultInjectorClient, clusterIndex int) *TestDatabaseManager {
	return &TestDatabaseManager{
		faultInjector: faultInjector,
		clusterIndex:  clusterIndex,
		t:             t,
	}
}

// CreateDatabaseFromEnvConfig creates a database using EnvDatabaseConfig
func (m *TestDatabaseManager) CreateDatabaseFromEnvConfig(ctx context.Context, envConfig EnvDatabaseConfig, name string) (int, error) {
	// Convert EnvDatabaseConfig to DatabaseConfig
	dbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfig(envConfig, name)
	if err != nil {
		return 0, fmt.Errorf("failed to convert config: %w", err)
	}

	m.dbConfig = dbConfig
	return m.CreateDatabase(ctx, dbConfig)
}

// CreateDatabase creates a database and waits for it to be ready
// Returns the bdb_id of the created database
// If the port is unavailable, it will retry with incremented port numbers
func (m *TestDatabaseManager) CreateDatabase(ctx context.Context, dbConfig DatabaseConfig) (int, error) {
	resp, finalPort, err := m.faultInjector.CreateDatabaseConfigWithPortRetry(ctx, m.clusterIndex, dbConfig, 100)
	if err != nil {
		return 0, fmt.Errorf("failed to trigger database creation: %w", err)
	}

	if finalPort != dbConfig.Port && debugE2E() {
		fmt.Printf("[TestDatabaseManager] Database created on port %d (originally requested %d)\n", finalPort, dbConfig.Port)
	}

	// Wait for creation to complete
	status, err := m.faultInjector.WaitForAction(ctx, resp.ActionID,
		WithMaxWaitTime(5*time.Minute),
		WithPollInterval(5*time.Second))
	if err != nil {
		return 0, fmt.Errorf("failed to wait for database creation: %w", err)
	}

	if status.Status != StatusSuccess {
		return 0, fmt.Errorf("database creation failed: %v", status.Error)
	}

	// Extract bdb_id from output
	var bdbID int
	if status.Output != nil {
		if id, ok := status.Output["bdb_id"].(float64); ok {
			bdbID = int(id)
		} else if resultMap, ok := status.Output["result"].(map[string]interface{}); ok {
			if id, ok := resultMap["bdb_id"].(float64); ok {
				bdbID = int(id)
			}
		}
	}

	if bdbID == 0 {
		return 0, fmt.Errorf("failed to extract bdb_id from creation output")
	}

	m.createdBdbID = bdbID

	return bdbID, nil
}

// CreateDatabaseAndGetConfig creates a database and returns both the bdb_id and the full connection config from the fault injector response
// This includes endpoints, username, password, TLS settings, and raw_endpoints
// If the port is unavailable, it will retry with incremented port numbers
func (m *TestDatabaseManager) CreateDatabaseAndGetConfig(ctx context.Context, dbConfig DatabaseConfig) (int, EnvDatabaseConfig, error) {
	const maxPortRetries = 100
	currentPort := dbConfig.Port

	var status *ActionStatusResponse

	// Retry loop for port unavailable errors
	for attempt := 0; attempt < maxPortRetries; attempt++ {
		// Update the port in the config
		dbConfig.Port = currentPort

		resp, err := m.faultInjector.CreateDatabase(ctx, m.clusterIndex, dbConfig)
		if err != nil {
			// Check if it's a port unavailable error at trigger time
			if isPortUnavailableErrorString(err.Error()) {
				if debugE2E() {
					fmt.Printf("[TestDatabaseManager] Port %d unavailable at trigger, trying port %d\n", currentPort, currentPort+1)
				}
				currentPort++
				continue
			}
			return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to trigger database creation: %w", err)
		}

		// Wait for creation to complete
		status, err = m.faultInjector.WaitForAction(ctx, resp.ActionID,
			WithMaxWaitTime(5*time.Minute),
			WithPollInterval(5*time.Second))
		if err != nil {
			return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to wait for database creation: %w", err)
		}

		if status.Status == StatusSuccess {
			// Success! Break out of retry loop
			if currentPort != dbConfig.Port && debugE2E() {
				fmt.Printf("[TestDatabaseManager] Database created on port %d (originally requested %d)\n", currentPort, dbConfig.Port)
			}
			break
		}

		// Check if the error is port unavailable
		errorStr := ""
		if status.Error != nil {
			errorStr = fmt.Sprintf("%v", status.Error)
		}
		if isPortUnavailableErrorString(errorStr) {
			if debugE2E() {
				fmt.Printf("[TestDatabaseManager] Port %d unavailable after action, trying port %d\n", currentPort, currentPort+1)
			}
			currentPort++
			continue
		}

		// Different error, don't retry
		return 0, EnvDatabaseConfig{}, fmt.Errorf("database creation failed: %v", status.Error)
	}

	// Check if we exhausted retries
	if status == nil || status.Status != StatusSuccess {
		return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to create database after %d port retries (last port tried: %d)", maxPortRetries, currentPort)
	}

	// Extract database configuration from output
	var envConfig EnvDatabaseConfig
	if status.Output == nil {
		return 0, EnvDatabaseConfig{}, fmt.Errorf("no output in creation response")
	}

	// Extract bdb_id
	var bdbID int
	if id, ok := status.Output["bdb_id"].(float64); ok {
		bdbID = int(id)
		envConfig.BdbID = bdbID
	} else {
		return 0, EnvDatabaseConfig{}, fmt.Errorf("failed to extract bdb_id from creation output")
	}

	// Extract username
	if username, ok := status.Output["username"].(string); ok {
		envConfig.Username = username
	}

	// Extract password
	if password, ok := status.Output["password"].(string); ok {
		envConfig.Password = password
	}

	// Extract TLS setting
	if tls, ok := status.Output["tls"].(bool); ok {
		envConfig.TLS = tls
	}

	// Extract endpoints
	if endpoints, ok := status.Output["endpoints"].([]interface{}); ok {
		envConfig.Endpoints = make([]string, 0, len(endpoints))
		for _, ep := range endpoints {
			if epStr, ok := ep.(string); ok {
				envConfig.Endpoints = append(envConfig.Endpoints, epStr)
			}
		}
	}

	// Extract raw_endpoints
	if rawEndpoints, ok := status.Output["raw_endpoints"].([]interface{}); ok {
		envConfig.RawEndpoints = make([]DatabaseEndpoint, 0, len(rawEndpoints))
		for _, rawEp := range rawEndpoints {
			if rawEpMap, ok := rawEp.(map[string]interface{}); ok {
				var dbEndpoint DatabaseEndpoint

				// Extract addr
				if addr, ok := rawEpMap["addr"].([]interface{}); ok {
					dbEndpoint.Addr = make([]string, 0, len(addr))
					for _, a := range addr {
						if aStr, ok := a.(string); ok {
							dbEndpoint.Addr = append(dbEndpoint.Addr, aStr)
						}
					}
				}

				// Extract other fields
				if addrType, ok := rawEpMap["addr_type"].(string); ok {
					dbEndpoint.AddrType = addrType
				}
				if dnsName, ok := rawEpMap["dns_name"].(string); ok {
					dbEndpoint.DNSName = dnsName
				}
				if preferredEndpointType, ok := rawEpMap["oss_cluster_api_preferred_endpoint_type"].(string); ok {
					dbEndpoint.OSSClusterAPIPreferredEndpointType = preferredEndpointType
				}
				if preferredIPType, ok := rawEpMap["oss_cluster_api_preferred_ip_type"].(string); ok {
					dbEndpoint.OSSClusterAPIPreferredIPType = preferredIPType
				}
				if port, ok := rawEpMap["port"].(float64); ok {
					dbEndpoint.Port = int(port)
				}
				if proxyPolicy, ok := rawEpMap["proxy_policy"].(string); ok {
					dbEndpoint.ProxyPolicy = proxyPolicy
				}
				if uid, ok := rawEpMap["uid"].(string); ok {
					dbEndpoint.UID = uid
				}

				envConfig.RawEndpoints = append(envConfig.RawEndpoints, dbEndpoint)
			}
		}
	}

	m.createdBdbID = bdbID
	return bdbID, envConfig, nil
}

// DeleteDatabase deletes the created database
func (m *TestDatabaseManager) DeleteDatabase(ctx context.Context) error {
	if m.createdBdbID == 0 {
		return fmt.Errorf("no database to delete (bdb_id is 0)")
	}

	resp, err := m.faultInjector.DeleteDatabase(ctx, m.clusterIndex, m.createdBdbID)
	if err != nil {
		return fmt.Errorf("failed to trigger database deletion: %w", err)
	}

	// Wait for deletion to complete
	status, err := m.faultInjector.WaitForAction(ctx, resp.ActionID,
		WithMaxWaitTime(2*time.Minute),
		WithPollInterval(3*time.Second))
	if err != nil {
		return fmt.Errorf("failed to wait for database deletion: %w", err)
	}

	if status.Status != StatusSuccess {
		return fmt.Errorf("database deletion failed: %v", status.Error)
	}

	m.createdBdbID = 0

	return nil
}

// GetBdbID returns the created database ID
func (m *TestDatabaseManager) GetBdbID() int {
	return m.createdBdbID
}

// Cleanup ensures the database is deleted (safe to call multiple times)
func (m *TestDatabaseManager) Cleanup(ctx context.Context) {
	if m.createdBdbID != 0 {
		if err := m.DeleteDatabase(ctx); err != nil {
			m.t.Logf("Warning: Failed to cleanup database: %v", err)
		}
	}
}

// SetupTestDatabaseFromEnv creates a database from environment config and returns a cleanup function
// Usage:
//
//	cleanup := SetupTestDatabaseFromEnv(t, ctx, "my-test-db")
//	defer cleanup()
func SetupTestDatabaseFromEnv(t *testing.T, ctx context.Context, databaseName string) (bdbID int, cleanup func()) {
	// Get environment config
	envConfig, err := GetEnvConfig()
	if err != nil {
		t.Fatalf("Failed to get environment config: %v", err)
	}

	// Get database config from environment
	databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
	if err != nil {
		t.Fatalf("Failed to get database config: %v", err)
	}

	// Get the specific database config
	var envDbConfig EnvDatabaseConfig
	var exists bool
	if databaseName == "" {
		// Get first database if no name provided
		for _, config := range databasesConfig {
			envDbConfig = config
			exists = true
			break
		}
	} else {
		envDbConfig, exists = databasesConfig[databaseName]
	}

	if !exists {
		t.Fatalf("Database %s not found in configuration", databaseName)
	}

	// Create fault injector with cleanup
	faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
	if err != nil {
		t.Fatalf("Failed to create fault injector: %v", err)
	}

	// Create database manager
	dbManager := NewTestDatabaseManager(t, faultInjector, 0)

	// Create the database
	testDBName := fmt.Sprintf("e2e-test-%s-%d", databaseName, time.Now().Unix())
	bdbID, err = dbManager.CreateDatabaseFromEnvConfig(ctx, envDbConfig, testDBName)
	if err != nil {
		fiCleanup()
		t.Fatalf("[ERROR] Failed to create test database: %v", err)
	}

	// Return combined cleanup function
	cleanup = func() {
		dbManager.Cleanup(ctx)
		fiCleanup()
	}

	return bdbID, cleanup
}

// SetupTestDatabaseWithConfig creates a database with custom config and returns a cleanup function
// Usage:
//
//	bdbID, cleanup := SetupTestDatabaseWithConfig(t, ctx, dbConfig)
//	defer cleanup()
func SetupTestDatabaseWithConfig(t *testing.T, ctx context.Context, dbConfig DatabaseConfig) (bdbID int, cleanup func()) {
	// Create fault injector with cleanup
	faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
	if err != nil {
		t.Fatalf("Failed to create fault injector: %v", err)
	}

	// Create database manager
	dbManager := NewTestDatabaseManager(t, faultInjector, 0)

	// Create the database
	bdbID, err = dbManager.CreateDatabase(ctx, dbConfig)
	if err != nil {
		fiCleanup()
		t.Fatalf("[ERROR] Failed to create test database: %v", err)
	}

	// Return combined cleanup function
	cleanup = func() {
		dbManager.Cleanup(ctx)
		fiCleanup()
	}

	return bdbID, cleanup
}

// SetupTestDatabaseAndFactory creates a database from environment config and returns both bdbID, factory, test mode config, and cleanup function
// This is the recommended way to setup tests as it ensures the client factory connects to the newly created database
//
// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it will use the Docker proxy setup (127.0.0.1:17000) instead of creating a new database.
// This allows tests to work with either the real fault injector OR the Docker proxy setup.
//
// Usage:
//
//	bdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactory(t, ctx, "standalone")
//	defer cleanup()
func SetupTestDatabaseAndFactory(t *testing.T, ctx context.Context, databaseName string) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, cleanup func()) {
	// Get environment config
	envConfig, err := GetEnvConfig()
	if err != nil {
		// No environment config - use Docker proxy setup
		t.Logf("No environment config found, using Docker proxy setup at 127.0.0.1:17000")

		// Determine cluster mode based on database name
		// "standalone" creates a non-cluster client for testing FAILING_OVER/FAILED_OVER notifications
		isClusterMode := databaseName != "standalone"

		// Create a simple Redis connection config for Docker proxy
		redisConfig := &RedisConnectionConfig{
			Host:          "127.0.0.1", // Use 127.0.0.1 to force IPv4
			Port:          17000,
			Username:      "",
			Password:      "",
			TLS:           false,
			BdbID:         0,
			IsClusterMode: isClusterMode,
		}

		factory = NewClientFactory(redisConfig)

		// Get proxy mock test mode config
		testMode = GetTestModeConfig()

		// No-op cleanup since we're not creating a database
		cleanup = func() {
			factory.DestroyAll()
		}

		return 0, factory, testMode, cleanup
	}

	// Get database config from environment
	databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
	if err != nil {
		t.Fatalf("Failed to get database config: %v", err)
	}

	// Get the specific database config
	var envDbConfig EnvDatabaseConfig
	var exists bool
	if databaseName == "" {
		// Get first database if no name provided
		for _, config := range databasesConfig {
			envDbConfig = config
			exists = true
			break
		}
	} else {
		envDbConfig, exists = databasesConfig[databaseName]
	}

	if !exists {
		t.Fatalf("Database %s not found in configuration", databaseName)
	}

	// Determine cluster mode based on database name
	// "standalone" creates a non-OSS-Cluster database for testing FAILING_OVER/FAILED_OVER notifications
	// Other names create OSS Cluster databases for testing SMIGRATING/SMIGRATED notifications
	clusterMode := databaseName != "standalone"

	// Convert to DatabaseConfig with appropriate cluster mode
	dbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfigWithMode(envDbConfig, fmt.Sprintf("e2e-test-%s-%d", databaseName, time.Now().Unix()), clusterMode)
	if err != nil {
		t.Fatalf("Failed to convert config: %v", err)
	}

	// Create fault injector with cleanup
	faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
	if err != nil {
		t.Fatalf("Failed to create fault injector: %v", err)
	}

	// Create database manager
	dbManager := NewTestDatabaseManager(t, faultInjector, 0)

	// Create the database and get the actual connection config from fault injector
	bdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)
	if err != nil {
		fiCleanup()
		t.Fatalf("[ERROR] Failed to create test database: %v", err)
	}

	// Use certificate location from original config if not provided by fault injector
	if newEnvConfig.CertificatesLocation == "" && envDbConfig.CertificatesLocation != "" {
		newEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation
	}

	// Propagate OSSCluster flag since fault injector response may not include it
	newEnvConfig.OSSCluster = dbConfig.OSSCluster

	// Convert EnvDatabaseConfig to RedisConnectionConfig
	redisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)
	if err != nil {
		dbManager.Cleanup(ctx)
		fiCleanup()
		t.Fatalf("Failed to convert database config: %v", err)
	}

	// Create client factory with the actual config from fault injector
	factory = NewClientFactory(redisConfig)

	// Get real fault injector test mode config
	testMode = GetTestModeConfig()

	// Combined cleanup function
	cleanup = func() {
		factory.DestroyAll()
		dbManager.Cleanup(ctx)
		fiCleanup()
	}

	return bdbID, factory, testMode, cleanup
}

// SetupTestDatabaseAndFactoryWithConfig creates a database with custom config and returns both bdbID, factory, test mode config, and cleanup function
//
// If REDIS_ENDPOINTS_CONFIG_PATH is not set, it will use the Docker proxy setup (127.0.0.1:17000) instead of creating a new database.
// This allows tests to work with either the real fault injector OR the Docker proxy setup.
//
// Usage:
//
//	bdbID, factory, testMode, cleanup := SetupTestDatabaseAndFactoryWithConfig(t, ctx, "standalone", dbConfig)
//	defer cleanup()
func SetupTestDatabaseAndFactoryWithConfig(t *testing.T, ctx context.Context, databaseName string, dbConfig DatabaseConfig) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, cleanup func()) {
	// Get environment config to use as template for connection details
	envConfig, err := GetEnvConfig()
	if err != nil {
		// No environment config - use Docker proxy setup
		t.Logf("No environment config found, using Docker proxy setup at 127.0.0.1:17000")

		// Create a simple Redis connection config for Docker proxy
		// The proxy simulates a cluster, so we set IsClusterMode to true
		redisConfig := &RedisConnectionConfig{
			Host:          "127.0.0.1", // Use 127.0.0.1 to force IPv4
			Port:          17000,
			Username:      "",
			Password:      "",
			TLS:           false,
			BdbID:         0,
			IsClusterMode: true, // Docker proxy simulates a cluster
		}

		factory = NewClientFactory(redisConfig)

		// Get proxy mock test mode config
		testMode = GetTestModeConfig()

		// No-op cleanup since we're not creating a database
		cleanup = func() {
			factory.DestroyAll()
		}

		return 0, factory, testMode, cleanup
	}

	// Get database config from environment
	databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
	if err != nil {
		t.Fatalf("Failed to get database config: %v", err)
	}

	// Get the specific database config as template
	var envDbConfig EnvDatabaseConfig
	var exists bool
	if databaseName == "" {
		// Get first database if no name provided
		for _, config := range databasesConfig {
			envDbConfig = config
			exists = true
			break
		}
	} else {
		envDbConfig, exists = databasesConfig[databaseName]
	}

	if !exists {
		t.Fatalf("Database %s not found in configuration", databaseName)
	}

	// Create fault injector with cleanup
	faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
	if err != nil {
		t.Fatalf("Failed to create fault injector: %v", err)
	}

	// Create database manager
	dbManager := NewTestDatabaseManager(t, faultInjector, 0)

	// Create the database and get the actual connection config from fault injector
	bdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)
	if err != nil {
		fiCleanup()
		t.Fatalf("[ERROR] Failed to create test database: %v", err)
	}

	// Use certificate location from original config if not provided by fault injector
	if newEnvConfig.CertificatesLocation == "" && envDbConfig.CertificatesLocation != "" {
		newEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation
	}

	// Convert EnvDatabaseConfig to RedisConnectionConfig
	redisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)
	if err != nil {
		dbManager.Cleanup(ctx)
		fiCleanup()
		t.Fatalf("Failed to convert database config: %v", err)
	}

	// Create client factory with the actual config from fault injector
	factory = NewClientFactory(redisConfig)

	// Get real fault injector test mode config
	testMode = GetTestModeConfig()

	// Combined cleanup function
	cleanup = func() {
		factory.DestroyAll()
		dbManager.Cleanup(ctx)
		fiCleanup()
	}

	return bdbID, factory, testMode, cleanup
}

// GetSlotMigrateRequirementsCount returns the number of database requirements for a given effect/variant.
// This is useful for iterating over all required databases when running tests.
// Returns 0 if the effect/variant combination is not found or if in proxy mode (which always returns 1).
func GetSlotMigrateRequirementsCount(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant) int {
	// Get environment config
	envConfig, err := GetEnvConfig()
	if err != nil {
		// No environment config - use Docker proxy setup (always 1 requirement)
		return 1
	}

	// Create fault injector client using the URL from environment config
	fiClient := NewFaultInjectorClient(envConfig.FaultInjectorURL)

	// Query the fault injector for the required database configuration
	triggersResp, err := fiClient.GetSlotMigrateTriggers(ctx, effect, 0)
	if err != nil {
		t.Logf("Warning: Failed to get slot-migrate triggers: %v", err)
		return 1
	}

	// Find the trigger that matches our variant
	for _, trigger := range triggersResp.Triggers {
		if trigger.Name == string(variant) || (variant == SlotMigrateVariantDefault && trigger.Name == "migrate") {
			return len(trigger.Requirements)
		}
	}

	return 1
}

// SetupTestDatabaseForSlotMigrate creates a database configured for a specific slot-migrate effect
// It queries the fault injector's GET /slot-migrate endpoint to get the required database configuration
// and creates the database with those settings.
//
// Usage:
//
//	bdbID, factory, testMode, fiClient, cleanup := SetupTestDatabaseForSlotMigrate(t, ctx, SlotMigrateEffectSlotShuffle, SlotMigrateVariantMigrate)
//	defer cleanup()
func SetupTestDatabaseForSlotMigrate(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, fiClient *FaultInjectorClient, cleanup func()) {
	return SetupTestDatabaseForSlotMigrateWithRequirementIndex(t, ctx, effect, variant, 0)
}

// SetupTestDatabaseForSlotMigrateWithRequirementIndex creates a database configured for a specific slot-migrate effect
// using the requirement at the specified index. This allows testing against all required database configurations.
//
// Usage:
//
//	reqCount := GetSlotMigrateRequirementsCount(t, ctx, effect, variant)
//	for i := 0; i < reqCount; i++ {
//	    bdbID, factory, testMode, fiClient, cleanup := SetupTestDatabaseForSlotMigrateWithRequirementIndex(t, ctx, effect, variant, i)
//	    defer cleanup()
//	    // run test...
//	}
func SetupTestDatabaseForSlotMigrateWithRequirementIndex(t *testing.T, ctx context.Context, effect SlotMigrateEffect, variant SlotMigrateVariant, requirementIndex int) (bdbID int, factory *ClientFactory, testMode *TestModeConfig, fiClient *FaultInjectorClient, cleanup func()) {
	// Get environment config
	envConfig, err := GetEnvConfig()
	if err != nil {
		// No environment config - use Docker proxy setup
		t.Logf("No environment config found, using Docker proxy setup at 127.0.0.1:17000")

		redisConfig := &RedisConnectionConfig{
			Host:          "127.0.0.1",
			Port:          17000,
			Username:      "",
			Password:      "",
			TLS:           false,
			BdbID:         0,
			IsClusterMode: true,
		}

		factory = NewClientFactory(redisConfig)
		testMode = GetTestModeConfig()

		// Create fault injector client for proxy mode using Docker fault injector URL
		fiClient = NewFaultInjectorClient("http://localhost:15000")

		// Call CreateDatabase to initialize the proxy fault injector with 2 nodes
		// This is required for slot-shuffle, remove, and remove-add effects which need >= 2 nodes
		resp, err := fiClient.CreateDatabase(ctx, 0, DatabaseConfig{})
		if err != nil {
			t.Fatalf("Failed to trigger CreateDatabase on proxy fault injector: %v", err)
		}

		// Wait for the action to complete
		status, err := fiClient.WaitForAction(ctx, resp.ActionID,
			WithMaxWaitTime(10*time.Second),
			WithPollInterval(100*time.Millisecond))
		if err != nil {
			t.Fatalf("Failed to wait for CreateDatabase action: %v", err)
		}
		if status.Status != StatusSuccess {
			t.Fatalf("CreateDatabase action failed: %v", status.Error)
		}

		cleanup = func() {
			factory.DestroyAll()
		}

		return 0, factory, testMode, fiClient, cleanup
	}

	// Create fault injector client using the URL from environment config
	fiClient = NewFaultInjectorClient(envConfig.FaultInjectorURL)

	// Query the fault injector for the required database configuration
	t.Logf("Querying fault injector for %s effect requirements...", effect)
	if debugE2E() {
		slotMigrateURL := fmt.Sprintf("%s/slot-migrate?effect=%s&cluster_index=0", envConfig.FaultInjectorURL, effect)
		t.Logf("  URL: %s", slotMigrateURL)

		// Get raw response for debugging
		rawResp, err := fiClient.GetSlotMigrateTriggersRaw(ctx, effect, 0)
		if err != nil {
			t.Logf("Warning: Failed to get raw slot-migrate triggers: %v", err)
		} else {
			t.Logf("  Raw response: %s", string(rawResp))
		}
	}

	triggersResp, err := fiClient.GetSlotMigrateTriggers(ctx, effect, 0)
	if err != nil {
		t.Fatalf("Failed to get slot-migrate triggers: %v", err)
	}

	// Log the full response (only in debug mode)
	if debugE2E() {
		t.Logf("  Response: Effect=%s, Triggers=%d", triggersResp.Effect, len(triggersResp.Triggers))
		for i, trigger := range triggersResp.Triggers {
			t.Logf("    Trigger[%d]: Name=%s, Description=%s, Requirements=%d", i, trigger.Name, trigger.Description, len(trigger.Requirements))
			for j, req := range trigger.Requirements {
				t.Logf("      Requirement[%d]: DBConfig=%v, Cluster=%v, Description=%s", j, req.DBConfig, req.Cluster, req.Description)
			}
		}
	}

	// Find the trigger that matches our variant
	var requiredDBConfig map[string]interface{}
	for _, trigger := range triggersResp.Triggers {
		if trigger.Name == string(variant) || (variant == SlotMigrateVariantDefault && trigger.Name == "migrate") {
			if requirementIndex < len(trigger.Requirements) {
				requiredDBConfig = trigger.Requirements[requirementIndex].DBConfig
				if debugE2E() {
					t.Logf("Found required DB config for %s/%s (requirement %d/%d): %v",
						effect, variant, requirementIndex+1, len(trigger.Requirements), requiredDBConfig)
				}
			} else if len(trigger.Requirements) > 0 {
				// Fall back to first requirement if index is out of bounds
				requiredDBConfig = trigger.Requirements[0].DBConfig
				t.Logf("Warning: requirement index %d out of bounds (max %d), using first requirement",
					requirementIndex, len(trigger.Requirements)-1)
			}
			break
		}
	}

	// Get database config from environment
	databasesConfig, err := GetDatabaseConfigFromEnv(envConfig.RedisEndpointsConfigPath)
	if err != nil {
		t.Fatalf("Failed to get database config: %v", err)
	}

	// Get the first database config
	var envDbConfig EnvDatabaseConfig
	for _, config := range databasesConfig {
		envDbConfig = config
		break
	}

	// Convert to DatabaseConfig
	dbConfig, err := ConvertEnvDatabaseConfigToFaultInjectorConfig(envDbConfig, fmt.Sprintf("e2e-slotmigrate-%d", time.Now().Unix()))
	if err != nil {
		t.Fatalf("Failed to convert config: %v", err)
	}

	// Apply the required configuration from fault injector
	if requiredDBConfig != nil {
		if shardsCount, ok := requiredDBConfig["shards_count"]; ok {
			if sc, ok := shardsCount.(float64); ok {
				dbConfig.ShardsCount = int(sc)
				if debugE2E() {
					t.Logf("Setting ShardsCount to %d (from fault injector)", dbConfig.ShardsCount)
				}
			}
		}
		if shardsPlacement, ok := requiredDBConfig["shards_placement"]; ok {
			if sp, ok := shardsPlacement.(string); ok {
				dbConfig.ShardsPlacement = sp
				if debugE2E() {
					t.Logf("Setting ShardsPlacement to %s (from fault injector)", dbConfig.ShardsPlacement)
				}
			}
		}
		if replication, ok := requiredDBConfig["replication"]; ok {
			if r, ok := replication.(bool); ok {
				dbConfig.Replication = r
				if debugE2E() {
					t.Logf("Setting Replication to %v (from fault injector)", dbConfig.Replication)
				}
			}
		}
		if ossCluster, ok := requiredDBConfig["oss_cluster"]; ok {
			if oc, ok := ossCluster.(bool); ok {
				dbConfig.OSSCluster = oc
				if debugE2E() {
					t.Logf("Setting OSSCluster to %v (from fault injector)", dbConfig.OSSCluster)
				}
			}
		}
		if sharding, ok := requiredDBConfig["sharding"]; ok {
			if s, ok := sharding.(bool); ok {
				dbConfig.Sharding = s
				if debugE2E() {
					t.Logf("Setting Sharding to %v (from fault injector)", dbConfig.Sharding)
				}
			}
		}
	}

	if debugE2E() {
		t.Logf("Creating database with config: ShardsCount=%d, ShardsPlacement=%s, Replication=%v, OSSCluster=%v",
			dbConfig.ShardsCount, dbConfig.ShardsPlacement, dbConfig.Replication, dbConfig.OSSCluster)
	}

	// Create fault injector with cleanup
	faultInjector, fiCleanup, err := CreateTestFaultInjectorWithCleanup()
	if err != nil {
		t.Fatalf("Failed to create fault injector: %v", err)
	}

	// Create database manager
	dbManager := NewTestDatabaseManager(t, faultInjector, 0)

	// Create the database and get the actual connection config from fault injector
	bdbID, newEnvConfig, err := dbManager.CreateDatabaseAndGetConfig(ctx, dbConfig)
	if err != nil {
		fiCleanup()
		t.Fatalf("[ERROR] Failed to create test database: %v", err)
	}

	// Use certificate location from original config if not provided by fault injector
	if newEnvConfig.CertificatesLocation == "" && envDbConfig.CertificatesLocation != "" {
		newEnvConfig.CertificatesLocation = envDbConfig.CertificatesLocation
	}

	// Propagate OSSCluster flag since fault injector response may not include it
	newEnvConfig.OSSCluster = dbConfig.OSSCluster

	// Convert EnvDatabaseConfig to RedisConnectionConfig
	redisConfig, err := ConvertEnvDatabaseConfigToRedisConnectionConfig(newEnvConfig)
	if err != nil {
		dbManager.Cleanup(ctx)
		fiCleanup()
		t.Fatalf("Failed to convert database config: %v", err)
	}

	// Create client factory with the actual config from fault injector
	factory = NewClientFactory(redisConfig)

	// Get real fault injector test mode config
	testMode = GetTestModeConfig()

	// Combined cleanup function
	cleanup = func() {
		factory.DestroyAll()
		dbManager.Cleanup(ctx)
		fiCleanup()
	}

	return bdbID, factory, testMode, fiClient, cleanup
}
