/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024 Taler Systems S.A.

 * LibEuFin is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation; either version 3, or
 * (at your option) any later version.

 * LibEuFin is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
 * Public License for more details.

 * You should have received a copy of the GNU Affero General Public
 * License along with LibEuFin; see the file COPYING.  If not, see
 * <http://www.gnu.org/licenses/>
 */

package tech.libeufin.common.test

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import tech.libeufin.common.*
import kotlin.test.assertEquals
import kotlinx.serialization.json.*

/* ----- Assert ----- */

suspend fun assertTime(min: Int, max: Int, lambda: suspend () -> Unit) {
    val start = System.currentTimeMillis()
    lambda()
    val end = System.currentTimeMillis()
    val time = end - start
    assert(time >= min) { "Expected to last at least $min ms, lasted $time" }
    assert(time <= max) { "Expected to last at most $max ms, lasted $time" }
}

suspend inline fun <reified B> HttpResponse.assertHistoryIds(size: Int, ids: (B) -> List<Long>): B {
    assertOk()
    val body = json<B>()
    val history = ids(body)
    val params = PageParams.extract(call.request.url.parameters)

    // testing the size is like expected.
    assertEquals(size, history.size, "bad history length: $history")
    if (params.limit < 0) {
        // testing that the first id is at most the 'offset' query param.
        assert(history[0] <= params.offset) { "bad history offset: $params $history" }
        // testing that the id decreases.
        if (history.size > 1)
            assert(history.windowed(2).all { (a, b) -> a > b }) { "bad history order: $history" }
    } else {
        // testing that the first id is at least the 'offset' query param.
        assert(history[0] >= params.offset) { "bad history offset: $params $history" }
        // testing that the id increases.
        if (history.size > 1)
            assert(history.windowed(2).all { (a, b) -> a < b }) { "bad history order: $history" }
    }

    return body
}

/* ----- Auth ----- */

typealias RequestLambda = suspend HttpRequestBuilder.() -> Unit;

/** Auto token auth GET request */
suspend fun HttpClient.getA(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Get, null, builder)
/** Auto token auth POST request */
suspend fun HttpClient.postA(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Post, null, builder)
/** Auto token auth PATCH request */
suspend fun HttpClient.patchA(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Patch, null, builder)
/** Auto token auth DELETE request */
suspend fun HttpClient.deleteA(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Delete, null, builder)

/** Admin token auth GET request */
suspend fun HttpClient.getAdmin(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Get, "admin", builder)
/** Admin token auth PATCH request */
suspend fun HttpClient.patchAdmin(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Patch, "admin", builder)
/** Admin token auth POST request */
suspend fun HttpClient.postAdmin(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Post, "admin", builder)
/** Admin token auth DELETE request */
suspend fun HttpClient.deleteAdmin(url: String, builder: RequestLambda = {}): HttpResponse
    = tokenAuthRequest(url, HttpMethod.Delete, "admin", builder)

/** Auto pw auth GET request */
suspend fun HttpClient.getPw(url: String, builder: RequestLambda = {}): HttpResponse
    = pwAuthRequest(url, HttpMethod.Get, null, builder)
/** Auto pw auth POST request */
suspend fun HttpClient.postPw(url: String, builder: RequestLambda = {}): HttpResponse
    = pwAuthRequest(url, HttpMethod.Post, null, builder)
/** Auto pw auth PATCH request */
suspend fun HttpClient.patchPw(url: String, builder: RequestLambda = {}): HttpResponse
    = pwAuthRequest(url, HttpMethod.Patch, null, builder)
/** Auto pw auth DELETE request */
suspend fun HttpClient.deletePw(url: String, builder: RequestLambda = {}): HttpResponse
    = pwAuthRequest(url, HttpMethod.Delete, null, builder)

private suspend fun HttpClient.tokenAuthRequest(
    url: String,
    method: HttpMethod,
    username: String?,
    builder: RequestLambda = {}
): HttpResponse = request(url) {
    this.method = method
    tokenAuth(this@tokenAuthRequest, username)
    builder(this)
}

private suspend fun HttpClient.pwAuthRequest(
    url: String,
    method: HttpMethod,
    username: String?,
    builder: RequestLambda = {}
): HttpResponse = request(url) {
    this.method = method
    pwAuth(username)
    builder(this)
}

private fun HttpRequestBuilder.extractUsername(username: String? = null): String?
    = when {
        username != null -> username
        url.pathSegments.contains("admin") -> "admin"
        url.pathSegments[1] == "accounts" -> url.pathSegments[2]
        else -> null
    }

/** Authenticate a request for [username] with basic auth */
fun HttpRequestBuilder.pwAuth(username: String? = null) {
    val username = extractUsername(username) ?: return
    basicAuth("$username", "$username-password")
}

val globalTestTokens = mutableMapOf<String, String>()

/** Get cached token or create it */
suspend fun HttpClient.cachedToken(username: String): String {
    // Get cached token or create it
    var token = globalTestTokens.get(username)
    if (token == null) {
        val response = this.post("/accounts/$username/token") {
            pwAuth()
            json {
                "scope" to "readwrite"
                "duration" to obj {
                    "d_us" to "forever"
                }
            }
        }.assertOkJson<JsonObject>()
        token = Json.decodeFromJsonElement<String>(response.get("access_token")!!)
        globalTestTokens.set(username, token)
    }
    return token
}

/** Authenticate a request for [username] with a bearer token */
suspend fun HttpRequestBuilder.tokenAuth(client: HttpClient, username: String? = null) {
    // Get username from arg or path
    val username = extractUsername(username) ?: return
    // Get cached token or create it
    var token = client.cachedToken(username)
    // Set authorization header
    headers[HttpHeaders.Authorization] = "Bearer $token"
}