/*
 * 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 org.junit.Test
import org.postgresql.jdbc.PgConnection
import io.ktor.client.request.*
import tech.libeufin.common.*
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.common.test.*
import tech.libeufin.nexus.*
import tech.libeufin.nexus.cli.*
import kotlin.math.max
import java.util.UUID;
import java.time.Instant

class Bench {

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

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

        conn.genData(amount, sequenceOf(
            "incoming_transactions(amount, subject, execution_time, debit_payto, uetr, tx_id, acct_svcr_ref)" to {
                val subject = if (it % 4 == 0) null else "subject ${it}"
                val debtor = if (it % 3 == 0) null else "debit_payto"
                if (it % 3 == 0) {
                    "(20,0)\t$subject\t0\t$debtor\t${UUID.randomUUID()}\t\\N\t\\N\n" + 
                    "(21,0)\t$subject\t0\t$debtor\t\\N\tTX_ID${it*2}\t\\N\n" + 
                    "(22,0)\t$subject\t0\t$debtor\t\\N\t\\N\tREF${it*2}\n"
                } else if (it % 3 == 1) {
                    "(30,0)\t$subject\t0\t$debtor\t${UUID.randomUUID()}\tTX_ID${it*2}\t\\N\n" + 
                    "(31,0)\t$subject\t0\t$debtor\t\\N\tTX_ID${it*2+1}\tREF${it*2}\n" +
                    "(32,0)\t$subject\t0\t$debtor\t${UUID.randomUUID()}\t\\N\tREF${it*2+1}\n"
                } else {
                    "(40,0)\t$subject\t0\t$debtor\t${UUID.randomUUID()}\tTX_ID${it*2}\tREF${it*2}\n" +
                    "(41,0)\t$subject\t0\t$debtor\t${UUID.randomUUID()}\tTX_ID${it*2+1}\tREF${it*2+1}\n" 
                }
            },
            "outgoing_transactions(amount, subject, execution_time, credit_payto, end_to_end_id, acct_svcr_ref)" to {
                val subject = if (it % 4 == 0) null else "subject ${it}"
                val creditor = if (it % 3 == 0) null else "credit_payto"
                if (it % 2 == 0) {
                    "(30,0)\t$subject\t0\t$creditor\t\\N\tREF${it*2}\n" + 
                    "(31,0)\t$subject\t0\t$creditor\tE2E_ID${it*2}\t\\N\n"
                } else {
                    "(30,0)\t$subject\t0\t$creditor\tE2E_ID${it*2}\tREF${it*2}\n" + 
                    "(31,0)\t$subject\t0\t$creditor\tE2E_ID${it*2+1}\tREF${it*2+1}\n"
                }
            },
            "initiated_outgoing_transactions(amount, subject, initiation_time, credit_payto, outgoing_transaction_id, end_to_end_id)" to {
                "(42,0)\tsubject\t0\tcredit_payto\t${it*2}\tE2E_ID$it\n"
            },
            "prepared_transfers(type, account_pub, authorization_pub, authorization_sig, recurrent, reference_number, registered_at, incoming_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 reference_number = subjectFmtQrBill(accountPubs[it])
                val pub = accountPubs[it].raw.encodeHex()
                val sig = token64.rand().encodeHex()
                "$type\t\\\\x$pub\t\\\\x$pub\t\\\\x$sig\t$recurrent\t$reference_number\t0\t$incoming_transaction_id\n"
            },
            "pending_recurrent_incoming_transactions(incoming_transaction_id, authorization_pub)" to {
                val hex = accountPubs[it].raw.encodeHex()
                "${it*2}\t\\\\x$hex\n"
            },
            "bounced_transactions(incoming_transaction_id, initiated_outgoing_transaction_id)" to {
                if (it % 10 == 0) {
                    "${it/2}\t${it/3}\n"
                } else {
                    ""
                }
            },
            "talerable_incoming_transactions(type, metadata, incoming_transaction_id)" 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"
                }
            },
            "talerable_outgoing_transactions(wtid, exchange_base_url, outgoing_transaction_id)" to {
                val hex = token32.rand().encodeHex()
                "\\\\x$hex\turl\t${it*2-1}\n"
            },
            "transfer_operations(initiated_outgoing_transaction_id, request_uid, wtid, exchange_base_url)" to {
                val hex32 = token32.rand().encodeHex()
                val hex64 = token64.rand().encodeHex()
                "$it\t\\\\x$hex64\t\\\\x$hex32\turl\n"
            }
        ))
    }

    @Test
    fun benchDb() {
        val ingestCfg = NexusIngestConfig.default(AccountType.exchange)

        bench { AMOUNT -> serverSetup { db ->
            // Generate data
            db.conn { genData(it, AMOUNT) }

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

            // Warm HTTP client
            client.getA("/taler-revenue/config").assertOk()
            
            // Register
            measureAction("register_in") {
                registerIn(db)
            }
            measureAction("register_incomplete_in") {
                registerIncompleteIn(db)
            }
            measureAction("register_completed_in") {
                registerCompletedIn(db)
            }
            measureAction("register_out") {
                registerOut(db)
            }
            measureAction("register_incomplete_out") {
                registerIncompleteOut(db)
            }
            measureAction("register_reserve") {
                talerableIn(db)
            }
            measureAction("register_prepared_reserve") {
                talerablePreparedIn(db)
            }
            measureAction("register_kyc") {
                talerableKycIn(db)
            }

            // Revenue API
            measureAction("transaction_revenue") {
                client.getA("/taler-revenue/history").assertOk()
            }

            // Wire gateway
            measureAction("wg_transfer") {
                client.postA("/taler-wire-gateway/transfer") {
                    json { 
                        "request_uid" to HashCode.rand()
                        "amount" to "CHF:0.0001"
                        "exchange_base_url" to "http://exchange.example.com/"
                        "wtid" to ShortHashCode.rand()
                        "credit_account" to grothoffPayto
                    }
                }.assertOk()
            }
            measureAction("wg_transfer_get") {
                client.getA("/taler-wire-gateway/transfers/42").assertOk()
            }
            measureAction("wg_transfer_page") {
                client.getA("/taler-wire-gateway/transfers").assertOk()
            }
            measureAction("wg_transfer_page_filter") {
                client.getA("/taler-wire-gateway/transfers?status=success").assertNoContent()
            }
            measureAction("wg_add") {
                client.postA("/taler-wire-gateway/admin/add-incoming") {
                    json { 
                        "amount" to "CHF:0.0001"
                        "reserve_pub" to EddsaPublicKey.randEdsaKey()
                        "debit_account" to grothoffPayto
                    }
                }.assertOk()
            }
            measureAction("wg_incoming") {
                client.getA("/taler-wire-gateway/history/incoming")
                    .assertOk()
            }
            measureAction("wg_outgoing") {
                client.getA("/taler-wire-gateway/history/outgoing")
                    .assertOk()
            }

            // Wire transfer
            /*measureAction("wt_register") {
                val (priv, pub) = accountPubs[it]
                val valid_req = obj {
                    "credit_account" to "payto://iban/CH7789144474425692816"
                    "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)
                }*/

            // Observability
            /*measureAction("metrics") {
                client.get("/taler-observability/metrics")
                    .assertOk()
            }*/
        }}
    }
}