package com.dangerousthings.aliro;

import javacard.framework.ISOException;
import javacard.framework.ISO7816;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
 * Tests for {@link StructuralCbor#decodeHeader} — RFC 8949 §3 header
 * decoding (major type + argument) used by the M2 DeviceResponse / IssuerAuth
 * walker.
 *
 * <p>Header byte layout (RFC 8949 §3):
 * <pre>
 *   bits 7..5: major type (0..7)
 *   bits 4..0: additional info
 *     0..23: immediate argument value
 *     24:    1-byte uint argument follows
 *     25:    2-byte uint argument follows (big-endian)
 *     26:    4-byte uint argument follows (big-endian)
 *     27:    8-byte uint argument follows (REJECTED — exceeds short range)
 *     28..30: reserved (REJECTED)
 *     31:    indefinite length (REJECTED — Aliro mdoc is canonical)
 * </pre>
 *
 * <p>Result encoding (matches {@link StructuralCbor#decodeHeader} javadoc):
 * <ul>
 *   <li>return value (short): high byte = major type, low byte = bytes consumed
 *   <li>argument: written big-endian into {@code argOut[argOff..argOff+4)}
 * </ul>
 */
class StructuralCborTest {

    /** Helper: decode at offset 0 and return major type. */
    private static short majorOf(short result) {
        return (short) ((result >> 8) & 0x07);
    }

    /** Helper: decode at offset 0 and return bytes consumed. */
    private static short consumedOf(short result) {
        return (short) (result & 0xFF);
    }

    /** Helper: read the 4-byte big-endian argument the codec wrote. */
    private static long argOf(byte[] argOut) {
        return ((long) (argOut[0] & 0xFF) << 24)
             | ((long) (argOut[1] & 0xFF) << 16)
             | ((long) (argOut[2] & 0xFF) <<  8)
             | ((long) (argOut[3] & 0xFF));
    }

    @Test
    void decodeHeader_uintImmediateZero() {
        byte[] buf = hex("00");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(0, majorOf(r), "major type 0 (uint)");
        assertEquals(1, consumedOf(r), "1 byte consumed");
        assertEquals(0L, argOf(arg), "argument value 0");
    }

    @Test
    void decodeHeader_uintImmediateMax() {
        // Additional info 23 is the largest immediate-argument value.
        byte[] buf = hex("17");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(0, majorOf(r));
        assertEquals(1, consumedOf(r));
        assertEquals(23L, argOf(arg));
    }

    @Test
    void decodeHeader_uintOneByteArgument() {
        // 0x18 = uint with 1-byte argument follow; argument = 0xff (255).
        byte[] buf = hex("18ff");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(0, majorOf(r));
        assertEquals(2, consumedOf(r));
        assertEquals(255L, argOf(arg));
    }

    @Test
    void decodeHeader_uintTwoByteArgument() {
        // 0x19 = uint with 2-byte argument; 0x0100 = 256.
        byte[] buf = hex("190100");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(0, majorOf(r));
        assertEquals(3, consumedOf(r));
        assertEquals(256L, argOf(arg));
    }

    @Test
    void decodeHeader_uintFourByteArgument() {
        // 0x1a = uint with 4-byte argument; 0x00010000 = 65536.
        byte[] buf = hex("1a00010000");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(0, majorOf(r));
        assertEquals(5, consumedOf(r));
        assertEquals(65536L, argOf(arg));
    }

    @Test
    void decodeHeader_bstrOneByteLen() {
        // 0x58 = bstr major type (2) + additional info 24 (1-byte len follows).
        // Length 200 won't overflow short.
        byte[] buf = hex("58c8");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(2, majorOf(r), "major type 2 (bstr)");
        assertEquals(2, consumedOf(r));
        assertEquals(200L, argOf(arg));
    }

    @Test
    void decodeHeader_tstrImmediate() {
        // 0x65 = tstr major type (3) + immediate length 5.
        byte[] buf = hex("65");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(3, majorOf(r), "major type 3 (tstr)");
        assertEquals(1, consumedOf(r));
        assertEquals(5L, argOf(arg));
    }

    @Test
    void decodeHeader_arrayImmediate() {
        // 0x83 = array major type (4) + immediate count 3.
        byte[] buf = hex("83");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(4, majorOf(r), "major type 4 (array)");
        assertEquals(1, consumedOf(r));
        assertEquals(3L, argOf(arg));
    }

    @Test
    void decodeHeader_mapImmediate() {
        // 0xa2 = map major type (5) + immediate count 2.
        byte[] buf = hex("a2");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(5, majorOf(r), "major type 5 (map)");
        assertEquals(1, consumedOf(r));
        assertEquals(2L, argOf(arg));
    }

    @Test
    void decodeHeader_tagImmediate() {
        // 0xc0 = tag major type (6) + immediate tag 0.
        byte[] buf = hex("c0");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0);
        assertEquals(6, majorOf(r), "major type 6 (tag)");
        assertEquals(1, consumedOf(r));
        assertEquals(0L, argOf(arg));
    }

    @Test
    void decodeHeader_atNonzeroOffset() {
        // Verify decodeHeader honours bufOff: pad with junk, decode at offset 3.
        byte[] buf = hex("deadbeef" + "190100");
        byte[] arg = new byte[4];
        short r = StructuralCbor.decodeHeader(buf, (short) 4, (short) (buf.length - 4), arg, (short) 0);
        assertEquals(0, majorOf(r));
        assertEquals(3, consumedOf(r));
        assertEquals(256L, argOf(arg));
    }

    @Test
    void decodeHeader_argOutAtNonzeroOffset() {
        // Verify argOff: write argument into arg[2..6) and leave arg[0..2) zero.
        byte[] buf = hex("190100");
        byte[] arg = new byte[8];
        short r = StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 2);
        assertEquals(0, majorOf(r));
        assertEquals(3, consumedOf(r));
        assertEquals(0, arg[0]);
        assertEquals(0, arg[1]);
        // Argument bytes land at arg[2..6).
        long argVal = ((long) (arg[2] & 0xFF) << 24)
                    | ((long) (arg[3] & 0xFF) << 16)
                    | ((long) (arg[4] & 0xFF) <<  8)
                    | ((long) (arg[5] & 0xFF));
        assertEquals(256L, argVal);
    }

    @Test
    void decodeHeader_indefiniteLengthRejected() {
        // 0x1f = uint major type + additional info 31 (indefinite length).
        // We refuse indefinite-length in Aliro mdoc — canonical CBOR only.
        byte[] buf = hex("1f");
        byte[] arg = new byte[4];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF,
            "indefinite-length must throw SW_DATA_INVALID");
    }

    @Test
    void decodeHeader_eightByteArgumentRejected() {
        // 0x1b = uint with 8-byte argument follow. Out of short-buffer scope.
        byte[] buf = hex("1b00000000000000ff");
        byte[] arg = new byte[4];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
    }

    @Test
    void decodeHeader_majorTypeSevenRejected() {
        // 0xf5 = major type 7 (CBOR true). We don't handle floats/null/bool.
        byte[] buf = hex("f5");
        byte[] arg = new byte[4];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
    }

    @Test
    void decodeHeader_reservedAdditionalInfoRejected() {
        // 0x1c = additional info 28 (reserved by RFC 8949).
        byte[] buf = hex("1c");
        byte[] arg = new byte[4];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
    }

    @Test
    void decodeHeader_truncatedArgumentRejected() {
        // 0x19 says "2-byte argument follows" but only 1 byte is present.
        byte[] buf = hex("1901");
        byte[] arg = new byte[4];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
    }

    @Test
    void decodeHeader_emptyBufferRejected() {
        byte[] buf = new byte[0];
        byte[] arg = new byte[4];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.decodeHeader(buf, (short) 0, (short) buf.length, arg, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
    }

    // ---------------------------------------------------------------------
    // elementSpan tests (M2B.2) — span of a complete CBOR element including
    // nested children, used by M2B.3 / M2D.1 to walk IssuerSigned / docRequests.
    // ---------------------------------------------------------------------

    /** Shared 4B scratch for elementSpan's decodeHeader calls. */
    private final byte[] scratch = new byte[4];

    @Test
    void elementSpan_uintImmediate() {
        // 0x17 = uint 23, immediate argument. One header byte, no payload.
        byte[] buf = hex("17");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(1, span);
    }

    @Test
    void elementSpan_uintOneByteArgument() {
        // 0x18 0x64 = uint 100. Header byte + 1B argument, no payload.
        byte[] buf = hex("1864");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(2, span);
    }

    @Test
    void elementSpan_bstrEmpty() {
        // 0x40 = bstr len 0. One header byte, zero payload.
        byte[] buf = hex("40");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(1, span);
    }

    @Test
    void elementSpan_bstrLen3() {
        // 0x43 0x01 0x02 0x03 = bstr h'010203'. Header + 3 payload bytes.
        byte[] buf = hex("43010203");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(4, span);
    }

    @Test
    void elementSpan_tstrHello() {
        // 0x65 + "hello" = tstr "hello". Header + 5 payload bytes.
        byte[] buf = hex("6568656c6c6f");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(6, span);
    }

    @Test
    void elementSpan_arrayOfThreeUints() {
        // 0x83 0x01 0x02 0x03 = [1, 2, 3]. Header + 3 single-byte children.
        byte[] buf = hex("83010203");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(4, span);
    }

    @Test
    void elementSpan_nestedArray() {
        // 0x82 0x82 0x01 0x02 0x03 = [[1, 2], 3]. Exercises recursion.
        byte[] buf = hex("8282010203");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(5, span);
    }

    @Test
    void elementSpan_mapTwoUintPairs() {
        // 0xa2 0x01 0x02 0x03 0x04 = {1: 2, 3: 4}. Header + 4 single-byte items.
        byte[] buf = hex("a201020304");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(5, span);
    }

    @Test
    void elementSpan_mapWithBstrUintPairs() {
        // 0xa2 0x43 010203 0x1864 0x41 ff 0x17
        //   = { h'010203': 100, h'ff': 23 }
        // Header(1) + bstr(4) + uint(2) + bstr(2) + uint(1) = 10.
        byte[] buf = hex("a2 43 010203 1864 41 ff 17");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(10, span);
    }

    @Test
    void elementSpan_tagUint() {
        // 0xc1 0x01 = tag(1, 1). Tag header + tagged element span.
        byte[] buf = hex("c101");
        short span = StructuralCbor.elementSpan(
            buf, (short) 0, (short) buf.length, scratch, (short) 0);
        assertEquals(2, span);
    }

    @Test
    void elementSpan_honoursBufOff() {
        // Pad with 4 junk bytes, then a 4-byte array [1,2,3]; span starts at offset 4.
        byte[] buf = hex("deadbeef" + "83010203");
        short span = StructuralCbor.elementSpan(
            buf, (short) 4, (short) (buf.length - 4), scratch, (short) 0);
        assertEquals(4, span);
    }

    // ---------------------------------------------------------------------
    // Canonical encoder tests (M2B.4) — uint / bstr-header / tstr-header
    // writers used by M2D.2 (DeviceResponseBuilder).
    //
    // Canonical CBOR (RFC 8949 §4.2.1): shortest argument size that fits the
    // value. Boundary at each size step (0..23 immediate, 24..0xff one-byte,
    // 0x100..0xffff two-byte, 0x10000..0xffffffff four-byte).
    // ---------------------------------------------------------------------

    @Test
    void encodeUint_zeroImmediate() {
        // 0 fits in the immediate range — single header byte 0x00.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeUint(0, out, (short) 0);
        assertEquals(1, n);
        assertArrayPrefix(out, "00", n);
    }

    @Test
    void encodeUint_twentyThreeImmediate() {
        // 23 is the largest immediate value — header byte 0x17.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeUint(23, out, (short) 0);
        assertEquals(1, n);
        assertArrayPrefix(out, "17", n);
    }

    @Test
    void encodeUint_twentyFourOneByteBoundary() {
        // 24 is the lower boundary of the 1-byte argument form — 0x18 0x18.
        // Canonical rule forbids encoding 24 as 0x19 0x00 0x18 etc.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeUint(24, out, (short) 0);
        assertEquals(2, n);
        assertArrayPrefix(out, "1818", n);
    }

    @Test
    void encodeUint_twoByteBoundary() {
        // 0x100 = 256 is the lower boundary of the 2-byte argument form.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeUint(0x100, out, (short) 0);
        assertEquals(3, n);
        assertArrayPrefix(out, "190100", n);
    }

    @Test
    void encodeUint_fourByteBoundary() {
        // 0x10000 = 65536 is the lower boundary of the 4-byte argument form.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeUint(0x10000, out, (short) 0);
        assertEquals(5, n);
        assertArrayPrefix(out, "1a00010000", n);
    }

    @Test
    void encodeUint_negativeRejected() {
        // Aliro mdoc CBOR never uses negative integers (those are major type 1).
        byte[] out = new byte[8];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.encodeUint(-1, out, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
    }

    @Test
    void encodeBstrHeader_zero() {
        // bstr len 0 — header byte 0x40 (major type 2 | additional info 0).
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeBstrHeader((short) 0, out, (short) 0);
        assertEquals(1, n);
        assertArrayPrefix(out, "40", n);
    }

    @Test
    void encodeBstrHeader_twentyThreeImmediate() {
        // Largest immediate-length bstr header — 0x57.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeBstrHeader((short) 23, out, (short) 0);
        assertEquals(1, n);
        assertArrayPrefix(out, "57", n);
    }

    @Test
    void encodeBstrHeader_twentyFourOneByteBoundary() {
        // 24 forces into 1-byte argument form — 0x58 0x18.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeBstrHeader((short) 24, out, (short) 0);
        assertEquals(2, n);
        assertArrayPrefix(out, "5818", n);
    }

    @Test
    void encodeBstrHeader_twoByteBoundary() {
        // 0x100 = 256 forces into 2-byte argument form — 0x59 0x01 0x00.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeBstrHeader((short) 0x100, out, (short) 0);
        assertEquals(3, n);
        assertArrayPrefix(out, "590100", n);
    }

    @Test
    void encodeBstrHeader_negativeLenRejected() {
        byte[] out = new byte[8];
        ISOException ex = assertThrows(ISOException.class,
            () -> StructuralCbor.encodeBstrHeader((short) -1, out, (short) 0));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason() & 0xFFFF);
    }

    @Test
    void encodeTstrHeader_zero() {
        // tstr len 0 — header byte 0x60 (major type 3 | additional info 0).
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeTstrHeader((short) 0, out, (short) 0);
        assertEquals(1, n);
        assertArrayPrefix(out, "60", n);
    }

    @Test
    void encodeTstrHeader_twentyFourOneByteBoundary() {
        // 24 forces into 1-byte argument form — 0x78 0x18.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeTstrHeader((short) 24, out, (short) 0);
        assertEquals(2, n);
        assertArrayPrefix(out, "7818", n);
    }

    @Test
    void encodeTstrHeader_twoByteBoundary() {
        // 0x100 forces into 2-byte argument form — 0x79 0x01 0x00.
        byte[] out = new byte[8];
        short n = StructuralCbor.encodeTstrHeader((short) 0x100, out, (short) 0);
        assertEquals(3, n);
        assertArrayPrefix(out, "790100", n);
    }

    /** Helper: assert {@code out[0..n)} equals the hex-decoded expected bytes. */
    private static void assertArrayPrefix(byte[] out, String expectedHex, short n) {
        byte[] expected = hex(expectedHex);
        assertEquals(expected.length, n, "expected " + expectedHex + " (" + expected.length + " bytes)");
        for (int i = 0; i < expected.length; i++) {
            assertEquals(expected[i], out[i], "byte " + i + " mismatch");
        }
    }

    private static byte[] hex(String s) {
        s = s.replaceAll("\\s+", "");
        byte[] out = new byte[s.length() / 2];
        for (int i = 0; i < out.length; i++) {
            out[i] = (byte) Integer.parseInt(s.substring(i * 2, i * 2 + 2), 16);
        }
        return out;
    }
}
