/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024, 2025, 2026 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/>
 */

import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlinx.coroutines.runBlocking
import tech.libeufin.common.*
import tech.libeufin.common.db.dbInit
import tech.libeufin.common.db.pgDataSource
import tech.libeufin.ebics.*
import tech.libeufin.nexus.*
import tech.libeufin.nexus.cli.registerIncomingPayment
import tech.libeufin.nexus.cli.registerOutgoingPayment
import tech.libeufin.nexus.db.Database
import tech.libeufin.nexus.db.InitiatedPayment
import tech.libeufin.nexus.db.TransferDAO.RegistrationResult
import tech.libeufin.nexus.iso20022.*
import java.time.Instant
import kotlin.io.path.Path
import kotlin.test.assertEquals

fun conf(
    conf: String = "test.conf",
    lambda: suspend (NexusConfig) -> Unit
) = runBlocking {
    val cfg = nexusConfig(Path("conf/$conf"))
    lambda(cfg) 
}

fun setup(
    conf: String = "test.conf",
    lambda: suspend (Database, NexusConfig) -> Unit
) = conf(conf) { cfg ->
    pgDataSource(cfg.dbCfg.dbConnStr).dbInit(cfg.dbCfg, "libeufin-nexus", true)
    cfg.withDb(lambda)
}

fun serverSetup(
    conf: String = "test.conf",
    lambda: suspend ApplicationTestBuilder.(Database) -> Unit
) = setup(conf) { db, cfg ->
    testApplication {
        application {
            nexusApi(db, cfg)
        }
        lambda(db)
    }
}

const val grothoffPayto = "payto://iban/CH4189144589712575493?receiver-name=Grothoff%20Hans"

val clientKeys = generateNewKeys()

/** Generates a payment initiation, given its subject */
fun genInitPay(
    endToEndId: String,
    subject: String = "init payment",
    amount: String = "KUDOS:44",
    creditor: IbanPayto = ibanPayto("CH4189144589712575493", "Test")
) = InitiatedPayment(
        id = -1,
        amount = TalerAmount(amount),
        creditor = creditor,
        subject = subject,
        initiationTime = Instant.now(),
        endToEndId = endToEndId
    )

/** Generates an incoming payment, given its subject */
fun genInPay(
    subject: String,
    amount: String = "KUDOS:44",
    executionTime: Instant = Instant.now()
) = IncomingPayment(
        amount = TalerAmount(amount),
        debtor = ibanPayto("DE84500105177118117964", "John Smith"),
        subject = subject,
        executionTime = executionTime,
        id = IncomingId(null, randEbicsId(), null)
    )

/** Generates an outgoing payment, given its subject and end-to-end ID */
fun genOutPay(
    subject: String, 
    endToEndId: String? = null,
    msgId: String? = null,
    executionTime: Instant = Instant.now()
) = OutgoingPayment(
        id = OutgoingId(msgId, endToEndId ?: randEbicsId(), null),
        amount = TalerAmount(44, 0, "KUDOS"),
        creditor = ibanPayto("CH4189144589712575493", "Test"),
        subject = subject,
        executionTime = executionTime,
    )

/** Perform a taler outgoing transaction */
suspend fun ApplicationTestBuilder.transfer() {
    client.postA("/taler-wire-gateway/transfer") {
        json {
            "request_uid" to HashCode.rand()
            "amount" to "CHF:55"
            "exchange_base_url" to "http://exchange.example.com/"
            "wtid" to ShortHashCode.rand()
            "credit_account" to grothoffPayto
        }
    }.assertOk()
}

/** Perform a taler incoming transaction of [amount] from merchant to exchange */
suspend fun ApplicationTestBuilder.addIncoming(amount: String) {
    client.postA("/taler-wire-gateway/admin/add-incoming") {
        json {
            "amount" to TalerAmount(amount)
            "reserve_pub" to EddsaPublicKey.randEdsaKey()
            "debit_account" to grothoffPayto
        }
    }.assertOk()
}

/** Perform a taler kyc transaction of [amount] from merchant to exchange */
suspend fun ApplicationTestBuilder.addKyc(amount: String) {
    client.postA("/taler-wire-gateway/admin/add-kycauth") {
        json {
            "amount" to TalerAmount(amount)
            "account_pub" to EddsaPublicKey.randEdsaKey()
            "debit_account" to grothoffPayto
        }
    }.assertOk()
}

/** Register a talerable outgoing transaction */
suspend fun talerableOut(db: Database, metadata: String? = null) {
    val wtid = EddsaPublicKey.randEdsaKey()
    registerOutgoingPayment(db, genOutPay(fmtOutgoingSubject(wtid, BaseURL.parse("http://exchange.example.com/"), metadata)))
}

/** Register a talerable reserve incoming transaction */
suspend fun talerableIn(
    db: Database,
    amount: String = "CHF:44",
    reserve_pub: EddsaPublicKey = EddsaPublicKey.randEdsaKey()
) {
    registerIncomingPayment(
        db, NexusIngestConfig.default(AccountType.exchange),
        genInPay("test with $reserve_pub reserve pub", amount)
    )
}

private suspend fun prepare(db: Database): String {
    val pub = EddsaPublicKey.randEdsaKey()
    val sig = EddsaSignature.rand()
    val referenceNumber = subjectFmtQrBill(pub)
    assertEquals(
        RegistrationResult.Success,
        db.transfer.register(
            type = TransferType.reserve,
            accountPub = pub,
            authPub = pub,
            authSig = sig,
            referenceNumber = referenceNumber,
            timestamp = Instant.now(),
            recurrent = false
        )
    )
    return referenceNumber
}

/** Register a talerable reserve prepared incoming transaction */
suspend fun talerablePreparedIn(db: Database, amount: String = "CHF:44") {
    val referenceNumber = prepare(db)
    registerIncomingPayment(
        db, NexusIngestConfig.default(AccountType.exchange),
        genInPay(referenceNumber, amount)
    )
}

/** Register an incomplete talerable reserve prepared incoming transaction */
suspend fun talerablePreparedIncompleteIn(db: Database, amount: String = "CHF:44") {
    val referenceNumber = prepare(db)
    val incomplete = genInPay(referenceNumber).copy(subject = null, debtor = null)
    registerIncomingPayment(
        db, NexusIngestConfig.default(AccountType.exchange), incomplete
    )
}

/** Register a completed talerable reserve prepared incoming transaction */
suspend fun talerablePreparedCompletedIn(db: Database, amount: String = "CHF:44") {
    val referenceNumber = prepare(db)
    val original = genInPay(referenceNumber, amount)
    val incomplete = original.copy(subject = null, debtor = null)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original)
}

/** Register an incomplete talerable reserve incoming transaction */
suspend fun talerableIncompleteIn(db: Database) {
    val reserve_pub = EddsaPublicKey.randEdsaKey()
    val incomplete = genInPay("test with $reserve_pub reserve pub").copy(subject = null, debtor = null)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
}

/** Register a completed talerable reserve  incoming transaction */
suspend fun talerableCompletedIn(db: Database) {
    val reserve_pub = EddsaPublicKey.randEdsaKey()
    val original = genInPay("test with $reserve_pub reserve pub")
    val incomplete = original.copy(subject = null, debtor = null)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original)
}

/** Register a talerable KYC incoming transaction */
suspend fun talerableKycIn(
    db: Database,
    amount: String = "CHF:44",
    account_pub: EddsaPublicKey = EddsaPublicKey.randEdsaKey()
) {
    registerIncomingPayment(
        db, NexusIngestConfig.default(AccountType.exchange),
        genInPay("test with KYC:$account_pub account pub", amount)
    )
}

/** Register an incoming transaction */
suspend fun registerIn(db: Database) {
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), genInPay("ignored"))
}

/** Register an incomplete incoming transaction */
suspend fun registerIncompleteIn(db: Database) {
    val incomplete = genInPay("ignored").copy(subject = null, debtor = null)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
}

/** Register a completed incoming transaction */
suspend fun registerCompletedIn(db: Database) {
    val original = genInPay("ignored")
    val incomplete = original.copy(subject = null, debtor = null)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete)
    registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original)
}

/** Register an outgoing transaction */
suspend fun registerOut(db: Database) {
    registerOutgoingPayment(db, genOutPay("ignored"))
}

/** Register an incomplete outgoing transaction */
suspend fun registerIncompleteOut(db: Database) {
    val original = genOutPay("ignored")
    val incomplete = original.copy(id = OutgoingId(null, null, original.id.endToEndId), creditor = null)
    registerOutgoingPayment(db, incomplete)
}

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

/** Auto auth get request */
suspend inline fun HttpClient.getA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    return get(url) {
        auth()
        builder(this)
    }
}

/** Auto auth post request */
suspend inline fun HttpClient.postA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    return post(url) {
        auth()
        builder(this)
    }
}

/** Auto auth patch request */
suspend inline fun HttpClient.patchA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    return patch(url) {
        auth()
        builder(this)
    }
}

/** Auto auth delete request */
suspend inline fun HttpClient.deleteA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
    return delete(url) {
        auth()
        builder(this)
    }
}

fun HttpRequestBuilder.auth() {
    headers[HttpHeaders.Authorization] = "Bearer secret-token"
}