/*
 * This file is part of LibEuFin.
 * Copyright (C) 2023-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 io.ktor.http.*
import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.common.*
import tech.libeufin.common.test.*
import kotlin.test.assertEquals

class ConversionApiTest {
    // GET /conversion-info/config
    @Test
    fun config() = bankSetup {
        client.get("/conversion-info/config").assertOkJson<ConversionConfig>()
        client.get("/conversion-rate-classes/1/conversion-info/config").assertOkJson<ConversionConfig>()
        client.get("/accounts/merchant/conversion-info/config").assertOkJson<ConversionConfig>()
    }

    // POST /conversion-info/conversion-rate
    @Test
    fun conversionRate() = bankSetup {
        val ok = obj {
            "cashin_ratio" to "0.8"
            "cashin_fee" to "KUDOS:0.02"
            "cashin_tiny_amount" to "KUDOS:0.01"
            "cashin_rounding_mode" to "nearest"
            "cashin_min_amount" to "EUR:0"
            "cashout_ratio" to "1.25"
            "cashout_fee" to "EUR:0.003"
            "cashout_tiny_amount" to "EUR:0.01"
            "cashout_rounding_mode" to "zero"
            "cashout_min_amount" to "KUDOS:0.1"
        }
        authRoutine(HttpMethod.Post, "/conversion-info/conversion-rate", requireAdmin = true)
        authRoutine(HttpMethod.Post, "/accounts/merchant/conversion-info/conversion-rate", requireAdmin = true)
        authRoutine(HttpMethod.Post, "/conversion-rate-classes/1/conversion-info/conversion-rate", requireAdmin = true)
        for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) {
            // Good rates
            client.postAdmin("$prefix/conversion-info/conversion-rate") {
                json(ok)
            }.assertNoContent()
            // Bad currency
            client.postAdmin("$prefix/conversion-info/conversion-rate") {
                json(ok) {
                    "cashout_fee" to "CHF:0.003"
                }
            }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
            // Zero tiny amount
            client.postAdmin("$prefix/conversion-info/conversion-rate") {
                json(ok) {
                    "cashout_tiny_amount" to "EUR:0"
                }
            }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID)
            client.postAdmin("$prefix/conversion-info/conversion-rate") {
                json(ok) {
                    "cashin_tiny_amount" to "KUDOS:0"
                }
            }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID)
            // Subcent cashout tiny amount
            client.postAdmin("$prefix/conversion-info/conversion-rate") {
                json(ok) {
                    "cashout_tiny_amount" to "EUR:0.0001"
                }
            }.assertBadRequest(TalerErrorCode.GENERIC_JSON_INVALID)
        }
    }

    // GET /conversion-info/rate
    @Test
    fun userRate() = bankSetup {
        authRoutine(HttpMethod.Get, "/accounts/merchant/conversion-info/rate", allowAdmin = true, optional = true)
        authRoutine(HttpMethod.Get, "/conversion-rate-classes/1/conversion-info/rate", requireAdmin = true)
        client.get("/conversion-info/rate").assertOkJson<ConversionRate>()
        client.getA("/accounts/merchant/conversion-info/rate").assertOkJson<ConversionRate>()
        client.get("/accounts/exchange/conversion-info/rate").assertOkJson<ConversionRate>()
        client.getAdmin("/conversion-rate-classes/1/conversion-info/rate").assertOkJson<ConversionRate>()
    }
    
    // GET /conversion-info/cashout-rate
    @Test
    fun cashoutRate() = bankSetup {
        authRoutine(HttpMethod.Get, "/accounts/merchant/conversion-info/cashout-rate?amount_debit=KUDOS:1", allowAdmin = true)
        authRoutine(HttpMethod.Get, "/conversion-rate-classes/1/conversion-info/cashout-rate?amount_debit=KUDOS:1", requireAdmin = true)
        for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) {
            // Check conversion to
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=KUDOS:1").assertOkJson<ConversionResponse> {
                assertEquals(TalerAmount("KUDOS:1"), it.amount_debit)
                assertEquals(TalerAmount("EUR:1.25"), it.amount_credit)
            }
            // Check conversion from
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=EUR:1.257").assertOkJson<ConversionResponse> {
                assertEquals(TalerAmount("KUDOS:1.01"), it.amount_debit)
                assertEquals(TalerAmount("EUR:1.257"), it.amount_credit)
            }

            // Too small
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=KUDOS:0.0008")
                .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION)
            // No amount
            client.getAdmin("$prefix/conversion-info/cashout-rate")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING)
            // Both amount
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=EUR:1&amount_credit=KUDOS:1")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
            // Wrong format
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=1")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=1")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
            // Wrong currency
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_debit=EUR:1")
                .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=KUDOS:1")
                .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
        }
        client.getA("/accounts/exchange/conversion-info/cashout-rate?amount_debit=KUDOS:1")
            .assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
    }

    // GET /conversion-info/cashin-rate
    @Test
    fun cashinRate() = bankSetup {
        authRoutine(HttpMethod.Get, "/conversion-rate-classes/1/conversion-info/cashin-rate?amount_debit=EUR:1", requireAdmin = true)
        for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/exchange")) {
            for ((amount, converted) in listOf(
                Pair(0.75, 0.58), Pair(0.32, 0.24), Pair(0.66, 0.51)
            )) {
                    // Check conversion to
                client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=EUR:$amount").assertOkJson<ConversionResponse> {
                    assertEquals(TalerAmount("KUDOS:$converted"), it.amount_credit)
                    assertEquals(TalerAmount("EUR:$amount"), it.amount_debit)
                }
                // Check conversion from
                client.getAdmin("$prefix/conversion-info/cashin-rate?amount_credit=KUDOS:$converted").assertOkJson<ConversionResponse> {
                    assertEquals(TalerAmount("KUDOS:$converted"), it.amount_credit)
                    assertEquals(TalerAmount("EUR:$amount"), it.amount_debit)
                }
            }

            // No amount
            client.getAdmin("$prefix/conversion-info/cashin-rate")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING)
            // Both amount
            client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=KUDOS:1&amount_credit=EUR:1")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
            // Wrong format
            client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=1")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
            client.getAdmin("$prefix/conversion-info/cashin-rate?amount_credit=1")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED)
            // Wrong currency
            client.getAdmin("$prefix/conversion-info/cashin-rate?amount_debit=KUDOS:1")
                .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
            client.getAdmin("$prefix/conversion-info/cashin-rate?amount_credit=EUR:1")
                .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
        }
        client.get("/accounts/merchant/conversion-info/cashin-rate?amount_debit=EUR:1")
            .assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
    }

    @Test
    fun noRate() = bankSetup { db ->
        db.serializable("DELETE FROM config WHERE key='conversion_rate'") {
            executeUpdate()
        }
        for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) {
            client.getAdmin("$prefix/conversion-info/config")
                .assertOkJson<ConversionConfig>()
            client.getAdmin("$prefix/conversion-info/cashout-rate")
                .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING)
            client.getAdmin("$prefix/conversion-info/cashout-rate?amount_credit=EUR:1")
                .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION)
        }
    }

    @Test
    fun notImplemented() = bankSetup("test_no_conversion.conf") {
        for (prefix in sequenceOf("", "/conversion-rate-classes/1", "/accounts/merchant")) {
            client.get("$prefix/conversion-info/cashin-rate")
                .assertNotImplemented()
            client.get("$prefix/conversion-info/cashout-rate")
                .assertNotImplemented()
        }
    }
}