/*
 * This file is part of LibEuFin.
 * Copyright (C) 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 tech.libeufin.common.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails

class SubjectTest {
    fun assertFailsMsg(msg: String, lambda: () -> Unit) {
        val failure = assertFails(lambda)
        assertEquals(msg, failure.message)
    }

    @Test
    fun parseIncoming() {
        val key = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0";
        val other = "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG";

        for (ty in sequenceOf(IncomingType.reserve, IncomingType.kyc, IncomingType.map)) {
            val prefix = when (ty) {
                IncomingType.reserve -> ""
                IncomingType.kyc -> "KYC"
                IncomingType.map -> "MAP"
            }
            val standard = "$prefix$key"
            val (standardL, standardR) = standard.chunked(standard.length / 2 + 1)
            val mixed = "${prefix}4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0"
            val (mixedL, mixedR) = mixed.chunked(mixed.length / 2 + 1)
            val other_standard = "$prefix$other"
            val other_mixed = "${prefix}TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60"
            val key = when (ty) {
                IncomingType.reserve -> IncomingSubject.Reserve(EddsaPublicKey(key))
                IncomingType.kyc -> IncomingSubject.Kyc(EddsaPublicKey(key))
                IncomingType.map -> IncomingSubject.Map(EddsaPublicKey(key))
            }

            // Check succeed if standard or mixed
            for (case in sequenceOf(standard, mixed)) {
                for (test in sequenceOf(
                    "noise $case noise",
                    "$case noise to the right",
                    "noise to the left $case",
                    "    $case     ",
                    "noise\n$case\nnoise",
                    "Test+$case"
                )) {
                    assertEquals(key, parseIncomingSubject(test))
                }
            }

            // Check succeed if standard or mixed and split
            for ((L, R) in sequenceOf(standardL to standardR, mixedL to mixedR)) {
                for (case in sequenceOf(
                    "left $L$R right",
                    "left $L $R right",
                    "left $L-$R right",
                    "left $L+$R right",
                    "left $L\n$R right",
                    "left $L%20$R right",
                    "left $L-+\n$R right",
                    "left $L - $R right",
                    "left $L + $R right",
                    "left $L \n $R right",
                    "left $L - + \n $R right",
                )) {
                    assertEquals(key, parseIncomingSubject(case))
                }
            }

            // Check concat parts
            for (chunkSize in 1 until standard.length) {
                val chunked = standard.chunked(chunkSize).joinToString(" ")
                for (case in sequenceOf(chunked, "left ${chunked} right")) {
                    assertEquals(key, parseIncomingSubject(case))
                }
            }

            // Check failed when multiple key
            for (case in sequenceOf(
                "$standard $other_standard",
                "$mixed $other_mixed",
            )) {
                assertFailsMsg("found multiple reserve public key") {
                    parseIncomingSubject(case)
                }
            }

            // Check accept redundant key
            for (case in sequenceOf(
                "$standard $standard $mixed $mixed",   // Accept redundant key
                "$mixedL-$mixedR $standardL-$standardR",
                "$standard $other_mixed",              // Prefer high quality
            )) {
                assertEquals(key, parseIncomingSubject(case))
            }

            // Check failure if malformed or missing
            for (case in sequenceOf(
                "does not contain any reserve",                        // Check fail if none
                standard.substring(1),                    // Check fail if missing char
                "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" // Check fail if not a valid key
            )) {
                assertFailsMsg("missing reserve public key") {
                    parseIncomingSubject(case)
                }
            }

            if (ty == IncomingType.kyc) {
                // Prefer prefixed over unprefixed
                for (case in sequenceOf(
                    "$other $standard", "$other $mixed"
                )) {
                    assertEquals(key, parseIncomingSubject(case))
                }
            }
        }

        // Admin balance adjust
        for (subject in sequenceOf(
            "ADMIN BALANCE ADJUST",
            "ADMIN:BALANCE:ADJUST",
            "AdminBalanceAdjust",
            "ignore aDmIn:BaLaNCe AdJUsT"
        )) {
            assertEquals(
                IncomingSubject.AdminBalanceAdjust,
                parseIncomingSubject(subject)
            )
        }
    }

    /** Test parsing logic using real use case */
    @Test
    fun realIncoming() {
        // Good reserve cases
        for ((subject, key) in sequenceOf(
            "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60" to "TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60",
            "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG" to "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG",
            "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
            "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG" to "KYCVEEXTBXBEMCS5R64C24GFNQVWBN5R2F9QSQ7PN8QXAP1NG4NG",
            "Taler%20NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to "NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
        )) {
            assertEquals(
                IncomingSubject.Reserve(EddsaPublicKey(key)),
                parseIncomingSubject(subject)
            )
        }
        // Good kyc cases
        for ((subject, key) in sequenceOf(
            "KYC JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZ FPW4YC3WJ2DWSJT70" to "JW398X85FWPKKMS0EYB6TQ1799RMY5DDXTZFPW4YC3WJ2DWSJT70"
        )) {
            assertEquals(
                IncomingSubject.Kyc(EddsaPublicKey(key)),
                parseIncomingSubject(subject)
            )
        }
    }

    @Test
    fun outgoing() {
        val key = ShortHashCode.rand()

        run {
            // Without metadata
            val subject = "$key http://exchange.example.com/"
            val parsed = parseOutgoingSubject(subject)
            assertEquals(parsed, Triple(key, BaseURL.parse("http://exchange.example.com/"), null))
            assertEquals(subject, fmtOutgoingSubject(parsed.first, parsed.second, parsed.third))
        }

        run {
            // With metadata
            val subject =
                "Accounting:id.42 $key http://exchange.example.com/"
            val parsed = parseOutgoingSubject(subject)
            assertEquals(
                parsed,
                Triple(key, BaseURL.parse("http://exchange.example.com/"), "Accounting:id.42")
            )
            assertEquals(subject, fmtOutgoingSubject(parsed.first, parsed.second, parsed.third))
        }
    }
}
