package backends

import (
	"context"
	"errors"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// TestConnectionPool_BasicOperations tests basic pool operations
func TestConnectionPool_BasicOperations(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 5,
		ConnectTimeout: 5 * time.Second,
		ReadTimeout:    3 * time.Second,
		WriteTimeout:   3 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	t.Run("GetAndPutConnection", func(t *testing.T) {
		ctx := context.Background()

		// Get a connection
		conn, err := pool.Get(ctx)
		require.NoError(t, err)
		require.NotNil(t, conn)

		// Verify connection works
		resp, err := conn.Do("PING")
		require.NoError(t, err)
		assert.Equal(t, "PONG", resp)

		// Return to pool
		pool.Put(conn)

		// Get again - should reuse same connection
		conn2, err := pool.Get(ctx)
		require.NoError(t, err)
		require.NotNil(t, conn2)

		pool.Put(conn2)
	})

	t.Run("Stats", func(t *testing.T) {
		stats := pool.Stats()
		require.NotNil(t, stats)

		assert.Contains(t, stats, "active_connections")
		assert.Contains(t, stats, "total_connections")
		assert.Contains(t, stats, "max_connections")
		assert.Equal(t, 5, stats["max_connections"])
	})
}

// TestConnectionPool_MaxConnections tests pool size limits
func TestConnectionPool_MaxConnections(t *testing.T) {
	mr := NewMiniredisServer(t)

	maxConns := 3
	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: maxConns,
		ConnectTimeout: 1 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	ctx := context.Background()

	// Get max connections
	conns := make([]*RedisConn, maxConns)
	for i := 0; i < maxConns; i++ {
		conn, err := pool.Get(ctx)
		require.NoError(t, err)
		conns[i] = conn
	}

	// Verify stats
	stats := pool.Stats()
	assert.Equal(t, int32(maxConns), stats["total_connections"])
	assert.Equal(t, int32(maxConns), stats["active_connections"])

	// Try to get one more - should block/timeout
	ctx2, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()

	conn, err := pool.Get(ctx2)
	require.Error(t, err)
	require.Nil(t, conn)

	// Return one connection
	pool.Put(conns[0])

	// Now we should be able to get a connection
	conn, err = pool.Get(context.Background())
	require.NoError(t, err)
	require.NotNil(t, conn)

	// Cleanup
	pool.Put(conn)
	for i := 1; i < maxConns; i++ {
		pool.Put(conns[i])
	}
}

// TestConnectionPool_ConcurrentAccess tests concurrent pool usage
func TestConnectionPool_ConcurrentAccess(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 10,
		ConnectTimeout: 5 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	ctx := context.Background()
	numGoroutines := 50
	numOperations := 20

	var wg sync.WaitGroup
	errors := make(chan error, numGoroutines*numOperations)

	// Spawn goroutines
	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()

			for j := 0; j < numOperations; j++ {
				conn, err := pool.Get(ctx)
				if err != nil {
					errors <- err
					continue
				}

				// Do some work
				_, err = conn.Do("PING")
				if err != nil {
					errors <- err
				}

				// Return to pool
				pool.Put(conn)

				// Small delay
				time.Sleep(time.Millisecond)
			}
		}(i)
	}

	wg.Wait()
	close(errors)

	// Check for errors
	errorCount := 0
	for err := range errors {
		t.Logf("Error: %v", err)
		errorCount++
	}

	assert.Equal(t, 0, errorCount, "Expected no errors in concurrent access")

	// Verify stats
	stats := pool.Stats()
	t.Logf("Final stats: %+v", stats)
	assert.LessOrEqual(t, stats["total_connections"].(int32), int32(10))
	assert.Equal(t, int32(0), stats["active_connections"])
}

// TestConnectionPool_ContextCancellation tests context cancellation
func TestConnectionPool_ContextCancellation(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 1,
		ConnectTimeout: 5 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	// Get the only connection
	conn, err := pool.Get(context.Background())
	require.NoError(t, err)

	// Try to get another with canceled context
	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	conn2, err := pool.Get(ctx)
	require.Error(t, err)
	require.Nil(t, conn2)
	assert.Contains(t, err.Error(), "context canceled")

	// Cleanup
	pool.Put(conn)
}

// TestConnectionPool_Authentication tests auth support
func TestConnectionPool_Authentication(t *testing.T) {
	mr := NewMiniredisServer(t)

	// Set password on miniredis
	mr.server.RequireAuth("secret-password")

	t.Run("CorrectPassword", func(t *testing.T) {
		config := &PoolConfig{
			Address:        mr.GetAddr(),
			Password:       "secret-password",
			MaxConnections: 2,
			ConnectTimeout: 5 * time.Second,
		}

		pool, err := NewConnectionPool(config)
		require.NoError(t, err)
		defer pool.Close()

		conn, err := pool.Get(context.Background())
		require.NoError(t, err)

		resp, err := conn.Do("PING")
		require.NoError(t, err)
		assert.Equal(t, "PONG", resp)

		pool.Put(conn)
	})

	t.Run("WrongPassword", func(t *testing.T) {
		t.Skip("Miniredis doesn't fully simulate AUTH errors like real Redis")

		config := &PoolConfig{
			Address:        mr.GetAddr(),
			Password:       "wrong-password",
			MaxConnections: 2,
			ConnectTimeout: 5 * time.Second,
		}

		_, err := NewConnectionPool(config)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "authentication failed")
	})
}

// TestConnectionPool_DatabaseSelection tests DB selection
func TestConnectionPool_DatabaseSelection(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		DB:             5,
		MaxConnections: 2,
		ConnectTimeout: 5 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	conn, err := pool.Get(context.Background())
	require.NoError(t, err)

	// Connection should be on DB 5
	resp, err := conn.Do("PING")
	require.NoError(t, err)
	assert.Equal(t, "PONG", resp)

	pool.Put(conn)
}

// TestConnectionPool_ClosedConnection tests handling closed connections
func TestConnectionPool_ClosedConnection(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 2,
		ConnectTimeout: 5 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	// Get connection
	conn, err := pool.Get(context.Background())
	require.NoError(t, err)

	// Close it manually
	conn.Close()

	// Try to use it
	_, err = conn.Do("PING")
	require.Error(t, err)
	assert.True(t, errors.Is(err, ErrBackendClosed))

	// Return to pool (should be discarded)
	pool.Put(conn)

	// Get new connection - should create a new one
	conn2, err := pool.Get(context.Background())
	require.NoError(t, err)
	require.NotNil(t, conn2)

	resp, err := conn2.Do("PING")
	require.NoError(t, err)
	assert.Equal(t, "PONG", resp)

	pool.Put(conn2)
}

// TestConnectionPool_Close tests pool closure
func TestConnectionPool_Close(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 5,
		ConnectTimeout: 5 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)

	// Get some connections
	conns := make([]*RedisConn, 3)
	for i := 0; i < 3; i++ {
		conn, err := pool.Get(context.Background())
		require.NoError(t, err)
		conns[i] = conn
	}

	// Return them
	for _, conn := range conns {
		pool.Put(conn)
	}

	// Close pool
	err = pool.Close()
	require.NoError(t, err)

	// Try to get connection from closed pool
	_, err = pool.Get(context.Background())
	require.Error(t, err)
	assert.True(t, errors.Is(err, ErrBackendClosed))

	// Close again should be no-op
	err = pool.Close()
	require.NoError(t, err)
}

// TestConnectionPool_Timeouts tests various timeout scenarios
func TestConnectionPool_Timeouts(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 2,
		ConnectTimeout: 100 * time.Millisecond,
		ReadTimeout:    100 * time.Millisecond,
		WriteTimeout:   100 * time.Millisecond,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	conn, err := pool.Get(context.Background())
	require.NoError(t, err)

	// Normal operation should work
	resp, err := conn.Do("PING")
	require.NoError(t, err)
	assert.Equal(t, "PONG", resp)

	pool.Put(conn)
}

// TestRedisConn_DoCommand tests the Do method
func TestRedisConn_DoCommand(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 2,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	conn, err := pool.Get(context.Background())
	require.NoError(t, err)
	defer pool.Put(conn)

	t.Run("SET and GET", func(t *testing.T) {
		// SET
		resp, err := conn.Do("SET", "testkey", "testvalue")
		require.NoError(t, err)
		assert.Equal(t, "OK", resp)

		// GET
		resp, err = conn.Do("GET", "testkey")
		require.NoError(t, err)
		assert.Equal(t, "testvalue", resp)
	})

	t.Run("DEL", func(t *testing.T) {
		// SET key first
		_, err := conn.Do("SET", "delkey", "delvalue")
		require.NoError(t, err)

		// DEL
		resp, err := conn.Do("DEL", "delkey")
		require.NoError(t, err)

		count, err := RESPInt(resp)
		require.NoError(t, err)
		assert.Equal(t, int64(1), count)
	})

	t.Run("EXISTS", func(t *testing.T) {
		// SET key first
		_, err := conn.Do("SET", "existskey", "value")
		require.NoError(t, err)

		// EXISTS - key exists
		resp, err := conn.Do("EXISTS", "existskey")
		require.NoError(t, err)

		count, err := RESPInt(resp)
		require.NoError(t, err)
		assert.Equal(t, int64(1), count)

		// EXISTS - key doesn't exist
		resp, err = conn.Do("EXISTS", "nonexistent")
		require.NoError(t, err)

		count, err = RESPInt(resp)
		require.NoError(t, err)
		assert.Equal(t, int64(0), count)
	})

	t.Run("TTL commands", func(t *testing.T) {
		// SETEX
		resp, err := conn.Do("SETEX", "ttlkey", "60", "ttlvalue")
		require.NoError(t, err)
		assert.Equal(t, "OK", resp)

		// TTL
		resp, err = conn.Do("TTL", "ttlkey")
		require.NoError(t, err)

		ttl, err := RESPInt(resp)
		require.NoError(t, err)
		assert.Greater(t, ttl, int64(0))
		assert.LessOrEqual(t, ttl, int64(60))
	})
}

// TestPoolConfig_Defaults tests default configuration values
func TestPoolConfig_Defaults(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address: mr.GetAddr(),
		// Leave other fields at zero values
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	// Should use defaults
	assert.Equal(t, 10, pool.config.MaxConnections)
	assert.Equal(t, 5*time.Second, pool.config.ConnectTimeout)

	// Verify it works
	conn, err := pool.Get(context.Background())
	require.NoError(t, err)
	pool.Put(conn)
}

// TestConnectionPool_NilConnection tests handling nil connections
func TestConnectionPool_NilConnection(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 2,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	// Putting nil should be safe
	pool.Put(nil)

	// Pool should still work
	conn, err := pool.Get(context.Background())
	require.NoError(t, err)
	require.NotNil(t, conn)
	pool.Put(conn)
}

// TestConnectionPool_StatsTracking tests metrics tracking
func TestConnectionPool_StatsTracking(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 5,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	ctx := context.Background()

	// Initial stats
	stats := pool.Stats()
	initialGets := stats["gets"].(int64)
	initialPuts := stats["puts"].(int64)

	// Perform operations
	numOps := 10
	for i := 0; i < numOps; i++ {
		conn, err := pool.Get(ctx)
		require.NoError(t, err)
		pool.Put(conn)
	}

	// Check updated stats
	stats = pool.Stats()
	assert.Equal(t, initialGets+int64(numOps), stats["gets"].(int64))
	assert.Equal(t, initialPuts+int64(numOps), stats["puts"].(int64))
	assert.Equal(t, int32(0), stats["active_connections"].(int32))
}

// TestRedisConn_TooManyArguments tests protection against allocation overflow
func TestRedisConn_TooManyArguments(t *testing.T) {
	mr := NewMiniredisServer(t)

	config := &PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 1,
		ConnectTimeout: 5 * time.Second,
		ReadTimeout:    3 * time.Second,
		WriteTimeout:   3 * time.Second,
	}

	pool, err := NewConnectionPool(config)
	require.NoError(t, err)
	defer pool.Close()

	ctx := context.Background()
	conn, err := pool.Get(ctx)
	require.NoError(t, err)
	defer pool.Put(conn)

	t.Run("AcceptableArgumentCount", func(t *testing.T) {
		// Should work with reasonable number of args
		args := make([]string, 100)
		for i := range args {
			args[i] = "value"
		}
		_, err := conn.Do("MSET", args...)
		// May fail due to Redis constraints, but shouldn't panic or error on overflow
		// Just verify it doesn't trigger our overflow protection
		if err != nil {
			assert.NotContains(t, err.Error(), "too many arguments")
		}
	})

	t.Run("RejectExcessiveArguments", func(t *testing.T) {
		// Create an absurdly large number of arguments that would cause overflow
		// Use 1M + 1 to exceed maxSafeArgs = (1<<20)-1 = 1048575
		args := make([]string, 1<<20) // 1,048,576 args
		for i := range args {
			args[i] = "x"
		}

		_, err := conn.Do("MSET", args...)
		require.Error(t, err)
		assert.Contains(t, err.Error(), "too many arguments")
	})

	t.Run("BoundaryCase", func(t *testing.T) {
		// Test exactly at the boundary (maxSafeArgs)
		args := make([]string, (1<<20)-1) // Exactly 1,048,575 args (max allowed)
		for i := range args {
			args[i] = "x"
		}

		_, err := conn.Do("ECHO", args...)
		// Should not error due to overflow protection
		if err != nil {
			assert.NotContains(t, err.Error(), "too many arguments")
		}
	})

}

// TestRedisConn_RejectOversizedArgumentBytes is a regression test for CodeQL
// alert #10 (go/allocation-size-overflow). A single argument larger than
// maxTotalArgBytes (64 MiB) must be rejected by the per-argument overflow
// guard in Do() before any allocation is attempted.
func TestRedisConn_RejectOversizedArgumentBytes(t *testing.T) {
	mr := NewMiniredisServer(t)

	pool, err := NewConnectionPool(&PoolConfig{
		Address:        mr.GetAddr(),
		MaxConnections: 1,
		ConnectTimeout: 5 * time.Second,
		ReadTimeout:    3 * time.Second,
		WriteTimeout:   3 * time.Second,
	})
	require.NoError(t, err)
	defer pool.Close()

	conn, err := pool.Get(context.Background())
	require.NoError(t, err)
	defer pool.Put(conn)

	largeArg := strings.Repeat("x", (64<<20)+1)

	_, err = conn.Do("SET", "k", largeArg)
	require.Error(t, err)
	assert.Contains(t, err.Error(), "arguments too large")
}
