/*
 * 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.factory;

/**
 * @date   15/04/2015 11:00:56
 *
 * Copyright (C) 2015 Scytl Secure Electronic Voting SA
 *
 * All rights reserved.
 *
 */

import static org.junit.Assert.assertEquals;

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

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.system.OperatingSystem;
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.configuration.ConfigGroupType;
import com.scytl.cryptolib.elgamal.configuration.ElGamalPolicy;
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.cryptolib.primitives.securerandom.configuration.ConfigSecureRandomAlgorithmAndProvider;

public class CryptoElGamalReEncrypterTest {

    private static BigInteger _p;

    private static BigInteger _q;

    private static BigInteger _g;

    private static ZpSubgroup _group_g2_q11;

    private static int _numKeys;

    private static ElGamalPolicy _cryptoElGamalPolicy;

    private static ElGamalFactory _cryptoElGamalFactory;

    private static ElGamalKeyPair _cryptoElGamalKeyPair;

    private static ElGamalPublicKey _pubKey;

    private static ElGamalPrivateKey _privKey;

    private static CryptoElGamalReEncrypter _reEncrypter;

    private static ElGamalEncryptionParameters _elGamalEncryptionParameters;

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        _p = new BigInteger("23");

        _q = new BigInteger("11");

        _g = new BigInteger("2");

        _group_g2_q11 = new ZpSubgroup(_g, _p, _q);

        _numKeys = 2;

        _cryptoElGamalPolicy = getCryptoElGamalPolicy();

        _cryptoElGamalFactory = new ElGamalFactory(_cryptoElGamalPolicy);

        _elGamalEncryptionParameters =
            new ElGamalEncryptionParameters(_p, _q, _g);
        _cryptoElGamalKeyPair =
            _cryptoElGamalFactory.createCryptoElGamalKeyPairGenerator()
                .generateKeys(_elGamalEncryptionParameters, _numKeys);

        _pubKey = _cryptoElGamalKeyPair.getPublicKeys();
        _privKey = _cryptoElGamalKeyPair.getPrivateKeys();

        _reEncrypter = _cryptoElGamalFactory.createReEncrypter(_pubKey);
    }

    @Test
    public void givenAValidInputWhenRecryptThenSucceed()
            throws GeneralCryptoLibException {

        ZpGroupElement element =
            new ZpGroupElement(new BigInteger("9"), _group_g2_q11);

        List<ZpGroupElement> messages = new ArrayList<ZpGroupElement>(6);
        for (int i = 0; i < _numKeys; i++) {
            messages.add(element);
        }

        CryptoElGamalEncrypter encrypter =
            _cryptoElGamalFactory.createEncrypter(_pubKey);

        ElGamalEncrypterValues encryptedMessages =
            encrypter.encryptGroupElements(messages);

        CryptoElGamalReEncrypter reEncrypter =
            _cryptoElGamalFactory.createReEncrypter(_pubKey);

        ElGamalEncrypterValues reEncryptedMessages =
            reEncrypter.reEncryptGroupElements(encryptedMessages
                .getComputationValues());

        CryptoElGamalDecrypter decrypter =
            _cryptoElGamalFactory.createDecrypter(_privKey);

        List<ZpGroupElement> decryptedMessages =
            decrypter.decrypt(reEncryptedMessages.getComputationValues(),
                true);

        assertEquals("The decrypted message is not the original message.",
            decryptedMessages, messages);
    }

    @Test
    public void testWhenPreComputeThenGammaAndPrePhisAreGroupMembers()
            throws GeneralCryptoLibException {

        ElGamalComputationsValues preComputedValues =
            _reEncrypter.preCompute().getComputationValues();

        // Assert that has gamma and prephis
        assertHasGammaAndPrePhis(preComputedValues);

        // Assert that precomputation values are group members
        assertPreComputeValuesAreGroupMembers(preComputedValues);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testKeySize2CiphertextSize3prePhiSize2ThenException()
            throws GeneralCryptoLibException {

        ZpGroupElement element =
            new ZpGroupElement(new BigInteger("9"), _group_g2_q11);

        ZpGroupElement gamma =
            new ZpGroupElement(new BigInteger("7"), _group_g2_q11);

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

        phis.add(new ZpGroupElement(BigInteger.ONE, _group_g2_q11));
        phis.add(new ZpGroupElement(new BigInteger("2"), _group_g2_q11));
        phis.add(new ZpGroupElement(new BigInteger("4"), _group_g2_q11));

        ElGamalComputationsValues ciphertext =
            new ElGamalComputationsValues(gamma, phis);

        List<ZpGroupElement> prephis =
            new ArrayList<ZpGroupElement>(_numKeys);
        for (int i = 0; i < _numKeys; i++) {
            prephis.add(element);
        }

        ElGamalComputationsValues preComputedValues =
            new ElGamalComputationsValues(element, prephis);

        Exponent randomExponent =
            new Exponent(_group_g2_q11.getQ(), new BigInteger("2"));

        ElGamalEncrypterValues values =
            new ElGamalEncrypterValues(randomExponent, preComputedValues);

        _reEncrypter.reEncryptGroupElements(ciphertext, values);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testKeySize2CiphertextSize2prePhiSize3ThenException()
            throws GeneralCryptoLibException {

        ZpGroupElement element =
            new ZpGroupElement(new BigInteger("9"), _group_g2_q11);

        ZpGroupElement gamma =
            new ZpGroupElement(new BigInteger("7"), _group_g2_q11);

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

        phis.add(new ZpGroupElement(BigInteger.ONE, _group_g2_q11));
        phis.add(new ZpGroupElement(new BigInteger("2"), _group_g2_q11));

        ElGamalComputationsValues ciphertext =
            new ElGamalComputationsValues(gamma, phis);

        List<ZpGroupElement> prephis = new ArrayList<ZpGroupElement>(3);
        for (int i = 0; i < 3; i++) {
            prephis.add(element);
        }

        ElGamalComputationsValues preComputedValues =
            new ElGamalComputationsValues(element, prephis);

        Exponent randomExponent =
            new Exponent(_group_g2_q11.getQ(), new BigInteger("2"));

        ElGamalEncrypterValues values =
            new ElGamalEncrypterValues(randomExponent, preComputedValues);

        _reEncrypter.reEncryptGroupElements(ciphertext, values);
    }

    private void assertHasGammaAndPrePhis(
            final ElGamalComputationsValues preComputationValues) {
        Assert.assertNotNull("The gamma should not be null",
            preComputationValues.getGamma());

        Assert.assertNotNull("The phis set should not be null",
            preComputationValues.getPhis());
    }

    private void assertPreComputeValuesAreGroupMembers(
            final ElGamalComputationsValues preComputationValues) {

        String errorMessage =
            "All precomputed elements should be group members";

        Assert.assertEquals(errorMessage, true,
            _group_g2_q11.isGroupMember(preComputationValues.getGamma()));

        for (ZpGroupElement element : preComputationValues.getPhis()) {
            Assert.assertEquals(errorMessage, true,
                _group_g2_q11.isGroupMember(element));
        }
    }

    private static ElGamalPolicy getCryptoElGamalPolicy() {

        return new ElGamalPolicy() {

            @Override
            public ConfigGroupType getGroupType() {
                return ConfigGroupType.ZP_2048_256;
            }

            @Override
            public ConfigSecureRandomAlgorithmAndProvider getSecureRandomAlgorithmAndProvider() {

                switch (OperatingSystem.current()) {

                case WINDOWS:
                    return ConfigSecureRandomAlgorithmAndProvider.PRNG_SUN_MSCAPI;
                case LINUX:
                    return ConfigSecureRandomAlgorithmAndProvider.NATIVE_PRNG_SUN;
                default:
                    throw new CryptoLibException("OS not supported");
                }
            }
        };
    }
}
