code

import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
import redis.clients.jedis.exceptions.JedisConnectionException
import java.net.URI
import kotlin.test.Test

/**
 * RedisClient wrapper test suite
 *
 * Test focus:
 * - Client creation and configuration
 * - Connection pool management
 * - Transaction encapsulation
 * - Pipeline encapsulation
 * - Exception handling
 * - Resource cleanup
 * - Edge cases and boundary conditions
 */
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RedisClientTest {

    private lateinit var client: RedisClient
    private val testHost = "localhost"
    private val testPort = 6379

    @BeforeAll
    fun setup() {
        client = RedisClient.create(
            host = testHost,
            port = testPort,
            maxTotal = 10,
            maxIdle = 5,
            minIdle = 2,
            maxWaitMillis = 3000
        )
    }

    @AfterAll
    fun teardown() {
        client.close()
    }

    @BeforeEach
    fun cleanupBefore() {
        // Clean up test data
        client.execute { it.flushDB() }
    }

    // ========== Client Creation Tests ==========

    @Test
    fun `should create client with default parameters`() {
        RedisClient.create().use { testClient ->
            assertNotNull(testClient)
            assertFalse(testClient.isClosed())
        }
    }

    @Test
    fun `should create client with custom parameters`() {
        RedisClient.create(
            host = testHost,
            port = testPort,
            database = 0,
            timeout = 5000,
            maxWaitMillis = 5000
        ).use { testClient ->
            assertNotNull(testClient)
            // Verify connection is usable
            testClient.set("test_key", "test_value")
            assertEquals("test_value", testClient.get("test_key"))
        }
    }

    @Test
    fun `should create client from URI string`() {
        val uri = "redis://$testHost:$testPort"
        RedisClient.create(uri).use { testClient ->
            assertNotNull(testClient)
            assertFalse(testClient.isClosed())
        }
    }

    @Test
    fun `should create client from URI instance`() {
        val uri = URI("redis://$testHost:$testPort")
        RedisClient.create(uri).use { testClient ->
            assertNotNull(testClient)
            assertFalse(testClient.isClosed())
        }
    }

    @Test
    fun `should create client with authentication`() {
        // Note: This test assumes Redis is configured with password "testpass"
        // Skip if Redis doesn't require authentication
        try {
            RedisClient.create(
                host = testHost,
                port = testPort,
                password = "testpass"
            ).use { testClient ->
                assertNotNull(testClient)
            }
        } catch (e: Exception) {
            // Skip test if authentication is not configured
            println("Skipping auth test - Redis authentication not configured")
        }
    }

    @Test
    fun `should create client with different database`() {
        RedisClient.create(
            host = testHost,
            port = testPort,
            database = 1
        ).use { testClient ->
            testClient.set("db_test", "value")
            assertEquals("value", testClient.get("db_test"))

            // Verify it's isolated from database 0
            assertNull(client.get("db_test"))
        }
    }

    @Test
    fun `should create client with custom pool configuration`() {
        RedisClient.create(
            host = testHost,
            port = testPort,
            maxTotal = 20,
            maxIdle = 10,
            minIdle = 5,
            maxWaitMillis = 5000
        ).use { testClient ->
            assertNotNull(testClient)
            // Verify pool works with custom settings
            repeat(15) { i ->
                testClient.set("pool_$i", "value_$i")
            }
        }
    }

    // ========== Connection Pool Management Tests ==========

    @Test
    fun `should properly manage connection lifecycle`() {
        val testClient = RedisClient.create(
            host = testHost,
            port = testPort
        )

        assertFalse(testClient.isClosed())

        // Execute operations
        testClient.set("key", "value")
        assertEquals("value", testClient.get("key"))

        // Close client
        testClient.close()
        assertTrue(testClient.isClosed())
    }

    @Test
    fun `should handle multiple concurrent operations`() {
        val threads = List(10) { threadIndex ->
            Thread {
                repeat(100) { i ->
                    val key = "concurrent_key_${threadIndex}_$i"
                    val value = "value_${threadIndex}_$i"
                    client.set(key, value)
                    assertEquals(value, client.get(key))
                }
            }
        }

        threads.forEach { it.start() }
        threads.forEach { it.join() }

        // Verify pool is still healthy
        assertFalse(client.isClosed())
    }

    @Test
    fun `should reuse connections from pool`() {
        // Execute multiple operations to verify connection reuse
        repeat(100) { i ->
            client.set("pool_test_$i", "value_$i")
            assertEquals("value_$i", client.get("pool_test_$i"))
        }

        assertFalse(client.isClosed())
    }

    @Test
    fun `should handle pool exhaustion gracefully`() {
        val smallPoolClient = RedisClient.create(
            host = testHost,
            port = testPort,
            maxTotal = 2,
            maxWaitMillis = 1000
        )

        smallPoolClient.use { testClient ->
            // Should handle limited pool size without errors
            val threads = List(5) {
                Thread {
                    repeat(10) { i ->
                        testClient.set("exhaustion_$i", "value_$i")
                    }
                }
            }

            threads.forEach { it.start() }
            threads.forEach { it.join() }
        }
    }

    @Test
    fun `should return connections to pool after use`() {
        // Execute operation and verify connection is returned
        client.set("test", "value")

        // Execute another operation - should reuse connection from pool
        client.get("test")

        // Pool should still be healthy
        assertFalse(client.isClosed())
    }

    // ========== execute Method Tests ==========

    @Test
    fun `should execute custom commands through execute method`() {
        val result = client.execute { jedis ->
            jedis.set("custom_key", "custom_value")
            jedis.get("custom_key")
        }

        assertEquals("custom_value", result)
    }

    @Test
    fun `should handle exceptions in execute block`() {
        assertThrows<Exception> {
            client.execute { jedis ->
                jedis.set("key", "value")
                throw RuntimeException("Test exception")
            }
        }
    }

    @Test
    fun `should support nested execute calls`() {
        val result = client.execute { outer ->
            outer.set("outer_key", "outer_value")

            client.execute { inner ->
                inner.set("inner_key", "inner_value")
                inner.get("outer_key")
            }
        }

        assertEquals("outer_value", result)
        assertEquals("inner_value", client.get("inner_key"))
    }

    @Test
    fun `should return correct types from execute`() {
        val stringResult: String? = client.execute { it.get("non_existent") }
        assertNull(stringResult)

        val longResult: Long = client.execute { it.incr("counter") }
        assertEquals(1L, longResult)

        val booleanResult: Boolean = client.execute { it.exists("counter") }
        assertTrue(booleanResult)
    }

    @Test
    fun `should handle null returns in execute`() {
        val result = client.execute { it.get("non_existent_key") }
        assertNull(result)
    }

    // ========== Transaction Tests ==========

    @Test
    fun `should execute commands atomically in transaction`() {
        val results = client.transaction { tx ->
            tx.set("tx_key1", "value1")
            tx.set("tx_key2", "value2")
            tx.incr("tx_counter")
        }

        assertNotNull(results)
        assertEquals("value1", client.get("tx_key1"))
        assertEquals("value2", client.get("tx_key2"))
        assertEquals(1L, client.get("tx_counter")?.toLong())
    }

    @Test
    fun `should rollback transaction on exception`() {
        // Set initial value
        client.set("rollback_key", "initial")

        assertThrows<RuntimeException> {
            client.transaction { tx ->
                tx.set("rollback_key", "updated")
                tx.set("new_key", "new_value")
                throw RuntimeException("Force rollback")
            }
        }

        // Verify transaction was rolled back
        assertEquals("initial", client.get("rollback_key"))
        assertNull(client.get("new_key"))
    }

    @Test
    fun `should handle empty transaction`() {
        val results = client.transaction { }
        assertEquals(emptyList<Any>(), results)
    }

    @Test
    fun `should support multiple operations in single transaction`() {
        client.set("counter", "0")

        val results = client.transaction { tx ->
            repeat(10) {
                tx.incr("counter")
            }
        }

        assertEquals(10, results.size)
        assertEquals(10L, client.get("counter")?.toLong())
    }

    @Test
    fun `should handle transaction with conditional operations`() {
        client.set("watch_key", "initial")

        val results = client.transaction { tx ->
            tx.set("watch_key", "updated")
            tx.get("watch_key")
        }

        assertNotNull(results)
        assertEquals("updated", client.get("watch_key"))
    }

    @Test
    fun `should handle nested transaction calls`() {
        // Our wrapper allows nested transaction calls as each uses independent connection
        // This is different from Redis's native MULTI behavior which doesn't support nesting
        val results = client.transaction { tx1 ->
            tx1.set("outer", "value1")

            // Inner transaction uses a separate connection from the pool
            client.transaction { tx2 ->
                tx2.set("inner", "value2")
            }
        }

        // Both transactions should complete independently
        assertNotNull(results)
        assertEquals("value1", client.get("outer"))
        assertEquals("value2", client.get("inner"))
    }

    @Test
    fun `should return empty list when transaction fails to exec`() {
        // Test edge case where exec returns null
        val results = client.transaction { tx ->
            tx.set("key", "value")
            // Transaction should still complete normally
        }

        assertNotNull(results)
    }

    // ========== Pipeline Tests ==========

    @Test
    fun `should execute commands in pipeline`() {
        client.pipeline { pipe ->
            repeat(100) { i ->
                pipe.set("pipe_key_$i", "value_$i")
            }
        }

        // Verify all commands were executed
        repeat(100) { i ->
            assertEquals("value_$i", client.get("pipe_key_$i"))
        }
    }

    @Test
    fun `should improve performance with pipeline`() {
        val normalStart = System.currentTimeMillis()
        repeat(1000) { i ->
            client.set("normal_$i", "value_$i")
        }
        val normalTime = System.currentTimeMillis() - normalStart

        // Cleanup
        client.execute { it.flushDB() }

        val pipelineStart = System.currentTimeMillis()
        client.pipeline { pipe ->
            repeat(1000) { i ->
                pipe.set("pipeline_$i", "value_$i")
            }
        }
        val pipelineTime = System.currentTimeMillis() - pipelineStart

        // Pipeline should be faster (typically 5-10x)
        println("Normal: ${normalTime}ms, Pipeline: ${pipelineTime}ms")
        assertTrue(pipelineTime < normalTime,
            "Pipeline ($pipelineTime ms) should be faster than normal ($normalTime ms)")
    }

    @Test
    fun `should handle exception in pipeline`() {
        assertThrows<RuntimeException> {
            client.pipeline { pipe ->
                pipe.set("key1", "value1")
                throw RuntimeException("Pipeline error")
            }
        }
    }

    @Test
    fun `should support mixed operations in pipeline`() {
        client.pipeline { pipe ->
            pipe.set("string_key", "string_value")
            pipe.lpush("list_key", "item1", "item2")
            pipe.sadd("set_key", "member1", "member2")
            pipe.hset("hash_key", "field1", "value1")
        }

        assertEquals("string_value", client.get("string_key"))
        assertEquals(2L, client.llen("list_key"))
        assertEquals(2L, client.scard("set_key"))
        assertEquals("value1", client.hget("hash_key", "field1"))
    }

    @Test
    fun `should handle empty pipeline`() {
        val results = client.pipeline { }
        assertEquals(emptyList<Any>(), results)
    }

    @Test
    fun `should execute pipelines concurrently`() {
        val threads = List(5) { threadIndex ->
            Thread {
                client.pipeline { pipe ->
                    repeat(100) { i ->
                        pipe.set("concurrent_pipe_${threadIndex}_$i", "value_$i")
                    }
                }
            }
        }

        threads.forEach { it.start() }
        threads.forEach { it.join() }

        // Verify all data was written
        repeat(5) { threadIndex ->
            repeat(100) { i ->
                assertEquals("value_$i", client.get("concurrent_pipe_${threadIndex}_$i"))
            }
        }
    }

    // ========== Exception Handling Tests ==========

    @Test
    fun `should handle connection timeout gracefully`() {
        // Use unreachable IP address
        assertThrows<Exception> {
            RedisClient.create(
                host = "192.0.2.1", // TEST-NET-1, guaranteed unreachable
                port = 6379,
                timeout = 100,
                maxWaitMillis = 500
            ).use { testClient ->
                testClient.set("key", "value")
            }
        }
    }

    @Test
    fun `should handle invalid operations`() {
        // Try to perform list operation on string value
        client.set("string_key", "string_value")

        assertThrows<Exception> {
            client.lpush("string_key", "item")
        }
    }

    @Test
    fun `should handle connection errors in execute`() {
        val badClient = RedisClient.create(
            host = "192.0.2.1",
            port = 6379,
            timeout = 100
        )

        assertThrows<JedisConnectionException> {
            badClient.execute { it.ping() }
        }

        badClient.close()
    }

    @Test
    fun `should handle operations on closed client`() {
        val testClient = RedisClient.create(host = testHost, port = testPort)
        testClient.close()

        assertTrue(testClient.isClosed())

        // Operations on closed client should fail
        assertThrows<Exception> {
            testClient.set("key", "value")
        }
    }

    // ========== Resource Cleanup Tests ==========

    @Test
    fun `should auto-close with use block`() {
        val testClient = RedisClient.create(host = testHost, port = testPort)

        testClient.use { client ->
            client.set("use_test", "value")
            assertEquals("value", client.get("use_test"))
        }

        // Verify it's closed
        assertTrue(testClient.isClosed())
    }

    @Test
    fun `should handle multiple close calls safely`() {
        val testClient = RedisClient.create(host = testHost, port = testPort)

        testClient.close()
        assertTrue(testClient.isClosed())

        // Multiple close calls should not throw
        assertDoesNotThrow {
            testClient.close()
        }
    }

    @Test
    fun `should cleanup resources in finally block`() {
        val testClient = RedisClient.create(host = testHost, port = testPort)

        try {
            testClient.set("key", "value")
            throw RuntimeException("Test exception")
        } catch (e: RuntimeException) {
            // Expected
        } finally {
            testClient.close()
        }

        assertTrue(testClient.isClosed())
    }

    // ========== Edge Cases and Boundary Tests ==========

    @Test
    fun `should handle empty string values`() {
        client.set("empty", "")
        assertEquals("", client.get("empty"))
    }

    @Test
    fun `should handle special characters in keys and values`() {
        val specialKey = "key:with:special:chars:🔑"
        val specialValue = "value with spaces, symbols !@#$%^&*(), and emoji 🎉"

        client.set(specialKey, specialValue)
        assertEquals(specialValue, client.get(specialKey))
    }

    @Test
    fun `should handle very long keys and values`() {
        val longKey = "k" + "x".repeat(1000)
        val longValue = "v" + "y".repeat(10000)

        client.set(longKey, longValue)
        assertEquals(longValue, client.get(longKey))
    }

    @Test
    fun `should handle null returns correctly`() {
        assertNull(client.get("non_existent_key"))
        assertNull(client.hget("non_existent_hash", "field"))
        assertNull(client.lpop("non_existent_list"))
    }

    @Test
    fun `should handle zero and negative numbers`() {
        client.set("zero", "0")
        assertEquals(0L, client.get("zero")?.toLong())

        client.set("negative", "-100")
        assertEquals(-100L, client.get("negative")?.toLong())

        assertEquals(1L, client.incrBy("zero", 1))
        assertEquals(-99L, client.incrBy("negative", 1))
    }

    @Test
    fun `should handle boundary values for increment operations`() {
        client.set("max", Long.MAX_VALUE.toString())

        // Incrementing max long should overflow (Redis behavior)
        assertThrows<Exception> {
            client.incr("max")
        }
    }

    @Test
    fun `should handle multiple deletes`() {
        client.set("key1", "value1")
        client.set("key2", "value2")
        client.set("key3", "value3")

        val deleted = client.del("key1", "key2", "key3", "non_existent")
        assertEquals(3L, deleted)
    }

    // ========== Integration Tests ==========

    @Test
    fun `should work with real-world scenario - user session management`() {
        val userId = "user123"
        val sessionId = "session_${System.currentTimeMillis()}"

        // Create session
        client.transaction { tx ->
            tx.hset("session:$sessionId", "userId", userId)
            tx.hset("session:$sessionId", "createdAt", System.currentTimeMillis().toString())
            tx.expire("session:$sessionId", 3600) // 1 hour expiration
        }

        // Verify session
        val storedUserId = client.hget("session:$sessionId", "userId")
        assertEquals(userId, storedUserId)

        val ttl = client.ttl("session:$sessionId")
        assertTrue(ttl > 0 && ttl <= 3600)
    }

    @Test
    fun `should work with real-world scenario - distributed counter`() {
        val counterKey = "page_views"

        // Simulate concurrent access
        val threads = List(10) { threadIndex ->
            Thread {
                repeat(100) {
                    client.incr(counterKey)
                }
            }
        }

        threads.forEach { it.start() }
        threads.forEach { it.join() }

        // Verify count is accurate
        assertEquals(1000L, client.get(counterKey)?.toLong())
    }

    @Test
    fun `should work with real-world scenario - leaderboard with sorted set`() {
        // Batch add scores
        client.pipeline { pipe ->
            pipe.zadd("leaderboard", 100.0, "player1")
            pipe.zadd("leaderboard", 200.0, "player2")
            pipe.zadd("leaderboard", 150.0, "player3")
            pipe.zadd("leaderboard", 300.0, "player4")
        }

        // Get leaderboard (descending order)
        val topPlayers = client.zrevrange("leaderboard", 0, 2)
        assertEquals(listOf("player4", "player2", "player3"), topPlayers)

        // Get player rank
        val rank = client.zrank("leaderboard", "player3")
        assertNotNull(rank)
    }

    @Test
    fun `should work with real-world scenario - cache with expiration`() {
        val cacheKey = "cache:user:123"
        val userData = """{"id":123,"name":"John","email":"john@example.com"}"""

        // Set cache with 5 second expiration
        client.setex(cacheKey, 5, userData)

        // Read immediately
        assertEquals(userData, client.get(cacheKey))

        // Check TTL
        val ttl = client.ttl(cacheKey)
        assertTrue(ttl in 1..5)

        // Delete cache
        client.del(cacheKey)
        assertNull(client.get(cacheKey))
    }

    @Test
    fun `should work with real-world scenario - rate limiting`() {
        val userId = "user456"
        val rateLimitKey = "rate_limit:$userId"
        val maxRequests = 10L
        val windowSeconds = 60L

        // Simulate rate limiting
        val currentRequests = client.incr(rateLimitKey)

        if (currentRequests == 1L) {
            client.expire(rateLimitKey, windowSeconds)
        }

        // Check if within limit
        assertTrue(currentRequests <= maxRequests)

        // Verify TTL is set
        val ttl = client.ttl(rateLimitKey)
        assertTrue(ttl > 0)
    }

    @Test
    fun `should work with real-world scenario - pub-sub message queue simulation`() {
        // Use list as a simple message queue
        val queueKey = "message_queue"

        // Producer: push messages
        client.pipeline { pipe ->
            repeat(10) { i ->
                pipe.lpush(queueKey, "message_$i")
            }
        }

        // Consumer: pop messages
        val messages = mutableListOf<String>()
        repeat(10) {
            client.rpop(queueKey)?.let { messages.add(it) }
        }

        assertEquals(10, messages.size)
        assertEquals(0L, client.llen(queueKey))
    }

    @Test
    fun `should work with real-world scenario - distributed lock simulation`() {
        val lockKey = "lock:resource:123"
        val lockValue = "lock_${System.currentTimeMillis()}"

        // Acquire lock (setnx returns 1 if successful)
        val acquired = client.setnx(lockKey, lockValue)
        assertEquals(1L, acquired)

        // Set expiration to prevent deadlock
        client.expire(lockKey, 10)

        // Try to acquire again (should fail)
        val acquiredAgain = client.setnx(lockKey, "another_value")
        assertEquals(0L, acquiredAgain)

        // Release lock
        client.del(lockKey)

        // Should be able to acquire now
        val acquiredAfterRelease = client.setnx(lockKey, lockValue)
        assertEquals(1L, acquiredAfterRelease)
    }
}