/*
 * This file is part of LibEuFin.
 * Copyright (C) 2024-2025 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 org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.bank.db.CashoutDAO.CashoutCreationResult
import tech.libeufin.bank.db.ExchangeDAO.TransferResult
import tech.libeufin.bank.db.TransactionDAO.BankTransactionResult
import tech.libeufin.bank.db.WithdrawalDAO.*
import tech.libeufin.common.*
import tech.libeufin.common.test.*
import tech.libeufin.common.db.*
import java.time.Duration
import java.time.Instant
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertIs

class GcTest {
    @Test
    fun gc() = bankSetup { db -> db.conn { conn ->
        fun assertNb(nb: Int, stmt: String) {
            assertEquals(nb, conn.talerStatement(stmt).one { it.getInt(1) })
        }
        fun assertNbAccount(nb: Int) = assertNb(nb, "SELECT count(*) from bank_accounts")
        fun assertNbTokens(nb: Int) = assertNb(nb, "SELECT count(*) from bearer_tokens")
        fun assertNbTan(nb: Int) = assertNb(nb, "SELECT count(*) from tan_challenges")
        fun assertNbCashout(nb: Int) = assertNb(nb, "SELECT count(*) from cashout_operations")
        fun assertNbWithdrawal(nb: Int) = assertNb(nb, "SELECT count(*) from taler_withdrawal_operations")
        fun assertNbBankTx(nb: Int) = assertNb(nb, "SELECT count(*) from bank_transaction_operations")
        fun assertNbTx(nb: Int) = assertNb(nb, "SELECT count(*) from bank_account_transactions")
        fun assertNbIncoming(nb: Int) = assertNb(nb, "SELECT count(*) from taler_exchange_incoming")
        fun assertNbOutgoing(nb: Int) = assertNb(nb, "SELECT count(*) from taler_exchange_outgoing")

        val ZERO = TalerAmount.zero("KUDOS")
        val MAX = TalerAmount.max("KUDOS")

        // Time calculation
        val abortAfter = Duration.ofMinutes(15)
        val cleanAfter = Duration.ofDays(14)
        val deleteAfter = Duration.ofDays(350)
        val now = Instant.now()
        val abort = now.minus(abortAfter)
        val clean = now.minus(cleanAfter)
        val delete = now.minus(deleteAfter)

        // Create test accounts
        val payto = IbanPayto.rand()
        client.post("/accounts") {
            json {
                "username" to "old_account"
                "password" to "old_account-password"
                "name" to "Old Account"
                "cashout_payto_uri" to payto
            }
        }.assertOkJson<RegisterAccountResponse>().internal_payto_uri
        client.post("/accounts") {
            json {
                "username" to "recent_account"
                "password" to "recent_account-password"
                "name" to "Recent Account"
                "cashout_payto_uri" to payto
            }

        }.assertOkJson<RegisterAccountResponse>().internal_payto_uri
        assertNbAccount(6)

        // Create test tokens
        for (time in listOf(now, clean)) {
            for (account in listOf("old_account", "recent_account")) {
                db.token.create(account, ByteArray(32).rand(), time, time, TokenScope.readonly, false, null, true)
                db.tan.new(account, Operation.cashout, Base32Crockford64B.rand(), Base32Crockford16B.rand(), "", time, 0, Duration.ZERO, TanChannel.sms, "")
            }
        }
        assertNbTokens(5)
        assertNbTan(4)

        // Create test operations
        val from = TalerAmount("KUDOS:1")
        val to = convert("KUDOS:1")
        for ((account, times) in listOf(
            Pair("old_account", listOf(delete)),
            Pair("recent_account", listOf(now, abort, clean, delete))
        )) {
            for (time in times) {
                val uuid = UUID.randomUUID()
                assertEquals(
                    db.withdrawal.create(account, uuid, from, null, false, time, ZERO, ZERO, MAX),
                    WithdrawalCreationResult.Success
                )
                assertIs<WithdrawalSelectionResult.Success>(
                    db.withdrawal.setDetails(uuid, exchangePayto, EddsaPublicKey.randEdsaKey(), null, ZERO, ZERO, MAX)
                )
                assertEquals(
                    db.withdrawal.confirm(account, uuid, time, null, false, ZERO, ZERO, MAX),
                    WithdrawalConfirmationResult.Success
                )
                assertIs<CashoutCreationResult.Success>(
                    db.cashout.create(account, ShortHashCode.rand(), from, to, "", time, false),
                )
                assertIs<BankTransactionResult.Success>(
                    db.transaction.create(customerPayto, account, "", from, time, false, ShortHashCode.rand(), ZERO, ZERO, MAX),
                )
            }
            for (time in listOf(now, abort, clean, delete)) {
                assertEquals(
                    db.withdrawal.create(account, UUID.randomUUID(), from, null, false, time, ZERO, ZERO, MAX),
                    WithdrawalCreationResult.Success
                )
            }
        }
        for (time in listOf(now, abort, clean, delete)) {
            assertIs<TransferResult.Success>(
                db.exchange.transfer(
                    TransferRequest(HashCode.rand(), from, BaseURL.parse("http://localhost/"), ShortHashCode.rand(), customerPayto),
                    "exchange", time, false
                )
            )
        }

        assertNbTx(38)
        assertNbCashout(5)
        assertNbBankTx(5)
        assertNbWithdrawal(13)
        assertNbIncoming(5)
        assertNbOutgoing(4)

        // Check soft delete
        conn.execSQLUpdate("UPDATE bank_accounts SET balance = (0, 0)::taler_amount")
        for (account in listOf("old_account", "recent_account")) {
            client.deleteA("/accounts/$account").assertNoContent()
        }
        assertNbAccount(6)

        db.gc.collect(
            Instant.now(),
            abortAfter,
            cleanAfter,
            deleteAfter
        )
        // Check hard delete
        assertNbAccount(5)
        assertNbTokens(3)
        assertNbTan(1)
        assertNbTx(24)
        assertNbCashout(3)
        assertNbBankTx(3)
        assertNbWithdrawal(4)
        assertNbIncoming(3)
        assertNbOutgoing(3)
    }}
}