/*
 * 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 com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.testing.test
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.common.asUtf8
import tech.libeufin.nexus.*
import tech.libeufin.ebics.*
import tech.libeufin.nexus.cli.LibeufinNexus
import tech.libeufin.ebisync.cli.LibeufinEbisync
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import kotlin.io.path.*
import kotlin.test.Test
import kotlin.test.assertEquals

val nexusCmd = LibeufinNexus()
val ebisyncCmd = LibeufinEbisync()

fun CliktCommand.testErr(cmd: String, msg: String) {
    val prevOut = System.err
    val tmpOut = ByteArrayOutputStream()
    System.setErr(PrintStream(tmpOut))
    val result = test(cmd)
    System.setErr(prevOut)
    val tmpStr = tmpOut.asUtf8()
    println(tmpStr)
    assertEquals(1, result.statusCode, "'$cmd' should have failed")
    val line = tmpStr.substringAfterLast(" - ").trimEnd('\n')
    println(line)
    assertEquals(msg, line)
}

class CliTest {
    /** Test error format related to the keying process */
    @Test
    fun keys() {
        val nexusCmds = listOf("ebics-submit", "ebics-fetch")
        val nexusAllCmds = listOf("ebics-submit", "ebics-fetch", "ebics-setup")
        val ebiSyncCmds = listOf("fetch")
        val ebiSyncAllCmds = listOf("fetch", "setup")
        val conf = "conf/cli.conf"
        val nexusCfg = nexusConfig(Path(conf))
        val cfg = nexusCfg.ebics
        val clientKeysPath = cfg.clientPrivateKeysPath
        val bankKeysPath = cfg.bankPublicKeysPath
        clientKeysPath.parent!!.createParentDirectories()
        clientKeysPath.parent!!.toFile().setWritable(true)
        bankKeysPath.parent!!.createDirectories()

        fun checkCmds(msg: String) {
            for (cmd in listOf("ebics-submit", "ebics-fetch")) {
                nexusCmd.testErr("$cmd -c $conf", msg.replace("SETUPCMD", "libeufin-nexus ebics-setup"))
            }
            for (cmd in listOf("fetch")) {
                ebisyncCmd.testErr("$cmd -c $conf", msg.replace("SETUPCMD", "libeufin-ebisync setup"))
            }
        }
        fun checkAllCmds(msg: String) {
            for (cmd in listOf("ebics-submit", "ebics-fetch", "ebics-setup")) {
                nexusCmd.testErr("$cmd -c $conf", msg)
            }
            for (cmd in listOf("fetch", "setup")) {
                ebisyncCmd.testErr("$cmd -c $conf", msg)
            }
        }
        
        // Missing client keys
        clientKeysPath.deleteIfExists()
        checkCmds("Missing client private keys file at '$clientKeysPath', run 'SETUPCMD' first")
        // Empty client file
        clientKeysPath.createFile()
        checkAllCmds("Could not decode client private keys at '$clientKeysPath': Expected start of the object '{', but had 'EOF' instead at path: $\nJSON input: ")
        // Bad client json
        clientKeysPath.writeText("CORRUPTION", Charsets.UTF_8)
        checkAllCmds("Could not decode client private keys at '$clientKeysPath': Unexpected JSON token at offset 0: Expected start of the object '{', but had 'C' instead at path: $\nJSON input: CORRUPTION")
        // Missing permission
        clientKeysPath.toFile().setReadable(false)
        if (!clientKeysPath.isReadable()) { // Skip if root
            checkAllCmds("Could not read client private keys at '$clientKeysPath': permission denied")
        }
        // Unfinished client
        persistClientKeys(generateNewKeys(), clientKeysPath)
        checkCmds("Unsubmitted client private keys, run 'SETUPCMD' first")

        // Missing bank keys
        persistClientKeys(generateNewKeys().apply {
            submitted_hia = true
            submitted_ini = true
        }, clientKeysPath)
        bankKeysPath.deleteIfExists()
        checkCmds("Missing bank public keys at '$bankKeysPath', run 'SETUPCMD' first")
        // Empty bank file
        bankKeysPath.createFile()
        checkAllCmds("Could not decode bank public keys at '$bankKeysPath': Expected start of the object '{', but had 'EOF' instead at path: $\nJSON input: ")
        // Bad bank json
        bankKeysPath.writeText("CORRUPTION", Charsets.UTF_8)
        checkAllCmds("Could not decode bank public keys at '$bankKeysPath': Unexpected JSON token at offset 0: Expected start of the object '{', but had 'C' instead at path: $\nJSON input: CORRUPTION")
        // Missing permission
        bankKeysPath.toFile().setReadable(false)
        if (!bankKeysPath.isReadable()) { // Skip if root
            checkAllCmds("Could not read bank public keys at '$bankKeysPath': permission denied")
        }
        // Unfinished bank
        persistBankKeys(BankPublicKeysFile(
            bank_authentication_public_key = CryptoUtil.genRSAPublic(2048),
            bank_encryption_public_key = CryptoUtil.genRSAPublic(2048),
            accepted = false
        ), bankKeysPath)
        checkCmds("Unaccepted bank public keys, run 'SETUPCMD' until accepting the bank keys")

        // Missing permission
        clientKeysPath.deleteIfExists()
        clientKeysPath.parent!!.toFile().setWritable(false)
        if (!clientKeysPath.parent!!.isWritable()) { // Skip if root
            nexusCmd.testErr("ebics-setup -c $conf", "Could not write client private keys at '$clientKeysPath': permission denied on '${clientKeysPath.parent}'")
            ebisyncCmd.testErr("setup -c $conf", "Could not write client private keys at '$clientKeysPath': permission denied on '${clientKeysPath.parent}'")
        }
    }
}