package com.dangerousthings.aliro;

import java.util.ArrayList;
import java.util.List;

import javacard.framework.ISO7816;
import javacard.framework.ISOException;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Locks down the byte-for-byte field order written by
 * {@link CredentialStore#writeTo(SerializationSink)} / read back by
 * {@link CredentialStore#readFrom(SerializationSource)}.
 *
 * <p>This test exists today against an in-tree
 * {@link SerializationSink}/{@link SerializationSource} pair (see
 * {@code RecordingSink} below) so the field shape is testable before the
 * GP Amendment H {@code org.globalplatform.upgrade.Element} adapter is
 * wired in. When the AMD-H adapter lands, it just bridges {@code Element}
 * to those two interfaces and this test stays the regression spec.
 */
class CredentialStoreSerializationTest {

    private static final byte[] KNOWN_32_BYTES = makeRange(32, 0xA0);
    private static final byte[] KNOWN_64_BYTES_B = makeRange(64, 0x80);
    private static final byte[] KNOWN_64_BYTES_ISSUER = makeRange(64, 0x40);

    private static byte[] makeRange(int len, int start) {
        byte[] out = new byte[len];
        for (int i = 0; i < len; i++) {
            out[i] = (byte) (start + i);
        }
        return out;
    }

    @Test
    void credentialStoreRoundTripsViaSerializationContract() {
        // Deliberately distinguishable mix (T/T/F/T/F) so that a silent
        // reorder among the boolean writeTo lines would change which field
        // ends up true/false after readFrom — and fail the per-predicate
        // assertions below. If all five booleans were true the reorder
        // would be undetectable.
        CredentialStore src = CredentialStore.bootstrap();
        src.resetForTesting();
        src.setCredentialPrivKey(KNOWN_32_BYTES, (short) 0);
        // Intentionally do NOT setCredentialPubKey: leave credentialPubKeySet=false.
        src.setReaderPubKey(KNOWN_64_BYTES_B, (short) 0);
        // Exercise the new credentialIssuerPubKey slot (M2A.3, FIELD_VERSION=3).
        src.setCredentialIssuerPubKey(KNOWN_64_BYTES_ISSUER, (short) 0);
        // Intentionally do NOT finalizeAccessDocument: leave accessDocumentFinalized=false
        // and accessDocumentLen=0. With len=0, copyAccessDocument() returns an empty array.
        src.commit();

        RecordingSink buf = new RecordingSink();
        src.writeTo(buf);

        CredentialStore restored = CredentialStore.readFrom(buf.toSource());

        // Byte-array fields that were set must round-trip exactly.
        assertArrayEquals(KNOWN_32_BYTES, restored.copyCredentialPrivKey());
        assertArrayEquals(KNOWN_64_BYTES_B, restored.copyReaderPubKey());

        // Issuer pubkey round-trips as 0x04 || x || y (uncompressed SEC1).
        byte[] expectedIssuerUncomp = new byte[65];
        expectedIssuerUncomp[0] = 0x04;
        System.arraycopy(KNOWN_64_BYTES_ISSUER, 0, expectedIssuerUncomp, 1, 64);
        byte[] gotIssuerUncomp = new byte[65];
        restored.copyCredentialIssuerPubKeyUncomp(gotIssuerUncomp, (short) 0);
        assertArrayEquals(expectedIssuerUncomp, gotIssuerUncomp);
        assertTrue(restored.hasCredentialIssuerPubKey());

        // The unset credential pubkey buffer should round-trip as all-zero —
        // resetForTesting cleared it and we never wrote to it.
        assertArrayEquals(new byte[CredentialStore.CRED_PUBK_LEN],
                          restored.copyCredentialPubKey());

        // accessDocumentLen=0, so copyAccessDocument() returns an empty array.
        assertArrayEquals(new byte[0], restored.copyAccessDocument());

        // Per-predicate assertions: any swap in the boolean section of
        // writeTo flips at least one of these.
        assertTrue(restored.isCommitted());
        assertTrue(restored.hasCredentialPrivKey());
        assertFalse(restored.hasCredentialPubKey());
        assertTrue(restored.hasReaderPubKey());
        assertFalse(restored.hasAccessDocument());
    }

    @Test
    void accessDocumentVerifiedRoundTripsThroughSerialize() {
        CredentialStore s1 = CredentialStore.bootstrap();
        s1.resetForTesting();
        s1.markAccessDocumentVerifiedForTesting();

        RecordingSink buf = new RecordingSink();
        s1.writeTo(buf);
        CredentialStore s2 = CredentialStore.readFrom(buf.toSource());

        assertTrue(s2.isAccessDocumentVerified(),
                   "verified flag must survive serialize/deserialize round-trip");
    }

    /**
     * Forges a payload with a bogus FIELD_VERSION byte and asserts that
     * {@link CredentialStore#readFrom} rejects it with
     * {@code ISOException(SW_DATA_INVALID)}. The version check is the very
     * first read in readFrom, so no further padding is required — the
     * replay source is never read past the version byte.
     */
    @Test
    void readFromRejectsWrongFieldVersion() {
        RecordingSink buf = new RecordingSink();
        buf.write((byte) 0x7F); // != FIELD_VERSION

        ISOException ex = assertThrows(ISOException.class,
            () -> CredentialStore.readFrom(buf.toSource()));
        assertEquals(ISO7816.SW_DATA_INVALID, ex.getReason());
    }

    /**
     * Test-only buffer that captures sink writes as a typed event log and
     * replays them as a {@link SerializationSource}. Mirrors AMD-H
     * {@code Element} semantics by making a defensive copy of every
     * {@code byte[]} handed to {@link #write(byte[])} — once written, the
     * stored data must not be affected by subsequent caller mutations.
     */
    private static final class RecordingSink implements SerializationSink {
        private final List<Object> events = new ArrayList<Object>();

        @Override
        public SerializationSink write(byte b) {
            events.add(Byte.valueOf(b));
            return this;
        }

        @Override
        public SerializationSink write(short s) {
            events.add(Short.valueOf(s));
            return this;
        }

        @Override
        public SerializationSink write(boolean z) {
            events.add(Boolean.valueOf(z));
            return this;
        }

        @Override
        public SerializationSink write(byte[] arr) {
            // Defensive copy: Element treats stored references as
            // immutable from the caller's perspective.
            byte[] copy = new byte[arr.length];
            System.arraycopy(arr, 0, copy, 0, arr.length);
            events.add(copy);
            return this;
        }

        SerializationSource toSource() {
            return new ReplaySource(events);
        }
    }

    private static final class ReplaySource implements SerializationSource {
        private final List<Object> events;
        private int idx;

        ReplaySource(List<Object> events) {
            this.events = events;
            this.idx = 0;
        }

        @Override
        public byte readByte() {
            return ((Byte) events.get(idx++)).byteValue();
        }

        @Override
        public short readShort() {
            return ((Short) events.get(idx++)).shortValue();
        }

        @Override
        public boolean readBoolean() {
            return ((Boolean) events.get(idx++)).booleanValue();
        }

        @Override
        public byte[] readByteArray() {
            return (byte[]) events.get(idx++);
        }
    }
}
