import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.Locale;
import java.util.Set;
import org.bouncycastle.crypto.fpe.FPEFF1Engine;
import org.bouncycastle.crypto.params.FPEParameters;
import org.bouncycastle.crypto.params.KeyParameter;

/** Executable article examples. The public fixture key is NEVER a production key. */
public final class VerifyCodes {
    private static final char[] ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
    private static final SecureRandom RNG = new SecureRandom();
    private static final long N = RedeemCodeFf1.DOMAIN_SIZE;

    public static String randomCode() {
        char[] chars = new char[6];
        for (int i = 0; i < chars.length; i++) chars[i] = ALPHABET[RNG.nextInt(ALPHABET.length)];
        return new String(chars);
    }

    public static String base36(long id) {
        if (id < 0 || id >= N) throw new IllegalArgumentException("id outside domain");
        return String.format("%6s", Long.toString(id, 36)).replace(' ', '0').toUpperCase(Locale.ROOT);
    }

    // Shuffle example, NOT a credential-security boundary.
    public static String affineCode(long id) {
        if (id < 0 || id >= N) throw new IllegalArgumentException("id outside domain");
        long value = (id * 1_000_000_007L + 123_456_789L) % N;
        return base36(value); // Fixed constants fit in long over the checked domain.
    }

    private static void check(boolean condition, String message) {
        if (!condition) throw new AssertionError(message);
    }
    private static void rejected(Runnable call) {
        try { call.run(); } catch (IllegalArgumentException expected) { return; }
        throw new AssertionError("expected rejection");
    }

    public static void main(String[] args) {
        byte[] key = HexFormat.of().parseHex("2b7e151628aed2a6abf7158809cf4f3c");
        // NIST FF1 samples, Sample #1: radix 10, empty tweak, AES-128.
        // https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/FF1samples.pdf
        byte[] plain = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        byte[] expected = {2, 4, 3, 3, 4, 7, 7, 4, 8, 4};
        byte[] encrypted = new byte[plain.length];
        byte[] decrypted = new byte[plain.length];
        FPEFF1Engine engine = new FPEFF1Engine();
        engine.init(true, new FPEParameters(new KeyParameter(key), 10, new byte[0]));
        engine.processBlock(plain, 0, plain.length, encrypted, 0);
        check(Arrays.equals(expected, encrypted), "NIST encryption sample #1");
        engine.init(false, new FPEParameters(new KeyParameter(key), 10, new byte[0]));
        engine.processBlock(encrypted, 0, encrypted.length, decrypted, 0);
        check(Arrays.equals(plain, decrypted), "NIST decryption sample #1");

        RedeemCodeFf1 codec = new RedeemCodeFf1(key, "article-fixture-v1");
        for (long id : new long[] {0, 1, N - 2, N - 1})
            check(codec.decode(codec.encode(id)) == id, "boundary round trip");
        rejected(() -> codec.encode(-1));
        rejected(() -> codec.encode(N));
        rejected(() -> base36(-1));
        rejected(() -> affineCode(N));
        for (String invalid : new String[] {null, "abc123", "ＡBC123", "١23ABC", "ABC12", "ABC1234"})
            rejected(() -> codec.decode(invalid));
        Set<String> seen = new HashSet<>();
        for (long id = 0; id < 100_000; id++) {
            String code = codec.encode(id);
            check(code.matches("[0-9A-Z]{6}"), "ASCII output");
            check(seen.add(code), "duplicate in fixed-domain sample");
            check(codec.decode(code) == id, "sample round trip");
        }
        check(codec.encode(42).equals(codec.encode(42)), "determinism");
        check(base36(35).equals("00000Z") && base36(36).equals("000010"), "Base36 boundaries");
        for (int i = 0; i < 100; i++) check(randomCode().matches("[0-9A-Z]{6}"), "random format");
        // Cross-key/tweak outputs need not differ for EVERY input. Test database overlaps separately.
        check(codec.decode("ZZZZZZ") >= 0, "decoding does not authenticate issuance");
        if (args.length == 1) System.out.println(codec.encode(Long.parseLong(args[0])));
        else System.out.println("PASS: NIST sample #1, boundaries, ASCII rejection, determinism, 100000 unique round trips");
    }
}
