/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.ov.mixing.core.elgamal;

import static org.junit.Assert.assertTrue;

import java.math.BigInteger;
import java.security.Security;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;

import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.CiphertextImpl;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
import com.scytl.cryptolib.elgamal.bean.ElGamalPrivateKey;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalDecrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.ov.mixing.commons.beans.proofs.BayerGrothProof;
import com.scytl.ov.mixing.commons.beans.proofs.Proof;
import com.scytl.ov.mixing.commons.configuration.MatrixDimensions;
import com.scytl.ov.mixing.commons.homomorphic.Cryptosystem;
import com.scytl.ov.mixing.commons.homomorphic.impl.GjosteenElGamal;
import com.scytl.ov.mixing.commons.tools.CiphertextTools;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.MultiExponentiationImpl;
import com.scytl.ov.mixing.core.InvalidMixablesException;
import com.scytl.ov.mixing.core.JobState;
import com.scytl.ov.mixing.core.MixAndProveJob;
import com.scytl.ov.mixing.core.MixJob;
import com.scytl.ov.mixing.core.Mixable;
import com.scytl.ov.mixing.core.MixerContext;
import com.scytl.ov.mixing.core.MixerImpl;
import com.scytl.ov.mixing.core.MixingException;
import com.scytl.ov.mixing.core.ProofGenerationConfigParams;
import com.scytl.ov.mixing.core.VerifyJob;
import com.scytl.ov.mixing.proofs.bg.shuffle.ShuffleProofVerifier;

/**
 * This is an integration test that shows how a client might mix a list of mixables.
 * <P>
 * The first step is to create an instance of a Mixer engine. The Mixer follows a pluggable design, that allows new
 * implementations of MixerSPI to be added. Different implementations of MixerSPI can provide new mixing algorithms.
 * During the construction of a Mixer, at least one instance of an implementation of MixerSPI must be added
 * (registered).
 * <P>
 * At the moment of creating a job, a client specifies an algorithm. The Mixer then searches its available
 * implementations to see if it has one that matches the requested algorithm. If it finds one that matches then it uses
 * that implementation to create a job.
 */
public class ClientITest {

    public static BigInteger p = TestData.P_MEDIUM_SIZE;

    public static BigInteger q = TestData.Q_MEDIUM_SIZE;

    public static BigInteger g = TestData.G_MEDIUM_SIZE;

    public static ElGamalServiceAPI elGamalService;

    public static ElGamalPublicKey publicKey;

    public static ElGamalPrivateKey privateKey;

    public static List<List<ZpGroupElement>> plaintext;

    public static List<Mixable> mixables;

    public static final int NUM_MIXABLES = 5;

    public static final int NUM_ELEMENTS_PER_MIXABLE = 4;

    @BeforeClass
    public static void setUp() {
        Security.addProvider(new BouncyCastleProvider());
    }

    @Test
    public void mix_recover_plaintext() throws InvalidMixablesException, MixingException, GeneralCryptoLibException {

        /////////////////////////////////////////////////
        //
        // Setup group info
        //
        /////////////////////////////////////////////////

        ZpSubgroup group = new ZpSubgroup(g, p, q);

        ElGamalEncryptionParameters encryptionParameters = new ElGamalEncryptionParameters(p, q, g);

        /////////////////////////////////////////////////
        //
        // Setup the ProofGenerationConfigParams
        //
        /////////////////////////////////////////////////

        int m = 1;
        int n = NUM_MIXABLES;
        MatrixDimensions matrixDimensions = new MatrixDimensions(m, n);
        int numIterations = 0;
        int mu = 2;
        ProofGenerationConfigParams proofGenerationConfigParams =
            new ProofGenerationConfigParams(matrixDimensions, numIterations, mu, group);

        /////////////////////////////////////////////////
        //
        // Generate keys
        //
        /////////////////////////////////////////////////

        elGamalService = new ElGamalService();

        ElGamalKeyPair keyPair = elGamalService.getElGamalKeyPairGenerator()
            .generateKeys(new ElGamalEncryptionParameters(p, q, g), NUM_ELEMENTS_PER_MIXABLE);

        publicKey = keyPair.getPublicKeys();
        privateKey = keyPair.getPrivateKeys();

        /////////////////////////////////////////////////
        //
        // Generate plaintext data
        //
        /////////////////////////////////////////////////

        plaintext = new ArrayList<>();

        for (int i = 0; i < NUM_MIXABLES; i++) {

            List<ZpGroupElement> list = new ArrayList<>();

            // The approach taken here for generating "random" group elements is to create a "random" exponent (its not
            // really random, we use the loop indexes), and then exponentiating the generator to that exponent. This
            // ensures the generation of group elements.

            for (int j = 0; j < NUM_ELEMENTS_PER_MIXABLE; j++) {
                int semiRandomExponentAsInt = i + j + 10;
                Exponent semiRandomExponent = new Exponent(q, BigInteger.valueOf(semiRandomExponentAsInt));
                list.add(group.getGenerator().exponentiate(semiRandomExponent));
            }

            plaintext.add(list);
        }

        // printPlaintext("Randomly generated data", plaintext);

        /////////////////////////////////////////////////
        //
        // Encrypt the data
        //
        /////////////////////////////////////////////////

        mixables = new ArrayList<>();
        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(publicKey);

        int arbitraryIntField = 10;
        for (List<ZpGroupElement> singlePlaintext : plaintext) {
            Ciphertext ciphertext = encrypter.encryptGroupElements(singlePlaintext);
            mixables.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));
        }

        // printMixables("Encrypted data", mixables);

        /////////////////////////////////////////////////
        //
        // Create job and start
        //
        /////////////////////////////////////////////////

        // Don't have a MixerContext implementation yet, to be done in next iteration
        MixerContext mixerContext = null;

        MixerImpl mixer = new MixerImpl.MixerImplBuilder(mixerContext).withSPI(ElGamalMixerSPI.getInstance()).build();

        MixJob elGamalMixJob = mixer.newMixJob(ElGamalMixerSPI.getInstance().algorithm());

        elGamalMixJob
            .setParameters(new ElGamalJobParameters(encryptionParameters, publicKey, proofGenerationConfigParams));

        elGamalMixJob.start(mixables);

        /////////////////////////////////////////////////
        //
        // Get back the mixed data
        //
        /////////////////////////////////////////////////

        assertTrue(elGamalMixJob.progress() == 100);
        assertTrue(elGamalMixJob.state() == JobState.FINISHED);

        List<ClientMixableForTesting> mixedMixables = elGamalMixJob.getMix(ClientMixableForTesting.class);

        // printMixables("Mixed data", mixedMixables);

        /////////////////////////////////////////////////
        //
        // Confirm field specific to class is preserved
        //
        /////////////////////////////////////////////////

        for (ClientMixableForTesting mixed : mixedMixables) {
            assertTrue(mixed.getSomeClientField() >= 10);
        }

        ////////////////////////////////////////////////////////////////////
        //
        // Decrypt and confirm that decrypted data matches original plaintext
        //
        ////////////////////////////////////////////////////////////////////

        List<List<ZpGroupElement>> decrypted = getDecryptedOld(mixedMixables);

        // printPlaintext("Final decrypted data", decrypted);

        Set<List<ZpGroupElement>> plaintextAsSet = new HashSet<>(plaintext);
        Set<List<ZpGroupElement>> decryptedAsSet = new HashSet<>(decrypted);

        assertTrue(plaintextAsSet.equals(decryptedAsSet));
    }

    @Test
    public void mix_recover_plaintext_verify_proof_low_level()
            throws InvalidMixablesException, MixingException, GeneralCryptoLibException {

        /////////////////////////////////////////////////
        //
        // Setup group info
        //
        /////////////////////////////////////////////////

        ZpSubgroup group = new ZpSubgroup(g, p, q);

        ElGamalEncryptionParameters encryptionParameters = new ElGamalEncryptionParameters(p, q, g);

        /////////////////////////////////////////////////
        //
        // Setup the ProofGenerationConfigParams
        //
        /////////////////////////////////////////////////

        int m = 1;
        int n = NUM_MIXABLES;
        MatrixDimensions matrixDimensions = new MatrixDimensions(m, n);
        int numIterations = 0;
        int mu = 2;
        ProofGenerationConfigParams proofGenerationConfigParams =
            new ProofGenerationConfigParams(matrixDimensions, numIterations, mu, group);

        /////////////////////////////////////////////////
        //
        // Generate keys
        //
        /////////////////////////////////////////////////

        elGamalService = new ElGamalService();

        ElGamalKeyPair keyPair = elGamalService.getElGamalKeyPairGenerator()
            .generateKeys(new ElGamalEncryptionParameters(p, q, g), NUM_ELEMENTS_PER_MIXABLE);

        publicKey = keyPair.getPublicKeys();
        privateKey = keyPair.getPrivateKeys();

        /////////////////////////////////////////////////
        //
        // Generate plaintext data
        //
        /////////////////////////////////////////////////

        plaintext = new ArrayList<>();

        for (int i = 0; i < NUM_MIXABLES; i++) {

            List<ZpGroupElement> list = new ArrayList<>();

            // The approach taken here for generating "random" group elements is to create a "random" exponent (its not
            // really random, we use the loop indexes), and then exponentiating the generator to that exponent. This
            // ensures the generation of group elements.

            for (int j = 0; j < NUM_ELEMENTS_PER_MIXABLE; j++) {
                int semiRandomExponentAsInt = i + j + 10;
                Exponent semiRandomExponent = new Exponent(q, BigInteger.valueOf(semiRandomExponentAsInt));
                list.add(group.getGenerator().exponentiate(semiRandomExponent));
            }

            plaintext.add(list);
        }

        printPlaintext("Randomly generated data", plaintext);

        /////////////////////////////////////////////////
        //
        // Encrypt the data
        //
        /////////////////////////////////////////////////

        List<Mixable> copyOfCiphertextForVerification = new ArrayList<>();

        mixables = new ArrayList<>();
        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(publicKey);

        int arbitraryIntField = 10;
        for (List<ZpGroupElement> singlePlaintext : plaintext) {
            Ciphertext ciphertext = encrypter.encryptGroupElements(singlePlaintext);
            mixables.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));

            // we also save another copy that will be used for verification of the proofs
            copyOfCiphertextForVerification.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));
        }

        printMixabless("Encrypted data", mixables);

        /////////////////////////////////////////////////
        //
        // Create job and start
        //
        /////////////////////////////////////////////////

        // Don't have a MixerContext implementation yet, to be done in next iteration
        MixerContext mixerContext = null;

        MixerImpl mixer = new MixerImpl.MixerImplBuilder(mixerContext).withSPI(ElGamalMixerSPI.getInstance()).build();

        MixAndProveJob elGamalMixJob = mixer.newMixAndProveJob(ElGamalMixerSPI.getInstance().algorithm());

        elGamalMixJob
            .setParameters(new ElGamalJobParameters(encryptionParameters, publicKey, proofGenerationConfigParams));

        elGamalMixJob.start(mixables);

        /////////////////////////////////////////////////
        //
        // Get back the mixed data
        //
        /////////////////////////////////////////////////

        assertTrue(elGamalMixJob.progress() == 100);
        assertTrue(elGamalMixJob.state() == JobState.FINISHED);

        List<ClientMixableForTesting> mixedMixables = elGamalMixJob.getMix(ClientMixableForTesting.class);

        printMixables("Mixed data", mixedMixables);

        /////////////////////////////////////////////////
        //
        // Get the proof
        //
        /////////////////////////////////////////////////

        Proof proof = elGamalMixJob.getProof();

        /////////////////////////////////////////////////
        //
        // Confirm field specific to class is preserved
        //
        /////////////////////////////////////////////////

        for (ClientMixableForTesting mixed : mixedMixables) {
            assertTrue(mixed.getSomeClientField() >= 10);
        }

        ////////////////////////////////////////////////////////////////////
        //
        // Decrypt and confirm that decrypted data matches original plaintext
        //
        ////////////////////////////////////////////////////////////////////

        List<List<ZpGroupElement>> decrypted = getDecryptedOld(mixedMixables);

        printPlaintext("Final decrypted data", decrypted);

        Set<List<ZpGroupElement>> plaintextAsSet = new HashSet<>(plaintext);
        Set<List<ZpGroupElement>> decryptedAsSet = new HashSet<>(decrypted);

        assertTrue(plaintextAsSet.equals(decryptedAsSet));

        ////////////////////////////////////////////////////////////////////
        //
        // Verify proof
        //
        // This does not necessarily need to be done by the process that generated
        // the proof. Often this would be done later by an independent verifier.
        //
        ////////////////////////////////////////////////////////////////////

        BayerGrothProof bayerGrothProof = null;
        if (proof instanceof BayerGrothProof) {
            bayerGrothProof = (BayerGrothProof) proof;
        } else {
            System.out.println("Not a BayerGrothProof!");
            assertTrue(false);
        }

        MultiExponentiation limMultiExpo = MultiExponentiationImpl.getInstance();
        CiphertextTools ciphertextTools = new CiphertextTools(limMultiExpo);

        Cryptosystem cryptosystem = new GjosteenElGamal(group, publicKey);

        final Ciphertext[][] encryptedCiphertexts = arrangeListTo2DArray(copyOfCiphertextForVerification, m, n);

        final Ciphertext[][] mixedEncryptedCiphertexts = arrangeListTo2DArray(mixedMixables, m, n);

        ShuffleProofVerifier verifier =
            new ShuffleProofVerifier(group, cryptosystem, bayerGrothProof.getCommitmentParams(), encryptedCiphertexts,
                mixedEncryptedCiphertexts, m, n, mu, numIterations, ciphertextTools, limMultiExpo);

        boolean result = verifier.verifyProof(bayerGrothProof.getInitialMessage(), bayerGrothProof.getFirstAnswer(),
            bayerGrothProof.getSecondAnswer());

        System.out.println("Result: " + result);
        assertTrue(result);
    }

    @Test
    public void mix_recover_plaintext_verify_proof_using_job_1value_per_ciphertext()
            throws InvalidMixablesException, MixingException, GeneralCryptoLibException {

        /////////////////////////////////////////////////
        //
        // Setup group info
        //
        /////////////////////////////////////////////////

        ZpSubgroup group = new ZpSubgroup(g, p, q);

        ElGamalEncryptionParameters encryptionParameters = new ElGamalEncryptionParameters(p, q, g);

        /////////////////////////////////////////////////
        //
        // Setup the ProofGenerationConfigParams
        //
        /////////////////////////////////////////////////

        int m = 1;
        int n = NUM_MIXABLES;
        int NUM_ELEMENTS_PER_MIXABLE = 1;
        MatrixDimensions matrixDimensions = new MatrixDimensions(m, n);
        int numIterations = 0;
        int mu = 2;
        ProofGenerationConfigParams proofGenerationConfigParams =
            new ProofGenerationConfigParams(matrixDimensions, numIterations, mu, group);

        /////////////////////////////////////////////////
        //
        // Generate keys
        //
        /////////////////////////////////////////////////

        elGamalService = new ElGamalService();

        ElGamalKeyPair keyPair = elGamalService.getElGamalKeyPairGenerator()
            .generateKeys(new ElGamalEncryptionParameters(p, q, g), NUM_ELEMENTS_PER_MIXABLE);

        publicKey = keyPair.getPublicKeys();
        privateKey = keyPair.getPrivateKeys();

        /////////////////////////////////////////////////
        //
        // Generate plaintext data
        //
        /////////////////////////////////////////////////

        plaintext = new ArrayList<>();

        for (int i = 0; i < NUM_MIXABLES; i++) {

            List<ZpGroupElement> list = new ArrayList<>();

            // The approach taken here for generating "random" group elements is to create a "random" exponent (its not
            // really random, we use the loop indexes), and then exponentiating the generator to that exponent. This
            // ensures the generation of group elements.

            for (int j = 0; j < NUM_ELEMENTS_PER_MIXABLE; j++) {
                int semiRandomExponentAsInt = i + j + 10;
                Exponent semiRandomExponent = new Exponent(q, BigInteger.valueOf(semiRandomExponentAsInt));
                list.add(group.getGenerator().exponentiate(semiRandomExponent));
            }

            plaintext.add(list);
        }

        printPlaintext("Randomly generated data", plaintext);

        /////////////////////////////////////////////////
        //
        // Encrypt the data
        //
        /////////////////////////////////////////////////

        List<ClientMixableForTesting> copyOfCiphertextForVerification = new ArrayList<>();

        mixables = new ArrayList<>();
        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(publicKey);

        int arbitraryIntField = 10;
        for (List<ZpGroupElement> singlePlaintext : plaintext) {
            Ciphertext ciphertext = encrypter.encryptGroupElements(singlePlaintext);
            mixables.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));

            // we also save another copy that will be used for verification of the proofs
            copyOfCiphertextForVerification.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));
        }

        printMixabless("Encrypted data", mixables);

        /////////////////////////////////////////////////
        //
        // Create job and start
        //
        /////////////////////////////////////////////////

        // Don't have a MixerContext implementation yet, not yet needed
        MixerContext mixerContext = null;

        MixerImpl mixer = new MixerImpl.MixerImplBuilder(mixerContext).withSPI(ElGamalMixerSPI.getInstance()).build();

        MixAndProveJob elGamalMixJob = mixer.newMixAndProveJob(ElGamalMixerSPI.getInstance().algorithm());

        elGamalMixJob
            .setParameters(new ElGamalJobParameters(encryptionParameters, publicKey, proofGenerationConfigParams));

        elGamalMixJob.start(mixables);

        /////////////////////////////////////////////////
        //
        // Get back the mixed data
        //
        /////////////////////////////////////////////////

        assertTrue(elGamalMixJob.progress() == 100);
        assertTrue(elGamalMixJob.state() == JobState.FINISHED);

        List<ClientMixableForTesting> mixedMixables = elGamalMixJob.getMix(ClientMixableForTesting.class);

        printMixables("Mixed data", mixedMixables);

        /////////////////////////////////////////////////
        //
        // Get the proof
        //
        /////////////////////////////////////////////////

        Proof proof = elGamalMixJob.getProof();

        /////////////////////////////////////////////////
        //
        // Confirm field specific to class is preserved
        //
        /////////////////////////////////////////////////

        for (ClientMixableForTesting mixed : mixedMixables) {
            assertTrue(mixed.getSomeClientField() >= 10);
        }

        ////////////////////////////////////////////////////////////////////
        //
        // Decrypt and confirm that decrypted data matches original plaintext
        //
        ////////////////////////////////////////////////////////////////////

        List<List<ZpGroupElement>> decrypted = getDecryptedOld(mixedMixables);

        printPlaintext("Final decrypted data", decrypted);

        Set<List<ZpGroupElement>> plaintextAsSet = new HashSet<>(plaintext);
        Set<List<ZpGroupElement>> decryptedAsSet = new HashSet<>(decrypted);

        assertTrue(plaintextAsSet.equals(decryptedAsSet));

        ////////////////////////////////////////////////////////////////////
        //
        // Verify proof
        //
        // This does not necessarily need to be done by the process that generated
        // the proof. Often this would be done later by an independent verifier.
        //
        ////////////////////////////////////////////////////////////////////

        VerifyJob verifyJob = mixer.newVerifyJob(ElGamalMixerSPI.getInstance().algorithm());

        verifyJob.setParameters(new ElGamalJobParameters(encryptionParameters, publicKey, proofGenerationConfigParams));

        verifyJob.start(copyOfCiphertextForVerification, mixedMixables, proof);
    }

    @Test
    public void mix_recover_plaintext_verify_proof_using_job_5values_per_ciphertext()
            throws InvalidMixablesException, MixingException, GeneralCryptoLibException {

        /////////////////////////////////////////////////
        //
        // Setup group info
        //
        /////////////////////////////////////////////////

        ZpSubgroup group = new ZpSubgroup(g, p, q);

        ElGamalEncryptionParameters encryptionParameters = new ElGamalEncryptionParameters(p, q, g);

        /////////////////////////////////////////////////
        //
        // Setup the ProofGenerationConfigParams
        //
        /////////////////////////////////////////////////

        int m = 1;
        int n = NUM_MIXABLES;
        MatrixDimensions matrixDimensions = new MatrixDimensions(m, n);
        int numIterations = 0;
        int mu = 2;
        ProofGenerationConfigParams proofGenerationConfigParams =
            new ProofGenerationConfigParams(matrixDimensions, numIterations, mu, group);

        /////////////////////////////////////////////////
        //
        // Generate keys
        //
        /////////////////////////////////////////////////

        elGamalService = new ElGamalService();

        ElGamalKeyPair keyPair = elGamalService.getElGamalKeyPairGenerator()
            .generateKeys(new ElGamalEncryptionParameters(p, q, g), NUM_ELEMENTS_PER_MIXABLE);

        publicKey = keyPair.getPublicKeys();
        privateKey = keyPair.getPrivateKeys();

        /////////////////////////////////////////////////
        //
        // Generate plaintext data
        //
        /////////////////////////////////////////////////

        plaintext = new ArrayList<>();

        for (int i = 0; i < NUM_MIXABLES; i++) {

            List<ZpGroupElement> list = new ArrayList<>();

            // The approach taken here for generating "random" group elements is to create a "random" exponent (its not
            // really random, we use the loop indexes), and then exponentiating the generator to that exponent. This
            // ensures the generation of group elements.

            for (int j = 0; j < NUM_ELEMENTS_PER_MIXABLE; j++) {
                int semiRandomExponentAsInt = i + j + 10;
                Exponent semiRandomExponent = new Exponent(q, BigInteger.valueOf(semiRandomExponentAsInt));
                list.add(group.getGenerator().exponentiate(semiRandomExponent));
            }

            plaintext.add(list);
        }

        printPlaintext("Randomly generated data", plaintext);

        /////////////////////////////////////////////////
        //
        // Encrypt the data
        //
        /////////////////////////////////////////////////

        List<ClientMixableForTesting> copyOfCiphertextForVerification = new ArrayList<>();

        mixables = new ArrayList<>();
        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(publicKey);

        int arbitraryIntField = 10;
        for (List<ZpGroupElement> singlePlaintext : plaintext) {
            Ciphertext ciphertext = encrypter.encryptGroupElements(singlePlaintext);
            mixables.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));

            // we also save another copy that will be used for verification of the proofs
            copyOfCiphertextForVerification.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));
        }

        printMixabless("Encrypted data", mixables);

        /////////////////////////////////////////////////
        //
        // Create job and start
        //
        /////////////////////////////////////////////////

        // Don't have a MixerContext implementation yet, not yet needed
        MixerContext mixerContext = null;

        MixerImpl mixer = new MixerImpl.MixerImplBuilder(mixerContext).withSPI(ElGamalMixerSPI.getInstance()).build();

        MixAndProveJob elGamalMixJob = mixer.newMixAndProveJob(ElGamalMixerSPI.getInstance().algorithm());

        elGamalMixJob
            .setParameters(new ElGamalJobParameters(encryptionParameters, publicKey, proofGenerationConfigParams));

        elGamalMixJob.start(mixables);

        /////////////////////////////////////////////////
        //
        // Get back the mixed data
        //
        /////////////////////////////////////////////////

        assertTrue(elGamalMixJob.progress() == 100);
        assertTrue(elGamalMixJob.state() == JobState.FINISHED);

        List<ClientMixableForTesting> mixedMixables = elGamalMixJob.getMix(ClientMixableForTesting.class);

        printMixables("Mixed data", mixedMixables);

        /////////////////////////////////////////////////
        //
        // Get the proof
        //
        /////////////////////////////////////////////////

        Proof proof = elGamalMixJob.getProof();

        /////////////////////////////////////////////////
        //
        // Confirm field specific to class is preserved
        //
        /////////////////////////////////////////////////

        for (ClientMixableForTesting mixed : mixedMixables) {
            assertTrue(mixed.getSomeClientField() >= 10);
        }

        ////////////////////////////////////////////////////////////////////
        //
        // Decrypt and confirm that decrypted data matches original plaintext
        //
        ////////////////////////////////////////////////////////////////////

        List<List<ZpGroupElement>> decrypted = getDecryptedOld(mixedMixables);

        printPlaintext("Final decrypted data", decrypted);

        Set<List<ZpGroupElement>> plaintextAsSet = new HashSet<>(plaintext);
        Set<List<ZpGroupElement>> decryptedAsSet = new HashSet<>(decrypted);

        assertTrue(plaintextAsSet.equals(decryptedAsSet));

        ////////////////////////////////////////////////////////////////////
        //
        // Verify proof
        //
        // This does not necessarily need to be done by the process that generated
        // the proof. Often this would be done later by an independent verifier.
        //
        ////////////////////////////////////////////////////////////////////

        VerifyJob verifyJob = mixer.newVerifyJob(ElGamalMixerSPI.getInstance().algorithm());

        verifyJob.setParameters(new ElGamalJobParameters(encryptionParameters, publicKey, proofGenerationConfigParams));

        verifyJob.start(copyOfCiphertextForVerification, mixedMixables, proof);
    }

    @Test
    public void test_multiple_ciphertexts()
            throws InvalidMixablesException, MixingException, GeneralCryptoLibException {

        int NUM_KEYS_TEMP = 1;
        int NUM_ELEMENTS_PER_MIXABLE_TEMP = 5;
        /////////////////////////////////////////////////
        //
        // Setup group info
        //
        /////////////////////////////////////////////////

        ZpSubgroup group = new ZpSubgroup(g, p, q);

        ElGamalEncryptionParameters encryptionParameters = new ElGamalEncryptionParameters(p, q, g);

        /////////////////////////////////////////////////
        //
        // Setup the ProofGenerationConfigParams
        //
        /////////////////////////////////////////////////

        int m = 1;
        int n = NUM_MIXABLES;
        MatrixDimensions matrixDimensions = new MatrixDimensions(m, n);
        int numIterations = 0;
        int mu = 2;
        ProofGenerationConfigParams proofGenerationConfigParams =
            new ProofGenerationConfigParams(matrixDimensions, numIterations, mu, group);

        /////////////////////////////////////////////////
        //
        // Generate keys
        //
        /////////////////////////////////////////////////

        elGamalService = new ElGamalService();

        ElGamalKeyPair keyPair = elGamalService.getElGamalKeyPairGenerator()
            .generateKeys(new ElGamalEncryptionParameters(p, q, g), NUM_KEYS_TEMP);

        publicKey = keyPair.getPublicKeys();
        privateKey = keyPair.getPrivateKeys();

        /////////////////////////////////////////////////
        //
        // Generate plaintext data
        //
        /////////////////////////////////////////////////

        plaintext = new ArrayList<>();

        for (int i = 0; i < NUM_MIXABLES; i++) {

            List<ZpGroupElement> list = new ArrayList<>();

            // The approach taken here for generating "random" group elements is to create a "random" exponent (its not
            // really random, we use the loop indexes), and then exponentiating the generator to that exponent. This
            // ensures the generation of group elements.

            for (int j = 0; j < NUM_ELEMENTS_PER_MIXABLE_TEMP; j++) {
                int semiRandomExponentAsInt = i + j + 10;
                Exponent semiRandomExponent = new Exponent(q, BigInteger.valueOf(semiRandomExponentAsInt));
                list.add(group.getGenerator().exponentiate(semiRandomExponent));
            }

            plaintext.add(list);
        }

        printPlaintext("Randomly generated data", plaintext);

        /////////////////////////////////////////////////
        //
        // Encrypt the data
        //
        /////////////////////////////////////////////////

        List<Mixable> copyOfCiphertextForVerification = new ArrayList<>();

        mixables = new ArrayList<>();
        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(publicKey);

        int arbitraryIntField = 10;
        for (List<ZpGroupElement> singlePlaintext : plaintext) {

            List<Ciphertext> listCiphertexts = new ArrayList<>();

            for (ZpGroupElement z : singlePlaintext) {

                List<ZpGroupElement> zz = new ArrayList<>();
                zz.add(z);

                Ciphertext c = encrypter.encryptGroupElements(zz);
                listCiphertexts.add(c);
            }

            Ciphertext ciphertext = compress(listCiphertexts);

            // Ciphertext ciphertext = encrypter.encryptGroupElements(singlePlaintext);
            mixables.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));

            // we also save another copy that will be used for verification of the proofs
            copyOfCiphertextForVerification.add(new ClientMixableForTesting(ciphertext, arbitraryIntField++));
        }

        printMixabless("Encrypted data", mixables);

        /////////////////////////////////////////////////
        //
        // Duplicate public key
        //
        /////////////////////////////////////////////////

        int i = publicKey.getKeys().size();
        System.out.println("i: " + i);
        List<ZpGroupElement> keys = new ArrayList<>();
        ZpGroupElement key = publicKey.getKeys().get(0);
        for (int j = 0; j < 9; j++) {
            keys.add(key);
        }
        ElGamalPublicKey keyUsedDuringReEncryption = new ElGamalPublicKey(keys, group);

        /////////////////////////////////////////////////
        //
        // Create job and start
        //
        /////////////////////////////////////////////////

        // Don't have a MixerContext implementation yet, to be done in next iteration
        MixerContext mixerContext = null;

        MixerImpl mixer = new MixerImpl.MixerImplBuilder(mixerContext).withSPI(ElGamalMixerSPI.getInstance()).build();

        MixAndProveJob elGamalMixJob = mixer.newMixAndProveJob(ElGamalMixerSPI.getInstance().algorithm());

        elGamalMixJob.setParameters(
            new ElGamalJobParameters(encryptionParameters, keyUsedDuringReEncryption, proofGenerationConfigParams));

        elGamalMixJob.start(mixables);

        /////////////////////////////////////////////////
        //
        // Get back the mixed data
        //
        /////////////////////////////////////////////////

        assertTrue(elGamalMixJob.progress() == 100);
        assertTrue(elGamalMixJob.state() == JobState.FINISHED);

        List<ClientMixableForTesting> mixedMixables = elGamalMixJob.getMix(ClientMixableForTesting.class);

        printMixables("Mixed data", mixedMixables);

        /////////////////////////////////////////////////
        //
        // Get the proof
        //
        /////////////////////////////////////////////////

        Proof proof = elGamalMixJob.getProof();

        /////////////////////////////////////////////////
        //
        // Confirm field specific to class is preserved
        //
        /////////////////////////////////////////////////

        for (ClientMixableForTesting mixed : mixedMixables) {
            assertTrue(mixed.getSomeClientField() >= 10);
        }

        ////////////////////////////////////////////////////////////////////
        //
        // Decrypt and confirm that decrypted data matches original plaintext
        //
        ////////////////////////////////////////////////////////////////////

        List<List<ZpGroupElement>> decrypted = getDecrypted(mixedMixables, group);

        printPlaintext("Final decrypted data", decrypted);

        // assertTrue(plaintextAsSet.equals(decryptedAsSet));

        ////////////////////////////////////////////////////////////////////
        //
        // Verify proof
        //
        // This does not necessarily need to be done by the process that generated
        // the proof. Often this would be done later by an independent verifier.
        //
        ////////////////////////////////////////////////////////////////////

        BayerGrothProof bayerGrothProof = null;
        if (proof instanceof BayerGrothProof) {
            bayerGrothProof = (BayerGrothProof) proof;
        } else {
            System.out.println("Not a BayerGrothProof!");
            assertTrue(false);
        }

        MultiExponentiation limMultiExpo = MultiExponentiationImpl.getInstance();
        CiphertextTools ciphertextTools = new CiphertextTools(limMultiExpo);

        // Cryptosystem cryptosystem = new GjosteenElGamal(group, publicKey);

        Cryptosystem cryptosystem = new GjosteenElGamal(group, keyUsedDuringReEncryption);

        final Ciphertext[][] encryptedCiphertexts = arrangeListTo2DArray(copyOfCiphertextForVerification, m, n);

        final Ciphertext[][] mixedEncryptedCiphertexts = arrangeListTo2DArray(mixedMixables, m, n);

        ShuffleProofVerifier verifier =
            new ShuffleProofVerifier(group, cryptosystem, bayerGrothProof.getCommitmentParams(), encryptedCiphertexts,
                mixedEncryptedCiphertexts, m, n, mu, numIterations, ciphertextTools, limMultiExpo);

        boolean result = verifier.verifyProof(bayerGrothProof.getInitialMessage(), bayerGrothProof.getFirstAnswer(),
            bayerGrothProof.getSecondAnswer());

        System.out.println("Result: " + result);
        assertTrue(result);
    }

    private Ciphertext compress(List<Ciphertext> listCiphertexts) throws GeneralCryptoLibException {

        ZpGroupElement gamma = listCiphertexts.get(0).getGamma();
        List<ZpGroupElement> phis = new ArrayList<>();

        phis.add(listCiphertexts.get(0).getPhis().get(0));

        for (int i = 1; i < listCiphertexts.size(); i++) {
            phis.add(listCiphertexts.get(i).getGamma());
            phis.add(listCiphertexts.get(i).getPhis().get(0));

        }

        return new CiphertextImpl(gamma, phis);
    }

    private <T> Ciphertext[][] arrangeListTo2DArray(List<T> mixables, int m, int n) {

        List<Ciphertext> ciphertexts = new ArrayList<>();
        for (T mixable : mixables) {
            ciphertexts.add(((ElGamalMixable) mixable).getCiphertext());
        }
        final Ciphertext[][] encryptedCiphertexts =
            com.scytl.ov.mixing.commons.tools.MatrixArranger.arrangeInCiphertextMatrix(ciphertexts, m, n);

        return encryptedCiphertexts;
    }

    private List<List<ZpGroupElement>> getDecrypted(List<ClientMixableForTesting> returnMixables, ZpSubgroup group)
            throws GeneralCryptoLibException {

        List<Ciphertext> reencrypted = new ArrayList<>();

        for (Mixable m : returnMixables) {
            ElGamalMixable elGamalMixable = (ElGamalMixable) m;
            reencrypted.add(elGamalMixable.getCiphertext());
        }

        // CryptoAPIElGamalDecrypter decrypter = elGamalService.createDecrypter(getPrivateKey(privateKey, group));
        CryptoAPIElGamalDecrypter decrypter = elGamalService.createDecrypter(privateKey);

        List<List<ZpGroupElement>> allDecrypted = new ArrayList<>();

        for (Ciphertext singleCiphertext : reencrypted) {

            List<ZpGroupElement> oneDecrypted = new ArrayList<>();

            List<ZpGroupElement> list1 = new ArrayList<>();
            list1.add(singleCiphertext.getPhis().get(0));
            ElGamalComputationsValues singleCiphertextAsElGamalComputationsValues1 =
                new ElGamalComputationsValues(singleCiphertext.getGamma(), list1);
            List<ZpGroupElement> singleDecrypted1 =
                decrypter.decrypt(singleCiphertextAsElGamalComputationsValues1, false);
            oneDecrypted.add(singleDecrypted1.get(0));

            List<ZpGroupElement> list2 = new ArrayList<>();
            list2.add(singleCiphertext.getPhis().get(2));
            ElGamalComputationsValues singleCiphertextAsElGamalComputationsValues2 =
                new ElGamalComputationsValues(singleCiphertext.getGamma(), list2);
            List<ZpGroupElement> singleDecrypted2 =
                decrypter.decrypt(singleCiphertextAsElGamalComputationsValues2, false);
            oneDecrypted.add(singleDecrypted2.get(0));

            List<ZpGroupElement> list3 = new ArrayList<>();
            list3.add(singleCiphertext.getPhis().get(3));
            list3.add(singleCiphertext.getPhis().get(4));
            ElGamalComputationsValues singleCiphertextAsElGamalComputationsValues3 =
                new ElGamalComputationsValues(list3);
            List<ZpGroupElement> singleDecrypted3 =
                decrypter.decrypt(singleCiphertextAsElGamalComputationsValues3, false);
            oneDecrypted.add(singleDecrypted3.get(0));

            List<ZpGroupElement> list4 = new ArrayList<>();
            list4.add(singleCiphertext.getPhis().get(5));
            list4.add(singleCiphertext.getPhis().get(6));
            ElGamalComputationsValues singleCiphertextAsElGamalComputationsValues4 =
                new ElGamalComputationsValues(list4);
            List<ZpGroupElement> singleDecrypted4 =
                decrypter.decrypt(singleCiphertextAsElGamalComputationsValues4, false);
            oneDecrypted.add(singleDecrypted4.get(0));

            List<ZpGroupElement> list5 = new ArrayList<>();
            list5.add(singleCiphertext.getPhis().get(7));
            list5.add(singleCiphertext.getPhis().get(8));
            ElGamalComputationsValues singleCiphertextAsElGamalComputationsValues5 =
                new ElGamalComputationsValues(list5);
            List<ZpGroupElement> singleDecrypted5 =
                decrypter.decrypt(singleCiphertextAsElGamalComputationsValues5, false);
            oneDecrypted.add(singleDecrypted5.get(0));

            allDecrypted.add(oneDecrypted);

        }

        return allDecrypted;
    }

    private List<List<ZpGroupElement>> getDecryptedOld(List<ClientMixableForTesting> returnMixables)
            throws GeneralCryptoLibException {

        List<Ciphertext> reencrypted = new ArrayList<>();

        for (Mixable m : returnMixables) {
            ElGamalMixable elGamalMixable = (ElGamalMixable) m;
            reencrypted.add(elGamalMixable.getCiphertext());
        }

        CryptoAPIElGamalDecrypter decrypter = elGamalService.createDecrypter(privateKey);

        List<List<ZpGroupElement>> allDecrypted = new ArrayList<>();

        for (Ciphertext singleCiphertext : reencrypted) {

            ElGamalComputationsValues singleCiphertextAsElGamalComputationsValues =
                new ElGamalComputationsValues(singleCiphertext.getElements());

            List<ZpGroupElement> singleDecrypted =
                decrypter.decrypt(singleCiphertextAsElGamalComputationsValues, false);
            allDecrypted.add(singleDecrypted);
        }

        return allDecrypted;
    }

    @SuppressWarnings("unused")
    private static void printPlaintext(String message, List<List<ZpGroupElement>> plaintext) {

        System.out.println("-----------" + message + "---------------");

        for (int i = 0; i < plaintext.size(); i++) {

            for (int j = 0; j < plaintext.get(i).size(); j++) {

                System.out.print(plaintext.get(i).get(j).getValue());
                System.out.print(" ");
            }

            System.out.println();
        }
    }

    @SuppressWarnings("unused")
    private static void printMixabless(String message, List<Mixable> mixables) {

        System.out.println("-----------" + message + "---------------");

        for (Mixable mixable : mixables) {

            ElGamalMixable m = (ElGamalMixable) mixable;

            Ciphertext mixableCiphertext = m.getCiphertext();

            List<ZpGroupElement> elementsOfCiphertext = mixableCiphertext.getElements();

            // System.out.print("Arbitrary int field: " + mixable.getSomeClientField());

            System.out.print("Ciphertext: ");

            for (ZpGroupElement element : elementsOfCiphertext) {
                System.out.print(element.getValue());
                System.out.print(" ");
            }
            System.out.println();
        }
    }

    @SuppressWarnings("unused")
    private static void printMixables(String message, List<ClientMixableForTesting> mixables) {

        System.out.println("-----------" + message + "---------------");

        for (ClientMixableForTesting mixable : mixables) {

            Ciphertext mixableCiphertext = mixable.getCiphertext();

            List<ZpGroupElement> elementsOfCiphertext = mixableCiphertext.getElements();

            System.out.print("Arbitrary int field: " + mixable.getSomeClientField());

            System.out.print(", Ciphertext: ");

            for (ZpGroupElement element : elementsOfCiphertext) {
                System.out.print(element.getValue());
                System.out.print(" ");
            }
            System.out.println();
        }
    }
}
