package de.krisenvorrat.server

import de.krisenvorrat.server.db.DatabaseFactory
import de.krisenvorrat.server.model.AssignInventoryRequest
import de.krisenvorrat.server.model.CreateUserRequest
import de.krisenvorrat.server.model.InventoryWithUsersDto
import de.krisenvorrat.server.model.UserDto
import de.krisenvorrat.shared.model.CategoryDto
import de.krisenvorrat.shared.model.InventoryDto
import de.krisenvorrat.shared.model.ItemDto
import de.krisenvorrat.shared.model.LocationDto
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 kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test

class InventorySharingTest {

    private val json = Json { ignoreUnknownKeys = true; encodeDefaults = 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:sharing_test_${System.nanoTime()};DB_CLOSE_DELAY=-1",
                driver = "org.h2.Driver",
                adminPassword = "test-admin-pw"
            )
            configurePlugins()
        }
        block()
    }

    // --- Helper: create a user and return their JWT token + userId ---

    private suspend fun ApplicationTestBuilder.createUserAndGetToken(
        username: String, password: String = "pass123"
    ): Pair<String, String> {
        // Create via admin API
        client.post("/api/admin/users") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(CreateUserRequest(username, password)))
        }
        // Login to get token
        val loginRes = client.post("/api/auth/login") {
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(mapOf("username" to username, "password" to password)))
        }
        val loginBody = json.parseToJsonElement(loginRes.bodyAsText())
        val token = loginBody.let {
            (it as kotlinx.serialization.json.JsonObject)["accessToken"]!!.let { v ->
                (v as kotlinx.serialization.json.JsonPrimitive).content
            }
        }
        val userId = loginBody.let {
            (it as kotlinx.serialization.json.JsonObject)["userId"]!!.let { v ->
                (v as kotlinx.serialization.json.JsonPrimitive).content
            }
        }
        return Pair(token, userId)
    }

    // --- Tests ---

    @Test
    fun test_newUser_getsOwnInventory() = testApp {
        val (tokenA, _) = createUserAndGetToken("alice")
        val (tokenB, _) = createUserAndGetToken("bob")

        // Alice stores an item
        val inventoryA = InventoryDto(
            categories = listOf(CategoryDto(id = 1, name = "Konserven")),
            locations = listOf(LocationDto(id = 1, name = "Keller")),
            items = listOf(makeItem("item-a1", "Dosenbrot")),
            settings = emptyList()
        )
        client.put("/api/inventory") {
            bearerAuth(tokenA)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventoryA))
        }

        // Bob sees empty inventory (separate inventory by default)
        val res = client.get("/api/inventory") { bearerAuth(tokenB) }
        assertEquals(HttpStatusCode.OK, res.status)
        val bobInventory = json.decodeFromString<InventoryDto>(res.bodyAsText())
        assertTrue("Bob must not see Alice's items", bobInventory.items.isEmpty())
    }

    @Test
    fun test_assignSharedInventory_bothUsersSeesSameData() = testApp {
        val (tokenA, userIdA) = createUserAndGetToken("alice2")
        val (tokenB, userIdB) = createUserAndGetToken("bob2")

        // Alice stores an item in her inventory
        val inventoryA = InventoryDto(
            categories = listOf(CategoryDto(id = 1, name = "Konserven")),
            locations = listOf(LocationDto(id = 1, name = "Keller")),
            items = listOf(makeItem("item-a1", "Dosenbrot")),
            settings = emptyList()
        )
        client.put("/api/inventory") {
            bearerAuth(tokenA)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventoryA))
        }

        // Get Alice's inventoryId from admin user list
        val usersRes = client.get("/api/admin/users") { bearerAuth(adminToken) }
        val users = json.decodeFromString<List<UserDto>>(usersRes.bodyAsText())
        val aliceDto = users.find { it.username == "alice2" }
        assertNotNull("Alice must exist", aliceDto)
        val aliceInventoryId = aliceDto!!.inventoryId
        assertNotNull("Alice must have an inventoryId", aliceInventoryId)

        // Assign Bob to Alice's inventory
        val assignRes = client.put("/api/admin/users/$userIdB/inventory") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(AssignInventoryRequest(aliceInventoryId!!)))
        }
        assertEquals(HttpStatusCode.OK, assignRes.status)

        // Bob now sees Alice's inventory
        val bobRes = client.get("/api/inventory") { bearerAuth(tokenB) }
        assertEquals(HttpStatusCode.OK, bobRes.status)
        val bobInventory = json.decodeFromString<InventoryDto>(bobRes.bodyAsText())
        assertEquals(1, bobInventory.items.size)
        assertEquals("Dosenbrot", bobInventory.items[0].name)
    }

    @Test
    fun test_sharedInventory_bobCanWrite_aliceSeesChange() = testApp {
        val (tokenA, userIdA) = createUserAndGetToken("alice3")
        val (tokenB, userIdB) = createUserAndGetToken("bob3")

        // Get Alice's inventoryId
        val usersRes = client.get("/api/admin/users") { bearerAuth(adminToken) }
        val users = json.decodeFromString<List<UserDto>>(usersRes.bodyAsText())
        val aliceInventoryId = users.find { it.username == "alice3" }!!.inventoryId!!

        // Assign Bob to Alice's inventory
        client.put("/api/admin/users/$userIdB/inventory") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(AssignInventoryRequest(aliceInventoryId)))
        }

        // Bob writes to the shared inventory
        val sharedInventory = InventoryDto(
            categories = listOf(CategoryDto(id = 2, name = "Getränke")),
            locations = listOf(LocationDto(id = 2, name = "Keller")),
            items = listOf(makeItem("item-b1", "Wasser")),
            settings = emptyList()
        )
        client.put("/api/inventory") {
            bearerAuth(tokenB)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), sharedInventory))
        }

        // Alice sees Bob's changes
        val aliceRes = client.get("/api/inventory") { bearerAuth(tokenA) }
        assertEquals(HttpStatusCode.OK, aliceRes.status)
        val aliceInventory = json.decodeFromString<InventoryDto>(aliceRes.bodyAsText())
        assertEquals(1, aliceInventory.items.size)
        assertEquals("Wasser", aliceInventory.items[0].name)
    }

    @Test
    fun test_createNewInventory_disconnectsFromShared() = testApp {
        val (_, userIdA) = createUserAndGetToken("alice4")
        val (tokenB, userIdB) = createUserAndGetToken("bob4")

        // Get Alice's inventoryId and assign Bob to it
        val users = json.decodeFromString<List<UserDto>>(
            client.get("/api/admin/users") { bearerAuth(adminToken) }.bodyAsText()
        )
        val aliceInventoryId = users.find { it.username == "alice4" }!!.inventoryId!!

        client.put("/api/admin/users/$userIdB/inventory") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(AssignInventoryRequest(aliceInventoryId)))
        }

        // Create a new inventory for Bob (disconnect)
        val newInvRes = client.post("/api/admin/users/$userIdB/inventory/new") {
            bearerAuth(adminToken)
        }
        assertEquals(HttpStatusCode.Created, newInvRes.status)

        // Bob should no longer be in Alice's inventory group
        val inventoriesRes = client.get("/api/admin/inventories") { bearerAuth(adminToken) }
        assertEquals(HttpStatusCode.OK, inventoriesRes.status)
        val inventories = json.decodeFromString<List<InventoryWithUsersDto>>(inventoriesRes.bodyAsText())
        val aliceGroup = inventories.find { it.inventoryId == aliceInventoryId }
        assertNotNull(aliceGroup)
        assertTrue("Bob must not be in Alice's group", aliceGroup!!.users.none { it.username == "bob4" })
    }

    @Test
    fun test_deleteUser_orphanedEmptyInventoryIsRemoved() = testApp {
        val (_, userIdC) = createUserAndGetToken("charlie")

        // Get Charlie's inventoryId
        val users = json.decodeFromString<List<UserDto>>(
            client.get("/api/admin/users") { bearerAuth(adminToken) }.bodyAsText()
        )
        val charlieInventoryId = users.find { it.username == "charlie" }!!.inventoryId!!

        // Delete Charlie
        val deleteRes = client.delete("/api/admin/users/$userIdC") { bearerAuth(adminToken) }
        assertEquals(HttpStatusCode.NoContent, deleteRes.status)

        // Charlie's empty inventory should be cleaned up
        val inventories = json.decodeFromString<List<InventoryWithUsersDto>>(
            client.get("/api/admin/inventories") { bearerAuth(adminToken) }.bodyAsText()
        )
        assertTrue(
            "Charlie's orphaned inventory must be deleted",
            inventories.none { it.inventoryId == charlieInventoryId }
        )
    }

    @Test
    fun test_listInventories_asAdmin_returnsAllInventories() = testApp {
        createUserAndGetToken("diana")
        createUserAndGetToken("eve")

        val res = client.get("/api/admin/inventories") { bearerAuth(adminToken) }
        assertEquals(HttpStatusCode.OK, res.status)
        val inventories = json.decodeFromString<List<InventoryWithUsersDto>>(res.bodyAsText())
        // admin + diana + eve → at least 3 inventories
        assertTrue(inventories.size >= 3)
    }

    @Test
    fun test_assignInventory_nonAdmin_returns403() = testApp {
        val (tokenA, userIdA) = createUserAndGetToken("frank")
        val users = json.decodeFromString<List<UserDto>>(
            client.get("/api/admin/users") { bearerAuth(adminToken) }.bodyAsText()
        )
        val frankInventoryId = users.find { it.username == "frank" }!!.inventoryId!!

        val res = client.put("/api/admin/users/$userIdA/inventory") {
            bearerAuth(tokenA) // non-admin token
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(AssignInventoryRequest(frankInventoryId)))
        }
        assertEquals(HttpStatusCode.Forbidden, res.status)
    }

    @Test
    fun test_assignInventory_nonExistentInventory_returns404() = testApp {
        val (_, userIdA) = createUserAndGetToken("grace")

        val res = client.put("/api/admin/users/$userIdA/inventory") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(AssignInventoryRequest("non-existent-inventory-id")))
        }
        assertEquals(HttpStatusCode.NotFound, res.status)
    }

    private fun makeItem(id: String, name: String) = ItemDto(
        id = id,
        name = name,
        categoryId = 1,
        quantity = 1.0,
        unit = "Stück",
        unitPrice = 1.99,
        kcalPerKg = null,
        expiryDate = null,
        locationId = 1,
        notes = "",
        lastUpdated = System.currentTimeMillis()
    )
}
