/*
 * 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.request.*
import io.ktor.http.*
import org.junit.Test
import org.postgresql.jdbc.PgConnection
import tech.libeufin.bank.*
import tech.libeufin.common.*
import tech.libeufin.common.crypto.PwCrypto
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.common.test.*
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
import kotlin.math.max

class Bench {

    /** Generate [amount] rows to fill the database */
    fun genData(conn: PgConnection, amount: Int) {
        val amount = max(amount, 10)

        // Skip 4 accounts created by bankSetup
        val skipAccount = 4 
        // Customer account will be used in tests so we want to generate more data for him
        val customerAccount = 3
        val exchangeAccount = 2
        // In general half of the data is for generated account and half is for customer
        val mid = amount / 2

        val password = PwCrypto.Bcrypt(cost = 4).hashpw("password")
        
        val token16 = ByteArray(16)
        val token32 = ByteArray(32)
        val token64 = ByteArray(64)

        val accountPubs = List(amount*2) { EddsaPublicKey.randEdsaKey() } 

        conn.genData(amount, sequenceOf(
            "customers(username, name, password_hash, cashout_payto)" to {
                "account_$it\t$password\tMr n°$it\t$unknownPayto\n"
            },
            "conversion_rate_classes(name)" to {
                "Class n0$it\n"
            },
            "bank_accounts(internal_payto, owning_customer_id, is_public,conversion_rate_class_id)" to {
                val conversionId = when (it%5) {
                    0 -> "\\N"
                    1, 2 -> "1"
                    3 -> "2"
                    else -> it%10
                }
                "payto://x-taler-bank/localhost/account_$it\t${it+skipAccount}\t${it%3==0}\t$conversionId\n"
            },
            "bearer_tokens(content, creation_time, expiration_time, scope, is_refreshable, bank_customer, description, last_access)" to {
                val account = if (it > mid) customerAccount else it+4
                val hex = token32.rand().encodeHex()
                "\\\\x$hex\t0\t0\treadonly\tfalse\t$account\t\\N\t0\n"
            },
            "bank_account_transactions(creditor_payto, creditor_name, debtor_payto, debtor_name, subject, amount, transaction_date, direction, bank_account_id)" to {
                val account = if (it > mid) customerAccount else it+4
                "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n" + 
                "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n"
            },
            "bank_transaction_operations" to {
                val hex = token32.rand().encodeHex()
                "\\\\x$hex\t$it\n"
            },
            "tan_challenges(uuid, hbody, salt, op, code, creation_date, expiration_date, retry_counter, customer, tan_channel, tan_info)" to {
                val account = if (it > mid) customerAccount else it+4
                val uuid = UUID.randomUUID()
                val hex16 = token16.rand().encodeHex()
                val hex64 = token64.rand().encodeHex()
                "$uuid\t\\\\x$hex64\t\\\\x$hex16\taccount_reconfig\tcode\t0\t0\t0\t$account\tsms\tinfo\n"
            },
            "taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account, reserve_pub, creation_date)" to {
                val account = if (it > mid) customerAccount else it+4
                val hex = token32.rand().encodeHex()
                val uuid = UUID.randomUUID()
                "$uuid\t$account\t\\\\x$hex\t0\n"
            },
            "prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, registered_at, bank_transaction_id)" to {
                val type = if (it % 2 == 0) "reserve" else "kyc"
                val recurrent = if (it % 3 == 0) "true" else "false"
                val incoming_transaction_id = if (it % 5 == 0) "\\N" else "${it*2}"
                val hex = accountPubs[it].raw.encodeHex()
                val hex64 = token64.rand().encodeHex()
                "$type\t\\\\x$hex\t\\\\x$hex\t\\\\x$hex64\t$recurrent\t0\t$incoming_transaction_id\n"
            },
            "pending_recurrent_incoming_transactions(bank_transaction_id, debtor_account_id, authorization_pub)" to {
                val hex = accountPubs[it].raw.encodeHex()
                "${it*2}\t${it}\t\\\\x$hex\n"
            },
            "taler_exchange_outgoing(bank_transaction)" to {
                "${it*2-1}\n"
            },
            "transfer_operations(wtid, request_uid, amount, exchange_base_url, exchange_outgoing_id, exchange_id, transfer_date, creditor_payto, status, status_msg)" to {
                val hex32 = token32.rand().encodeHex()
                val hex64 = token64.rand().encodeHex()
                if (it % 2 == 0) {
                    "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t$it\t$it\t0\tpayto://x-taler-bank/localhost/10\tsuccess\t\\N\n"
                } else {
                    "\\\\x$hex32\t\\\\x$hex64\t(42, 0)\turl\t\\N\t$it\t0\tpayto://x-taler-bank/localhost/10\tpermanent_failure\tfailure\n"
                }   
            },
            "taler_exchange_incoming(type, metadata, bank_transaction)" to {
                val hex = token32.rand().encodeHex()
                if (it % 2 == 0) {
                    "reserve\t\\\\x$hex\t${it*2}\n"
                } else {
                    "kyc\t\\\\x$hex\t${it*2}\n"
                }
            },
            "bank_stats(timeframe, start_time)" to {
                val instant = Instant.ofEpochSecond(it.toLong())
                val date = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"))
                "day\t$date\n"
            },
            "cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)" to {
                val account = if (it > mid) customerAccount else it+4
                val hex = token32.rand().encodeHex()
                "\\\\x$hex\t(0,0)\t(0,0)\tsubject\t0\t$account\t$it\n"
            }
        ))
    }

    @Test
    fun benchDb() = bench { AMOUNT -> bankSetup { db ->
        // Prepare customer accounts
        fillCashoutInfo("customer")
        setMaxDebt("customer", "KUDOS:1000000")

        // Generate data
        db.conn { genData(it, AMOUNT) }

        val accountPubs = List(AMOUNT) { EddsaPublicKey.randEdsaKeyPair() } 

        // Warm HTTP client
        client.get("/config").assertOk()
        
        // Accounts
        measureAction("account_create") {
            client.post("/accounts") {
                json {
                    "username" to "account_bench_$it"
                    "password" to "account_bench_$it-password"
                    "name" to "Bench Account $it"
                }
            }.assertOkJson<RegisterAccountResponse>().internal_payto_uri
        }
        measureAction("account_reconfig") {
            client.patchA("/accounts/account_bench_$it") {
                json {
                    "name" to "New Bench Account $it"
                }
            }.assertNoContent()
        }
        measureAction("account_reconfig_auth") {
            client.patchA("/accounts/account_bench_$it/auth") {
                json {
                    "old_password" to "account_bench_$it-password"
                    "new_password" to "account_bench_$it-password"
                }
            }.assertNoContent()
        }
        measureAction("account_list") {
            client.getAdmin("/accounts").assertOk()
        }
        measureAction("account_list_class") {
            client.getAdmin("/accounts?conversion_rate_class=${it%10}").assertOk()
        }
        measureAction("account_list_name") {
            client.getAdmin("/accounts?name=Mr").assertOk()
        }
        measureAction("account_list_public") {
            client.get("/public-accounts").assertOk()
        }
        measureAction("account_get") {
            client.getA("/accounts/account_bench_$it").assertOk()
        }
        
        // Tokens
        val tokens = measureAction("token_create") {
            client.postPw("/accounts/customer/token") {
                json { 
                    "scope" to "readonly"
                    "refreshable" to true
                }
            }.assertOkJson<TokenSuccessResponse>().access_token
        }
        measureAction("token_refresh") {
            client.post("/accounts/customer/token") {
                headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}"
                json { "scope" to "readonly" }
            }.assertOk()
        }
        measureAction("token_list") {
            client.getA("/accounts/customer/tokens").assertOk()
        }
        measureAction("token_delete") {
            client.delete("/accounts/customer/token") {
                headers[HttpHeaders.Authorization] = "Bearer ${tokens[it]}"
            }.assertNoContent()
        }

        // Conversion rate classes
        val classes = measureAction("class_create") {
            client.postAdmin("/conversion-rate-classes") {
                json { 
                    "name" to "Gen class $it"
                }
            }.assertOkJson<ConversionRateClassResponse>().conversion_rate_class_id
        }
        measureAction("class_patch") {
            client.patchAdmin("/conversion-rate-classes/${classes[it]}") {
                json {
                    "name" to "Gen class $it"
                    "description" to "test $it"
                }
            }.assertNoContent()
        }
        measureAction("class_get") {
            client.getAdmin("/conversion-rate-classes/${classes[it]}").assertOk()
        }
        measureAction("class_list") {
            client.getAdmin("/conversion-rate-classes").assertOk()
        }
        measureAction("class_delete") {
            client.deleteAdmin("/conversion-rate-classes/${classes[it]}").assertNoContent()
        }

        // Transaction
        val transactions = measureAction("transaction_create") {
            client.postA("/accounts/customer/transactions") {
                json { 
                    "payto_uri" to "$merchantPayto?receiver-name=Test&message=payout"
                    "amount" to "KUDOS:0.0001"
                }
            }.assertOkJson<TransactionCreateResponse>().row_id
        }
        measureAction("transaction_get") {
            client.getA("/accounts/customer/transactions/${transactions[it]}").assertOk()
        }
        measureAction("transaction_history") {
            client.getA("/accounts/customer/transactions").assertOk()
        }
        measureAction("transaction_revenue") {
            client.getA("/accounts/merchant/taler-revenue/history").assertOk()
        }

        // Withdrawal
        val withdrawals = measureAction("withdrawal_create") {
            client.postA("/accounts/customer/withdrawals") {
                json { 
                    "amount" to "KUDOS:0.0001"
                }
            }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id
        }
        measureAction("withdrawal_get") {
            client.get("/withdrawals/${withdrawals[it]}").assertOk()
        }
        measureAction("withdrawal_status") {
            client.get("/taler-integration/withdrawal-operation/${withdrawals[it]}").assertOk()
        }
        measureAction("withdrawal_select") {
            client.post("/taler-integration/withdrawal-operation/${withdrawals[it]}") {
                json {
                    "reserve_pub" to EddsaPublicKey.randEdsaKey()
                    "selected_exchange" to exchangePayto
                }
            }.assertOk()
        }
        measureAction("withdrawal_confirm") {
            client.postA("/accounts/customer/withdrawals/${withdrawals[it]}/confirm")
                .assertNoContent()
        }
        measureAction("withdrawal_abort") {
            val uuid = client.postA("/accounts/customer/withdrawals") {
                json { 
                    "amount" to "KUDOS:0.0001"
                }
            }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id
            client.postA("/accounts/customer/withdrawals/$uuid/abort")
                .assertNoContent()
        }

        // Cashout
        convert("KUDOS:0.1")
        val cashouts = measureAction("cashout_create") {
            client.postA("/accounts/customer/cashouts") {
                json { 
                    "request_uid" to ShortHashCode.rand()
                    "amount_debit" to "KUDOS:0.1"
                    "amount_credit" to convert("KUDOS:0.1")
                }
            }.assertOkJson<CashoutResponse>().cashout_id
        }
        measureAction("cashout_get") {
            client.getA("/accounts/customer/cashouts/${cashouts[it]}").assertOk()
        }
        measureAction("cashout_history") {
            client.getA("/accounts/customer/cashouts").assertOk()
        }
        measureAction("cashout_history_admin") {
            client.getAdmin("/cashouts").assertOk()
        }

        // Wire gateway
        val transfers = measureAction("wg_transfer") {
            client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
                json { 
                    "request_uid" to HashCode.rand()
                    "amount" to "KUDOS:0.0001"
                    "exchange_base_url" to "http://exchange.example.com/"
                    "wtid" to ShortHashCode.rand()
                    "credit_account" to customerPayto.canonical
                }
            }.assertOkJson<TransferResponse>().row_id
        }
        measureAction("wg_transfer_get") {
            client.getA("/accounts/exchange/taler-wire-gateway/transfers/${transfers[it]}").assertOk()
        }
        measureAction("wg_transfer_page") {
            client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOk()
        }
        measureAction("wg_transfer_page_filter") {
            client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOk()
        }
        measureAction("wg_add") {
            client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
                json { 
                    "amount" to "KUDOS:0.0001"
                    "reserve_pub" to EddsaPublicKey.randEdsaKey()
                    "debit_account" to customerPayto.canonical
                }
            }.assertOk()
        }
        measureAction("wg_incoming") {
            client.getA("/accounts/exchange/taler-wire-gateway/history/incoming")
                .assertOk()
        }
        measureAction("wg_outgoing") {
            client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing")
                .assertOk()
        }

        // TAN challenges
        val challenges = measureAction("tan_send") {
            val res = client.patchA("/accounts/account_bench_$it") {
                json { 
                    "contact_data" to obj {
                        "phone" to "+99"
                        "email" to "email@example.com"
                    }
                    "tan_channel" to "sms"
                }
            }.assertAcceptedJson<ChallengeResponse>()
            val challenge = res.challenges[0]
            client.postA("/accounts/account_bench_$it/challenge/${challenge.challenge_id}").assertOk()
            val code = tanCode(challenge.tan_info)
            Pair(challenge.challenge_id, code)
        }
        measureAction("tan_confirm") {
            val (id, code) = challenges[it]
            client.postA("/accounts/account_bench_$it/challenge/$id/confirm") {
                json { "tan" to code }
            }.assertNoContent()
        }

        // Wire transfer
        /*measureAction("wt_register") {
            val (priv, pub) = accountPubs[it]
            val valid_req = obj {
                "credit_account" to exchangePayto
                "credit_amount" to "KUDOS:55"
                "type" to "reserve"
                "alg" to "EdDSA"
                "account_pub" to pub
                "authorization_pub" to pub
                "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv)
                "recurrent" to false
            }
            client.post("/taler-prepared-transfer/registration") {
                json(valid_req)
            }.assertOkJson<SubjectResult>()
            client.post("/taler-prepared-transfer/registration") {
                json(valid_req)
            }.assertOkJson<SubjectResult>()
        }
        measureAction("wt_unregister") {
            val (priv, pub) = accountPubs[it]
            val now = Instant.now().toString()
            val valid_req = obj {
                "timestamp" to now
                "authorization_pub" to pub
                "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
            }
            client.post("/taler-prepared-transfer/unregistration") {
                json(valid_req)
            }.assertNoContent()
            client.post("/taler-prepared-transfer/unregistration") {
                json(valid_req)
            }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
            }*/

        // Delete accounts

        measureAction("account_delete") {
            client.deleteA("/accounts/account_bench_$it").assertNoContent()
        }

        // Other
        measureAction("monitor") {
            client.getAdmin("/monitor").assertOk()
        }
        db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO)
        measureAction("gc") {
            db.gc.collect(Instant.now(), java.time.Duration.ZERO, java.time.Duration.ZERO, java.time.Duration.ZERO)
        }
    } }
}