/*
 * 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.config.commands.electionevent;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.write;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.Security;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.io.FileUtils;
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.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.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.api.scytl.keystore.ScytlKeyStoreServiceAPI;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.certificates.service.CertificatesService;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomString;
import com.scytl.products.oscore.logging.api.writer.LoggingWriter;
import com.scytl.products.ov.commons.beans.AuthenticationParams;
import com.scytl.products.ov.commons.beans.ElectionInformationParams;
import com.scytl.products.ov.commons.beans.EncryptionParameters;
import com.scytl.products.ov.commons.beans.VotingWorkflowContextData;
import com.scytl.products.ov.config.commands.electionevent.datapacks.beans.ElectionInputDataPack;
import com.scytl.products.ov.config.commands.electionevent.datapacks.generators.ElGamalCredentialDataPackGenerator;
import com.scytl.products.ov.config.commands.electionevent.datapacks.generators.ElectionCredentialDataPackGenerator;
import com.scytl.products.ov.config.commands.progress.ProgressManager_old;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.datapacks.beans.ConfigurationInput;
import com.scytl.products.ov.datapacks.beans.CreateElectionEventCertificatePropertiesContainer;
import com.scytl.products.ov.datapacks.generators.CertificateDataBuilder;
import com.scytl.products.ov.datapacks.generators.X509CertificateGenerator;
import com.scytl.products.ov.datapacks.helpers.ReplacementsHolder;
import com.scytl.products.ov.readers.ConfigurationInputReader;
import com.scytl.products.ov.utils.ConfigObjectMapper;
import com.scytl.products.ov.utils.EncryptionParametersLoader;

import mockit.Deencapsulation;
import mockit.Mocked;

public class CreateElectionEventSerializerTest {

    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static ConfigurationInputReader configurationInputReader;

    private static AsymmetricServiceAPI asymmetricService;

    private static CertificatesServiceAPI certificatesServices;

    private static CertificateDataBuilder certificateDataBuilder;

    private static X509CertificateGenerator x509CertificateGenerator;

    private static ScytlKeyStoreServiceAPI storesService;

    private static PrimitivesServiceAPI primitivesService;

    private static ElGamalServiceAPI elGamalService;

    private static CreateElectionEventSerializer createElectionEventSerializer;

    private static CryptoAPIRandomString cryptoRandomString;

    private static ElectionCredentialDataPackGenerator electionCredentialDataPackGenerator;

    private static ElGamalCredentialDataPackGenerator elGamalCredentialDataPackGenerator;

    private static EncryptionParametersLoader encryptionParametersLoader;

    private static CreateElectionEventGenerator createElectionEventGenerator;

    private static ProgressManager_old _votersProgressManager = new ProgressManager_old();

    private final String _eeid = "314bd34dcf6e4de4b771a92fa3849d3d";

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        asymmetricService = new AsymmetricService();
        certificatesServices = new CertificatesService();
        storesService = new ScytlKeyStoreService();
        primitivesService = new PrimitivesService();
        elGamalService = new ElGamalService();

        configurationInputReader = new ConfigurationInputReader();
        certificateDataBuilder = new CertificateDataBuilder();

        x509CertificateGenerator = new X509CertificateGenerator(certificatesServices, certificateDataBuilder);
        createElectionEventSerializer = new CreateElectionEventSerializer(primitivesService);
        cryptoRandomString = primitivesService.get32CharAlphabetCryptoRandomString();

        electionCredentialDataPackGenerator = new ElectionCredentialDataPackGenerator(asymmetricService,
            x509CertificateGenerator, storesService, primitivesService, cryptoRandomString);

        elGamalCredentialDataPackGenerator =
            new ElGamalCredentialDataPackGenerator(elGamalService, storesService, primitivesService);
        encryptionParametersLoader = new EncryptionParametersLoader(new ConfigObjectMapper());
        createElectionEventGenerator = new CreateElectionEventGenerator(electionCredentialDataPackGenerator,
            elGamalCredentialDataPackGenerator, encryptionParametersLoader, _votersProgressManager);
    }

    @AfterClass
    public static void cleanup() throws IOException {

        FileUtils.deleteDirectory(new File("./target/output"));
    }

    @Test
    public void serializeTest(@Mocked final LoggingWriter logWriter)
            throws IOException, GeneralCryptoLibException, URISyntaxException, ConfigurationException {

        final Properties inputProperties = new Properties();
        inputProperties.load(this.getClass().getClassLoader().getResourceAsStream("properties/config.properties"));

        final ElectionInputDataPack electionInputDataPack = new ElectionInputDataPack();
        electionInputDataPack.setEeid(_eeid);

        final ReplacementsHolder replacementsHolder = new ReplacementsHolder(_eeid);
        electionInputDataPack.setReplacementsHolder(replacementsHolder);

        setInputDataProperties(inputProperties, electionInputDataPack);

        final String challengeResExpTime = (String) inputProperties.get("challengeResExpTime");
        final String authTokenExpTime = (String) inputProperties.get("authTokenExpTime");
        final String challengeLength = (String) inputProperties.get("challengeLength");
        final String numVotesPerVotingCard = (String) inputProperties.get("numVotesPerVotingCard");
        final String numVotesPerAuthToken = (String) inputProperties.get("numVotesPerAuthToken");
        final String maxNumberOfAttempts = (String) inputProperties.get("maxNumberOfAttempts");

        final ElectionInformationParams electionInformationParams =
            new ElectionInformationParams(numVotesPerVotingCard, numVotesPerAuthToken);

        final AuthenticationParams authenticationParams =
            new AuthenticationParams(challengeResExpTime, authTokenExpTime, challengeLength);

        VotingWorkflowContextData votingWorkflowContextData = new VotingWorkflowContextData();
        votingWorkflowContextData.setMaxNumberOfAttempts(maxNumberOfAttempts);

        final String outputPath = "./target/output";

        final Path electionPath = Paths.get(outputPath, _eeid);

        final Path offlinePath = electionPath.resolve(Constants.OFFLINE_DIRECTORY);

        final Path onlinePath = electionPath.resolve(Constants.ONLINE_DIRECTORY);

        final Path autenticationPath = onlinePath.resolve(Constants.AUTHENTICATION_DIRECTORY);

        final Path electionInformationPath = onlinePath.resolve(Constants.ELECTION_INFORMATION_DIRECTORY);

        final Path votingWorkFlowPath = onlinePath.resolve(Constants.VOTING_WORKFLOW_DIRECTORY);

        CreateElectionEventCertificatePropertiesContainer createElectionEventCertificateProperties =
            getCertificateProperties();

        final CreateElectionEventParametersHolder holder = new CreateElectionEventParametersHolder(
            electionInputDataPack, Paths.get(outputPath), electionPath, offlinePath, autenticationPath,
            electionInformationPath, votingWorkFlowPath, authenticationParams, electionInformationParams,
            votingWorkflowContextData, Constants.EMPTY, createElectionEventCertificateProperties);

        createDirectories(electionPath);
        EncryptionParameters parameters = new EncryptionParameters();
        parameters.setP("23");
        parameters.setQ("11");
        parameters.setG("11");
        String json = new ConfigObjectMapper().fromJavaToJSON(parameters);
        byte[] bytes = json.getBytes(UTF_8);
        write(electionPath.resolve(Constants.ENCRYPTION_PARAMS_FILENAME), bytes);

        // Read from configuration
        final URL url = this.getClass().getResource("/keys_config.json");
        final ConfigurationInput configurationInput = configurationInputReader.fromFileToJava(new File(url.toURI()));
        holder.setConfigurationInput(configurationInput);

        // This has to be done because the annotation it's not executed without
        // Spring
        Deencapsulation.setField(electionCredentialDataPackGenerator, logWriter);
        // This has to be done because the annotation it's not executed without
        // Spring
        Deencapsulation.setField(createElectionEventGenerator, logWriter);

        final CreateElectionEventOutput createElectionEventOutput = createElectionEventGenerator.generate(holder);

        Deencapsulation.setField(createElectionEventSerializer, logWriter);

        createElectionEventSerializer.serialize(holder, createElectionEventOutput);

        // check folders structure
        final File outputFolder = new File("./target", "output");
        Assert.assertTrue(outputFolder.exists());
        final File eeidFolder = new File(outputFolder, _eeid);
        Assert.assertTrue(eeidFolder.exists());
        final File offLineFolder = new File(eeidFolder, Constants.OFFLINE_DIRECTORY);
        Assert.assertTrue(offLineFolder.exists());
        final File onLineFolder = new File(eeidFolder, Constants.ONLINE_DIRECTORY);
        Assert.assertTrue(onLineFolder.exists());
        final File authenticationFolder = new File(onLineFolder, Constants.AUTHENTICATION_DIRECTORY);
        Assert.assertTrue(authenticationFolder.exists());
        final File electionInformationFolder = new File(onLineFolder, Constants.ELECTION_INFORMATION_DIRECTORY);
        Assert.assertTrue(electionInformationFolder.exists());
        final File votingWorkflowFolder = new File(onLineFolder, Constants.VOTING_WORKFLOW_DIRECTORY);
        Assert.assertTrue(votingWorkflowFolder.exists());

        // check number of files generated
        Assert.assertEquals(10, offLineFolder.list().length);
        Assert.assertEquals(2, authenticationFolder.list().length);
        Assert.assertEquals(1, electionInformationFolder.list().length);
        Assert.assertEquals(1, votingWorkflowFolder.list().length);
    }

    /**
     * @param inputProperties
     * @param electionInputDataPack
     */
    private void setInputDataProperties(final Properties inputProperties,
            final ElectionInputDataPack electionInputDataPack) {
        final String start = (String) inputProperties.get("start");
        final String end = (String) inputProperties.get("end");

        // ISO_INSTANT format => 2011-12-03T10:15:30Z
        electionInputDataPack.setStartDate(ZonedDateTime.ofInstant(Instant.parse(start), ZoneOffset.UTC));
        electionInputDataPack.setEndDate(ZonedDateTime.ofInstant(Instant.parse(end), ZoneOffset.UTC));

        final Integer validityPeriod = Integer.parseInt((String) inputProperties.get("validityPeriod"));
        // inputDataPack.setValidityPeriod(validityPeriod);
    }

    private CreateElectionEventCertificatePropertiesContainer getCertificateProperties() throws IOException {

        String servicesCaCertificateProperties = "properties/servicesCAX509Certificate.properties";
        String electionCaCertificateProperties = "properties/electionCAX509Certificate.properties";
        String credentialsCaCertificateProperties = "properties/credentialsCAX509Certificate.properties";
        String authoritiesCaCertificateProperties = "properties/authoritiesCAX509Certificate.properties";
        String authTokenSignerCertificateProperties = "properties/authTokenSignerX509Certificate.properties";

        CreateElectionEventCertificatePropertiesContainer createElectionEventCertificatePropertiesContainer =
            new CreateElectionEventCertificatePropertiesContainer();

        Properties loadedServicesCaCertificatePropertiesAsString =
            getCertificateParameters(servicesCaCertificateProperties);
        Properties loadedElectionCaCertificatePropertiesAsString =
            getCertificateParameters(electionCaCertificateProperties);
        Properties loadedCredentialsCaCertificatePropertiesAsString =
            getCertificateParameters(credentialsCaCertificateProperties);
        Properties loadedAuthoritiesCaCertificatePropertiesAsString =
            getCertificateParameters(authoritiesCaCertificateProperties);

        Properties loadedAuthTokenSignerCertificatePropertiesAsString =
            getCertificateParameters(authTokenSignerCertificateProperties);

        Map<String, Properties> configProperties = new HashMap<>();
        configProperties.put("electioneventca", loadedElectionCaCertificatePropertiesAsString);
        configProperties.put("authoritiesca", loadedAuthoritiesCaCertificatePropertiesAsString);
        configProperties.put("servicesca", loadedServicesCaCertificatePropertiesAsString);
        configProperties.put("credentialsca", loadedCredentialsCaCertificatePropertiesAsString);

        createElectionEventCertificatePropertiesContainer
            .setAuthTokenSignerCertificateProperties(loadedAuthTokenSignerCertificatePropertiesAsString);
        createElectionEventCertificatePropertiesContainer.setNameToCertificateProperties(configProperties);

        return createElectionEventCertificatePropertiesContainer;
    }

    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;
    }
}
