/*
 * 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.cryptolib.elgamal.service;

import static org.junit.Assert.assertEquals;

import java.math.BigInteger;
import java.security.Security;
import java.util.List;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncrypterValues;
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.CryptoAPIElGamalDecrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalReEncrypter;
import com.scytl.cryptolib.elgamal.utils.ElGamalTestDataGenerator;
import com.scytl.cryptolib.ffc.cryptoapi.FFCDomainParameters;
import com.scytl.cryptolib.ffc.cryptoapi.FFCPrime;
import com.scytl.cryptolib.ffc.cryptoapi.FFCPrimes;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.mathematical.groups.utils.MathematicalTestDataGenerator;
import com.scytl.cryptolib.test.tools.utils.CommonTestDataGenerator;

/**
 * Tests of the ElGamal service API.
 */
public class ElGamalServiceTest {

    private static final int MIN_NUM_TEST_ELEMENTS = 3;

    private static final int MAX_NUM_TEST_ELEMENTS = 10;

    private static ElGamalService _elGamalServiceForDefaultPolicy;

    private static ElGamalEncryptionParameters _encryptionParameters;

    private static int _numElements;

    private static ElGamalPublicKey _publicKey;

    private static ElGamalPrivateKey _privateKey;

    private static CryptoAPIElGamalEncrypter _encrypter;

    private static CryptoAPIElGamalDecrypter _decrypter;

    private static List<ZpGroupElement> _plaintext;

    private static List<String> _plaintextAsStrings;

    private static ElGamalComputationsValues _ciphertext;

    private static ElGamalEncrypterValues _preComputedValues;

    private static CryptoAPIElGamalEncrypter _encrypterForShortExponents;

    private static CryptoAPIElGamalDecrypter _decrypterForShortExponents;

    private static List<ZpGroupElement> _plaintextForShortExponents;

    private static ElGamalEncrypterValues _preComputedValuesFromShortExponent;

    private static CryptoAPIElGamalReEncrypter _reEncrypter;

    private static ElGamalComputationsValues _reEncryptedPlaintext;

    private static ElGamalComputationsValues _reEncryptedPlaintextFromPreComputedVaues;

    private static List<ZpGroupElement> _plaintextWithLessElements;

    private static ElGamalComputationsValues _ciphertextWithLessElements;

    private static List<String> _plaintextAsStringsWithLessElements;

    private static List<ZpGroupElement> _plaintextForShortExponentsWithLessElements;

    private static ElGamalComputationsValues _reEncryptedPlaintextWithLessElements;

    private static ElGamalComputationsValues _reEncryptedPlaintextWithLessElementsUsingPreComputedValues;

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        Security.addProvider(new BouncyCastleProvider());

        _elGamalServiceForDefaultPolicy = new ElGamalService();

        ZpSubgroup zpSubgroup =
            MathematicalTestDataGenerator.getZpSubgroup();
        _encryptionParameters = ElGamalTestDataGenerator
            .getElGamalEncryptionParameters(zpSubgroup);

        _numElements = CommonTestDataGenerator
            .getInt(MIN_NUM_TEST_ELEMENTS, MAX_NUM_TEST_ELEMENTS);

        ElGamalKeyPair elGamalKeyPair =
            _elGamalServiceForDefaultPolicy.getElGamalKeyPairGenerator()
                .generateKeys(_encryptionParameters, _numElements);
        _publicKey = elGamalKeyPair.getPublicKeys();
        _privateKey = elGamalKeyPair.getPrivateKeys();

        _encrypter =
            _elGamalServiceForDefaultPolicy.createEncrypter(_publicKey);
        _decrypter =
            _elGamalServiceForDefaultPolicy.createDecrypter(_privateKey);

        _plaintext = MathematicalTestDataGenerator
            .getZpGroupElements(zpSubgroup, _numElements);
        _plaintextAsStrings = MathematicalTestDataGenerator
            .zpGroupElementsToStrings(_plaintext);

        _ciphertext = _encrypter.encryptGroupElements(_plaintext)
            .getComputationValues();
        _preComputedValues = _encrypter.preCompute();

        ZpSubgroup qrSubgroup =
            MathematicalTestDataGenerator.getQrSubgroup();
        ElGamalEncryptionParameters encryptionParametersQr =
            ElGamalTestDataGenerator
                .getElGamalEncryptionParameters(qrSubgroup);
        ElGamalKeyPair elGamalKeyPairQr =
            _elGamalServiceForDefaultPolicy.getElGamalKeyPairGenerator()
                .generateKeys(encryptionParametersQr, _numElements);
        ElGamalPublicKey publicKeyQr = elGamalKeyPairQr.getPublicKeys();
        ElGamalPrivateKey privateKeyQr = elGamalKeyPairQr.getPrivateKeys();

        _encrypterForShortExponents = ElGamalTestDataGenerator
            .getEncrypterForShortExponents(publicKeyQr);
        _decrypterForShortExponents = ElGamalTestDataGenerator
            .getDecrypterForShortExponents(privateKeyQr);

        _plaintextForShortExponents = MathematicalTestDataGenerator
            .getZpGroupElements(qrSubgroup, _numElements);

        _preComputedValuesFromShortExponent =
            _encrypterForShortExponents.preCompute();

        _reEncrypter =
            _elGamalServiceForDefaultPolicy.createReEncrypter(_publicKey);

        _reEncryptedPlaintext = _reEncrypter
            .reEncryptGroupElements(_ciphertext).getComputationValues();
        _reEncryptedPlaintextFromPreComputedVaues =
            _reEncrypter.reEncryptGroupElements(_reEncryptedPlaintext,
                _reEncrypter.preCompute()).getComputationValues();

        _plaintextWithLessElements = MathematicalTestDataGenerator
            .getZpGroupElements(zpSubgroup, _numElements - 2);
        _plaintextAsStringsWithLessElements = MathematicalTestDataGenerator
            .zpGroupElementsToStrings(_plaintextWithLessElements);

        _ciphertextWithLessElements =
            _encrypter.encryptGroupElements(_plaintextWithLessElements)
                .getComputationValues();

        _plaintextForShortExponentsWithLessElements =
            MathematicalTestDataGenerator.getZpGroupElements(qrSubgroup,
                _numElements - 2);

        _reEncryptedPlaintextWithLessElements = _reEncrypter
            .reEncryptGroupElements(_ciphertextWithLessElements)
            .getComputationValues();

        _reEncryptedPlaintextWithLessElementsUsingPreComputedValues =
            _reEncrypter.reEncryptGroupElements(
                _reEncryptedPlaintextWithLessElements,
                _reEncrypter.preCompute()).getComputationValues();
    }

    @AfterClass
    public static void cleanUp() {

        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    }

    @Test
    public void testWhenGenerateKeyPairThenKeysHaveExpectedLengths() {

        Assert.assertEquals(_publicKey.getKeys().size(), _numElements);
        Assert.assertEquals(_privateKey.getKeys().size(), _numElements);
    }

    @Test
    public void testWhenEncryptAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypter.decrypt(_ciphertext, true);

        Assert.assertEquals(decryptedPlaintext, _plaintext);
    }

    @Test
    public void testWhenEncryptUsingPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter.decrypt(
            _encrypter.encryptGroupElements(_plaintext, _preComputedValues)
                .getComputationValues(),
            true);

        Assert.assertEquals(decryptedPlaintext, _plaintext);
    }

    @Test
    public void testWhenEncryptPlaintextAsStringsAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter
            .decrypt(_encrypter.encryptStrings(_plaintextAsStrings)
                .getComputationValues(), true);

        Assert.assertEquals(decryptedPlaintext, _plaintext);
    }

    @Test
    public void testWhenEncryptPlaintextAsStringsUsingPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypter.decrypt(_encrypter
                .encryptStrings(_plaintextAsStrings, _preComputedValues)
                .getComputationValues(), true);

        Assert.assertEquals(decryptedPlaintext, _plaintext);
    }

    @Test
    public void testWhenEncryptUsingShortExponentAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypterForShortExponents
                .decrypt(_encrypterForShortExponents
                    .encryptGroupElementsWithShortExponent(
                        _plaintextForShortExponents)
                    .getComputationValues(), true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextForShortExponents);
    }

    @Test
    public void testWhenEncryptUsingShortExponentAndPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypterForShortExponents.decrypt(_encrypterForShortExponents
                .encryptGroupElements(_plaintextForShortExponents,
                    _preComputedValuesFromShortExponent)
                .getComputationValues(), true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextForShortExponents);
    }

    @Test
    public void testWhenEncryptAndReEncryptAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypter.decrypt(_reEncryptedPlaintext, true);

        Assert.assertEquals(decryptedPlaintext, _plaintext);
    }

    @Test
    public void testWhenEncryptAndReEncryptUsingPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter
            .decrypt(_reEncryptedPlaintextFromPreComputedVaues, true);

        Assert.assertEquals(decryptedPlaintext, _plaintext);
    }

    @Test
    public void testWhenEncryptPlaintextSmallerThanKeyAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypter.decrypt(_ciphertextWithLessElements, true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextWithLessElements);
    }

    @Test
    public void testWhenEncryptPlaintextSmallerThanKeyUsingPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter.decrypt(
            _encrypter.encryptGroupElements(_plaintextWithLessElements,
                _preComputedValues).getComputationValues(),
            true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextWithLessElements);
    }

    @Test
    public void testWhenEncryptPlaintextSmallerThanKeyAsStringsAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter.decrypt(
            _encrypter.encryptStrings(_plaintextAsStringsWithLessElements)
                .getComputationValues(),
            true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextWithLessElements);
    }

    @Test
    public void testWhenEncryptPlaintextSmallerThanKeyAsStringsUsingPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter.decrypt(
            _encrypter.encryptStrings(_plaintextAsStringsWithLessElements,
                _preComputedValues).getComputationValues(),
            true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextWithLessElements);
    }

    @Test
    public void testWhenEncryptPlaintextShorterThanKeyUsingShortExponentAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypterForShortExponents.decrypt(_encrypterForShortExponents
                .encryptGroupElementsWithShortExponent(
                    _plaintextForShortExponentsWithLessElements)
                .getComputationValues(), true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextForShortExponentsWithLessElements);
    }

    @Test
    public void testWhenEncryptPlaintextShorterThanKeyUsingShortExponentAndPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext =
            _decrypterForShortExponents.decrypt(_encrypterForShortExponents
                .encryptGroupElements(
                    _plaintextForShortExponentsWithLessElements,
                    _preComputedValuesFromShortExponent)
                .getComputationValues(), true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextForShortExponentsWithLessElements);
    }

    @Test
    public void testWhenEncryptAndReEncryptPlaintextSmallerThanKeyAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter
            .decrypt(_reEncryptedPlaintextWithLessElements, true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextWithLessElements);
    }

    @Test
    public void testWhenEncryptAndReEncryptPlaintextSmallerThanKeyUsingPreComputedValuesAndDecryptThenOk()
            throws GeneralCryptoLibException {

        List<ZpGroupElement> decryptedPlaintext = _decrypter.decrypt(
            _reEncryptedPlaintextWithLessElementsUsingPreComputedValues,
            true);

        Assert.assertEquals(decryptedPlaintext,
            _plaintextWithLessElements);
    }

    @Test
    public final void testWhenEncryptAndReEncryptResultingCiphertextsNotEqual()
            throws GeneralCryptoLibException {

        Assert.assertNotEquals(_reEncryptedPlaintext, _ciphertext);
        Assert.assertNotEquals(_reEncryptedPlaintextFromPreComputedVaues,
            _ciphertext);
        Assert.assertNotEquals(_reEncryptedPlaintextWithLessElements,
            _ciphertextWithLessElements);
        Assert.assertNotEquals(
            _reEncryptedPlaintextWithLessElementsUsingPreComputedValues,
            _ciphertextWithLessElements);
    }

    @Test
    public void testWhenSerializeAndDeserializeEncryptionParametersThenOk()
            throws GeneralCryptoLibException {

        ElGamalEncryptionParameters deserializedEncryptionParameters =
            ElGamalEncryptionParameters
                .fromJson(_encryptionParameters.toJson());

        assertEquals(deserializedEncryptionParameters,
            _encryptionParameters);
    }

    @Test
    public void testWhenSerializeAndDeserializePublicKeyThenOk()
            throws GeneralCryptoLibException {

        ElGamalPublicKey deserializedPublicKey =
            ElGamalPublicKey.fromJson(_publicKey.toJson());

        assertEquals(deserializedPublicKey, _publicKey);
    }

    @Test
    public void testWhenSerializeAndDeserializePrivateKeyThenOk()
            throws GeneralCryptoLibException {

        ElGamalPrivateKey deserializedPrivateKey =
            ElGamalPrivateKey.fromJson(_privateKey.toJson());

        assertEquals(deserializedPrivateKey, _privateKey);
    }

    @Test
    public void testWhenSerializeAndDeserializeCiphertextThenOk()
            throws GeneralCryptoLibException {

        ElGamalComputationsValues deserializedCiphertext =
            ElGamalComputationsValues.fromJson(_ciphertext.toJson());

        assertEquals(deserializedCiphertext, _ciphertext);
    }

    @Test
    public void testGenerateEncryptionParametersFFCDomainParameters()
            throws GeneralCryptoLibException {
        FFCPrime p =
            new FFCPrime(BigInteger.valueOf(23), BigInteger.ZERO, 1);
        FFCPrime q =
            new FFCPrime(BigInteger.valueOf(11), BigInteger.ZERO, 1);
        FFCPrimes primes = new FFCPrimes(p, q, BigInteger.ZERO);
        BigInteger g = BigInteger.valueOf(4);

        ElGamalService service = new ElGamalService();

        ElGamalEncryptionParameters parameters =
            service.generateEncryptionParameters(
                new FFCDomainParameters(primes, g));

        assertEquals(p.value(), parameters.getP());
        assertEquals(q.value(), parameters.getQ());
        assertEquals(g, parameters.getG());
    }
}
