/*
 * 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.products.ov.mixnet;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.mixnet.commons.ballots.ElGamalEncryptedBallots;
import com.scytl.products.ov.mixnet.commons.beans.proofs.ShuffleProof;
import com.scytl.products.ov.mixnet.commons.configuration.ApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.MixerConfig;
import com.scytl.products.ov.mixnet.commons.configuration.VerifierConfig;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationsProvider;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamal;
import com.scytl.products.ov.mixnet.commons.io.CommitmentParamsReader;
import com.scytl.products.ov.mixnet.commons.io.ElGamalEncryptedBallotsLoader;
import com.scytl.products.ov.mixnet.commons.io.ElgamalPublicKeyReader;
import com.scytl.products.ov.mixnet.commons.io.JSONProofsReader;
import com.scytl.products.ov.mixnet.commons.proofs.bg.commitments.CommitmentParams;
import com.scytl.products.ov.mixnet.commons.tests.categories.UltraSlowTest;
import com.scytl.products.ov.mixnet.commons.tools.CiphertextTools;
import com.scytl.products.ov.mixnet.commons.tools.MatrixArranger;
import com.scytl.products.ov.mixnet.commons.tools.MultiExponentiation;
import com.scytl.products.ov.mixnet.commons.tools.MultiExponentiationImpl;
import com.scytl.products.ov.mixnet.commons.validation.EncryptedBallotsDuplicationValidator;
import com.scytl.products.ov.mixnet.commons.validation.EncryptedBallotsValidator;
import com.scytl.products.ov.mixnet.commons.validation.EncryptedBallotsValidatorManager;
import com.scytl.products.ov.mixnet.proofs.bg.shuffle.ShuffleProofVerifier;

/**
 * This abstract test class is the base of other tests which perform the steps of the verifier. It includes concrete
 * methods (implemented here) and abstract methods. The abstract methods are implemented in the test classes which
 * extend this class. This test, and the tests which extend this class, perform the following steps: Verifier steps:
 * <ul>
 * <li>Load an ElGamal public key</li>
 * <li>Load a set of commitment parameters</li>
 * <li>Load original encrypted ballots</li>
 * <li>Load the shuffled and reencrypted ballots</li>
 * <li>Verify the proofs</li>
 * </ul>
 */
@Category(UltraSlowTest.class)
public class BaseBGMixnetIOVerifierITest extends BaseBGMixnet {

    private static final String FILENAME_ENCRYPTED_BALLOTS = "encryptedBallots_n512_p256_q32";

    private static final String FILENAME_PUBLIC_KEY = "publicKey";

    private static ZpSubgroup zp;

    private static int m;

    private static int n;

    private static int mu;

    private static int numiterations;

    private static CommitmentParams commitmentParams;

    private static GjosteenElGamal elgamal;

    private static LocationsProvider locations;

    private static JSONProofsReader proofsReader;

    private static ElGamalEncryptedBallotsLoader elgamalEncryptedBallotsLoader;

    private static MultiExponentiation limMultiExpo;

    private static CiphertextTools ciphertextTools;

    @BeforeClass
    public static void setUp() throws IOException, GeneralCryptoLibException {

        final ApplicationConfig mixnetConfig = new ApplicationConfig();
        final MixerConfig mixerConfig = new MixerConfig();
        InputStream resourceAsStream =
            BaseBGMixnetIOVerifierITest.class.getClassLoader().getResourceAsStream("input/encryption_parameters.json");
        zp = ZpSubgroup.fromJson(IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8));
        mixerConfig.setEncryptionParametersInput(resourceAsStream);
        mixerConfig.setEncryptedBallotsInput(
            new FileInputStream(Paths.get("src/test/resources/input/" + FILENAME_ENCRYPTED_BALLOTS + ".csv").toFile()));
        mixerConfig.setPublicKeyInput(new FileInputStream(
            Paths.get("src/test/resources/input/verifier_IO_test_inputs/" + FILENAME_PUBLIC_KEY + ".json").toFile()));
        List<OutputStream> mixedBallots = new ArrayList<>();
        mixedBallots.add(new ByteArrayOutputStream());
        List<InputStream> mixedBallotsInput = new ArrayList<>();
        mixedBallotsInput.add(new ByteArrayInputStream("".getBytes()));
        mixerConfig.setDecryptedVotesOutputOfBatch(mixedBallots);
        mixerConfig.setEncryptedBallotsOutputOfBatch(mixedBallots);
        mixnetConfig.setMixer(mixerConfig);
        final VerifierConfig verifierConfig = new VerifierConfig();
        verifierConfig.setDecryptedVotesInputOfBatch(mixedBallotsInput);
        mixnetConfig.setVerifier(verifierConfig);

        locations = LocationsProvider.build(mixnetConfig);

        try {
            InputStream resource =
                BaseBGMixnetIOVerifierITest.class.getClassLoader().getResourceAsStream("input/encryption_parameters.json");
            zp = ZpSubgroup.fromJson(IOUtils.toString(resource, StandardCharsets.UTF_8));
        } catch (GeneralCryptoLibException ex) {
            throw new CryptoLibException(ex);
        }

        m = 8;

        n = 64;

        mu = 2;

        numiterations = 0;

        ElGamalPublicKey pubKey = readPublicKeyFromFile();

        elgamal = new GjosteenElGamal(zp, pubKey);

        proofsReader = new JSONProofsReader();

        final List<EncryptedBallotsValidator> listValidators = new ArrayList<>();
        listValidators.add(new EncryptedBallotsDuplicationValidator());
        EncryptedBallotsValidatorManager validator = new EncryptedBallotsValidatorManager(listValidators);
        elgamalEncryptedBallotsLoader = new ElGamalEncryptedBallotsLoader(validator);

        limMultiExpo = MultiExponentiationImpl.getInstance();

        ciphertextTools = new CiphertextTools(limMultiExpo);
    }

    private static ElGamalPublicKey readPublicKeyFromFile() throws IOException, GeneralCryptoLibException {

        return ElgamalPublicKeyReader
            .readPublicKeyFromStream(locations.getMixerLocationsProvider().getPublicKeyInput());
    }

    private static CommitmentParams readCommitmentParamsFromFile() throws IOException {

        final Path pathCommitmentParamsFile =
            Paths.get("src/test/resources/input/verifier_IO_test_inputs/commitmentParameters.json");
        FileInputStream fis = new FileInputStream(pathCommitmentParamsFile.toFile());
        return CommitmentParamsReader.readCommitmentParamsFromStream(zp, fis);
    }

    @Test
    public void givenSmallConfigWhenShuffleThenOK()
            throws IOException, GeneralCryptoLibException {

        // /////////////////////////////////////////////////
        //
        // Verify the proofs
        //
        // /////////////////////////////////////////////////

        final ElGamalEncryptedBallots encryptedBallots_VerifierCopy =
            elgamalEncryptedBallotsLoader.loadCSV(zp, locations.getMixerLocationsProvider().getEncryptedBallotsInput());

        final Ciphertext[][] originalCiphertexts_VerifierCopy =
            MatrixArranger.arrangeInCiphertextMatrix(encryptedBallots_VerifierCopy, m, n);

        final ElGamalEncryptedBallots reencryptedBallots =
            elgamalEncryptedBallotsLoader.loadCSV(zp, new FileInputStream(
                Paths.get("src/test/resources/input/verifier_IO_test_inputs/reencryptedBallots.csv").toFile()));

        final Ciphertext[][] reencryptedCiphertexts_VerifierCopy =
            MatrixArranger.arrangeInCiphertextMatrix(reencryptedBallots, m, n);

        commitmentParams = readCommitmentParamsFromFile();

        final ShuffleProofVerifier verifier = new ShuffleProofVerifier(zp, elgamal, commitmentParams,
            originalCiphertexts_VerifierCopy, reencryptedCiphertexts_VerifierCopy, m, n, mu, numiterations,
            ciphertextTools, limMultiExpo);

        final ShuffleProof shuffleProof_VerifierCopy = proofsReader.read(
            new FileInputStream(Paths.get("src/test/resources/input/verifier_IO_test_inputs/proofs.json").toFile()));

        final boolean testResult = verifier.verifyProof(shuffleProof_VerifierCopy.getInitialMessage(),
            shuffleProof_VerifierCopy.getFirstAnswer(), shuffleProof_VerifierCopy.getSecondAnswer());

        final String errorMsg = "proofs failed to validate";
        Assert.assertTrue(errorMsg, testResult);
    }
}
