package com.korancrew.brizzi.hce

import org.junit.Assert.assertEquals
import org.junit.Test

class BrizziApduRouterTest {
    private val router = BrizziApduRouter(routingAidHex = "F0010203040506")

    @Test
    fun `select aid1 then read card identity`() {
        assertEquals("9100", handle(BrizziCommandCatalog.SELECT_AID_1_ISO))
        assertEquals(
            "425249601350013966565806081400349100",
            handle(BrizziCommandCatalog.GET_CARD_INFO_ISO),
        )
        assertEquals(
            "0608146161" + "00".repeat(27) + "9100",
            handle(BrizziCommandCatalog.GET_CARD_STATUS_ISO),
        )
    }

    @Test
    fun `debit is visible before commit and rolled back by abort`() {
        authenticateAid3()

        assertEquals("A08601009100", handle(BrizziCommandCatalog.GET_BALANCE_ISO))
        assertEquals("000000009100", handle(BrizziCommandCatalog.debitIso("A08601")))
        assertEquals("000000009100", handle(BrizziCommandCatalog.GET_BALANCE_ISO))
        assertEquals("9100", handle(BrizziCommandCatalog.ABORT_TRANSACTION_ISO))
        assertEquals("A08601009100", handle(BrizziCommandCatalog.GET_BALANCE_ISO))
    }

    @Test
    fun `credit persists after log and commit`() {
        authenticateAid3()

        assertEquals("40420F009100", handle(BrizziCommandCatalog.creditIso("A0BB0D")))
        assertEquals(
            "9100",
            handle("3B0100000020000000000019384600003130303030303030151117151112EFA0BB0DA0860140420F"),
        )
        assertEquals("9100", handle("3D03000000070000151117A0860100"))
        assertEquals("9100", handle(BrizziCommandCatalog.COMMIT_TRANSACTION_ISO))
        assertEquals("40420F009100", handle(BrizziCommandCatalog.GET_BALANCE_ISO))
        assertEquals("151117A08601009100", handle(BrizziCommandCatalog.GET_LAST_TXN_AND_AKUM_DEBET_ISO))
        assertEquals(1_000_000, router.snapshot().balance)
    }

    @Test
    fun `get log transaction returns known sample record`() {
        authenticateAid3()

        assertEquals(
            "00000019384600003130303030303030151117151112EFA0BB0DA0860140420F9100",
            handle(BrizziCommandCatalog.GET_LOG_TRANSACTION_ISO),
        )
    }

    @Test
    fun `reading transaction area before authentication is rejected`() {
        assertEquals("9100", handle(BrizziCommandCatalog.SELECT_AID_3_ISO))
        assertEquals("6985", handle(BrizziCommandCatalog.GET_BALANCE_ISO))
    }

    @Test
    fun `reset clears auth state to avoid transaction leakage`() {
        authenticateAid3()
        assertEquals("A08601009100", handle(BrizziCommandCatalog.GET_BALANCE_ISO))

        router.resetSession()
        assertEquals("9100", handle(BrizziCommandCatalog.SELECT_AID_3_ISO))
        assertEquals("6985", handle(BrizziCommandCatalog.GET_BALANCE_ISO))
    }

    @Test
    fun `malformed or unsupported commands are handled without crash`() {
        assertEquals("6A82", handle(byteArrayOf(0x5A.toByte())))
        assertEquals("6A82", handle(byteArrayOf(0x90.toByte())))
        assertEquals("6A82", handle(byteArrayOf(0x90.toByte(), 0xBD.toByte())))
        assertEquals("6A82", handle(byteArrayOf(0x90.toByte(), 0xDC.toByte(), 0x00.toByte(), 0x00.toByte())))
        assertEquals("6700", handle(byteArrayOf(0x00.toByte(), 0xA4.toByte(), 0x04.toByte())))

        val parsed = BrizziApduRouter.parseApduSummary("90BD000007BD0000000017000000")
        assertEquals("CLA=90 INS=BD P1=00 P2=00 Lc=07 data-len=9", parsed)
        assertEquals("6700", handle(byteArrayOf(0x00, 0xA4.toByte(), 0x04, 0x00)))
        assertEquals("6700", handle("00A40408075A".hexToBytes()))
        assertEquals("6700", handle("00A40400075A0000030300".hexToBytes()))
    }

    @Test
    fun `replay select variants fixture`() {
        replayFixture("fixtures/brizzi_reader_trace_select_variants.txt")
    }

    @Test
    fun `native and wrapped debit credit variants behave consistently`() {
        assertEquals("9100", handle(BrizziCommandCatalog.SELECT_AID_3_ISO))
        assertEquals("5B907A2C398EFC91AF", handle(BrizziCommandCatalog.REQUEST_KEY_CARD_00_ISO))
        assertEquals(
            "56192EA6E27939539100",
            handle(BrizziCommandCatalog.authenticateIso("09B2D1F3B54A9F9B0CA3F33CC0B5321D")),
        )

        handle(BrizziCommandCatalog.GET_BALANCE_NATIVE)
        val wrappedDebit = handle(BrizziCommandCatalog.debitIso("A08601"))
        val nativeDebit = handle(BrizziCommandCatalog.debitNative("A08601"))
        assertEquals(wrappedDebit, nativeDebit)
        assertEquals("9100", handle(BrizziCommandCatalog.ABORT_TRANSACTION_ISO))
        val wrappedCredit = handle(BrizziCommandCatalog.creditIso("A0BB0D"))
        assertEquals("9100", handle(BrizziCommandCatalog.ABORT_TRANSACTION_ISO))
        val nativeCredit = handle(BrizziCommandCatalog.creditNative("A0BB0D"))
        assertEquals(wrappedCredit, nativeCredit)

        assertEquals("9100", handle(BrizziCommandCatalog.COMMIT_TRANSACTION_ISO))
    }

    @Test
    fun `replay reader trace fixture`() {
        replayFixture("fixtures/brizzi_reader_trace.txt")
    }

    @Test
    fun `replay full topup trace fixture`() {
        replayFixture("fixtures/brizzi_reader_trace_full_topup.txt", routingAidHex = "5A00000303000000")
    }

    @Test
    fun `replay full deduct trace fixture`() {
        replayFixture("fixtures/brizzi_reader_trace_full_deduct.txt", routingAidHex = "5A00000303000000")
    }

    @Test
    fun `replay negative out-of-order fixture`() {
        replayFixture("fixtures/brizzi_reader_trace_negative_out_of_order.txt")
    }

    @Test
    fun `replay unauthenticated transaction fixture`() {
        replayFixture("fixtures/brizzi_reader_trace_unauthenticated_transactions.txt")
    }

    @Test
    fun `replay bad select fixture`() {
        replayFixture("fixtures/brizzi_reader_trace_bad_select_variants.txt")
    }

    @Test
    fun `iso select command by AID length field is accepted`() {
        val routingRouter = BrizziApduRouter(
            routingAidHex = "5A00000301000000,5A00000303000000",
        )
        assertEquals("9100", routingRouter.handle(BrizziCommandCatalog.SELECT_AID_1_NATIVE_ISO.hexToBytes()).toHex())
        assertEquals("9100", routingRouter.handle(BrizziCommandCatalog.SELECT_AID_3_NATIVE_ISO.hexToBytes()).toHex())
    }

    @Test
    fun `select by name APDU (00A4040C) is also accepted`() {
        val routingRouter = BrizziApduRouter(
            routingAidHex = "5A00000301000000,5A00000303000000",
        )
        assertEquals("9100", routingRouter.handle(BrizziCommandCatalog.SELECT_AID_1_BY_NAME_NATIVE_ISO.hexToBytes()).toHex())
        assertEquals("9100", routingRouter.handle(BrizziCommandCatalog.SELECT_AID_3_BY_NAME_NATIVE_ISO.hexToBytes()).toHex())
    }

    @Test
    fun `unknown routing aid select is still accepted with generic FCI`() {
        val routingRouter = BrizziApduRouter(routingAidHex = "5A00000301000000")
        assertEquals("9100", routingRouter.handle(BrizziCommandCatalog.SELECT_UNKNOWN_AID_NATIVE_ISO.hexToBytes()).toHex())
    }

    @Test
    fun `requesting continuation frame without queued records returns conditions not satisfied`() {
        assertEquals("6985", handle(byteArrayOf(0xAF.toByte())))
        assertEquals("6985", handle("90AF000000".hexToBytes()))
    }

    private fun authenticateAid3() {
        assertEquals("9100", handle(BrizziCommandCatalog.SELECT_AID_3_ISO))
        assertEquals("5B907A2C398EFC91AF", handle(BrizziCommandCatalog.REQUEST_KEY_CARD_00_ISO))
        assertEquals(
            "56192EA6E27939539100",
            handle(BrizziCommandCatalog.authenticateIso("09B2D1F3B54A9F9B0CA3F33CC0B5321D")),
        )
    }

    private fun handle(hex: String): String = router.handle(hex.hexToBytes()).toHex()
    private fun handle(bytes: ByteArray): String = router.handle(bytes).toHex()

    private fun replayFixture(path: String, routingAidHex: String = "5A00000301000000,5A00000303000000") {
        val resource = javaClass.classLoader?.getResourceAsStream(path)
            ?: throw IllegalArgumentException("Missing fixture file: $path")
        val lines = resource.bufferedReader().readLines()
        val traceRouter = BrizziApduRouter(routingAidHex = routingAidHex)
        for (line in lines) {
            val trimmed = line.trim()
            if (trimmed.isEmpty() || trimmed.startsWith("#")) continue
            val parts = trimmed.split("=").map { it.trim() }
            require(parts.size == 2) { "Invalid fixture line: $trimmed" }
            val request = parts[0]
            val expected = parts[1]
            assertEquals(expected, traceRouter.handle(request.hexToBytes()).toHex())
        }
    }

    private fun assertError(hex: String, expectedStatus: String, routingAidHex: String = "F0010203040506") {
        val routingRouter = BrizziApduRouter(routingAidHex = routingAidHex)
        assertEquals(expectedStatus, routingRouter.handle(hex.hexToBytes()).toHex())
    }

    @Test
    fun `transaction commands without authentication are rejected`() {
        assertEquals("9100", handle(BrizziCommandCatalog.SELECT_AID_3_ISO))
        assertEquals("6985", handle(BrizziCommandCatalog.GET_BALANCE_ISO))
        assertEquals("6985", handle(BrizziCommandCatalog.debitIso("A08601")))
        assertEquals("6985", handle(BrizziCommandCatalog.creditIso("A0BB0D")))
        assertEquals("6985", handle(BrizziCommandCatalog.GET_LAST_TXN_AND_AKUM_DEBET_ISO))
        assertEquals("6985", handle("0C000004A0BB0D00"))
        assertEquals("6985", handle("DC000004A0860100"))
        assertEquals("6985", handle("3B0100000020000000000019384600003130303030303030151117151112EFA0BB0DA0860140420F00"))
        assertEquals("6985", handle("3D0300000007000015111711A0860100"))
    }
}
