code

package site.daydream.colleen

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertThrows
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

/**
 * Comprehensive test suite for TestClient
 * Tests all HTTP methods, headers, cookies, query parameters,
 * form data, multipart uploads, and assertion helpers
 */
class TestClientTest {

    private lateinit var app: Colleen

    @BeforeEach
    fun setup() {
        app = Colleen()
    }

    @Nested
    inner class BasicHttpMethods {

        @Test
        fun `test GET request`() {
            app.get("/hello") { "Hello World" }

            val response = app.testGet("/hello")

            response.assertStatus(200)
                .assertBodyEquals("Hello World")
        }

        @Test
        fun `test POST request`() {
            app.post("/echo") { ctx ->
                ctx.request.body()
            }

            val response = app.testClient()
                .post("/echo")
                .body("test content")
                .send()

            response.assertStatus(200)
                .assertBodyEquals("test content")
        }

        @Test
        fun `test PUT request`() {
            app.put("/update/:id") { ctx ->
                "Updated ${ctx.pathParam("id")}"
            }

            val response = app.testClient()
                .put("/update/123")
                .send()

            response.assertStatus(200)
                .assertBodyContains("123")
        }

        @Test
        fun `test DELETE request`() {
            app.delete("/users/:id") { ctx ->
                mapOf("deleted" to ctx.pathParam("id"))
            }

            val response = app.testClient()
                .delete("/users/456")
                .send()

            response.assertStatus(200)
            val json = response.json<Map<String, String>>()
            assertEquals("456", json?.get("deleted"))
        }

        @Test
        fun `test PATCH request`() {
            app.patch("/partial/:id") { ctx ->
                "Patched ${ctx.pathParam("id")}"
            }

            val response = app.testClient()
                .patch("/partial/789")
                .send()

            response.assertStatus(200)
                .assertBodyEquals("Patched 789")
        }

        @Test
        fun `test HEAD request`() {
            app.head("/resource") { ctx ->
                ctx.response.header("X-Resource-Size", "1024")
                ""
            }

            val response = app.testClient()
                .head("/resource")
                .send()

            response.assertStatus(200)
                .assertHeader("X-Resource-Size", "1024")
        }

        @Test
        fun `test OPTIONS request`() {
            app.options("/api") { ctx ->
                ctx.response.header("Allow", "GET, POST, PUT, DELETE")
                ""
            }

            val response = app.testClient()
                .options("/api")
                .send()

            response.assertHeader("Allow")
        }
    }

    @Nested
    inner class HeaderTests {

        @Test
        fun `test single header`() {
            app.get("/auth") { ctx ->
                ctx.request.header("Authorization") ?: "No auth"
            }

            val response = app.testClient()
                .get("/auth")
                .header("Authorization", "Bearer token123")
                .send()

            response.assertBodyEquals("Bearer token123")
        }

        @Test
        fun `test multiple headers`() {
            app.post("/headers") { ctx ->
                val auth = ctx.request.header("Authorization")
                val contentType = ctx.request.header("Content-Type")
                "$auth | $contentType"
            }

            val response = app.testClient()
                .post("/headers")
                .header("Authorization", "Bearer xyz")
                .header("Content-Type", "application/json")
                .body("{}")
                .send()

            response.assertBodyContains("Bearer xyz")
                .assertBodyContains("application/json")
        }

        @Test
        fun `test custom headers`() {
            app.get("/custom") { ctx ->
                mapOf(
                    "xRequestId" to ctx.request.header("X-Request-ID"),
                    "xApiKey" to ctx.request.header("X-API-Key")
                )
            }

            val response = app.testClient()
                .get("/custom")
                .header("X-Request-ID", "req-123")
                .header("X-API-Key", "key-456")
                .send()

            val json = response.json<Map<String, String>>()
            assertEquals("req-123", json?.get("xRequestId"))
            assertEquals("key-456", json?.get("xApiKey"))
        }
    }

    @Nested
    inner class QueryParameterTests {

        @Test
        fun `test single query parameter`() {
            app.get("/search") { ctx ->
                "Query: ${ctx.queryParam("q")}"
            }

            val response = app.testClient()
                .get("/search")
                .query("q", "kotlin")
                .send()

            response.assertBodyEquals("Query: kotlin")
        }

        @Test
        fun `test multiple query parameters`() {
            app.get("/filter") { ctx ->
                val page = ctx.queryParam("page")
                val size = ctx.queryParam("size")
                val sort = ctx.queryParam("sort")
                "page=$page&size=$size&sort=$sort"
            }

            val response = app.testClient()
                .get("/filter")
                .query("page", "1")
                .query("size", "10")
                .query("sort", "name")
                .send()

            response.assertBodyContains("page=1")
                .assertBodyContains("size=10")
                .assertBodyContains("sort=name")
        }

        @Test
        fun `test query parameters with special characters`() {
            app.get("/special") { ctx ->
                ctx.queryParam("text") ?: ""
            }

            val response = app.testClient()
                .get("/special")
                .query("text", "hello world & special=chars")
                .send()

            response.assertBodyEquals("hello world & special=chars")
        }

        @Test
        fun `test multiple values for same query parameter`() {
            app.get("/tags") { ctx ->
                ctx.queryParams("tag").joinToString(",")
            }

            val response = app.testClient()
                .get("/tags")
                .query("tag", "kotlin")
                .query("tag", "java")
                .query("tag", "spring")
                .send()

            response.assertBodyContains("kotlin")
                .assertBodyContains("java")
                .assertBodyContains("spring")
        }
    }

    @Nested
    inner class CookieTests {

        @Test
        fun `test single cookie`() {
            app.get("/check-session") { ctx ->
                ctx.cookie("session") ?: "No session"
            }

            val response = app.testClient()
                .get("/check-session")
                .cookie("session", "abc123")
                .send()

            response.assertBodyEquals("abc123")
        }

        @Test
        fun `test multiple cookies`() {
            app.get("/cookies") { ctx ->
                val session = ctx.cookie("session")
                val user = ctx.cookie("user")
                val pref = ctx.cookie("preference")
                "session=$session, user=$user, pref=$pref"
            }

            val response = app.testClient()
                .get("/cookies")
                .cookie("session", "sess123")
                .cookie("user", "alice")
                .cookie("preference", "dark")
                .send()

            response.assertBodyContains("sess123")
                .assertBodyContains("alice")
                .assertBodyContains("dark")
        }
    }

    @Nested
    inner class RequestBodyTests {

        @Test
        fun `test string body`() {
            app.post("/echo") { ctx ->
                ctx.request.body()
            }

            val response = app.testClient()
                .post("/echo")
                .body("Hello from test")
                .send()

            response.assertBodyEquals("Hello from test")
        }

        @Test
        fun `test byte array body`() {
            app.post("/bytes") { ctx ->
                ctx.request.bodyAsBytes()?.size ?: 0
            }

            val testBytes = byteArrayOf(1, 2, 3, 4, 5)
            val response = app.testClient()
                .post("/bytes")
                .body(testBytes)
                .send()

            val size = response.json<Int>()
            assertEquals(5, size)
        }

        @Test
        fun `test JSON body with data class`() {
            data class User(val name: String, val age: Int)

            app.post("/user") { ctx ->
                val user = ctx.bodyAs<User>()
                "User: ${user.name}, Age: ${user.age}"
            }

            val response = app.testClient()
                .post("/user")
                .json(User("Alice", 30))
                .send()

            response.assertBodyContains("Alice")
                .assertBodyContains("30")
        }

        @Test
        fun `test JSON body with map`() {
            app.post("/data") { ctx ->
                val data = ctx.bodyAs<Map<String, Any>>()
                data
            }

            val inputData = mapOf(
                "key1" to "value1",
                "key2" to 42,
                "key3" to true
            )

            val response = app.testClient()
                .post("/data")
                .json(inputData)
                .send()

            val result = response.json<Map<String, Any>>()
            assertEquals("value1", result?.get("key1"))
            assertEquals(42, (result?.get("key2") as? Number)?.toInt())
            assertEquals(true, result?.get("key3"))
        }

        @Test
        fun `test JSON array body`() {
            app.post("/list") { ctx ->
                val list = ctx.bodyAs<List<String>>()
                list.size
            }

            val response = app.testClient()
                .post("/list")
                .json(listOf("a", "b", "c"))
                .send()

            val size = response.json<Int>()
            assertEquals(3, size)
        }
    }

    @Nested
    inner class FormDataTests {

        @Test
        fun `test form data with vararg`() {
            app.post("/login") { ctx ->
                val username = ctx.formParam("username")
                val password = ctx.formParam("password")
                "Logged in: $username"
            }

            val response = app.testClient()
                .post("/login")
                .form("username" to "alice", "password" to "secret123")
                .send()

            response.assertBodyEquals("Logged in: alice")
        }

        @Test
        fun `test form data with map`() {
            app.post("/register") { ctx ->
                val data = mapOf(
                    "email" to ctx.formParam("email"),
                    "name" to ctx.formParam("name")
                )
                data
            }

            val formData = mapOf(
                "email" to "alice@example.com",
                "name" to "Alice Smith"
            )

            val response = app.testClient()
                .post("/register")
                .form(formData)
                .send()

            val result = response.json<Map<String, String>>()
            assertEquals("alice@example.com", result?.get("email"))
            assertEquals("Alice Smith", result?.get("name"))
        }

        @Test
        fun `test form data with special characters`() {
            app.post("/feedback") { ctx ->
                ctx.formParam("message") ?: ""
            }

            val response = app.testClient()
                .post("/feedback")
                .form("message" to "Hello & goodbye! Special=chars")
                .send()

            response.assertBodyContains("Hello")
                .assertBodyContains("Special")
        }
    }

    @Nested
    inner class MultipartTests {

        @Test
        fun `test file upload with bytes`() {
            app.post("/upload") { ctx ->
                val file = ctx.file("file")
                mapOf(
                    "filename" to file?.filename,
                    "size" to file?.size,
                    "contentType" to file?.contentType
                )
            }

            val fileContent = "Hello, this is a test file!".toByteArray()
            val response = app.testClient()
                .post("/upload")
                .file("file", "test.txt", fileContent, "text/plain")
                .send()

            val result = response.json<Map<String, Any>>()
            assertEquals("test.txt", result?.get("filename"))
            assertEquals(fileContent.size.toLong(), (result?.get("size") as? Number)?.toLong())
            assertEquals("text/plain", result?.get("contentType"))
        }

        @Test
        fun `test file upload from File object`() {
            app.post("/upload-doc") { ctx ->
                val file = ctx.file("document")
                file?.filename ?: "No file"
            }

            // Create temporary file
            val tempFile = File.createTempFile("test", ".txt")
            tempFile.writeText("Document content")

            try {
                val response = app.testClient()
                    .post("/upload-doc")
                    .file("document", tempFile)
                    .send()

                response.assertBodyEquals(tempFile.name)
            } finally {
                tempFile.delete()
            }
        }

        @Test
        fun `test multiple file upload`() {
            app.post("/upload-multiple") { ctx ->
                val files = ctx.files()
                files.size
            }

            val response = app.testClient()
                .post("/upload-multiple")
                .file("file1", "doc1.txt", "Content 1".toByteArray())
                .file("file2", "doc2.txt", "Content 2".toByteArray())
                .file("file3", "doc3.txt", "Content 3".toByteArray())
                .send()

            val count = response.json<Int>()
            assertEquals(3, count)
        }

        @Test
        fun `test multipart with form fields and files`() {
            app.post("/upload-with-metadata") { ctx ->
                val title = ctx.formParam("title")
                val description = ctx.formParam("description")
                val file = ctx.file("attachment")
                
                mapOf(
                    "title" to title,
                    "description" to description,
                    "filename" to file?.filename
                )
            }

            val response = app.testClient()
                .post("/upload-with-metadata")
                .multipart(
                    FormItem("title", "My Document"),
                    FormItem("description", "Important file"),
                    FileItem(
                        "attachment",
                        "doc.pdf",
                        "application/pdf",
                        100L,
                        "PDF content".toByteArray().inputStream()
                    )
                )
                .send()

            val result = response.json<Map<String, String>>()
            assertEquals("My Document", result?.get("title"))
            assertEquals("Important file", result?.get("description"))
            assertEquals("doc.pdf", result?.get("filename"))
        }
    }

    @Nested
    inner class ResponseAssertionTests {

        @Test
        fun `test assertStatus`() {
            app.get("/ok") { "OK" }
            app.get("/not-found") { ctx ->
                ctx.status(404)
                "Not found"
            }

            app.testGet("/ok").assertStatus(200)
            app.testGet("/not-found").assertStatus(404)
        }

        @Test
        fun `test assertSuccess`() {
            app.get("/success") { ctx ->
                ctx.status(201)
                "Created"
            }

            app.testGet("/success").assertSuccess()
        }

        @Test
        fun `test assertClientError`() {
            app.get("/bad-request") { ctx ->
                ctx.status(400)
                "Bad request"
            }

            app.testGet("/bad-request").assertClientError()
        }

        @Test
        fun `test assertServerError`() {
            app.get("/error") { ctx ->
                ctx.status(500)
                "Internal error"
            }

            app.testGet("/error").assertServerError()
        }

        @Test
        fun `test assertBodyContains`() {
            app.get("/message") { "Hello World from Kotlin" }

            app.testGet("/message")
                .assertBodyContains("Hello")
                .assertBodyContains("World")
                .assertBodyContains("Kotlin")
        }

        @Test
        fun `test assertBodyEquals`() {
            app.get("/exact") { "Exact match" }

            app.testGet("/exact").assertBodyEquals("Exact match")
        }

        @Test
        fun `test assertHeader exists`() {
            app.get("/with-header") { ctx ->
                ctx.response.header("X-Custom-Header", "value")
                "OK"
            }

            app.testGet("/with-header").assertHeader("X-Custom-Header")
        }

        @Test
        fun `test assertHeader with value`() {
            app.get("/content") { ctx ->
                ctx.response.header("Content-Type", "application/json")
                "{}"
            }

            app.testGet("/content")
                .assertHeader("Content-Type", "application/json")
        }

        @Test
        fun `test assertContentType`() {
            app.get("/json") { ctx ->
                ctx.json(mapOf("key" to "value"))
            }

            app.testGet("/json").assertContentType("application/json")
        }

        @Test
        fun `test assertion failures throw AssertionError`() {
            app.get("/test") { "Hello" }

            assertThrows<AssertionError> {
                app.testGet("/test").assertStatus(404)
            }

            assertThrows<AssertionError> {
                app.testGet("/test").assertBodyEquals("Wrong")
            }

            assertThrows<AssertionError> {
                app.testGet("/test").assertBodyContains("NotThere")
            }
        }

        @Test
        fun `test chained assertions`() {
            app.post("/api/create") { ctx ->
                ctx.status(201)
                ctx.response.header("Location", "/api/items/123")
                ctx.json(mapOf("id" to 123, "status" to "created"))
            }

            app.testClient()
                .post("/api/create")
                .json(mapOf("name" to "New Item"))
                .send()
                .assertStatus(201)
                .assertSuccess()
                .assertHeader("Location")
                .assertContentType("application/json")
        }
    }

    @Nested
    inner class ResponseDataExtractionTests {

        @Test
        fun `test text extraction`() {
            app.get("/text") { "Plain text response" }

            val text = app.testGet("/text").text()
            assertEquals("Plain text response", text)
        }

        @Test
        fun `test json extraction with inline reified`() {
            data class Person(val name: String, val age: Int)

            app.get("/person") { ctx ->
                ctx.json(Person("Bob", 25))
            }

            val person = app.testGet("/person").json<Person>()
            assertNotNull(person)
            assertEquals("Bob", person.name)
            assertEquals(25, person.age)
        }

        @Test
        fun `test json extraction with class parameter`() {
            app.get("/number") { ctx ->
                ctx.json(42)
            }

            val number = app.testGet("/number").json(Int::class.java)
            assertEquals(42, number)
        }

        @Test
        fun `test bytes extraction`() {
            app.get("/binary") { ctx ->
                ctx.bytes(byteArrayOf(1, 2, 3, 4, 5))
            }

            val bytes = app.testGet("/binary").bytes()
            assertNotNull(bytes)
            assertEquals(5, bytes.size)
            assertEquals(1, bytes[0])
            assertEquals(5, bytes[4])
        }

        @Test
        fun `test header extraction`() {
            app.get("/with-headers") { ctx ->
                ctx.response.header("X-Request-ID", "req-123")
                ctx.response.header("X-API-Version", "v2")
                "OK"
            }

            val response = app.testGet("/with-headers")
            assertEquals("req-123", response.header("X-Request-ID"))
            assertEquals("v2", response.header("X-API-Version"))
        }

        @Test
        fun `test headers extraction returns list`() {
            app.get("/multi-header") { ctx ->
                ctx.response.header("Set-Cookie", "session=abc")
                ctx.response.header("Set-Cookie", "user=xyz")
                "OK"
            }

            val response = app.testGet("/multi-header")
            val cookies = response.headers("Set-Cookie")
            assertEquals(2, cookies.size)
            assertTrue(cookies.contains("session=abc"))
            assertTrue(cookies.contains("user=xyz"))
        }

        @Test
        fun `test null responses`() {
            app.get("/empty") { ctx ->
                ctx.status(204)
                ctx.response.body = Response.Body.Empty
            }

            val response = app.testGet("/empty")
            assertNull(response.text())
            assertNull(response.bytes())
            assertNull(response.json<Map<String, Any>>())
        }
    }

    @Nested
    inner class ConvenienceMethodTests {

        @Test
        fun `test testGet convenience method`() {
            app.get("/quick") { "Quick test" }

            val response = app.testGet("/quick")
            response.assertStatus(200).assertBodyEquals("Quick test")
        }

        @Test
        fun `test testPost convenience method`() {
            app.post("/quick-post") { ctx ->
                val data = ctx.bodyAs<Map<String, String>>()
                "Received: ${data["message"]}"
            }

            val response = app.testPost("/quick-post", mapOf("message" to "Hello"))
            response.assertBodyContains("Hello")
        }

        @Test
        fun `test testClient factory method`() {
            app.get("/client") { "Test client" }

            val client = app.testClient()
            val response = client.get("/client").send()
            response.assertStatus(200)
        }

        @Test
        fun `test invoke operator for GET requests`() {
            app.get("/operator") { "Operator test" }

            val response = app.testClient().get("/operator")()
            response.assertStatus(200)
        }
    }

    @Nested
    inner class EdgeCasesAndErrorHandling {

        @Test
        fun `test empty path`() {
            app.get("/") { "Root" }

            val response = app.testGet("/")
            response.assertBodyEquals("Root")
        }

        @Test
        fun `test path with special characters`() {
            app.get("/api/items/:id") { ctx ->
                ctx.pathParam("id") ?: ""
            }

            val response = app.testGet("/api/items/abc-123_def")
            response.assertBodyContains("abc-123_def")
        }

        @Test
        fun `test very large body`() {
            app.post("/large") { ctx ->
                ctx.request.bodyAsBytes()?.size ?: 0
            }

            val largeContent = "x".repeat(1_000_000)
            val response = app.testClient()
                .post("/large")
                .body(largeContent)
                .send()

            val size = response.json<Int>()
            assertEquals(1_000_000, size)
        }

        @Test
        fun `test empty query parameters`() {
            app.get("/no-params") { ctx ->
                ctx.request.queryString.isEmpty()
            }

            val response = app.testGet("/no-params")
            val isEmpty = response.json<Boolean>()
            assertEquals(true, isEmpty)
        }

        @Test
        fun `test empty headers`() {
            app.get("/minimal") { "Minimal" }

            val response = app.testClient()
                .get("/minimal")
                .send()

            response.assertStatus(200)
        }

        @Test
        fun `test dump method for debugging`() {
            app.get("/debug") { ctx ->
                ctx.response.header("X-Debug", "true")
                "Debug info"
            }

            // dump() should not throw and should return self
            val response = app.testGet("/debug").dump()
            response.assertStatus(200)
        }
    }

    @Nested
    inner class IntegrationTests {

        @Test
        fun `test complete REST API workflow`() {
            data class Todo(val id: Int, val title: String, val completed: Boolean)
            val todos = mutableListOf<Todo>()

            // Create
            app.post("/todos") { ctx ->
                val todo = ctx.bodyAs<Todo>()
                todos.add(todo)
                ctx.status(201)
                todo
            }

            // List
            app.get("/todos") { todos }

            // Get one
            app.get("/todos/:id") { ctx ->
                val id = ctx.pathParam("id")?.toInt()
                todos.find { it.id == id } ?: ctx.status(404)
            }

            // Update
            app.put("/todos/:id") { ctx ->
                val id = ctx.pathParam("id")?.toInt()
                val updated = ctx.bodyAs<Todo>()
                val index = todos.indexOfFirst { it.id == id }
                if (index >= 0) {
                    todos[index] = updated
                    updated
                } else {
                    ctx.status(404)
                }
            }

            // Delete
            app.delete("/todos/:id") { ctx ->
                val id = ctx.pathParam("id")?.toInt()
                todos.removeIf { it.id == id }
                ctx.status(204)
            }

            val client = app.testClient()

            // Create todo
            client.post("/todos")
                .json(Todo(1, "Learn Kotlin", false))
                .send()
                .assertStatus(201)

            // List todos
            client.get("/todos")
                .send()
                .assertStatus(200)
                .json<List<Todo>>()
                .let { list ->
                    assertEquals(1, list?.size)
                }

            // Update todo
            client.put("/todos/1")
                .json(Todo(1, "Learn Kotlin", true))
                .send()
                .assertStatus(200)

            // Delete todo
            client.delete("/todos/1")
                .send()
                .assertStatus(204)
        }

        @Test
        fun `test authentication flow`() {
            var sessionToken: String? = null

            app.post("/login") { ctx ->
                val creds = ctx.bodyAs<Map<String, String>>()
                if (creds["username"] == "admin" && creds["password"] == "pass") {
                    sessionToken = "token-${System.currentTimeMillis()}"
                    ctx.response.header("Set-Cookie", "session=$sessionToken")
                    mapOf("success" to true, "token" to sessionToken)
                } else {
                    ctx.status(401)
                    mapOf("success" to false)
                }
            }

            app.get("/protected") { ctx ->
                val token = ctx.cookie("session")
                if (token == sessionToken) {
                    "Protected data"
                } else {
                    ctx.status(403)
                    "Forbidden"
                }
            }

            val client = app.testClient()

            // Login
            val loginResponse = client.post("/login")
                .json(mapOf("username" to "admin", "password" to "pass"))
                .send()

            loginResponse.assertStatus(200)
            val token = loginResponse.header("Set-Cookie")?.substringAfter("session=")

            // Access protected resource
            client.get("/protected")
                .cookie("session", token ?: "")
                .send()
                .assertStatus(200)
                .assertBodyEquals("Protected data")
        }
    }
}