package opencrypto.test;


import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;

import java.util.Map.Entry;

import javax.smartcardio.CardException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;

import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.jce.spec.ECParameterSpec;

import java.io.FileOutputStream;
import java.io.IOException;
import java.security.Security;
import java.util.HashMap;
import java.util.concurrent.ThreadLocalRandom;
import javacard.framework.ISO7816;
import opencrypto.jcmathlib.OCUnitTests;
import opencrypto.jcmathlib.SecP256r1;

/**
 * 
 * @author Vasilios Mavroudis and Petr Svenda
 */
public class TestClient {
    public static boolean _FAIL_ON_ASSERT = false;
    public static int NUM_OP_REPEATS = 10;
    
    // Base length of test inputs (in bits) which corresponds to same length of ECC 
    // E.g., if you like to test 256b ECC, operations with 256b Bignat numbers are performed 
    // Important: applet is compiled with MAX_BIGNAT_SIZE constant which must be large enough to hold intermediate computations. 
    // MAX_BIGNAT_SIZE constant must be at least 2*(ECC_BASE_TEST_LENGTH / 8) + 1.
    public final static int BIGNAT_BASE_TEST_LENGTH = 256; 
    
    public static boolean _TEST_BN = false;
    public static boolean _TEST_INT = true;
    public static boolean _TEST_EC = false;
    
    public static boolean _MEASURE_PERF = false;
    public static boolean _MEASURE_PERF_ONLY_TARGET = false;
    


    static ArrayList<Entry<String, Long>> m_perfResults = new ArrayList<>();

    public static String format = "%-40s:%s%n\n-------------------------------------------------------------------------------\n";

    public static byte[] OPENCRYPTO_UNITTEST_APPLET_AID = {0x55, 0x6e, 0x69, 0x74, 0x54, 0x65, 0x73, 0x74, 0x73};
    public static byte[] APDU_CLEANUP = {OCUnitTests.CLA_OC_UT, OCUnitTests.INS_CLEANUP, (byte) 0x00, (byte) 0x00, (byte) 0x00};
    public static byte[] APDU_GET_PROFILE_LOCKS = {OCUnitTests.CLA_OC_UT, OCUnitTests.INS_GET_PROFILE_LOCKS, (byte) 0x00, (byte) 0x00, (byte) 0x00};


    public static void main(String[] args) throws Exception {
    	try {
            Integer targetReader = 0;
            if (args.length > 0) {
                targetReader = Integer.getInteger(args[0]);
            }

            PerfTests perfTests = new PerfTests();
            if (_MEASURE_PERF) {
                RunConfig runCfg = RunConfig.getConfig(_TEST_BN, _TEST_INT, _TEST_EC, NUM_OP_REPEATS, RunConfig.CARD_TYPE.PHYSICAL);
                runCfg.numRepeats = 1;
                runCfg.targetReaderIndex = targetReader;
                perfTests.RunPerformanceTests(runCfg);
            }
            else if (_MEASURE_PERF_ONLY_TARGET) {
                RunConfig runCfg = RunConfig.getConfig(_TEST_BN, _TEST_INT, _TEST_EC, NUM_OP_REPEATS, RunConfig.CARD_TYPE.PHYSICAL);
                runCfg.targetReaderIndex = targetReader;
                runCfg.bMeasureOnlyTargetOp = true;
                perfTests.RunPerformanceTests(runCfg);
            }
            else {
                RunConfig runCfg = RunConfig.getConfig(_TEST_BN, _TEST_INT, _TEST_EC, NUM_OP_REPEATS, RunConfig.CARD_TYPE.JCARDSIMLOCAL);
                runCfg.targetReaderIndex = targetReader;
                
                // First run debug operations on simulator and real card (if any)
/*              OpenCryptoFunctionalTests_debug(runCfg);
                runCfg.failedTestsList.clear();
                runCfg.testCardType = RunConfig.CARD_TYPE.PHYSICAL;
                OpenCryptoFunctionalTests_debug(runCfg);
                runCfg.failedTestsList.clear();
*/                
                // Run standard tests on simulator then real card (if any)
                //runCfg.testCardType = RunConfig.CARD_TYPE.JCARDSIMLOCAL;
                //OpenCryptoFunctionalTests(runCfg);
                //runCfg.failedTestsList.clear();
                runCfg.testCardType = RunConfig.CARD_TYPE.PHYSICAL;
                OpenCryptoFunctionalTests(runCfg);
                runCfg.failedTestsList.clear();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static boolean OpenCryptoFunctionalTests(RunConfig runCfg) throws Exception {
        try {
            CardManager cardMngr = new CardManager(true, OPENCRYPTO_UNITTEST_APPLET_AID);

            System.out.print("Connecting to card...");
            if (!cardMngr.Connect(runCfg)) {
                return false;
            }
            System.out.println(" Done.");

            System.out.println("\n--------------Unit Tests--------------");

            CommandAPDU cmd;
            ResponseAPDU response;
            String operationName = "";
            boolean bResult = false;

            m_perfResults.clear();
            String logFileName = String.format("OC_PERF_log_%d.csv", System.currentTimeMillis());
            FileOutputStream perfFile = new FileOutputStream(logFileName);

            cardMngr.transmit(new CommandAPDU(PerfTests.PERF_COMMAND_NONE));
            cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 

            // Obtain allocated bytes in RAM and EEPROM
            byte[] bogusArray = new byte[1]; // Bogus array with single zero byte - NXP J3H145G P60 fails when no data are provided
            cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_GET_ALLOCATOR_STATS, 0, 0, bogusArray);            
            response = cardMngr.transmit(cmd);
            byte[] data = response.getData();
            System.out.println(String.format("Data allocator: RAM = %d, EEPROM = %d", Util.getShort(data, (short) 0), Util.getShort(data, (short) 2)));
            // Print memory snapshots from allocation
            for (int offset = 4; offset < data.length; offset += 6) {
                System.out.println(String.format("Tag '%d': RAM = %d, EEPROM = %d", Util.getShort(data, offset), Util.getShort(data, (short) (offset + 2)), Util.getShort(data, (short) (offset + 4))));
            }

            if (runCfg.bTestBN) {
                operationName = "BigNatural Storage: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_STR, 0, 0, num.toByteArray());
                    performCommand(operationName, cardMngr, cmd, num, perfFile, runCfg.failedTestsList);
                }

                operationName = "BigNatural Addition: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH - 1);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH - 1);//Generate Int2
                    BigInteger result = num1.add(num2); 
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_ADD, num1.toByteArray().length, 0, Util.concat(num1.toByteArray(), num2.toByteArray()));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList);
                }

                operationName = "BigNatural Subtraction: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH - 1);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH - 1);//Generate Int2
                    BigInteger result = num1.subtract(num2); 
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_SUB, num1.toByteArray().length, 0, Util.concat(num1.toByteArray(), num2.toByteArray()));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList);
                }

                operationName = "BigNatural Multiplication: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int2
                    BigInteger result = num1.multiply(num2); 
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_MUL, num1.toByteArray().length, 0, Util.concat(num1.toByteArray(), num2.toByteArray()));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList);
                }

                operationName = "BigNatural Multiplication schoolbook: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int2
                    BigInteger result = num1.multiply(num2);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_MUL_SCHOOL, num1.toByteArray().length, 0, Util.concat(num1.toByteArray(), num2.toByteArray()));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList);
                }

                operationName = "BigNatural Exponentiation: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH / 4); //Generate Int1		
                    BigInteger num2 = BigInteger.valueOf(3); //Generate Int2
                    BigInteger result = num1.pow(3); 
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_EXP, num1.toByteArray().length, result.toByteArray().length, Util.concat(num1.toByteArray(), num2.toByteArray()));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList);
                }

                operationName = "BigNatural Modulo: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH - 1);//Generate Int2
                    BigInteger result = num1.mod(num2); 
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_MOD, (num1.toByteArray()).length, 0, Util.concat((num1.toByteArray()), (num2.toByteArray())));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList, true);
                }
/*                
                operationName = "BigNatural sqrt_FP: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(256);//Generate Int1
                    BigInteger resSqrt = tonelli_shanks(num1, new BigInteger(1, SecP256r1.p));
                    cmd = new CommandAPDU(Configuration.CLA_MPC, Configuration.INS_BN_SQRT, (num1.toByteArray()).length, 0,num1.toByteArray());
                    performCommand(operationName, cardMngr, cmd, resSqrt, perfFile, true);
                }                
/**/
                operationName = "BigNatural Addition (Modulo): ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int2
                    BigInteger num3 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH / 8);//Generate Int3
                    BigInteger result = (num1.add(num2)).mod(num3);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_ADD_MOD, (num1.toByteArray()).length, (num2.toByteArray()).length, Util.concat((num1.toByteArray()), (num2.toByteArray()), (num3.toByteArray())));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList, true);
                }

                operationName = "BigNatural Subtraction (Modulo): ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int2
                    BigInteger num3 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH / 8);//Generate Int3
                    BigInteger result = (num1.subtract(num2)).mod(num3);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_SUB_MOD, (num1.toByteArray()).length, (num2.toByteArray()).length, Util.concat((num1.toByteArray()), (num2.toByteArray()), (num3.toByteArray())));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList, true);
                }

                operationName = "BigNatural Multiplication (Modulo): ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int1
                    BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int2
                    BigInteger num3 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH / 8);//Generate Int3
                    BigInteger result = (num1.multiply(num2)).mod(num3);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_MUL_MOD, (num1.toByteArray()).length, (num2.toByteArray()).length, Util.concat((num1.toByteArray()), (num2.toByteArray()), (num3.toByteArray())));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList, true);
                }

                operationName = "BigNatural Exponentiation (Modulo): ";
                int power = 2;
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH); //Generate Int1 (base)
                    BigInteger num2 = BigInteger.valueOf(power); //Generate Int2 (exp)
                    BigInteger num3 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int3 (mod)
                    BigInteger result = (num1.modPow(num2, num3));
                    System.out.println(String.format("num1: %s", num1.toString(16)));
                    System.out.println(String.format("num2: %s", num2.toString(16)));
                    System.out.println(String.format("num3: %s", num3.toString(16)));

                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_EXP_MOD, Util.trimLeadingZeroes(num1.toByteArray()).length, Util.trimLeadingZeroes(num2.toByteArray()).length, Util.concat(Util.trimLeadingZeroes(num1.toByteArray()), Util.trimLeadingZeroes(num2.toByteArray()), Util.trimLeadingZeroes(num3.toByteArray())));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList, true);
                }
                
                operationName = "BigNatural Power2 (Modulo): ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH); //Generate Int1 (base)
                    BigInteger mod = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int3 (mod)
                    BigInteger result = (num1.modPow(BigInteger.valueOf(2), mod));

                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_POW2_MOD, Util.trimLeadingZeroes(num1.toByteArray()).length, Util.trimLeadingZeroes(mod.toByteArray()).length, Util.concat(Util.trimLeadingZeroes(num1.toByteArray()), Util.trimLeadingZeroes(mod.toByteArray())));
                    performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList, true);
                }

                operationName = "BigNatural Inversion (Modulo): ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH + BIGNAT_BASE_TEST_LENGTH / 2); //Generate base
                    BigInteger num2 = new BigInteger(1,SecP256r1.p);//Generate mod
                    BigInteger num3 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int3 (mod)
                    System.out.println(String.format("num1: %s", num1.toString(16)));
                    System.out.println(String.format("num2: %s", num2.toString(16)));
                    System.out.println(String.format("num3: %s", num3.toString(16)));
                    BigInteger result = num1.modInverse(num2).multiply(num1).mod(num3);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_INV_MOD, Util.trimLeadingZeroes(num1.toByteArray()).length, 0, Util.concat(Util.trimLeadingZeroes(num1.toByteArray()), Util.trimLeadingZeroes(num2.toByteArray())));
                    response = cardMngr.transmit(cmd);
                    if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
                        BigInteger respInt = new BigInteger(1, response.getData()).multiply(num1).mod(num3);
                        bResult = result.compareTo(respInt) == 0;

                        if (!bResult) {
                            System.out.println(String.format("Expected: %s", result.toString(16)));
                            System.out.println(String.format("Obtained: %s", respInt.toString(16)));
                        }
                    } else {
                        bResult = false;
                        System.out.println(String.format("fail (0x%x)", response.getSW()));
                    }
                    logResponse(operationName, bResult, cardMngr.m_lastTransmitTime, perfFile, runCfg.failedTestsList);
                    if (!bResult) {
                        String failedAPDU = formatFailedAPDUCmd(cmd);
                        runCfg.failedTestsList.add(failedAPDU);
                    }

                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }
            }

            if (runCfg.bTestINT) {
                operationName = "Integer Storage: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    int num = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_INT_STR, 0, 0, Util.IntToBytes(num));
                    response = cardMngr.transmit(cmd);
                    verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, num, perfFile, runCfg.failedTestsList);
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }

                operationName = "Integer Addition: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    int num_add_1 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    int num_add_2 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    //num_add_1 = Integer.MAX_VALUE;
                    //num_add_2 = Integer.MAX_VALUE;
                    int num_add = num_add_1 + num_add_2;

                    //System.out.println("op1   : " + bytesToHex(Util.IntToBytes(num_add_1)));
                    //System.out.println("op2   : " + bytesToHex(Util.IntToBytes(num_add_2)));
                    //System.out.println("result: " + bytesToHex(Util.IntToBytes(num_add)));
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_INT_ADD, Util.IntToBytes(num_add_1).length, 0, Util.concat(Util.IntToBytes(num_add_1), Util.IntToBytes(num_add_2)));
                    response = cardMngr.transmit(cmd);
                    verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, num_add, perfFile, runCfg.failedTestsList);
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }

                operationName = "Integer Subtraction: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    int num_sub_1 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    int num_sub_2 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    //num_sub_1 = Integer.MAX_VALUE-1;
                    //num_sub_2 = Integer.MAX_VALUE;
                    int num_sub = num_sub_1 - num_sub_2;
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_INT_SUB, Util.IntToBytes(num_sub_1).length, 0, Util.concat(Util.IntToBytes(num_sub_1), Util.IntToBytes(num_sub_2)));
                    response = cardMngr.transmit(cmd);
                    verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, num_sub, perfFile, runCfg.failedTestsList);
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }

                operationName = "Integer Multiplication: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    int num_mul_1 = ThreadLocalRandom.current().nextInt((int) (Math.sqrt(Integer.MIN_VALUE)), (int) (Math.sqrt(Integer.MAX_VALUE)));
                    int num_mul_2 = ThreadLocalRandom.current().nextInt((int) (Math.sqrt(Integer.MIN_VALUE)), (int) (Math.sqrt(Integer.MAX_VALUE)));

                    //Avoid overflows
                    int num_mul = 0; //(java int may overflow!!)
                    while (num_mul == 0) {
                        try {
                            num_mul = Math.multiplyExact(num_mul_1, num_mul_2);
                            //num_mul = 6;
                        } catch (Exception e) { // or your specific exception
                        }

                    }
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_INT_MUL, Util.IntToBytes(num_mul_1).length, 0, Util.concat(Util.IntToBytes(num_mul_1), Util.IntToBytes(num_mul_2)));
                    response = cardMngr.transmit(cmd);
                    verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, num_mul, perfFile, runCfg.failedTestsList);
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }


                operationName = "Integer Division: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    int num_div_1 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    int num_div_2 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    int num_div = num_div_1 / num_div_2;
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_INT_DIV, Util.IntToBytes(num_div_1).length, 0, Util.concat(Util.IntToBytes(num_div_1), Util.IntToBytes(num_div_2)));
                    response = cardMngr.transmit(cmd);
                    verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, num_div, perfFile, runCfg.failedTestsList);
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }

                /*
                //14^8 is quite/very close to the max integer value
                System.out.print("Integer Exponentiation: ");
                int base = ThreadLocalRandom.current().nextInt(-14, 14);
                int exp = ThreadLocalRandom.current().nextInt(-8, 8);
                int num_exp = Math.pow(base, exp);
                cmd = new CommandAPDU(Configuration.CLA_MPC, Configuration.INS_INT_DIV, Util.IntToBytes(num_div_1).length, 0, Util.concat(Util.IntToBytes(num_div_1), Util.IntToBytes(num_div_2)));
                response = transmit(cmd);
                System.out.println(BytesToInt(response.getData())==num_div);			}
                */

                operationName = "Integer Modulo: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    int num_mod_1 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    int num_mod_2 = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE);
                    int num_mod = num_mod_1 % num_mod_2;
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_INT_MOD, Util.IntToBytes(num_mod_1).length, 0, Util.concat(Util.IntToBytes(num_mod_1), Util.IntToBytes(num_mod_2)));
                    response = cardMngr.transmit(cmd);
                    verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, num_mod, perfFile, runCfg.failedTestsList);
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
               }
            }

            if (runCfg.bTestEC) {
                operationName = "EC Point Generation: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_EC_GEN, 0, 0, bogusArray);
                    response = cardMngr.transmit(cmd);
                    PerfTests.writePerfLog(operationName, response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff), cardMngr.m_lastTransmitTime, m_perfResults, perfFile);
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }

                operationName = "EC Point Add: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    ECPoint pnt_1 = Util.randECPoint();
                    ECPoint pnt_2 = Util.randECPoint();
                    ECPoint pnt_sum = pnt_1.add(pnt_2);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_EC_ADD, 0, 0, Util.concat(pnt_1.getEncoded(false), pnt_2.getEncoded(false)));
                    response = cardMngr.transmit(cmd);

                    if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
                        bResult = Arrays.equals(pnt_sum.getEncoded(false), response.getData());

                        if (!bResult) {
                            System.out.println(String.format("Expected: %s", Util.toHex(pnt_sum.getEncoded(false))));
                            System.out.println(String.format("Obtained: %s", Util.toHex(response.getData())));
                        }
                    } else {
                        bResult = false;
                        System.out.println(String.format("fail (0x%x)", response.getSW()));
                    }

                    logResponse(operationName, bResult, cardMngr.m_lastTransmitTime, perfFile, runCfg.failedTestsList);
                    if (!bResult) {
                        String failedAPDU = formatFailedAPDUCmd(cmd);
                        runCfg.failedTestsList.add(failedAPDU);
                    }
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }

                operationName = "ECPoint Negation: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    ECPoint pnt = Util.randECPoint();
                    ECPoint negPnt = pnt.negate();
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_EC_NEG, pnt.getEncoded(false).length, 0, pnt.getEncoded(false));
                    response = cardMngr.transmit(cmd);
                    bResult = verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, negPnt, perfFile, runCfg.failedTestsList);
                    if (!bResult) {
                        String failedAPDU = formatFailedAPDUCmd(cmd);
                        runCfg.failedTestsList.add(failedAPDU);
                    }
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP));
                }

                operationName = "EC scalar_Point Multiplication: ";
                Security.addProvider(new BouncyCastleProvider());
                ECParameterSpec ecSpec2 = ECNamedCurveTable.getParameterSpec("secp256r1");
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    // NOTE: we will keep G same (standard), just keep changing the scalar. 
                    // If you want to test different G, INS_EC_SETCURVE_G must be called before same as for EC Point Double.  
                    ECPoint base = ecSpec2.getG();
                    BigInteger priv1 = Util.randomBigNat(256);
                    ECPoint pub = base.multiply(priv1);
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_EC_MUL, priv1.toByteArray().length, 0, Util.concat(priv1.toByteArray(), base.getEncoded(false)));
                    response = cardMngr.transmit(cmd);
                    if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
                        bResult = Arrays.equals(pub.getEncoded(false), response.getData());
                        if (!bResult) {
                            System.out.println(String.format("Expected: %s", Util.toHex(pub.getEncoded(false))));
                            System.out.println(String.format("Obtained: %s", Util.toHex(response.getData())));
                        }
                    } else {
                        bResult = false;
                        System.out.println(String.format("fail (0x%x)", response.getSW()));
                    }
                    logResponse(operationName, bResult, cardMngr.m_lastTransmitTime, perfFile, runCfg.failedTestsList);
                    if (!bResult) {
                        String failedAPDU = formatFailedAPDUCmd(cmd);
                        runCfg.failedTestsList.add(failedAPDU);
                    }
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }

                operationName = "EC isEqual: ";
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    ECPoint pnt_1 = Util.randECPoint();
                    ECPoint pnt_2 = Util.randECPoint();
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_EC_COMPARE, pnt_1.getEncoded(false).length, pnt_2.getEncoded(false).length, Util.concat(pnt_1.getEncoded(false), pnt_2.getEncoded(false)));
                    response = cardMngr.transmit(cmd);
                    bResult = verifyAndLogResponse(operationName, response, cardMngr.m_lastTransmitTime, 0, perfFile, runCfg.failedTestsList);
                    if (!bResult) {
                        String failedAPDU = formatFailedAPDUCmd(cmd);
                        runCfg.failedTestsList.add(failedAPDU);
                    }
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP));
                }
                
                operationName = "EC Point Double: ";
                Security.addProvider(new BouncyCastleProvider());
                //ECParameterSpec ecSpec2 = ECNamedCurveTable.getParameterSpec("secp256r1");
                boolean bSetRandomG = true;
                boolean bReallocateWholeCurve = false;
                for (int repeat = 0; repeat < runCfg.numRepeats; repeat++) {
                    System.out.println(String.format("%s (%d)", operationName, repeat));
                    ECPoint pnt;
                    if (bSetRandomG) {
                        pnt = Util.randECPoint();
                        System.out.println(String.format("Random ECPoint == G: %s", Util.toHex(pnt.getEncoded(false))));
                        // Set modified parameter G of the curve (our random point)    
                        cardMngr.transmit(new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_EC_SETCURVE_G, pnt.getEncoded(false).length, bReallocateWholeCurve ? (byte) 1 : (byte) 0, pnt.getEncoded(false)));
                    } else {
                        pnt = ecSpec2.getG();
                    }

                    ECPoint doubled = pnt.add(pnt); // expected results
                    // Perform EC double operation    
                    cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_EC_DBL, 0, 0, pnt.getEncoded(false));
                    try {
                        response = cardMngr.transmit(cmd);
                        if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
                            bResult = Arrays.equals(doubled.getEncoded(false), response.getData());
                            if (!bResult) {
                                System.out.println(String.format("Expected: %s", Util.toHex(doubled.getEncoded(false))));
                                System.out.println(String.format("Obtained: %s", Util.toHex(response.getData())));
                            }
                        } else {
                            bResult = false;
                            System.out.println(String.format("fail (0x%x)", response.getSW()));
                        }
                    } catch (Exception e) {
                        bResult = false;
                        e.printStackTrace();
                    }
                    logResponse(operationName, bResult, cardMngr.m_lastTransmitTime, perfFile, runCfg.failedTestsList);
                    if (!bResult) {
                        String failedAPDU = formatFailedAPDUCmd(cmd);
                        runCfg.failedTestsList.add(failedAPDU);
                    }
                    cardMngr.transmit(new CommandAPDU(APDU_CLEANUP)); 
                }                
            }

            System.out.println("\n--------------Unit Tests--------------\n\n");

            cardMngr.transmit(new CommandAPDU(APDU_GET_PROFILE_LOCKS));
            
            
            System.out.print("Disconnecting from card...");
            cardMngr.Disconnect(true);
            System.out.println(" Done.");

            if (runCfg.failedTestsList.size() > 0) {
                System.out.println("#########################");
                System.out.println("!!! SOME TESTS FAILED !!!");
                System.out.println("#########################");
                for (String test : runCfg.failedTestsList) {
                    System.out.println(test);
                }

                printFailedTestStats(runCfg);

            }
            else {
                System.out.println("##########################");
                System.out.println("ALL TESTS PASSED CORRECTLY");
                System.out.println("##########################");            
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }
    
    
    static void OpenCryptoFunctionalTests_debug(RunConfig runCfg) throws Exception {
        try {
            CardManager cardMngr = new CardManager(true, OPENCRYPTO_UNITTEST_APPLET_AID);
            System.out.print("Connecting to card...");
            cardMngr.Connect(runCfg);
            System.out.println(" Done.");

            m_perfResults.clear();

            cardMngr.transmit(new CommandAPDU(PerfTests.PERF_COMMAND_NONE));
            cardMngr.transmit(new CommandAPDU(APDU_CLEANUP));

            String logFileName = String.format("OC_PERF_log_%d.csv", System.currentTimeMillis());
            FileOutputStream perfFile = new FileOutputStream(logFileName);

            CommandAPDU cmd;
            ResponseAPDU response;
            String operationName = "";
            boolean bResult = false;
            

            System.out.println("\n-------------- Problematic inputs tests --------------");
/*
            // TODO: add code for debugging
            operationName = "BigNatural Multiplication schoolbook: ";
            for (int repeat = 0; repeat < 10; repeat++) {
                System.out.println(String.format("%s (%d)", operationName, repeat));
                BigInteger num1 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int1
                BigInteger num2 = Util.randomBigNat(BIGNAT_BASE_TEST_LENGTH);//Generate Int2
                BigInteger result = num1.multiply(num2);
                cmd = new CommandAPDU(OCUnitTests.CLA_OC_UT, OCUnitTests.INS_BN_MUL, num1.toByteArray().length, 0, Util.concat(num1.toByteArray(), num2.toByteArray()));
                performCommand(operationName, cardMngr, cmd, result, perfFile, runCfg.failedTestsList);
            }
            int a=0;
/*            
            operationName = "EC Point Add: ";
            for (int repeat = 0; repeat < 1000000; repeat++) {
                System.out.println(String.format("%s (%d)", operationName, repeat));
                ECPoint pnt_1 = Util.randECPoint();
                ECPoint pnt_2 = Util.randECPoint();
                ECPoint pnt_sum = pnt_1.add(pnt_2);
                cmd = new CommandAPDU(Consts.CLA_OC_UT, Consts.INS_EC_ADD, 0, 0, Util.concat(pnt_1.getEncoded(false), pnt_2.getEncoded(false)));
                response = cardMngr.transmit(cmd);

                if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
                    byte[] x = pnt_sum.getXCoord().getEncoded();
                    byte[] y = pnt_sum.getYCoord().getEncoded();
                    x = pnt_sum.getEncoded(false);
                    bResult = Arrays.equals(pnt_sum.getEncoded(false), response.getData());

                    if (!bResult) {
                        System.out.println(String.format("Expected: %s", Util.toHex(pnt_sum.getEncoded(false))));
                        System.out.println(String.format("Obtained: %s", Util.toHex(response.getData())));
                    }
                } else {
                    bResult = false;
                    System.out.println(String.format("fail (0x%x)", response.getSW()));
                }    
            }
*/        
            
            System.out.println("\n-------------- Problematic inputs tests --------------\n\n");

            System.out.print("Disconnecting from card...");
            cardMngr.Disconnect(true);
            System.out.println(" Done.");

            if (runCfg.failedTestsList.size() > 0) {
                System.out.println("#########################");
                System.out.println("!!! SOME TESTS FAILED !!!");
                System.out.println("#########################");
                for (String test : runCfg.failedTestsList) {
                    System.out.println(test);
                }

                printFailedTestStats(runCfg);
            } else {
                System.out.println("##########################");
                System.out.println("ALL TESTS PASSED CORRECTLY");
                System.out.println("##########################");
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }    
    
    static HashMap<String, Integer> printFailedTestStats(RunConfig runCfg) {
        // Parse failed tests, output statistics
        HashMap<String, Integer> numFailsTest = new HashMap<>();
        for (String test : runCfg.failedTestsList) {
            if (!test.contains("failedCommand")) { // ignore output of apdu
                if (numFailsTest.containsKey(test)) {
                    Integer count = numFailsTest.get(test);
                    count++;
                    numFailsTest.replace(test, count);
                } else {
                    numFailsTest.put(test, 1);
                }
            }
        }

        System.out.println("\n***FAILED TESTS STATS***");
        for (String test : numFailsTest.keySet()) {
            System.out.println(String.format("%40s: %d / %d", test, numFailsTest.get(test), runCfg.numRepeats));
        }      
        System.out.println("************************\n");
        
        return numFailsTest;
    }

    
    interface CmdResultsComparator {
        public boolean compare(ResponseAPDU response);  
    }
    
    class ArrayMatchComparator implements CmdResultsComparator {
        ECPoint m_expected = null;
        ArrayMatchComparator(ECPoint expected) {
            this.m_expected = expected;
        }
        public boolean compare(ResponseAPDU response) {
            boolean bResult = false;
            if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
                bResult = Arrays.equals(m_expected.getEncoded(), response.getData());
            } else {
                bResult = false;
                System.out.println(String.format("fail (0x%x)", response.getSW()));
            }
            return bResult;
        }
    }
    
    static String formatFailedAPDUCmd(CommandAPDU failedCommand) {
        // If command corectness verification failed, then output for easy resend during debugging later
        byte[] failedcmd = failedCommand.getBytes();
        String failedAPDU = String.format("\tcardMngr.transmit(new CommandAPDU(hexStringToByteArray(\"%s\")));", Util.bytesToHex(failedcmd));
        failedAPDU += "\n\tstatic byte[] failedCommand = {";
        for (int i = 0; i < failedcmd.length; i++) {
            failedAPDU += String.format("(byte) 0x%x", failedcmd[i]);
            if (i != failedcmd.length - 1) {
                failedAPDU += ", ";
            }
        }
        failedAPDU += "};";
        failedAPDU += "\n\t";
        
        return failedAPDU;
    }
    static void performCommand(String operationName, CardManager cardManager, CommandAPDU command, BigInteger expectedResult, FileOutputStream perfFile, ArrayList<String> failedTestsList) throws CardException, IOException {
        performCommand(operationName, cardManager, command, expectedResult, perfFile, failedTestsList, false);
    }

    static void performCommand(String operationName, CardManager cardManager, CommandAPDU command, BigInteger expectedResult, FileOutputStream perfFile, ArrayList<String> failedTestsList, boolean bSigNum) throws CardException, IOException {
        ResponseAPDU response = cardManager.transmit(command);
        boolean bSuccess = false;
        
        if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
            byte[] data = response.getData();
            BigInteger respInt;
            if (bSigNum) { respInt = new BigInteger(1, data); } 
            else { respInt = new BigInteger(data); }

            bSuccess = expectedResult.compareTo(respInt) == 0;

            if (!bSuccess) {
                System.out.println(String.format("Expected: %s", expectedResult.toString(16)));
                System.out.println(String.format("Obtained: %s", respInt.toString(16)));
            }
        } else {
            System.out.println(String.format("fail (0x%x)", response.getSW()));
        }

        logResponse(operationName, bSuccess, cardManager.m_lastTransmitTime, perfFile, failedTestsList);

        if (!bSuccess) {
            String failedAPDU = formatFailedAPDUCmd(command);
            failedTestsList.add(failedAPDU);
        }

        cardManager.transmit(new CommandAPDU(APDU_CLEANUP)); 
    }
 
    static boolean verifyAndLogResponse(String operationName, ResponseAPDU response, Long lastTransmitTime, int expected, FileOutputStream perfFile, ArrayList<String> failedTestsList) throws IOException {
        boolean bResult = false;
        if (response.getSW () == (ISO7816.SW_NO_ERROR & 0xffff)) {
            bResult = Util.BytesToInt(response.getData()) == expected;
        }
        else {
            System.out.println(String.format("fail (0x%x)", response.getSW()));
        }
        logResponse(operationName, bResult, lastTransmitTime, perfFile, failedTestsList);
        return bResult;
    }
    static boolean verifyAndLogResponse(String operationName, ResponseAPDU response, Long lastTransmitTime, ECPoint expected, FileOutputStream perfFile, ArrayList<String> failedTestsList) throws IOException {
        boolean bResult = false;
        if (response.getSW() == (ISO7816.SW_NO_ERROR & 0xffff)) {
            bResult = Arrays.equals(expected.getEncoded(false), response.getData());
            if (!bResult) {
                System.out.println(String.format("Expected: %s", Util.toHex(expected.getEncoded(false))));
                System.out.println(String.format("Obtained: %s", Util.toHex(response.getData())));
            }
        } else {
            bResult = false;
            System.out.println(String.format("fail (0x%x)", response.getSW()));
        }
        logResponse(operationName, bResult, lastTransmitTime, perfFile, failedTestsList);
        return bResult;
    }    
    
    static void logResponse(String operationName, boolean bResult, Long lastTransmitTime, FileOutputStream perfFile, ArrayList<String> failedTestsList) throws IOException {
        System.out.println(String.format("%s [%d ms]", bResult, lastTransmitTime));
        if (bResult == false && _FAIL_ON_ASSERT) {
            assert (bResult);
        }
        if (bResult == false) {
            // Add name of failed operation 
            failedTestsList.add(operationName);
        }
        PerfTests.writePerfLog(operationName, bResult, lastTransmitTime, m_perfResults, perfFile);
    }
    
}