package de.krisenvorrat.server

import de.krisenvorrat.server.db.DatabaseFactory
import io.ktor.client.plugins.websocket.WebSockets as ClientWebSockets
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.config.*
import io.ktor.server.testing.*
import io.ktor.websocket.*
import kotlinx.coroutines.withTimeout
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test

class WebSocketTest {

    private val json = Json { ignoreUnknownKeys = true }

    private val adminToken = createTestAccessToken(
        userId = TEST_ADMIN_ID,
        username = TEST_ADMIN_USERNAME,
        isAdmin = true
    )

    private fun testApp(block: suspend ApplicationTestBuilder.() -> Unit) = testApplication {
        environment {
            config = MapApplicationConfig(*testMapConfig().toTypedArray())
        }
        application {
            DatabaseFactory.init(
                jdbcUrl = "jdbc:h2:mem:ws_test_${System.nanoTime()};DB_CLOSE_DELAY=-1",
                driver = "org.h2.Driver",
                adminPassword = "test-admin-pw"
            )
            configurePlugins()
        }
        block()
    }

    private suspend fun ApplicationTestBuilder.createUser(username: String, password: String = "pass123"): String {
        client.post("/api/admin/users") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody("""{"username":"$username","password":"$password"}""")
        }
        val listResponse = client.get("/api/admin/users") { bearerAuth(adminToken) }
        val users = json.decodeFromString<kotlinx.serialization.json.JsonArray>(listResponse.bodyAsText())
        return users.first {
            it as JsonObject
            it["username"]?.jsonPrimitive?.content == username
        }.let { (it as JsonObject)["id"]!!.jsonPrimitive.content }
    }

    // ── Connect ──────────────────────────────────────────────────────────────

    @Test
    fun test_webSocket_validToken_connectsSuccessfully() = testApp {
        val token = createTestAccessToken()
        val wsClient = createClient {
            install(ClientWebSockets)
        }

        wsClient.webSocket("/ws/sync?token=$token") {
            // Connection established – send close
            close(CloseReason(CloseReason.Codes.NORMAL, "Test done"))
        }
        // If we reach here without exception, the connection was successful
    }

    @Test
    fun test_webSocket_missingToken_closesWithViolatedPolicy() = testApp {
        val wsClient = createClient {
            install(ClientWebSockets)
        }

        wsClient.webSocket("/ws/sync") {
            // Server should close the connection
            val reason = closeReason.await()
            assertNotNull(reason)
            assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code)
            assertTrue(reason.message.contains("Missing token"))
        }
    }

    @Test
    fun test_webSocket_invalidToken_closesWithViolatedPolicy() = testApp {
        val wsClient = createClient {
            install(ClientWebSockets)
        }

        wsClient.webSocket("/ws/sync?token=invalid-jwt-token") {
            val reason = closeReason.await()
            assertNotNull(reason)
            assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code)
            assertTrue(reason.message.contains("Invalid or expired token"))
        }
    }

    @Test
    fun test_webSocket_expiredToken_closesWithViolatedPolicy() = testApp {
        val expiredToken = createExpiredAccessToken()
        val wsClient = createClient {
            install(ClientWebSockets)
        }

        wsClient.webSocket("/ws/sync?token=$expiredToken") {
            val reason = closeReason.await()
            assertNotNull(reason)
            assertEquals(CloseReason.Codes.VIOLATED_POLICY.code, reason!!.code)
        }
    }

    // ── Events ───────────────────────────────────────────────────────────────

    @Test
    fun test_webSocket_inventoryUpdate_receivesEvent() = testApp {
        val token = createTestAccessToken()
        val wsClient = createClient {
            install(ClientWebSockets)
        }

        wsClient.webSocket("/ws/sync?token=$token") {
            // Trigger an inventory update from outside via PUT
            val httpClient = this@testApp.client
            val inventory = """{"categories":[],"locations":[],"items":[],"settings":[]}"""
            httpClient.put("/api/inventory") {
                bearerAuth(token)
                contentType(ContentType.Application.Json)
                setBody(inventory)
            }

            // Expect a fullSyncRequired event
            val frame = withTimeout(5_000) { incoming.receive() }
            assertTrue(frame is Frame.Text)
            val payload = (frame as Frame.Text).readText()
            val event = json.decodeFromString<JsonObject>(payload)
            assertEquals("fullSyncRequired", event["type"]?.jsonPrimitive?.content)
            assertNotNull(event["timestamp"])

            close(CloseReason(CloseReason.Codes.NORMAL, "Test done"))
        }
    }

    @Test
    fun test_webSocket_newMessage_receivesEvent() = testApp {
        // Given – create bob and alice, connect bob's WebSocket
        val bobId = createUser("bob")
        val aliceId = createUser("alice")
        val bobToken = createTestAccessToken(userId = bobId, username = "bob")
        val aliceToken = createTestAccessToken(userId = aliceId, username = "alice")

        val wsClient = createClient {
            install(ClientWebSockets)
        }

        // Bob connects WebSocket
        wsClient.webSocket("/ws/sync?token=$bobToken") {
            // Alice sends a message to Bob via HTTP
            val httpClient = this@testApp.client
            httpClient.post("/api/messages") {
                bearerAuth(aliceToken)
                contentType(ContentType.Application.Json)
                setBody("""{"receiverId":"$bobId","body":"Hello Bob!","sentAt":1700000000000}""")
            }

            // Bob should receive new_message event
            val frame = withTimeout(5_000) { incoming.receive() }
            assertTrue(frame is Frame.Text)
            val payload = (frame as Frame.Text).readText()
            val event = json.decodeFromString<JsonObject>(payload)
            assertEquals("new_message", event["type"]?.jsonPrimitive?.content)
            assertEquals("Hello Bob!", event["body"]?.jsonPrimitive?.content)
            assertEquals(aliceId, event["senderId"]?.jsonPrimitive?.content)

            close(CloseReason(CloseReason.Codes.NORMAL, "Test done"))
        }
    }

    // ── Disconnect ───────────────────────────────────────────────────────────

    @Test
    fun test_webSocket_clientDisconnect_serverHandlesGracefully() = testApp {
        val token = createTestAccessToken()
        val wsClient = createClient {
            install(ClientWebSockets)
        }

        // Connect and immediately disconnect
        wsClient.webSocket("/ws/sync?token=$token") {
            close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnecting"))
        }

        // Verify the server is still healthy after disconnect
        val healthResponse = client.get("/api/health")
        assertEquals(HttpStatusCode.OK, healthResponse.status)
    }

    @Test
    fun test_webSocket_undeliveredMessages_sentOnConnect() = testApp {
        // Given – create users and send a message while bob is offline
        val bobId = createUser("bob")
        val aliceId = createUser("alice")
        val aliceToken = createTestAccessToken(userId = aliceId, username = "alice")
        val bobToken = createTestAccessToken(userId = bobId, username = "bob")

        // Alice sends a message while Bob is offline
        client.post("/api/messages") {
            bearerAuth(aliceToken)
            contentType(ContentType.Application.Json)
            setBody("""{"receiverId":"$bobId","body":"Offline message","sentAt":1700000000000}""")
        }

        // When – Bob connects via WebSocket
        val wsClient = createClient {
            install(ClientWebSockets)
        }

        wsClient.webSocket("/ws/sync?token=$bobToken") {
            // Then – Bob should receive the pending message on connect
            val frame = withTimeout(5_000) { incoming.receive() }
            assertTrue(frame is Frame.Text)
            val payload = (frame as Frame.Text).readText()
            val event = json.decodeFromString<JsonObject>(payload)
            assertEquals("new_message", event["type"]?.jsonPrimitive?.content)
            assertEquals("Offline message", event["body"]?.jsonPrimitive?.content)

            close(CloseReason(CloseReason.Codes.NORMAL, "Test done"))
        }
    }
}
