/*
 * This file is part of LibEuFin.
 * Copyright (C) 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 org.junit.Test
import tech.libeufin.common.*
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.common.test.*
import java.time.Instant
import kotlin.test.*

class PreparedTransferApiTest {
    // GET /taler-prepared-transfer/config
    @Test
    fun config() = bankSetup {
        client.get("/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>()
    }

    // POST /taler-prepared-transfer/registration
    @Test
    fun registration() = bankSetup {
        val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
        val amount = TalerAmount("KUDOS:1")
        val valid_req = SubjectRequest(
            exchangePayto,
            TransferType.reserve,
            false,
            amount,
            PublicKeyAlg.EdDSA,
            pub,
            pub,
            EddsaSignature.rand()
        )

        val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount)

        // Valid
        val subjects = client.post("/taler-prepared-transfer/registration") {
            json(valid_req.sign(priv))
        }.assertOkJson<SubjectResult> {
            assertEquals(it.subjects[1], simpleSubject)
            assertIs<TransferSubject.Uri>(it.subjects[0])
        }.subjects

        // Idempotent
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.sign(priv))
        }.assertOkJson<SubjectResult> {
            assertEquals(it.subjects, subjects)
        }

        // KYC has a different withdrawal uri
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(type = TransferType.kyc).sign(priv))
        }.assertOkJson<SubjectResult> {
            assertEquals(it.subjects[1], simpleSubject)
            val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0])
            assertNotEquals(subjects[0], uriSubject)
        }

        // Recurrent only has simple subject
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(recurrent = true).sign(priv))
        }.assertOkJson<SubjectResult> {
            assertEquals(it.subjects, listOf(simpleSubject))
        }

        // Bad signature
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req)
        }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE)

        // Not exchange
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(credit_account = merchantPayto).sign(priv))
        }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)

        // Unknown account
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(credit_account = unknownPayto).sign(priv))
        }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)

        assertBalance("customer", "+KUDOS:0")
        assertBalance("exchange", "+KUDOS:0")

        // Non recurrent accept on then bounce
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.sign(priv))
        }.assertOkJson<SubjectResult> {
            val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/')
            client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve
            tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
            tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
            assertBalance("customer", "-KUDOS:1")
            assertBalance("exchange", "+KUDOS:1")
        }

        // Withdrawal is aborted on completion
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(type = TransferType.kyc).sign(priv))
        }.assertOkJson<SubjectResult> {
            val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/')
            println("UUID $uuid")
            tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc
            tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
            client.postA("/accounts/customer/withdrawals/$uuid/confirm")
                .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted
            assertBalance("customer", "-KUDOS:2")
            assertBalance("exchange", "+KUDOS:2")
        }

        // Recurrent accept one and delay others
        val newKey = EddsaPublicKey.randEdsaKey()
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(account_pub = newKey, recurrent = true).sign(priv))
        }
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
        assertBalance("customer", "-KUDOS:7")
        assertBalance("exchange", "+KUDOS:7")

        // Complete pending on recurrent update
        val kycKey = EddsaPublicKey.randEdsaKey()
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey, recurrent = true).sign(priv))
        }.assertOkJson<SubjectResult>()
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(account_pub = kycKey, recurrent = true).sign(priv))
        }.assertOkJson<SubjectResult>()
        assertBalance("customer", "-KUDOS:7")
        assertBalance("exchange", "+KUDOS:7")

        // Kyc key reuse keep pending ones
        tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey")
        assertBalance("customer", "-KUDOS:8")
        assertBalance("exchange", "+KUDOS:8")

        // Switching to non recurrent cancel pending
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey).sign(priv))
        }.assertOkJson<SubjectResult>()
        assertBalance("customer", "-KUDOS:6")
        assertBalance("exchange", "+KUDOS:6")

        // Check authorization field in incoming history
        val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair()
        val testKey = EddsaPublicKey.randEdsaKey()
        val qr = subjectFmtQrBill(testAuth)
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv))
        }.assertOkJson<SubjectResult>()
        tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
        tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
        tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth")
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(type=TransferType.kyc, account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv))
        }.assertOkJson<SubjectResult>()
        val otherPub = EddsaPublicKey.randEdsaKey()
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(account_pub=otherPub, authorization_pub=testAuth, recurrent=true).sign(testPriv))
        }.assertOkJson<SubjectResult>()
        val lastPub = EddsaPublicKey.randEdsaKey()
        tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub")
        tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub")
        val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5")
            .assertOkJson<IncomingHistory>().incoming_transactions.map {
                when (it) {
                    is IncomingKycAuthTransaction -> Pair(it.account_pub, it.authorization_pub)
                    is IncomingReserveTransaction -> Pair(it.reserve_pub, it.authorization_pub)
                    else -> throw UnsupportedOperationException()
                }
            }
        assertContentEquals(history, listOf(
            Pair(lastPub, null),
            Pair(lastPub, null),
            Pair(otherPub, testAuth),
            Pair(testKey, testAuth),
            Pair(testKey, testAuth)
        ))
    }

    // DELETE /taler-prepared-transfer/registration
    @Test
    fun unregistration() = bankSetup {
        val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
        val valid_req = SubjectRequest(
            exchangePayto,
            TransferType.reserve,
            false,
            TalerAmount("KUDOS:1"),
            PublicKeyAlg.EdDSA,
            pub,
            pub,
            EddsaSignature.rand()
        ).sign(priv)

        val now = TalerTimestamp(Instant.now())
        val req = Unregistration(
            now,
            pub,
            EddsaSignature.rand()
        ).sign(priv)
      
        // Unknown
        client.post("/taler-prepared-transfer/unregistration") {
            json(req)
        }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
        
        // Know
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req)
        }.assertOkJson<SubjectResult>()
        client.post("/taler-prepared-transfer/unregistration") {
            json(req)
        }.assertNoContent()

        // Idempotent
        client.post("/taler-prepared-transfer/unregistration") {
            json(req)
        }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)

        // Bad signature
        client.post("/taler-prepared-transfer/unregistration") {
            json(
                Unregistration(
                    now,
                    pub,
                    EddsaSignature.rand()
                )
            )
        }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE)

        // Old timestamp
        client.post("/taler-prepared-transfer/unregistration") {
            json(
                Unregistration(
                    TalerTimestamp(Instant.now().minusSeconds(1000000)),
                    pub,
                    EddsaSignature.rand()
                ).sign(priv)
            )
        }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP)

        // Unknown bounce
        assertBalance("customer", "+KUDOS:0")
        assertBalance("exchange", "+KUDOS:0")
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce
        assertBalance("customer", "+KUDOS:0")
        assertBalance("exchange", "+KUDOS:0")

        // Pending bounced after deletion
        val newKey = EddsaPublicKey.randEdsaKey()
        client.post("/taler-prepared-transfer/registration") {
            json(valid_req.copy(account_pub=newKey, recurrent=true).sign(priv))
        }.assertOkJson<SubjectResult>()
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
        tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending
        assertBalance("customer", "-KUDOS:3")
        assertBalance("exchange", "+KUDOS:3")
        client.post("/taler-prepared-transfer/unregistration") {
            json(req)
        }.assertNoContent()
        assertBalance("customer", "-KUDOS:1")
        assertBalance("exchange", "+KUDOS:1")
    }
}