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.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class InputValidationTest {

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

    private fun validItem() = ItemDto(
        id = "test-item-id-001",
        name = "Wasser",
        categoryId = 1,
        quantity = 10.0,
        unit = "L",
        unitPrice = 0.5,
        kcalPerKg = null,
        expiryDate = "2027-12-31",
        locationId = 1,
        notes = "",
        lastUpdated = System.currentTimeMillis()
    )

    private fun validInventory(items: List<ItemDto> = emptyList()) = InventoryDto(
        categories = listOf(CategoryDto(id = 1, name = "Getränke")),
        locations = listOf(LocationDto(id = 1, name = "Keller")),
        items = items,
        settings = emptyList()
    )

    // ── String length validation ──────────────────────────────────────────────

    @Test
    fun test_putInventory_itemNameTooLong_returns400() = testApp {
        val oversizedItem = validItem().copy(name = "A".repeat(256))
        val body = json.encodeToString(validInventory(listOf(oversizedItem)))

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_putInventory_itemUnitTooLong_returns400() = testApp {
        val oversizedItem = validItem().copy(unit = "X".repeat(51))
        val body = json.encodeToString(validInventory(listOf(oversizedItem)))

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_putInventory_itemIdTooLong_returns400() = testApp {
        val oversizedItem = validItem().copy(id = "X".repeat(37))
        val body = json.encodeToString(validInventory(listOf(oversizedItem)))

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_putInventory_categoryNameTooLong_returns400() = testApp {
        val inventory = validInventory().copy(
            categories = listOf(CategoryDto(id = 1, name = "A".repeat(256)))
        )
        val body = json.encodeToString(inventory)

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_putInventory_locationNameTooLong_returns400() = testApp {
        val inventory = validInventory().copy(
            locations = listOf(LocationDto(id = 1, name = "A".repeat(256)))
        )
        val body = json.encodeToString(inventory)

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_putInventory_settingKeyTooLong_returns400() = testApp {
        val inventory = validInventory().copy(
            settings = listOf(SettingDto(key = "A".repeat(256), value = "v"))
        )
        val body = json.encodeToString(inventory)

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    // ── expiryDate format validation ─────────────────────────────────────────

    @Test
    fun test_putInventory_invalidExpiryDate_returns400() = testApp {
        val itemWithBadDate = validItem().copy(expiryDate = "not-a-date")
        val body = json.encodeToString(validInventory(listOf(itemWithBadDate)))

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_putInventory_validExpiryDate_returns200() = testApp {
        val itemWithGoodDate = validItem().copy(expiryDate = "2030-01-15")
        val body = json.encodeToString(validInventory(listOf(itemWithGoodDate)))

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.OK, response.status)
    }

    @Test
    fun test_putInventory_nullExpiryDate_returns200() = testApp {
        val itemWithNullDate = validItem().copy(expiryDate = null)
        val body = json.encodeToString(validInventory(listOf(itemWithNullDate)))

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.OK, response.status)
    }

    // ── Array size limits ────────────────────────────────────────────────────

    @Test
    fun test_putInventory_tooManyItems_returnsClientError() = testApp {
        // 10 001 items always exceed the 1 MB body limit, so the server may return
        // 413 (body too large) before reaching the array-size check (422).
        // Both status codes correctly indicate the server rejected the oversized request.
        val items = (1..10_001).map { i -> validItem().copy(id = "item-$i".padEnd(36, '0').take(36)) }
        val body = json.encodeToString(validInventory(items))

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertTrue(
            "Expected 413 or 422, got ${response.status}",
            response.status == HttpStatusCode.UnprocessableEntity || response.status == HttpStatusCode.PayloadTooLarge
        )
    }

    @Test
    fun test_putInventory_tooManyCategories_returns422() = testApp {
        val inventory = validInventory().copy(
            categories = (1..501).map { i -> CategoryDto(id = i, name = "Cat $i") }
        )
        val body = json.encodeToString(inventory)

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.UnprocessableEntity, response.status)
    }

    @Test
    fun test_putInventory_tooManyLocations_returns422() = testApp {
        val inventory = validInventory().copy(
            locations = (1..501).map { i -> LocationDto(id = i, name = "Loc $i") }
        )
        val body = json.encodeToString(inventory)

        val response = client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.UnprocessableEntity, response.status)
    }

    // ── PATCH item validation ────────────────────────────────────────────────

    @Test
    fun test_patchItem_invalidExpiryDate_returns400() = testApp {
        // First save an item
        val setupBody = json.encodeToString(validInventory(listOf(validItem())))
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(setupBody)
        }

        val response = client.patch("/api/inventory/items/${validItem().id}") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody("""{"expiryDate":"31.12.2027"}""")
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_patchItem_nameTooLong_returns400() = testApp {
        val setupBody = json.encodeToString(validInventory(listOf(validItem())))
        client.put("/api/inventory") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody(setupBody)
        }

        val response = client.patch("/api/inventory/items/${validItem().id}") {
            bearerAuth(token)
            contentType(ContentType.Application.Json)
            setBody("""{"name":"${"N".repeat(256)}"}""")
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    // ── Admin username length validation ─────────────────────────────────────

    @Test
    fun test_createUser_usernameTooLong_returns400() = testApp {
        val body = """{"username":"${"U".repeat(256)}","password":"ValidP@ss1"}"""

        val response = client.post("/api/admin/users") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.BadRequest, response.status)
    }

    @Test
    fun test_createUser_validUsername255Chars_returns201() = testApp {
        val body = """{"username":"${"U".repeat(255)}","password":"ValidP@ss1"}"""

        val response = client.post("/api/admin/users") {
            bearerAuth(adminToken)
            contentType(ContentType.Application.Json)
            setBody(body)
        }

        assertEquals(HttpStatusCode.Created, response.status)
    }
}
