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

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

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.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.mixnet.api.MixingServiceAPI;
import com.scytl.products.ov.mixnet.api.WarmUpService;
import com.scytl.products.ov.mixnet.commons.beans.WarmUpOutput;
import com.scytl.products.ov.mixnet.commons.configuration.ApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedMixerConfig;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedVerifierConfig;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationConstants;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationsProvider;
import com.scytl.products.ov.mixnet.commons.homomorphic.Randomness;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamal;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamalPlaintext;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamalRandomness;
import com.scytl.products.ov.mixnet.spring.MixerSpringConfig;

import mockit.Deencapsulation;
import mockit.Mock;
import mockit.MockUp;

@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {MixerSpringConfig.class })
public class MixingControllerTest {

    @Autowired
    private MixingController _target;

    @Autowired
    private VerifyingController verifyingController;

    @Autowired
    private MixingServiceAPI mixingService;

    private WarmUpService warmUpService = new WarmUpService();

    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    @Test(expected = IllegalArgumentException.class)
    public void throw_expected_exception_when_passed_null_config_when_mix() {

        ApplicationConfig applicationConfigNull = null;
        LocationsProvider locationsProviderNull = null;
        _target.mix(applicationConfigNull, locationsProviderNull);

    }

    @Test(expected = IllegalArgumentException.class)
    public void throw_expected_exception_when_passed_null_config_when_warm_up() {

        ApplicationConfig applicationConfigNull = null;
        _target.warmUp(applicationConfigNull);

    }

    @Test
    public void warm_up_the_mixing_and_mix_with_1_batch_and_warm_up_size_equal_to_the_number_of_ballots()
            throws IOException, GeneralCryptoLibException {
        performWarmUpAndMixAndVerify(256, 512, 512);
    }

    @Test
    public void warm_up_the_mixing_and_mix_with_1_batch_and_warm_up_size_higher_to_the_number_of_ballots()
            throws IOException, GeneralCryptoLibException {
        performWarmUpAndMixAndVerify(256, 512, 1000);
    }

    @Test
    public void warm_up_the_mixing_and_mix_with_2_batch_and_warm_up_size_equal_to_the_number_of_ballots()
            throws IOException, GeneralCryptoLibException {
        performWarmUpAndMixAndVerify(256, 256, 512);
    }

    @Test
    public void warm_up_the_mixing_and_mix_with_1_batch_and_warm_up_size_lower_than__the_number_of_ballots()
            throws IOException, GeneralCryptoLibException {
        performWarmUpAndMixAndVerify(256, 512, 200);
    }

    @Test
    public void warm_up_the_mixing_and_mix_with_1_batch_and_warm_up_size_of_1()
            throws IOException, GeneralCryptoLibException {
        performWarmUpAndMixAndVerify(256, 512, 1);
    }

    @Test
    public void warm_up_the_mixing_and_mix_with_2_batch_and_warm_up_size_lower_than__the_number_of_ballots()
            throws IOException, GeneralCryptoLibException {
        performWarmUpAndMixAndVerify(256, 256, 200);
    }

    private void performWarmUpAndMixAndVerify(final int numBitsP, final int minimumBatchSize, final int warmUpSize)
            throws IOException, GeneralCryptoLibException {

        Path tempFolder = temporaryFolder.newFolder().toPath();

        FileBasedApplicationConfig applicationConfig = new FileBasedApplicationConfig();

        applicationConfig.setConcurrencyLevel(2);
        applicationConfig.setMinimumBatchSize(minimumBatchSize);
        applicationConfig.setNumberOfParallelBatches(1);
        applicationConfig.setNumOptions(1);
        final FileBasedMixerConfig mixer = new FileBasedMixerConfig();
        mixer.setOutputDirectory(tempFolder.toString());
        mixer.setMix(true);
        mixer.setWarmUpSize(warmUpSize);
        final Map<String, String> inputMap = new HashMap<>();
        final String inputFolder = "src/test/resources/input/n512_p" + numBitsP + "_q32";
        inputMap.put(LocationConstants.ENCRYPTION_PARAMETERS_JSON_TAG,
            "src/test/resources/input/encryption_parameters");
        inputMap.put(LocationConstants.PUBLIC_KEY_JSON_TAG, inputFolder + "/public_key");
        inputMap.put(LocationConstants.BALLOT_DECRYPTION_KEY_JSON_TAG, inputFolder + "/private_key");
        inputMap.put(LocationConstants.ENCRYPTED_BALLOTS_JSON_TAG, inputFolder + "/encrypted_ballots");

        mixer.setInput(inputMap);
        applicationConfig.setMixer(mixer);
        final FileBasedVerifierConfig verifier = new FileBasedVerifierConfig();
        verifier.setVerify(true);
        verifier.setInputDirectory(tempFolder.toString());
        verifier.setOutputDirectory(tempFolder.toString());
        applicationConfig.setVerifier(verifier);

        final ZpSubgroup[] group = new ZpSubgroup[1];

        @SuppressWarnings("unused")
        MockUp<WarmUpService> mockUp = new MockUp<WarmUpService>() {

            @Mock
            Randomness[] getRandomness(final ZpSubgroup zpGroup, final Integer warmUpSize) {
                group[0] = zpGroup;
                final Randomness[] returnValue = new Randomness[warmUpSize];
                for (int i = 1; i <= returnValue.length; i++) {

                    try {
                        returnValue[i - 1] = new GjosteenElGamalRandomness(i, zpGroup.getQ());
                    } catch (GeneralCryptoLibException e) {
                        throw new CryptoLibException(e);
                    }
                }
                return returnValue;
            }
        };

        _target.warmUp(FileBasedApplicationConfig.convertToStreamConfig(applicationConfig));

        final LocationsProvider locationsProvider =
            LocationsProvider.build(FileBasedApplicationConfig.convertToStreamConfig(applicationConfig));
        ElGamalPublicKey publicKey = warmUpService.loadPublicKey(locationsProvider);

        final BigInteger order = group[0].getQ();
        final GjosteenElGamal _elGamalEncrypter = new GjosteenElGamal(group[0], publicKey);

        final ZpGroupElement[] arrayOfIdentityElement =
            warmUpService.getArrayOfIdentityElement(group[0], applicationConfig.getNumOptions());

        WarmUpOutput warmUpOutput = Deencapsulation.getField(mixingService, "warmUpOutput");
        final List<Ciphertext> preComputations = warmUpOutput.getPreComputations();

        for (int i = 1; i <= warmUpSize; i++) {

            try {
                final Ciphertext ciphertext =
                    _elGamalEncrypter.encrypt(new GjosteenElGamalPlaintext(arrayOfIdentityElement),
                        new GjosteenElGamalRandomness(new GjosteenElGamalRandomness(i, order).getExponent()));

                assertThat(preComputations.get(i - 1).getElements().get(0).getValue()
                    .compareTo(ciphertext.getGamma().getValue()) == 0, is(true));

                assertThat(preComputations.get(i - 1).getElements().get(1).getValue()
                    .compareTo(ciphertext.getPhis().get(0).getValue()) == 0, is(true));

            } catch (GeneralCryptoLibException e) {
                throw new CryptoLibException(e);
            }
        }

        _target.mix(FileBasedApplicationConfig.convertToStreamConfig(applicationConfig), locationsProvider);

        applicationConfig.getMixer().setOutputDirectory(null);
        final Boolean verify = verifyingController
            .verify(FileBasedApplicationConfig.convertToStreamConfig(applicationConfig), locationsProvider);

        assertThat(verify, is(true));

    }
}
