/*
 * 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/>
 */

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.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 token32 = ByteArray(32)
        val token64 = ByteArray(64)

        conn.genData(amount, sequenceOf(
            "customers(username, name, password_hash, cashout_payto)" to {
                "account_$it\t$password\tMr n°$it\t$unknownPayto\n"
            },
            "bank_accounts(internal_payto, owning_customer_id, is_public)" to {
                "payto://x-taler-bank/localhost/account_$it\t${it+skipAccount}\t${it%3==0}\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(body, op, code, creation_date, expiration_date, retry_counter, customer)" to {
                val account = if (it > mid) customerAccount else it+4
                "body\taccount_reconfig\tcode\t0\t0\t0\t$account\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"
            },
            "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, reserve_pub, account_pub, bank_transaction)" to {
                val hex = token32.rand().encodeHex()
                if (it % 2 == 0) {
                    "reserve\t\\\\x$hex\t\\N\t${it*2}\n"
                } else {
                    "kyc\t\\N\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() {
        val ITER = System.getenv("BENCH_ITER")?.toIntOrNull() ?: 1
        val AMOUNT = System.getenv("BENCH_AMOUNT")?.toIntOrNull() ?: 10

        if (ITER == 0) {
            println("Skip benchmark, missing BENCH_ITER")
            return
        }
        println("Bench $ITER times with $AMOUNT rows")

        bench(ITER) { bankSetup { db ->
            // Prepare custoemr accounts
            fillCashoutInfo("customer")
            setMaxDebt("customer", "KUDOS:1000000")

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

            // 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_public") {
                client.get("/public-accounts").assertOk()
            }
            measureAction("account_get") {
                client.getA("/accounts/account_bench_$it").assertOk()
            }
            measureAction("account_delete") {
                client.deleteA("/accounts/account_bench_$it").assertNoContent()
            }
            
            // 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()
            }
    
            // 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.rand()
                        "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.rand()
                        "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 id = client.patchA("/accounts/customer") {
                    json { 
                        "contact_data" to obj {
                            "phone" to "+99"
                            "email" to "email@example.com"
                        }
                        "tan_channel" to "sms"
                    }
                }.assertAcceptedJson<TanChallenge>().challenge_id
                val res = client.postA("/accounts/customer/challenge/$id").assertOkJson<TanTransmission>()
                val code = tanCode(res.tan_info)
                Pair(id, code)
            }
            measureAction("tan_send") {
                val (id, code) = challenges[it]
                client.postA("/accounts/customer/challenge/$id/confirm") {
                    json { "tan" to code }
                }.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)
            }
        } }
    }
}