/*
 * 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.client.request.*
import io.ktor.http.*
import io.ktor.server.testing.*
import org.junit.Test
import tech.libeufin.common.*
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.nexus.cli.registerOutgoingPayment
import tech.libeufin.ebics.randEbicsId
import java.time.Instant
import kotlin.test.*

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

    // POST /taler-wire-gateway/transfer
    @Test
    fun transfer() = serverSetup { 
        val valid_req = obj {
            "request_uid" to HashCode.rand()
            "amount" to "CHF:55"
            "exchange_base_url" to "http://exchange.example.com/"
            "wtid" to ShortHashCode.rand()
            "credit_account" to grothoffPayto
        }

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

        // Check OK
        client.postA("/taler-wire-gateway/transfer") {
            json(valid_req)
        }.assertOk()

        // check idempotency
        client.postA("/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("/taler-wire-gateway/transfer") {
            json(with_metadata)
        }.assertOk()
        client.postA("/taler-wire-gateway/transfer") {
            json(with_metadata)
        }.assertOk()
        
        // Malformed metadata
        listOf("bad_id", "bad id", "bad@id.com", "A".repeat(41)).forEach {
            client.postA("/taler-wire-gateway/transfer") {
                json(valid_req) {
                    "request_uid" to HashCode.rand()
                    "metadata" to it
                    "wtid" to ShortHashCode.rand()
                }
            }.assertBadRequest()
        }

        // Trigger conflict due to reused request_uid
        client.postA("/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("/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "request_uid" to HashCode.rand()
            }
        }.assertConflict(TalerErrorCode.BANK_TRANSFER_WTID_REUSED)

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

        // Bad BASE32 wtid
        client.postA("/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "wtid" to "I love chocolate"
            }
        }.assertBadRequest()
        
        // Bad BASE32 len wtid
        client.postA("/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "wtid" to Base32Crockford.encode(ByteArray(31).rand())
            }
        }.assertBadRequest()

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

        // Bad BASE32 len wtid
        client.postA("/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "request_uid" to Base32Crockford.encode(ByteArray(65).rand())
            }
        }.assertBadRequest()

        // Missing receiver-name
        client.postA("/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "credit_account" to "payto://iban/CH7389144832588726658"
            }
        }.assertBadRequest()

        // Bad payto kind
        client.postA("/taler-wire-gateway/transfer") {
            json(valid_req) { 
                "credit_account" to "payto://x-taler-bank/bank.hostname.test/bar?receiver-name=Mr+Tom"
            }
        }.assertBadRequest()

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

    // GET /taler-wire-gateway/transfers/{ROW_ID}
    @Test
    fun transferById() = serverSetup { 
        val wtid = ShortHashCode.rand()
        val valid_req = obj {
            "request_uid" to HashCode.rand()
            "amount" to "CHF:55"
            "exchange_base_url" to "http://exchange.example.com/"
            "wtid" to wtid
            "credit_account" to grothoffPayto
        }

        authRoutine(HttpMethod.Get, "/taler-wire-gateway/transfers/1")

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

        // Check OK
        client.getA("/taler-wire-gateway/transfers/${resp.row_id}")
            .assertOkJson<TransferStatus> { tx ->
            assertEquals(TransferStatusState.pending, tx.status)
            assertEquals(TalerAmount("CHF:55"), 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("/taler-wire-gateway/transfer") {
            json(valid_req) {
                "request_uid" to HashCode.rand()
                "metadata" to "ID"
                "wtid" to ShortHashCode.rand()
            }
        }.assertOkJson<TransferResponse> {
            client.getA("/taler-wire-gateway/transfers/${it.row_id}")
                .assertOkJson<TransferStatus> { tx ->
                assertEquals(tx.metadata, "ID")
            }
        }

        // Check unknown transaction
        client.getA("/taler-wire-gateway/transfers/42")
            .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    }

    // GET /accounts/{USERNAME}/taler-wire-gateway/transfers
    @Test
    fun transferPage() = serverSetup { db ->
        authRoutine(HttpMethod.Get, "/taler-wire-gateway/transfers")

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

        repeat(6) {
            client.postA("/taler-wire-gateway/transfer") {
                json {
                    "request_uid" to HashCode.rand()
                    "amount" to "CHF:55"
                    "exchange_base_url" to "http://exchange.example.com/"
                    "wtid" to ShortHashCode.rand()
                    "credit_account" to grothoffPayto
                }
            }.assertOkJson<TransferResponse>()
            db.initiated.batch(Instant.now(), randEbicsId(), false)
        }
        client.getA("/taler-wire-gateway/transfers")
            .assertOkJson<TransferList> {
            assertEquals(6, it.transfers.size)
            assertEquals(
                it, 
                client.getA("/taler-wire-gateway/transfers?status=pending").assertOkJson<TransferList>()
            )
        }
        client.getA("/taler-wire-gateway/transfers?status=success").assertNoContent()

        db.initiated.batchSubmissionSuccess(1, Instant.now(), "ORDER1")
        db.initiated.batchSubmissionFailure(2, Instant.now(), "Failure")
        db.initiated.batchSubmissionFailure(3, Instant.now(), "Failure")
        client.getA("/taler-wire-gateway/transfers?status=transient_failure").assertOkJson<TransferList> {
            assertEquals(2, it.transfers.size)
        }
        client.getA("/taler-wire-gateway/transfers?status=pending").assertOkJson<TransferList> {
            assertEquals(4, it.transfers.size)
        }
    }
    
    // GET /taler-wire-gateway/history/incoming
    @Test
    fun historyIncoming() = serverSetup { db ->
        authRoutine(HttpMethod.Get, "/taler-wire-gateway/history/incoming")
        historyRoutine<IncomingHistory>(
            url = "/taler-wire-gateway/history/incoming",
            ids = { it.incoming_transactions.map { it.row_id } },
            registered = listOf(
                // Reserve transactions using clean add incoming logic
                { addIncoming("CHF:12") },

                // Reserve transactions using raw bank transaction logic
                { talerableIn(db) },
                { talerableCompletedIn(db) },
                { talerablePreparedIn(db) },
                { talerablePreparedCompletedIn(db) },

                // KYC transactions using clean add incoming logic
                { addKyc("CHF:12") },

                // KYC transactions using raw bank transaction logic
                { talerableKycIn(db) },
            ),
            ignored = listOf(
                // Ignore malformed incoming transaction
                { registerIn(db) },

                // Ignore malformed incomplete
                { registerIncompleteIn(db) },

                // Ignore malformed completed
                { registerCompletedIn(db) },

                // Ignore incompleted
                { talerableIncompleteIn(db) },

                // Ignore outgoing transaction
                { talerableOut(db) },

                // Ignore prepared incomplete
                { talerablePreparedIncompleteIn(db) },
            )
        )
    }

    // GET /taler-wire-gateway/history/outgoing
    @Test
    fun historyOutgoing() = serverSetup { db ->
        authRoutine(HttpMethod.Get, "/taler-wire-gateway/history/outgoing")
        historyRoutine<OutgoingHistory>(
            url = "/taler-wire-gateway/history/outgoing",
            ids = { it.outgoing_transactions.map { it.row_id } },
            registered = listOf(
                // Transfer using raw bank transaction logic
                { talerableOut(db) },
                
                // And with metadata
                { talerableOut(db, "CON.ID") }
            ),
            ignored = listOf(
                // Ignore pending transfers
                { transfer() },

                // Ignore manual incoming transaction
                { talerableIn(db) },

                // Ignore malformed incoming transaction
                { registerIn(db) },

                // Ignore malformed outgoing transaction
                { registerOutgoingPayment(db, genOutPay("ignored")) },
            )
        )
        println(client.getA("/taler-wire-gateway/history/outgoing?limit=2")
            .assertOkJson<OutgoingHistory>()
            .outgoing_transactions
            .map { it.amount.toString() to it.metadata })
        assertContentEquals(
            client.getA("/taler-wire-gateway/history/outgoing?limit=2")
                .assertOkJson<OutgoingHistory>()
                .outgoing_transactions
                .map { it.amount.toString() to it.metadata }
            ,listOf(
                "CHF:44" to null,
                "CHF:44" 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(
                Payto.parse("payto://iban/CH7789144474425692816"),
                TransferType.reserve,
                false,
                TalerAmount("CHF:44"),
                PublicKeyAlg.EdDSA,
                pub,
                pub,
                EddsaSignature.rand()
            ).sign(priv))
        }.assertOkJson<SubjectResult>()
        val valid_req = obj {
            "amount" to "CHF:44"
            key to pub
            "debit_account" to grothoffPayto
        }

        authRoutine(HttpMethod.Post, "/taler-wire-gateway/admin/$path")

        // Check OK
        client.postA("/taler-wire-gateway/admin/$path") {
            json(valid_req)
        }.assertOk()

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

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

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

        // Bad payto kind
        client.postA("/taler-wire-gateway/admin/$path") {
            json(valid_req) { 
                "debit_account" to "payto://x-taler-bank/bank.hostname.test/bar"
            }
        }.assertBadRequest()
    }

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

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

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

    @Test
    fun addIncomingMix() = serverSetup { db ->
        addIncoming("CHF:1")
        addKyc("CHF:2")
        talerableIn(db, amount = "CHF:3")
        talerableKycIn(db, amount = "CHF:4")
        talerablePreparedIn(db, amount = "CHF:5")
        client.getA("/taler-wire-gateway/history/incoming?limit=25").assertOkJson<IncomingHistory> {
            assertEquals(5, it.incoming_transactions.size)
            it.incoming_transactions.forEachIndexed { i, tx ->
                assertEquals(TalerAmount("CHF:${i+1}"), tx.amount)
                if (i % 2 == 1) {
                    val tmp = assertIs<IncomingKycAuthTransaction>(tx)
                    if (i < 4) {
                        assertNull(tmp.authorization_pub)
                        assertNull(tmp.authorization_sig)
                    } else {
                        assertNotNull(tmp.authorization_pub)
                        assertNotNull(tmp.authorization_sig)
                    }
                } else {
                    val tmp = assertIs<IncomingReserveTransaction>(tx)
                    if (i < 4) {
                        assertNull(tmp.authorization_pub)
                        assertNull(tmp.authorization_sig)
                    } else {
                        assertNotNull(tmp.authorization_pub)
                        assertNotNull(tmp.authorization_sig)
                    }
                }
               
            }
        }
    }

    // POST /taler-wire-gateway/account/check
    @Test
    fun accountCheck() = serverSetup {
        client.getA("/taler-wire-gateway/account/check").assertNotImplemented()
    }

    @Test
    fun noApi() = serverSetup("mini.conf") {
        client.get("/taler-wire-gateway/config").assertNotImplemented()
    }

    @Test
    fun auth() = serverSetup("auth.conf") {
        authRoutine(HttpMethod.Get, "/taler-wire-gateway/history/incoming", false)
        client.get("/taler-wire-gateway/history/incoming") {
            basicAuth("username", "password")
        }.assertNoContent()
    }
}