/*
 * This file is part of LibEuFin.
 * Copyright (C) 2023, 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.http.*
import io.ktor.server.testing.*
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 kotlin.test.*

class WireGatewayApiTest {
    // GET /accounts/{USERNAME}/taler-wire-gateway/config
    @Test
    fun config() = bankSetup {
        client.get("/accounts/merchant/taler-wire-gateway/config").assertOk()
    }

    // POST /accounts/{USERNAME}/taler-wire-gateway/transfer
    @Test
    fun transfer() = bankSetup { 
        val valid_req = obj {
            "request_uid" to HashCode.rand()
            "amount" to "KUDOS:55"
            "exchange_base_url" to "http://exchange.example.com/"
            "wtid" to ShortHashCode.rand()
            "credit_account" to merchantPayto.canonical
        }

        authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/transfer", valid_req)

        // Checking exchange debt constraint.
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req)
        }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)

        // Giving debt allowance and checking the OK case.
        setMaxDebt("exchange", "KUDOS:1000")
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req)
        }.assertOk()

        // check idempotency
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req)
        }.assertOk()
        
        val with_metadata = obj(valid_req) {
            "request_uid" to HashCode.rand()
            "metadata" to "ID"
            "wtid" to ShortHashCode.rand()
        }
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(with_metadata)
        }.assertOk()
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(with_metadata)
        }.assertOk()
        
        // Malformed metadata
        listOf("bad_id", "bad id", "bad@id.com", "A".repeat(41)).forEach {
            client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
                json(valid_req) {
                    "metadata" to it
                }
            }.assertBadRequest()
        }

        val new_req = obj(valid_req) {
            "request_uid" to HashCode.rand()
            "wtid" to ShortHashCode.rand()
            "credit_account" to adminPayto
        }

        // Check conversion bounce
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(new_req)
        }.assertOk()

        // check idempotency
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(new_req)
        }.assertOk()
      

        // Trigger conflict due to reused request_uid
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "wtid" to ShortHashCode.rand()
                "exchange_base_url" to "http://different-exchange.example.com/"
            }
        }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED)
        
        // Trigger conflict due to reused wtid
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "request_uid" to HashCode.rand()
            }
        }.assertConflict(TalerErrorCode.BANK_TRANSFER_WTID_REUSED)

        // Currency mismatch
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) {
                "amount" to "EUR:33"
            }
        }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)

        // Same account
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "request_uid" to HashCode.rand()
                "wtid" to ShortHashCode.rand()
                "credit_account" to exchangePayto
            }
        }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)

        // Bad BASE32 wtid
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "wtid" to "I love chocolate"
            }
        }.assertBadRequest()
        
        // Bad BASE32 len wtid
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "wtid" to  randBase32Crockford(31)
            }
        }.assertBadRequest()

        // Bad BASE32 request_uid
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "request_uid" to "I love chocolate"
            }
        }.assertBadRequest()

        // Bad BASE32 len wtid
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "request_uid" to randBase32Crockford(65)
            }
        }.assertBadRequest()

        // Bad baseURL
        for (bad in sequenceOf("not-a-url", "file://not.http.com/", "no.transport.com/", "https://not.a/base/url")) {
            client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
                json(valid_req) {
                    "exchange_base_url" to bad
                }
            }.assertBadRequest()
        }
    }

    @Test
    fun transferNoConversion() = bankSetup("test_no_conversion.conf") {
        val valid_req = obj {
            "request_uid" to HashCode.rand()
            "amount" to "KUDOS:55"
            "exchange_base_url" to "http://exchange.example.com/"
            "wtid" to ShortHashCode.rand()
            "credit_account" to merchantPayto.canonical
        }
        setMaxDebt("exchange", "KUDOS:1000")

        // Transfer works for common accounts
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req)
        }.assertOk()

        // But fails to admin accounts
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) {
                "credit_account" to adminPayto
                "request_uid" to HashCode.rand()
                "wtid" to ShortHashCode.rand()
            }
        }.assertConflict(TalerErrorCode.BANK_ADMIN_CREDITOR)
    }

    // GET /accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID}
    @Test
    fun transferById() = bankSetup { 
        var wtid = ShortHashCode.rand()
        val valid_req = obj {
            "request_uid" to HashCode.rand()
            "amount" to "KUDOS:0.12"
            "exchange_base_url" to "http://exchange.example.com/"
            "wtid" to wtid
            "credit_account" to merchantPayto.canonical
        }

        authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/transfers/1", requireExchange = true)

        val resp = client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req)
        }.assertOkJson<TransferResponse>()

        // Check OK
        client.getA("/accounts/exchange/taler-wire-gateway/transfers/${resp.row_id}")
            .assertOkJson<TransferStatus> { tx ->
            assertEquals(TransferStatusState.success, tx.status)
            assertEquals(TalerAmount("KUDOS:0.12"), tx.amount)
            assertEquals("http://exchange.example.com/", tx.origin_exchange_url)
            assertNull(tx.metadata)
            assertEquals(wtid, tx.wtid)
            assertEquals(resp.timestamp, tx.timestamp)
        }
        
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) {
                "request_uid" to HashCode.rand()
                "metadata" to "ID"
                "wtid" to ShortHashCode.rand()
            }
        }.assertOkJson<TransferResponse> {
            client.getA("/accounts/exchange/taler-wire-gateway/transfers/${it.row_id}")
                .assertOkJson<TransferStatus> { tx ->
                assertEquals(tx.metadata, "ID")
            }
        }
        
        // Unknown account
        wtid = ShortHashCode.rand()
        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "request_uid" to HashCode.rand()
                "wtid" to wtid
                "credit_account" to unknownPayto
            }
        }.assertOkJson<TransferResponse> { resp ->
            client.getA("/accounts/exchange/taler-wire-gateway/transfers/${resp.row_id}")
                .assertOkJson<TransferStatus> { tx ->
                assertEquals(TransferStatusState.permanent_failure, tx.status)
                assertEquals(TalerAmount("KUDOS:0.12"), tx.amount)
                assertEquals("http://exchange.example.com/", tx.origin_exchange_url)
                assertEquals(wtid, tx.wtid)
                assertEquals(resp.timestamp, tx.timestamp)
            }
        }
        // Check unknown transaction
        client.getA("/accounts/exchange/taler-wire-gateway/transfers/42")
            .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
        // Check another user's transaction
        client.getA("/accounts/merchant/taler-wire-gateway/transfers/${resp.row_id}")
            .assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
    }

    // GET /accounts/{USERNAME}/taler-wire-gateway/transfers
    @Test
    fun transferPage() = bankSetup {
        authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/transfers", requireExchange = true)

        client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertNoContent()

        repeat(5) {
            client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
                json {
                    "request_uid" to HashCode.rand()
                    "amount" to "KUDOS:0.12"
                    "exchange_base_url" to "http://exchange.example.com/"
                    "wtid" to ShortHashCode.rand()
                    "credit_account" to merchantPayto
                }
            }.assertOkJson<TransferResponse>()
        }
        client.getA("/accounts/exchange/taler-wire-gateway/transfers")
            .assertOkJson<TransferList> {
            assertEquals(5, it.transfers.size)
            assertEquals(
                it, 
                client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOkJson<TransferList>()
            )
        }
        client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=pending").assertNoContent()
        client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=permanent_failure").assertNoContent()

        client.postA("/accounts/exchange/taler-wire-gateway/transfer") {
            json {
                "request_uid" to HashCode.rand()
                "amount" to "KUDOS:0.12"
                "exchange_base_url" to "http://exchange.example.com/"
                "wtid" to ShortHashCode.rand()
                "credit_account" to unknownPayto
            }
        }.assertOkJson<TransferResponse>()
        client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOkJson<TransferList> {
            assertEquals(6, it.transfers.size)
        }
        client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOkJson<TransferList> {
            assertEquals(5, it.transfers.size)
        }
        client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=permanent_failure").assertOkJson<TransferList> {
            assertEquals(1, it.transfers.size)
        }
    }
    
    // GET /accounts/{USERNAME}/taler-wire-gateway/history/incoming
    @Test
    fun historyIncoming() = bankSetup { 
        // Give Foo reasonable debt allowance:
        setMaxDebt("merchant", "KUDOS:1000")
        authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/incoming", requireExchange = true)
        historyRoutine<IncomingHistory>(
            url = "/accounts/exchange/taler-wire-gateway/history/incoming",
            ids = { it.incoming_transactions.map { it.row_id } },
            registered = listOf(
                // Reserve transactions using clean add incoming logic
                { addIncoming("KUDOS:10") },

                // Reserve transactions using raw bank transaction logic
                { tx("merchant", "KUDOS:10", "exchange", "history test with ${EddsaPublicKey.randEdsaKey()} reserve pub") },

                // Reserve transactions using withdraw logic
                { withdrawal("KUDOS:9") },

                // KYC transaction using clean add incoming logic 
                { addKyc("KUDOS:2") },

                // KYC transactions using raw bank transaction logic
                { tx("merchant", "KUDOS:2", "exchange", "history test with KYC:${EddsaPublicKey.randEdsaKey()} account pub") },
            ),
            ignored = listOf(
                // Ignore malformed incoming transaction
                { tx("merchant", "KUDOS:10", "exchange", "ignored") },

                // Ignore malformed outgoing transaction
                { tx("exchange", "KUDOS:10", "merchant", "ignored") },
            )
        )
    }

    // GET /accounts/{USERNAME}/taler-wire-gateway/history/outgoing
    @Test
    fun historyOutgoing() = bankSetup {
        setMaxDebt("exchange", "KUDOS:1000000")
        authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/outgoing", requireExchange = true)
        historyRoutine<OutgoingHistory>(
            url = "/accounts/exchange/taler-wire-gateway/history/outgoing",
            ids = { it.outgoing_transactions.map { it.row_id } },
            registered = listOf(
                // Transactions using clean add incoming logic
                { transfer("KUDOS:10") },
                
                // And with metadata
                { transfer("KUDOS:12", metadata = "CON:ID") }
            ),
            ignored = listOf(
                // Failed transfer
                { transfer("KUDOS:10", unknownPayto) },
                
                // Ignore manual outgoing transaction
                { tx("exchange", "KUDOS:10", "merchant", "${ShortHashCode.rand()} http://exchange.example.com/") },

                // Ignore malformed incoming transaction
                { tx("merchant", "KUDOS:10", "exchange", "ignored") },

                // Ignore malformed outgoing transaction
                { tx("exchange", "KUDOS:10", "merchant", "ignored") },
            )
        )
        assertContentEquals(
            client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?limit=2")
                .assertOkJson<OutgoingHistory>()
                .outgoing_transactions
                .map { it.amount.toString() to it.metadata }
            ,listOf(
                "KUDOS:10" to null,
                "KUDOS:12" to "CON:ID",
            )
        )
    }

    suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type: IncomingType) {
        val (path, key) = when (type) {
            IncomingType.reserve -> Pair("add-incoming", "reserve_pub")
            IncomingType.kyc -> Pair("add-kycauth", "account_pub")
            IncomingType.map -> Pair("add-mapped", "authorization_pub")
        }
     
        val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
        client.post("/taler-prepared-transfer/registration") {
            json(SubjectRequest(
                exchangePayto,
                TransferType.reserve,
                false,
                TalerAmount("KUDOS:44"),
                PublicKeyAlg.EdDSA,
                pub,
                pub,
                EddsaSignature.rand()
            ).sign(priv))
        }.assertOkJson<SubjectResult>()
        val valid_req = obj {
            "amount" to "KUDOS:44"
            key to pub
            "debit_account" to merchantPayto.canonical
        }

        authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/admin/$path", valid_req, requireAdmin = true)

        // Checking exchange debt constraint.
        client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
            json(valid_req)
        }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)

        // Giving debt allowance and checking the OK case.
        setMaxDebt("merchant", "KUDOS:1000")
        client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
            json(valid_req)
        }.assertOk()

        when (type) {
            IncomingType.reserve -> {
                // Trigger conflict due to reused reserve_pub
                client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
                    json(valid_req)
                }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
            }
            IncomingType.kyc -> {
                // Non conflict on reuse
                client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
                    json(valid_req)
                }.assertOk()
            }
            IncomingType.map -> {
                // Trigger conflict due to reused authorization_pub
                client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
                    json(valid_req)
                }.assertConflict(TalerErrorCode.BANK_TRANSFER_MAPPING_REUSED)
                // Trigger conflict due to unknown authorization_pub
                client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
                    json(valid_req) {
                        key to EddsaPublicKey.randEdsaKey()
                    }
                }.assertConflict(TalerErrorCode.BANK_TRANSFER_MAPPING_UNKNOWN)
            }
        }

        // Currency mismatch
        client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
            json(valid_req) { "amount" to "EUR:33" }
        }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)

        // Unknown account
        client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
            json(valid_req) { 
                key to EddsaPublicKey.randEdsaKey()
                "debit_account" to unknownPayto
            }
        }.assertConflict(TalerErrorCode.BANK_UNKNOWN_DEBTOR)

        // Same account
        client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
            json(valid_req) { 
                key to EddsaPublicKey.randEdsaKey()
                "debit_account" to exchangePayto
            }
        }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)

        // Bad BASE32 reserve_pub
        client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
            json(valid_req) { 
                key to "I love chocolate"
            }
        }.assertBadRequest()
        
        // Bad BASE32 len reserve_pub
        client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
            json(valid_req) { 
                key to randBase32Crockford(31)
            }
        }.assertBadRequest()
    }

    // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming
    @Test
    fun addIncoming() = bankSetup {
        talerAddIncomingRoutine(IncomingType.reserve)
    }

    // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth
    @Test
    fun addKycAuth() = bankSetup {
        talerAddIncomingRoutine(IncomingType.kyc)
    }

    // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-mapped
    @Test
    fun addMapped() = bankSetup {
        talerAddIncomingRoutine(IncomingType.map)
    }

    @Test
    fun addIncomingMix() = bankSetup {
        addIncoming("KUDOS:1")
        addKyc("KUDOS:2")
        tx("merchant", "KUDOS:3", "exchange", "test with ${EddsaPublicKey.randEdsaKey()} reserve pub")
        tx("merchant", "KUDOS:4", "exchange", "test with KYC:${EddsaPublicKey.randEdsaKey()} account pub")
        client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=25").assertOkJson<IncomingHistory> {
            assertEquals(4, it.incoming_transactions.size)
            it.incoming_transactions.forEachIndexed { i, tx ->
                assertEquals(TalerAmount("KUDOS:${i+1}"), tx.amount)
                if (i % 2 == 1) {
                    assertIs<IncomingKycAuthTransaction>(tx)
                } else {
                    assertIs<IncomingReserveTransaction>(tx)
                }
            }
        }
    }

    // POST /taler-wire-gateway/account/check
    @Test
    fun accountCheck() = bankSetup {
        client.getA("/accounts/exchange/taler-wire-gateway/account/check").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING)
        client.getA("/accounts/exchange/taler-wire-gateway/account/check?account=$unknownPayto").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
        client.getA("/accounts/exchange/taler-wire-gateway/account/check?account=$merchantPayto").assertOkJson<AccountInfo>()
    }
}