package dev.ulfrx.recipe.auth

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import dev.ulfrx.recipe.shared.dto.MeResponse
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.routing.routing
import io.ktor.server.testing.testApplication
import kotlinx.serialization.json.Json
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
import org.jetbrains.exposed.sql.Database as ExposedDatabase

/**
 * Integration test for `GET /api/v1/me` — exercises the full
 * Auth → PrincipalResolver → Postgres path against a real database (AUTH-03,
 * AUTH-06, D-25, D-27).
 *
 * Uses Testcontainers `postgres:16` rather than ambient localhost Postgres so
 * the test is hermetic across dev machines / CI. Flyway runs once per JVM
 * process before any test executes; Exposed is connected through Hikari to
 * the container.
 */
@Testcontainers
class MeRouteTest {
    companion object {
        @Container
        @JvmStatic
        private val postgres = PostgreSQLContainer("postgres:16")
        private lateinit var dataSource: HikariDataSource
        private val json = Json { ignoreUnknownKeys = true }

        @JvmStatic
        @BeforeAll
        fun setUpClass() {
            Flyway
                .configure()
                .dataSource(postgres.jdbcUrl, postgres.username, postgres.password)
                .locations("classpath:db/migration")
                .baselineOnMigrate(true)
                .load()
                .migrate()
            dataSource =
                HikariDataSource(
                    HikariConfig().apply {
                        jdbcUrl = postgres.jdbcUrl
                        username = postgres.username
                        setPassword(postgres.password)
                        maximumPoolSize = 4
                        poolName = "me-route-test-pool"
                    },
                )
            ExposedDatabase.connect(dataSource)
        }

        @JvmStatic
        @AfterAll
        fun tearDownClass() {
            dataSource.close()
        }
    }

    private fun authConfigFor(support: JwtTestSupport): AuthConfig =
        AuthConfig(
            issuer = support.issuer,
            audience = support.audience,
            jwksUrl = "https://test-authentik.invalid/application/o/jwks/",
            leewaySeconds = 30,
        )

    @Test
    fun `valid token creates user row and returns MeResponse`() =
        testApplication {
            val support = JwtTestSupport()
            val resolver = PrincipalResolver()
            application {
                install(ContentNegotiation) { json() }
                configureAuthentication(authConfigFor(support), support.provider)
                routing { meRoute(resolver) }
            }
            val token = support.mint(sub = "auth0|user-create-123")

            val response =
                client.get("/api/v1/me") {
                    header(HttpHeaders.Authorization, "Bearer $token")
                }

            assertEquals(HttpStatusCode.OK, response.status)
            val body = json.decodeFromString(MeResponse.serializer(), response.bodyAsText())
            assertEquals("auth0|user-create-123", body.sub)
            assertEquals("test@example.invalid", body.email)
            assertEquals("Test User", body.displayName)
            assertTrue(body.id.isNotBlank(), "id should be a server-generated UUID, was: ${body.id}")
        }

    @Test
    fun `second token with same sub updates email and display name without duplicating`() =
        testApplication {
            val support = JwtTestSupport()
            val resolver = PrincipalResolver()
            application {
                install(ContentNegotiation) { json() }
                configureAuthentication(authConfigFor(support), support.provider)
                routing { meRoute(resolver) }
            }

            val first =
                client.get("/api/v1/me") {
                    header(
                        HttpHeaders.Authorization,
                        "Bearer ${support.mint(sub = "auth0|user-update-1", email = "old@example.invalid", name = "Old Name")}",
                    )
                }
            assertEquals(HttpStatusCode.OK, first.status)
            val firstBody = json.decodeFromString(MeResponse.serializer(), first.bodyAsText())

            val second =
                client.get("/api/v1/me") {
                    header(
                        HttpHeaders.Authorization,
                        "Bearer ${support.mint(sub = "auth0|user-update-1", email = "new@example.invalid", name = "New Name")}",
                    )
                }
            assertEquals(HttpStatusCode.OK, second.status)
            val secondBody = json.decodeFromString(MeResponse.serializer(), second.bodyAsText())

            assertEquals(firstBody.id, secondBody.id) // same row
            assertEquals(firstBody.sub, secondBody.sub)
            assertEquals("new@example.invalid", secondBody.email)
            assertEquals("New Name", secondBody.displayName)
            assertNotEquals(firstBody.email, secondBody.email)
        }
}
