/*
 * This file is part of LibEuFin.
 * Copyright (C) 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/>
 */

package tech.libeufin.ebics.test

import org.w3c.dom.Document
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.testing.test
import io.ktor.http.*
import io.ktor.http.content.*
import tech.libeufin.common.*
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.ebics.*
import kotlin.io.path.*
import kotlin.test.*
import java.security.interfaces.RSAPrivateCrtKey
import java.security.interfaces.RSAPublicKey
import java.time.LocalDate

class EbicsState {
    private val bankSignKey: RSAPrivateCrtKey = CryptoUtil.genRSAPrivate(2048)
    private val bankEncKey: RSAPrivateCrtKey = CryptoUtil.genRSAPrivate(2048)
    private val bankAuthKey: RSAPrivateCrtKey = CryptoUtil.genRSAPrivate(2048)

    private var clientSignPub: RSAPublicKey? = null
    private var clientEncrPub: RSAPublicKey? = null
    private var clientAuthPub: RSAPublicKey? = null

    private var transactionId: String? = null
    private var orderId: String? = null

    companion object {
        private val HEV_OK = XmlBuilder.toBytes("ebicsHEVResponse") {
            attr("xmlns", "http://www.ebics.org/H000")
            el("SystemReturnCode") {
                el("ReturnCode", "000000")
                el("ReportText", "[EBICS_OK] OK")
            }
            el("VersionNumber") {
                attr("ProtocolVersion", "H005")
                text("03.00")
            }
        }
        private val KEY_OK = XmlBuilder.toBytes("ebicsKeyManagementResponse") {
            attr("xmlns", "http://www.ebics.org/H005")
            el("header") {
                attr("authenticate", "true")
                el("mutable") {
                    el("ReturnCode", "000000")
                    el("ReportText", "[EBICS_OK] OK")
                }
            }
            el("body") {
                el("ReturnCode") {
                    attr("authenticate", "true")
                    text("000000")
                }
            }
        }

        private fun parseUnsecureRequest(body: String, order: String, root: String, parse: XmlDestructor.() -> Unit) {
            XmlDestructor.parse(body, "ebicsUnsecuredRequest") {
                val adminOrder = one("header").one("static").one("OrderDetails").one("AdminOrderType").text()
                assertEquals(adminOrder, order)
                val chunk = one("body").one("DataTransfer").one("OrderData").base64()
                val deflated = chunk.inputStream().inflate()
                XmlDestructor.parse(deflated, root) { parse() }
            }
        }
    }

    private fun signedResponse(doc: Document): ByteArray {
        XMLUtil.signEbicsDocument(doc, bankAuthKey)
        return XMLUtil.convertDomToBytes(doc)
    }

    private fun ebicsResponsePayload(payload: ByteArray, last: Boolean = true): ByteArray {
        transactionId = randEbicsId()
        val deflated = payload.inputStream().deflate()
        val (transactionKey, encryptedTransactionKey) = CryptoUtil.genEbicsE002Key(clientEncrPub!!)
        val encrypted = CryptoUtil.encryptEbicsE002(transactionKey, deflated)
        val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
            attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
            el("header") {
                attr("authenticate", "true")
                el("static") {
                    el("TransactionID", transactionId!!)
                    el("NumSegments", "1")
                }
                el("mutable") {
                    el("TransactionPhase", "Initialisation")
                    el("SegmentNumber") {
                        attr("lastSegment", last.toString())
                        text("1")
                    }
                    el("ReturnCode", "000000")
                    el("ReportText", "[EBICS_OK] OK")
                }
            }
            el("AuthSignature")
            el("body") {
                el("DataTransfer") {
                    el("DataEncryptionInfo") {
                        attr("authenticate", "true")
                        el("EncryptionPubKeyDigest") {
                            attr("Version", "E002")
                            attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
                            text(CryptoUtil.getEbicsPublicKeyHash(clientEncrPub!!).encodeBase64())
                        }
                        el("TransactionKey", encryptedTransactionKey.encodeBase64())
                    }
                    el("OrderData", encrypted.encodeBase64())
                }
                el("ReturnCode") {
                    attr("authenticate", "true")
                    text("000000")
                }
            }
        }
        return signedResponse(doc)
    }

    private fun ebicsResponseNoData(): ByteArray {
        val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
            attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
            el("header") {
                attr("authenticate", "true")
                el("static")
                el("mutable") {
                    el("TransactionPhase", "Initialisation")
                    el("ReturnCode", "000000")
                    el("ReportText", "[EBICS_OK] OK")
                }
            }
            el("AuthSignature")
            el("body") {
                el("ReturnCode") {
                    attr("authenticate", "true")
                    text("090005")
                }
            }
        }
        return signedResponse(doc)
    }

    fun hev(body: String): ByteArray {
        val hostId = XmlDestructor.parse(body, "ebicsHEVRequest") {
            one("HostID").text()
        }
        return HEV_OK
    }

    fun ini(body: String): ByteArray {
        parseUnsecureRequest(body, "INI", "SignaturePubKeyOrderData") {
            clientSignPub = one("SignaturePubKeyInfo") {
                val version = one("SignatureVersion").text()
                assertEquals(version, "A006")
                rsaPubKey()
            }
        }
        return KEY_OK
    }

    fun hia(body: String): ByteArray {
        parseUnsecureRequest(body, "HIA", "HIARequestOrderData") {
            clientAuthPub = one("AuthenticationPubKeyInfo") {
                val version = one("AuthenticationVersion").text()
                assertEquals(version, "X002")
                rsaPubKey()
            }
            clientEncrPub = one("EncryptionPubKeyInfo") {
                val version = one("EncryptionVersion").text()
                assertEquals(version, "E002")
                rsaPubKey()
            }
        }
        return KEY_OK
    }

    fun hpb(body: String): ByteArray {
        // Parse HPB request
        XmlDestructor.parse(body, "ebicsNoPubKeyDigestsRequest") {
            val order = one("header").one("static").one("OrderDetails").one("AdminOrderType").text()
            assertEquals(order, "HPB")
        }

        val payload = XmlBuilder.toBytes("HPBResponseOrderData") {
            el("AuthenticationPubKeyInfo") {
                el("PubKeyValue") {
                    el("RSAKeyValue") {
                        el("Modulus", bankAuthKey.modulus.encodeBase64())
                        el("Exponent", bankAuthKey.publicExponent.encodeBase64())
                    }
                }
                el("AuthenticationVersion", "X002")
            }
            el("EncryptionPubKeyInfo") {
                el("PubKeyValue") {
                    el("RSAKeyValue") {
                        el("Modulus", bankEncKey.modulus.encodeBase64())
                        el("Exponent", bankEncKey.publicExponent.encodeBase64())
                    }
                }
                el("EncryptionVersion", "E002")
            }
        }.inputStream().deflate()

        val (transactionKey, encryptedTransactionKey) = CryptoUtil.genEbicsE002Key(clientEncrPub!!)
        val encrypted = CryptoUtil.encryptEbicsE002(transactionKey, payload)
        
        return XmlBuilder.toBytes("ebicsKeyManagementResponse") {
            attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
            attr("xmlns", "http://www.ebics.org/H005")
            el("header") {
                attr("authenticate", "true")
                el("mutable") {
                    el("ReturnCode", "000000")
                    el("ReportText", "[EBICS_OK] OK")
                }
            }
            el("body") {
                el("DataTransfer") {
                    el("DataEncryptionInfo") {
                        attr("authenticate", "true")
                        el("EncryptionPubKeyDigest") {
                            attr("Version", "E002")
                            attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256")
                            text(CryptoUtil.getEbicsPublicKeyHash(clientEncrPub!!).encodeBase64())
                        }
                        el("TransactionKey", encryptedTransactionKey.encodeBase64())
                    }
                    el("OrderData", encrypted.encodeBase64())
                }
                el("ReturnCode") {
                    attr("authenticate", "true")
                    text("000000")
                }
            }
        }
    }

    private fun receipt(body: String, ok: Boolean): ByteArray {
        XmlDestructor.parse(body, "ebicsRequest") {
            one("header") {
                val id = one("static").one("TransactionID").text()
                assertEquals(id, transactionId)
                val phase = one("mutable").one("TransactionPhase").text()
                assertEquals(phase, "Receipt")
            }
            val code = one("body").one("TransferReceipt").one("ReceiptCode").text()
            assertEquals(code, if (ok) { "0" } else { "1" })
        }
        val response = signedResponse(XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
            attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
            el("header") {
                attr("authenticate", "true")
                el("static") {
                    el("TransactionID", transactionId!!)
                }
                el("mutable") {
                    el("TransactionPhase", "Receipt")
                    el("ReturnCode", "000000")
                    el("ReportText", "[EBICS_OK] OK")
                }
            }
            el("AuthSignature")
            el("body") {
                el("ReturnCode") {
                    attr("authenticate", "true")
                    text("000000")
                }
            }
        })
        transactionId = null
        return response
    }

    fun receiptOk(body: String): ByteArray = receipt(body, true)
    fun receiptErr(body: String): ByteArray = receipt(body, false)

    fun hkd(body: String): ByteArray {
        XmlDestructor.parse(body, "ebicsRequest") {
            one("header") {
                val adminOrder = one("static").one("OrderDetails").one("AdminOrderType").text()
                assertEquals(adminOrder, "HKD")
                val phase = one("mutable").one("TransactionPhase").text()
                assertEquals(phase, "Initialisation")
            }
        }
        return ebicsResponsePayload(
            XmlBuilder.toBytes("HKDResponseOrderData") {
                el("PartnerInfo") {
                    el("AddressInfo")
                    el("OrderInfo") {
                        el("AdminOrderType", "BTD")
                        el("Service") {
                            el("ServiceName", "STM")
                            el("Scope", "CH")
                            el("Container") {
                                attr("containerType", "ZIP")
                            }
                            el("MsgName") {
                                attr("version", "08")
                                text("camt.052")
                            }
                        }
                        el("Description")
                    }
                    el("OrderInfo") {
                        el("AdminOrderType", "BTU")
                        el("Service") {
                            el("ServiceName", "SCT")
                            el("MsgName") {
                                text("pain.001")
                            }
                        }
                        el("Description", "Direct Debit")
                    }
                    el("OrderInfo") {
                        el("AdminOrderType", "BTU")
                        el("Service") {
                            el("ServiceName", "SCI")
                            el("Scope", "DE")
                            el("MsgName") {
                                text("pain.001")
                            }
                        }
                        el("Description", "Instant Direct Debit")
                    }
                }
            }
        )
    }

    fun haa(body: String): ByteArray {
        XmlDestructor.parse(body, "ebicsRequest") {
            one("header") {
                val adminOrder = one("static").one("OrderDetails").one("AdminOrderType").text()
                assertEquals(adminOrder, "HAA")
                val phase = one("mutable").one("TransactionPhase").text()
                assertEquals(phase, "Initialisation")
            }
        }
        return ebicsResponsePayload(
            XmlBuilder.toBytes("HAAResponseOrderData") {
                el("Service") {
                    el("ServiceName", "STM")
                    el("Scope", "CH")
                    el("Container") {
                        attr("containerType", "ZIP")
                    }
                    el("MsgName") {
                        attr("version", "08")
                        text("camt.052")
                    }
                }
            }
        )
    }

    private fun btdDateCheck(body: String, pinned: LocalDate?): ByteArray {
        XmlDestructor.parse(body, "ebicsRequest") {
            one("header") {
                one("static").one("OrderDetails") {
                    val adminOrder = one("AdminOrderType").text()
                    assertEquals(adminOrder, "BTD")
                    val start = one("BTDOrderParams").opt("DateRange")?.opt("Start")?.date()
                    assertEquals(start, pinned)
                }
                val phase = one("mutable").one("TransactionPhase").text()
                assertEquals(phase, "Initialisation")
            }
        }
        return ebicsResponseNoData()
    }

    fun btdNoData(body: String): ByteArray = btdDateCheck(body, null)
    fun btdNoDataNow(body: String): ByteArray = btdDateCheck(body, LocalDate.now())
    fun btdNoDataPinned(body: String): ByteArray = btdDateCheck(body, LocalDate.parse("2024-06-05"))

    fun btuInit(body: String): ByteArray {
        XmlDestructor.parse(body, "ebicsRequest") {
            one("header") {
                one("static").one("OrderDetails") {
                    val adminOrder = one("AdminOrderType").text()
                    assertEquals(adminOrder, "BTU")
                }
                val phase = one("mutable").one("TransactionPhase").text()
                assertEquals(phase, "Initialisation")
            }
        }
        transactionId = randEbicsId()
        orderId = randEbicsId()
        val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
            attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
            el("header") {
                attr("authenticate", "true")
                el("static") {
                    el("TransactionID", transactionId!!)
                }
                el("mutable") {
                    el("TransactionPhase", "Initialisation")
                    el("OrderID", orderId!!)
                    el("ReturnCode", "000000")
                    el("ReportText", "[EBICS_OK] OK")
                }
            }
            el("AuthSignature")
            el("body") {
                el("ReturnCode") {
                    attr("authenticate", "true")
                    text("000000")
                }
            }
        }
        return signedResponse(doc)
    }

    fun btuPayload(body: String): ByteArray {
        lateinit var segment: String
        XmlDestructor.parse(body, "ebicsRequest") {
            one("header") {
                one("static") {
                    val txid = one("TransactionID").text()
                    assertEquals(txid, transactionId)
                }
                one("mutable") {
                    val phase = one("TransactionPhase").text()
                    assertEquals(phase, "Transfer")
                    segment = one("SegmentNumber").text()
                }
            }
        }
        val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") {
            attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005")
            el("header") {
                attr("authenticate", "true")
                el("static") {
                    el("TransactionID", transactionId!!)
                }
                el("mutable") {
                    el("TransactionPhase", "Transfer")
                    el("SegmentNumber", segment)
                    el("OrderID", orderId!!)
                    el("ReturnCode", "000000")
                    el("ReportText", "[EBICS_OK] OK")
                }
            }
            el("AuthSignature")
            el("body") {
                el("ReturnCode") {
                    attr("authenticate", "true")
                    text("000000")
                }
            }
        }
        return signedResponse(doc)
    }

    fun badRequest(body: String): ByteArray {
        throw BadRequest
    }

    fun initializeTx(body: String): ByteArray = ebicsResponsePayload(ByteArray(0), false)

    fun failure(body: String): ByteArray {
        throw Exception("Not reachable")
    }
}