package de.krisenvorrat.server

import de.krisenvorrat.server.db.DatabaseFactory
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 de.krisenvorrat.shared.model.SettingDto
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.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class DeltaSyncTest {

    private val json = Json {
        ignoreUnknownKeys = true
        encodeDefaults = true
    }

    private val token = createTestAccessToken()

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

    @Test
    fun test_deltaPull_withoutSince_returnsFullInventory() = testApp {
        // Given
        val inventory = createInventory(lastUpdated = 1000L)
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventory))
        }

        // When – pull without since (full sync)
        val response = client.get("/api/inventory") { bearerAuth(token) }

        // Then
        assertEquals(HttpStatusCode.OK, response.status)
        val result = json.decodeFromString<InventoryDto>(response.bodyAsText())
        assertEquals(2, result.items.size)
        assertTrue(result.deletedItemIds.isEmpty())
    }

    @Test
    fun test_deltaPull_withSince_returnsOnlyChangedItems() = testApp {
        // Given – push inventory with two items at t=1000
        val inventory = createInventory(lastUpdated = 1000L)
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventory))
        }

        // When – patch item-1 at t=2000
        val patchBody = """{"name":"Dosenbrot aktualisiert","lastUpdated":2000}"""
        client.patch("/api/inventory/items/item-1") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(patchBody)
        }

        // When – delta pull since=1500
        val response = client.get("/api/inventory?since=1500") { bearerAuth(token) }

        // Then – only item-1 returned (updated after since)
        assertEquals(HttpStatusCode.OK, response.status)
        val result = json.decodeFromString<InventoryDto>(response.bodyAsText())
        assertEquals(1, result.items.size)
        assertEquals("item-1", result.items[0].id)
        assertEquals("Dosenbrot aktualisiert", result.items[0].name)
    }

    @Test
    fun test_deltaPull_withSince_returnsDeletedItemIds() = testApp {
        // Given – push inventory
        val inventory = createInventory(lastUpdated = 1000L)
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventory))
        }

        // When – delete item-2
        val deleteResponse = client.delete("/api/inventory/items/item-2") { bearerAuth(token) }
        assertEquals(HttpStatusCode.NoContent, deleteResponse.status)

        // When – delta pull since=500
        val response = client.get("/api/inventory?since=500") { bearerAuth(token) }

        // Then – item-2 appears in deletedItemIds
        assertEquals(HttpStatusCode.OK, response.status)
        val result = json.decodeFromString<InventoryDto>(response.bodyAsText())
        assertTrue(result.deletedItemIds.contains("item-2"))
        // item-1 should still be in items (lastUpdated=1000 > since=500)
        assertEquals(1, result.items.size)
        assertEquals("item-1", result.items[0].id)
    }

    @Test
    fun test_deltaPull_withRecentSince_returnsNoItems() = testApp {
        // Given – push inventory with lastUpdated=1000
        val inventory = createInventory(lastUpdated = 1000L)
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventory))
        }

        // When – delta pull since=9999 (after all updates)
        val response = client.get("/api/inventory?since=9999") { bearerAuth(token) }

        // Then – no items, no deletions
        assertEquals(HttpStatusCode.OK, response.status)
        val result = json.decodeFromString<InventoryDto>(response.bodyAsText())
        assertEquals(0, result.items.size)
        assertTrue(result.deletedItemIds.isEmpty())
        // Categories and locations always returned
        assertEquals(1, result.categories.size)
        assertEquals(1, result.locations.size)
    }

    @Test
    fun test_deltaPull_saveInventoryTracksDeletedItems() = testApp {
        // Given – push inventory with 2 items
        val inventory = createInventory(lastUpdated = 1000L)
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventory))
        }

        // When – push again with only 1 item (item-2 removed)
        val updatedInventory = InventoryDto(
            categories = listOf(CategoryDto(id = 1, name = "Konserven")),
            locations = listOf(LocationDto(id = 1, name = "Keller")),
            items = listOf(
                ItemDto(
                    id = "item-1", name = "Dosenbrot", categoryId = 1,
                    quantity = 5.0, unit = "Stück", unitPrice = 3.99,
                    kcalPerKg = 250, expiryDate = "2027-06-15", locationId = 1,
                    notes = "", lastUpdated = 3000L
                )
            ),
            settings = emptyList()
        )
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), updatedInventory))
        }

        // When – delta pull since=500
        val response = client.get("/api/inventory?since=500") { bearerAuth(token) }

        // Then – item-2 in deletedItemIds, item-1 in items
        assertEquals(HttpStatusCode.OK, response.status)
        val result = json.decodeFromString<InventoryDto>(response.bodyAsText())
        assertTrue(result.deletedItemIds.contains("item-2"))
        assertEquals(1, result.items.size)
        assertEquals("item-1", result.items[0].id)
    }

    @Test
    fun test_deltaPull_invalidSinceParam_ignoredAsFullSync() = testApp {
        // Given
        val inventory = createInventory(lastUpdated = 1000L)
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(json.encodeToString(InventoryDto.serializer(), inventory))
        }

        // When – since is not a valid number
        val response = client.get("/api/inventory?since=abc") { bearerAuth(token) }

        // Then – treated as full sync
        assertEquals(HttpStatusCode.OK, response.status)
        val result = json.decodeFromString<InventoryDto>(response.bodyAsText())
        assertEquals(2, result.items.size)
    }

    private fun createInventory(lastUpdated: Long): InventoryDto = InventoryDto(
        categories = listOf(CategoryDto(id = 1, name = "Konserven")),
        locations = listOf(LocationDto(id = 1, name = "Keller")),
        items = listOf(
            ItemDto(
                id = "item-1", name = "Dosenbrot", categoryId = 1,
                quantity = 5.0, unit = "Stück", unitPrice = 3.99,
                kcalPerKg = 250, expiryDate = "2027-06-15", locationId = 1,
                notes = "", lastUpdated = lastUpdated
            ),
            ItemDto(
                id = "item-2", name = "Mineralwasser", categoryId = 1,
                quantity = 24.0, unit = "Liter", unitPrice = 0.49,
                kcalPerKg = 0, expiryDate = "2028-01-01", locationId = 1,
                notes = "", lastUpdated = lastUpdated
            )
        ),
        settings = emptyList()
    )
}
