/*
 * 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.sdm.config.spring.batch;

import static com.scytl.products.ov.config.commons.Constants.CHOICE_CODES_KEY_DELIMITER;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_BALLOTBOX_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_BALLOT_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_BASE_PATH;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_ELECTIONEVENT_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_ELECTORALAUTHORITY_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_JOB_INSTANCE_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_KEY_FOR_PROTECTING_KEYSTORE_PW;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_NUMBER_VOTING_CARDS;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_TENANT_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VALIDITY_PERIOD_END;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VALIDITY_PERIOD_START;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VERIFICATIONCARDSET_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VOTINGCARDSET_ID;
import static com.scytl.products.ov.config.commons.Constants.JOB_IN_PARAM_VOTING_CARD_SET_NAME;
import static com.scytl.products.ov.constants.Constants.VERIFICATION_CARDS_KEY_PAIR_DIRECTORY;
import static com.scytl.products.ov.constants.Constants.OFFLINE_DIRECTORY;
import static com.scytl.products.ov.constants.Constants.KEY;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.codehaus.jettison.json.JSONObject;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.test.JobLauncherTestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.certificates.CertificatesServiceAPI;
import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricServiceFactoryHelper;
import com.scytl.cryptolib.certificates.bean.CertificateData;
import com.scytl.cryptolib.certificates.bean.RootCertificateData;
import com.scytl.cryptolib.certificates.bean.ValidityDates;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.utils.X509CertificateTestDataGenerator;
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.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalKeyPairGenerator;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.commons.beans.Ballot;
import com.scytl.products.ov.commons.beans.Contest;
import com.scytl.products.ov.commons.beans.ElectionAttributes;
import com.scytl.products.ov.commons.beans.ElectionEvent;
import com.scytl.products.ov.commons.beans.ElectionOption;
import com.scytl.products.ov.commons.beans.EncryptionParameters;
import com.scytl.products.ov.commons.beans.Question;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.dto.ChoiceCodeGenerationDTO;
import com.scytl.products.ov.commons.dto.ChoiceCodeGenerationOutput;
import com.scytl.products.ov.commons.dto.ChoiceCodeGenerationResPayload;
import com.scytl.products.ov.commons.dto.ElGamalComputationsValuesCodec;
import com.scytl.products.ov.commons.dto.ElGamalComputationsValuesCodecImpl;
import com.scytl.products.ov.commons.sign.CryptolibPayloadSigner;
import com.scytl.products.ov.commons.sign.PayloadSigner;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.config.commands.voters.JobExecutionObjectContext;
import com.scytl.products.ov.config.commands.voters.VotersParametersHolder;
import com.scytl.products.ov.config.commons.Constants;
import com.scytl.products.ov.config.commons.beans.spring.batch.SensitiveAwareJobParametersBuilder;
import com.scytl.products.ov.config.spring.SecureLoggingConfig;
import com.scytl.products.ov.config.spring.SpringConfigServices;
import com.scytl.products.ov.config.spring.batch.CommonBatchInfrastructure;
import com.scytl.products.ov.datapacks.beans.CreateVotingCardSetCertificatePropertiesContainer;
import com.scytl.products.ov.utils.ConfigObjectMapper;
import com.scytl.products.ov.utils.EncryptionParametersLoader;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
public class VotingCardGenerationJobTest {

    private final static String ELECTION_EVENT_ID = "95a803354a1c4d39959ad7fedd3a41a0";

    @Configuration
    @EnableBatchProcessing
    @Import({SecureLoggingConfig.class, SpringConfigServices.class, CommonBatchInfrastructure.class,
            ConfigJobConfigProduct.class })
    static class JobConfiguration {

        @Bean
        JobLauncherTestUtils jobLauncherTestUtils() {
            return new JobLauncherTestUtils();
        }

        @Bean
        ElGamalComputationsValuesCodec elGamalComputationsValuesCodec() {
            return ElGamalComputationsValuesCodecImpl.getInstance();
        }

        @Bean
        ElGamalKeyPair primeEncryptionKeys(ElGamalServiceAPI elGamalService,
                EncryptionParametersLoader encryptionParametersLoader) throws GeneralCryptoLibException, IOException {
            CryptoAPIElGamalKeyPairGenerator generator = elGamalService.getElGamalKeyPairGenerator();
            Path file = Paths.get("src/test/resources/encryptionParameters.json").toAbsolutePath();
            EncryptionParameters encryptionParameters = encryptionParametersLoader.load(file);
            ElGamalEncryptionParameters parameters =
                new ElGamalEncryptionParameters(new BigInteger(encryptionParameters.getP()),
                    new BigInteger(encryptionParameters.getQ()), new BigInteger(encryptionParameters.getG()));
            return generator.generateKeys(parameters, 1);
        }
    }

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private JobExecutionObjectContext jobExecutionObjectContext;

    @Autowired
    private ElGamalServiceAPI elGamalService;

    @Autowired
    private ElGamalKeyPair primeEncryptionKeys;

    @Autowired
    private ElGamalComputationsValuesCodec codec;

    @Autowired
    private CertificatesServiceAPI certificateService;

    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    private KeyPair nodeSigningKeyPair = null;

    private X509Certificate[] nodeSigningCertificate = null;

    @BeforeClass
    public static void setup() throws GeneralCryptoLibException, IOException {
        Security.addProvider(new BouncyCastleProvider());
    }

    private void generateVerificationKeyPairFile() throws GeneralCryptoLibException, IOException {
        final String eeid = ELECTION_EVENT_ID;
        final String verificationCardID = "aaa";
        final String verificationCardSetID = "verificationCardSetId";

        final String baseFolder = temporaryFolder.newFolder(eeid).getAbsolutePath();

        Path file = Paths.get("src/test/resources/encryptionParameters.json").toAbsolutePath();
        EncryptionParameters encryptionParameters = new EncryptionParametersLoader(new ConfigObjectMapper()).load(file);
        ElGamalEncryptionParameters parameters =
            new ElGamalEncryptionParameters(new BigInteger(encryptionParameters.getP()),
                new BigInteger(encryptionParameters.getQ()), new BigInteger(encryptionParameters.getG()));

        ElGamalKeyPair keyPair = new ElGamalService().getElGamalKeyPairGenerator().generateKeys(parameters, 1);

        Path verificationCardsKeyPairsFilePath = Paths.get(baseFolder).resolve(OFFLINE_DIRECTORY)
            .resolve(VERIFICATION_CARDS_KEY_PAIR_DIRECTORY).resolve(verificationCardSetID);

        verificationCardsKeyPairsFilePath.toFile().mkdirs();

        verificationCardsKeyPairsFilePath = verificationCardsKeyPairsFilePath.resolve(verificationCardID + KEY);

        Files.write(verificationCardsKeyPairsFilePath,
            (keyPair.getPrivateKeys().toJson() + System.lineSeparator() + keyPair.getPublicKeys().toJson())
                .getBytes(StandardCharsets.UTF_8));
    }

    @Test
    public void runJob() throws Exception {

        generateVerificationKeyPairFile();

        final JobParameters jobParams = prepareJobParameters();
        final JobExecution jobExecution = jobLauncherTestUtils.launchJob(jobParams);

        while (true) {
            final BatchStatus status = jobExecution.getStatus();
            if (BatchStatus.COMPLETED.equals(status) || BatchStatus.FAILED.equals(status))
                break;
        }

        Assert.assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
    }

    private JobParameters prepareJobParameters() throws Exception {

        int ignored = 1;
        String tenantId = "tenantId";
        String electionEventId = ELECTION_EVENT_ID;
        String ballotId = "ballotId";
        String ballotBoxId = "ballotBoxId";
        String electoralAuthorityId = "electoralAuthorityId";
        String votingCardSetId = "votingCardSetId";
        String keyForProtectingKeystorePassword = "";
        String votingCardSetAlias = "votingCardSetAlias";
        String verificationCardSetId = "verificationCardSetId";
        final String TEST_ENCRYPTION_KEY =
            "{\"publicKey\":{\"zpSubgroup\":{\"g\":\"Ag==\",\"p\":\"Fw==\",\"q\":\"Cw==\"},\"elements\":[\"Ag==\",\"Ag==\"]}}";
        String choiceCodesKeyMessage = new JSONObject().put("publicKey", TEST_ENCRYPTION_KEY).toString();
        String choiceCodesKey1 = choiceCodesKeyMessage;
        String choiceCodesKey2 = choiceCodesKeyMessage;
        String choiceCodesKey3 = choiceCodesKeyMessage;
        String choiceCodesKey4 = choiceCodesKeyMessage;
        List<String> choiceCodesEncryptionKeysAsList = new ArrayList<>();
        choiceCodesEncryptionKeysAsList.add(choiceCodesKey1);
        choiceCodesEncryptionKeysAsList.add(choiceCodesKey2);
        choiceCodesEncryptionKeysAsList.add(choiceCodesKey3);
        choiceCodesEncryptionKeysAsList.add(choiceCodesKey4);
        String choiceCodesJoined = String.join(CHOICE_CODES_KEY_DELIMITER, choiceCodesEncryptionKeysAsList);

        // we need to create the folder structure needed for the job
        // electionEventId/ONLINE/electionInformation/ballots/ballotId/ballotBoxes/ballotBoxId/
        // electionEventId/ONLINE/electoralAuthorities/electoralAuthorityId/
        // electionEventId/ONLINE/voteVerification/verificationCardSetId/
        // electionEventId/OFFLINE
        final String baseFolder = temporaryFolder.getRoot().toPath().resolve(electionEventId).toString();
        Files.createDirectories(
            Paths.get(baseFolder, "ONLINE", "electionInformation", "ballots", ballotId, "ballotBoxes", ballotBoxId));
        Files.createDirectories(Paths.get(baseFolder, "ONLINE", "electoralAuthorities", electoralAuthorityId));
        final Path nodeContributionsPath = Paths.get(baseFolder, "ONLINE", "voteVerification", verificationCardSetId);
        Files.createDirectories(nodeContributionsPath);
        copyEncryptionParameters(baseFolder);

        final Path offlinePath = Files.createDirectories(Paths.get(baseFolder, "OFFLINE"));
        copyCertificates(offlinePath);

        prepareNodeContributions(tenantId, electionEventId, verificationCardSetId, nodeContributionsPath, offlinePath);

        // we need to have a ballot..
        String attributeId = "attrId";
        ElectionOption electionOption = new ElectionOption("eo1", "24083", attributeId);
        ElectionAttributes electionAttributes =
            new ElectionAttributes(attributeId, "eaAlias", Arrays.asList("eo1"), false);
        Question question = new Question("q1", 0, 0, 1, "false", Collections.emptyList(), "blankAttr", "", attributeId);
        Contest contest =
            new Contest("contestId", "title", "", "alias", "options", "false", Arrays.asList(electionOption),
                Arrays.asList(electionAttributes), Arrays.asList(question), "return false", "return false");
        Ballot ballot = new Ballot(ballotId, new ElectionEvent(electionEventId), Arrays.asList(contest));
        CreateVotingCardSetCertificatePropertiesContainer createVotingCardSetCertificateProperties =
            getCreateVotingCardSetCertificateProperties();
        VotersParametersHolder holder =
            new VotersParametersHolder(ignored, ballotId, ballot, ballotBoxId, votingCardSetId, verificationCardSetId,
                electoralAuthorityId, Paths.get(baseFolder), ignored, ignored, electionEventId, ZonedDateTime.now(),
                ZonedDateTime.now().plus(1, ChronoUnit.YEARS), keyForProtectingKeystorePassword, votingCardSetAlias,
                choiceCodesEncryptionKeysAsList, primeEncryptionKeys.getPrivateKeys().toJson(),
                PemUtils.certificateToPem(nodeSigningCertificate[2]), createVotingCardSetCertificateProperties);

        String jobInstanceId = UUID.randomUUID().toString();
        jobExecutionObjectContext.put(jobInstanceId, holder, VotersParametersHolder.class);

        final SensitiveAwareJobParametersBuilder jobParametersBuilder = new SensitiveAwareJobParametersBuilder();
        jobParametersBuilder.addString(JOB_IN_PARAM_JOB_INSTANCE_ID, jobInstanceId, true);
        jobParametersBuilder.addString(JOB_IN_PARAM_TENANT_ID, tenantId, true);
        jobParametersBuilder.addString(JOB_IN_PARAM_ELECTIONEVENT_ID, electionEventId, true);
        jobParametersBuilder.addString(JOB_IN_PARAM_BALLOTBOX_ID, ballotBoxId, true);
        jobParametersBuilder.addString(JOB_IN_PARAM_BALLOT_ID, ballotId);
        jobParametersBuilder.addString(JOB_IN_PARAM_ELECTORALAUTHORITY_ID, electoralAuthorityId);
        jobParametersBuilder.addString(JOB_IN_PARAM_VOTINGCARDSET_ID, votingCardSetId);
        jobParametersBuilder.addString(JOB_IN_PARAM_NUMBER_VOTING_CARDS, Integer.toString(ignored));
        jobParametersBuilder.addString(JOB_IN_PARAM_VOTING_CARD_SET_NAME, votingCardSetAlias);
        jobParametersBuilder.addString(JOB_IN_PARAM_VALIDITY_PERIOD_START,
            holder.getCertificatesStartValidityPeriod().toString());
        jobParametersBuilder.addString(JOB_IN_PARAM_VALIDITY_PERIOD_END,
            holder.getCertificatesEndValidityPeriod().toString());
        jobParametersBuilder.addSensitiveString(JOB_IN_PARAM_KEY_FOR_PROTECTING_KEYSTORE_PW,
            keyForProtectingKeystorePassword);
        jobParametersBuilder.addString(JOB_IN_PARAM_BASE_PATH, baseFolder);
        jobParametersBuilder.addString(JOB_IN_PARAM_VERIFICATIONCARDSET_ID, verificationCardSetId);
        jobParametersBuilder.addString(Constants.JOB_IN_PARAM_CHOICE_CODES_ENCRYPTION_KEY, choiceCodesJoined);
        jobParametersBuilder.addString(Constants.JOB_IN_PARAM_PRIME_ENCRYPTION_PRIVATE_KEY_JSON,
            primeEncryptionKeys.getPrivateKeys().toJson());
        jobParametersBuilder.addString(Constants.JOB_IN_PARAM_PLATFORM_ROOT_CA_CERTIFICATE,
            PemUtils.certificateToPem(nodeSigningCertificate[2]));

        return jobParametersBuilder.toJobParameters();
    }

    private CreateVotingCardSetCertificatePropertiesContainer getCreateVotingCardSetCertificateProperties()
            throws IOException {

        CreateVotingCardSetCertificatePropertiesContainer createVotingCardSetCertificateProperties =
            new CreateVotingCardSetCertificatePropertiesContainer();

        String votingCardSetCertificatePropertiesPath = "properties/votingCardSetX509Certificate.properties";
        Properties loadedVotingCardSetCertificateProperties =
            getCertificateParameters(votingCardSetCertificatePropertiesPath);

        String verificationCardSetCertificatePropertiesPath =
            "properties/verificationCardSetX509Certificate.properties";
        Properties loadedVerificationCardSetCertificateProperties =
            getCertificateParameters(verificationCardSetCertificatePropertiesPath);

        String credentailSignCertificatePropertiesPath = "properties/credentialSignX509Certificate.properties";
        Properties loadedcredentailSignCertificateProperties =
            getCertificateParameters(credentailSignCertificatePropertiesPath);

        String credentailAuthCertificatePropertiesPath = "properties/credentialSignX509Certificate.properties";
        Properties loadedCredentailAuthCertificateProperties =
            getCertificateParameters(credentailAuthCertificatePropertiesPath);

        createVotingCardSetCertificateProperties
            .setVotingCardSetCertificateProperties(loadedVotingCardSetCertificateProperties);
        createVotingCardSetCertificateProperties
            .setVerificationCardSetCertificateProperties(loadedVerificationCardSetCertificateProperties);
        createVotingCardSetCertificateProperties
            .setCredentialSignCertificateProperties(loadedcredentailSignCertificateProperties);
        createVotingCardSetCertificateProperties
            .setCredentialAuthCertificateProperties(loadedCredentailAuthCertificateProperties);

        return createVotingCardSetCertificateProperties;
    }

    private Properties getCertificateParameters(String path) throws IOException {

        final Properties props = new Properties();

        try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
            props.load(input);
        }

        return props;
    }

    private void copyEncryptionParameters(final String baseFolder) throws Exception {
        final Path encParams = Paths.get("src/test/resources/encryptionParameters.json").toAbsolutePath();
        Files.copy(encParams, Paths.get(baseFolder).resolve(encParams.toFile().getName()),
            StandardCopyOption.REPLACE_EXISTING);
    }

    private void prepareNodeContributions(String tenantId, String electionEventId, String verificationCardId,
            final Path destination, final Path offlinePath) throws Exception {

        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(primeEncryptionKeys.getPublicKeys());
        ZpSubgroup group = primeEncryptionKeys.getPrivateKeys().getGroup();
        ZpGroupElement prime = new ZpGroupElement(BigInteger.valueOf(24083), group);
        ZpGroupElement powerOfPrime = prime.multiply(prime);
        ElGamalComputationsValues key = encrypter.encryptGroupElements(singletonList(prime)).getComputationValues();
        ElGamalComputationsValues value =
            encrypter.encryptGroupElements(singletonList(powerOfPrime)).getComputationValues();
        Map<ElGamalComputationsValues, ElGamalComputationsValues> map = singletonMap(key, value);
        String choiceCodes = codec.encodeMap(map);

        ZpGroupElement bckElement = new ZpGroupElement(BigInteger.valueOf(2), group);
        ElGamalComputationsValues encryptedBckValues =
            encrypter.encryptGroupElements(singletonList(bckElement)).getComputationValues();
        String encryptedBck = codec.encodeSingle(encryptedBckValues);

        ZpGroupElement computedBckElement = bckElement.multiply(bckElement);
        ElGamalComputationsValues computedBckValues =
            encrypter.encryptGroupElements(singletonList(computedBckElement)).getComputationValues();
        String computedBck = codec.encodeSingle(computedBckValues);

        List<ChoiceCodeGenerationDTO<ChoiceCodeGenerationResPayload>> nodeContributions = new ArrayList<>();

        UUID correlationId = new UUID(0, 0);
        String requestId = "1";

        ChoiceCodeGenerationDTO<ChoiceCodeGenerationResPayload> ccDto1 = new ChoiceCodeGenerationDTO<>(correlationId,
            requestId, new ChoiceCodeGenerationResPayload(tenantId, electionEventId, verificationCardId, 0));
        ChoiceCodeGenerationDTO<ChoiceCodeGenerationResPayload> ccDto2 = new ChoiceCodeGenerationDTO<>(correlationId,
            requestId, new ChoiceCodeGenerationResPayload(tenantId, electionEventId, verificationCardId, 0));
        ChoiceCodeGenerationDTO<ChoiceCodeGenerationResPayload> ccDto3 = new ChoiceCodeGenerationDTO<>(correlationId,
            requestId, new ChoiceCodeGenerationResPayload(tenantId, electionEventId, verificationCardId, 0));
        ChoiceCodeGenerationDTO<ChoiceCodeGenerationResPayload> ccDto4 = new ChoiceCodeGenerationDTO<>(correlationId,
            requestId, new ChoiceCodeGenerationResPayload(tenantId, electionEventId, verificationCardId, 0));

        if (nodeSigningKeyPair == null) {
            nodeSigningKeyPair = asymmetricService().getKeyPairForSigning();
            nodeSigningCertificate = createCertificateChain(1, nodeSigningKeyPair);
        }

        ccDto1.getPayload().setChoiceCodeGenerationOutputList(Arrays.asList(new ChoiceCodeGenerationOutput("aaa",
            encryptedBck, computedBck, choiceCodes, "derivedKey1", "derivedKey2")));
        sign(ccDto1.getPayload());
        ccDto2.getPayload().setChoiceCodeGenerationOutputList(Arrays.asList(new ChoiceCodeGenerationOutput("aaa",
            encryptedBck, computedBck, choiceCodes, "derivedKey1", "derivedKey2")));
        sign(ccDto2.getPayload());
        ccDto3.getPayload().setChoiceCodeGenerationOutputList(Arrays.asList(new ChoiceCodeGenerationOutput("aaa",
            encryptedBck, computedBck, choiceCodes, "derivedKey1", "derivedKey2")));
        sign(ccDto3.getPayload());
        ccDto4.getPayload().setChoiceCodeGenerationOutputList(Arrays.asList(new ChoiceCodeGenerationOutput("aaa",
            encryptedBck, computedBck, choiceCodes, "derivedKey1", "derivedKey2")));
        sign(ccDto4.getPayload());

        nodeContributions.addAll(Arrays.asList(ccDto1, ccDto2, ccDto3, ccDto4));

        Path file = destination.resolve("nodeContributions.json");
        ObjectMappers.toJson(file, nodeContributions);
    }

    @Bean
    public AsymmetricServiceAPI asymmetricService() {
        GenericObjectPoolConfig config = new GenericObjectPoolConfig();
        config.setMaxTotal(Integer.parseInt("50"));
        try {
            return AsymmetricServiceFactoryHelper.getFactoryOfThreadSafeServices(config).create();
        } catch (GeneralCryptoLibException e) {
            throw new IllegalStateException("Exception while trying to create AsymmetricService", e);
        }
    }

    @Bean
    public PayloadSigner payloadSigner() {
        return new CryptolibPayloadSigner(asymmetricService());
    }

    private void sign(ChoiceCodeGenerationResPayload payload)
            throws PayloadSignatureException, GeneralCryptoLibException {
        payload.setSignature(payloadSigner().sign(payload, nodeSigningKeyPair.getPrivate(), nodeSigningCertificate));
    }

    private void copyCertificates(final Path offline) throws Exception {
        final Path certificates = Paths.get("src/test/resources/certificates");
        Files.list(certificates).forEach(f -> {
            try {
                Files.copy(f, offline.resolve(f.toFile().getName()), StandardCopyOption.REPLACE_EXISTING);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }

    /**
     * Create a certificate chain with a configurable number of intermediate
     * CAs.
     * 
     * @param intermediateCAs
     *            the number of intermediate CAs to build
     * @return a certificate chain
     * @throws GeneralCryptoLibException
     */
    private X509Certificate[] createCertificateChain(int intermediateCAs, KeyPair signingKeyPair)
            throws GeneralCryptoLibException {
        ZonedDateTime startDate = ZonedDateTime.now();
        ZonedDateTime endDate = startDate.plus(1, ChronoUnit.HOURS);
        ValidityDates validityDates =
            new ValidityDates(Date.from(startDate.toInstant()), Date.from(endDate.toInstant()));

        List<CryptoAPIX509Certificate> certificateChainBuilder = new ArrayList<>();
        // Create the self-signed root CA.
        X509DistinguishedName rootDN = getDistinguishedName("Root CA");
        KeyPair rootKeyPair = signingKeyPair;
        RootCertificateData rootCertificateData = X509CertificateTestDataGenerator.getRootCertificateData(rootKeyPair);
        rootCertificateData.setSubjectDn(rootDN);
        rootCertificateData.setValidityDates(validityDates);
        CryptoAPIX509Certificate rootCertificate =
            certificateService.createRootAuthorityX509Certificate(rootCertificateData, rootKeyPair.getPrivate());

        certificateChainBuilder.add(rootCertificate);

        // Create the required intermediate certificates.
        PrivateKey caSigningKey = rootKeyPair.getPrivate();
        X509DistinguishedName caIssuerDN = rootDN;
        for (int i = 0; i < intermediateCAs; i++) {
            // Build the intermediate CA.
            KeyPair intermediateKeyPair = signingKeyPair;
            X509DistinguishedName intermediateDN = getDistinguishedName("Intermediate CA #" + i);
            CertificateData intermediateCertificateData =
                X509CertificateTestDataGenerator.getCertificateData(intermediateKeyPair);
            intermediateCertificateData.setSubjectDn(intermediateDN);
            intermediateCertificateData.setIssuerDn(caIssuerDN);
            intermediateCertificateData.setValidityDates(validityDates);
            CryptoAPIX509Certificate intermediateCertificate = certificateService
                .createIntermediateAuthorityX509Certificate(intermediateCertificateData, caSigningKey);
            // Add the intermediate CA certificate to the certificate chain
            // builder.
            certificateChainBuilder.add(intermediateCertificate);
            // Keep this CA's private key to sign the next certificate.
            caSigningKey = intermediateKeyPair.getPrivate();
            // Keep this CA's subject DN to assign to the next certificate as
            // issuer.
            caIssuerDN = intermediateDN;
        }

        // Create the leaf certificate.
        KeyPair leafKeyPair = signingKeyPair;
        CertificateData leafCertificateData = X509CertificateTestDataGenerator.getCertificateData(leafKeyPair);
        leafCertificateData.setSubjectDn(getDistinguishedName("Leaf certificate"));
        leafCertificateData.setIssuerDn(caIssuerDN);
        leafCertificateData.setValidityDates(validityDates);
        CryptoAPIX509Certificate leafCertificate =
            certificateService.createSignX509Certificate(leafCertificateData, caSigningKey);
        certificateChainBuilder.add(leafCertificate);

        // Create a certificate array from the reversed certificate chain
        // builder.
        Collections.reverse(certificateChainBuilder);
        return certificateChainBuilder.stream().map(CryptoAPIX509Certificate::getCertificate)
            .toArray(X509Certificate[]::new);
    }

    /**
     * Builds an X509 distinguished name fit for test certificates.
     * 
     * @param commonName
     *            the common name
     * @return an X509 distinguished name
     * @throws GeneralCryptoLibException
     */
    private X509DistinguishedName getDistinguishedName(String commonName) throws GeneralCryptoLibException {
        return new X509DistinguishedName.Builder(commonName, "XX").addOrganization("Scytl").addLocality("Barcelona")
            .build();
    }

}
