/*
 * 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.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 org.junit.Assert.fail;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.UUID;

import org.junit.Assert;
import org.junit.Before;
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.Job;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.step.tasklet.Tasklet;
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.fasterxml.jackson.databind.ObjectMapper;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.certificates.bean.CertificateData;
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.service.CertificatesService;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.products.ov.commons.beans.EncryptionParameters;
import com.scytl.products.ov.commons.beans.VerificationCardSetData;
import com.scytl.products.ov.commons.beans.VoteVerificationContextData;
import com.scytl.products.ov.config.commands.voters.JobExecutionObjectContext;
import com.scytl.products.ov.config.commands.voters.VotersParametersHolder;
import com.scytl.products.ov.config.commands.voters.VotersSerializationDestProvider;
import com.scytl.products.ov.config.commands.voters.datapacks.beans.VerificationCardSetCredentialDataPack;
import com.scytl.products.ov.config.commands.voters.datapacks.beans.VotingCardSetCredentialDataPack;
import com.scytl.products.ov.utils.FingerprintGenerator;

/**
 * This tests a specific batch job step. By default, JobLauncherTestUtils
 * expects to find _only one_ Job bean in the context, that's why i made the
 * batch configuration class an inner class and not a "shared" class for all
 * tests. We could extract the class into a different file but i don't see any
 * advantage
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
public class WriteVerificationDataTaskletTest {

    private static final String STEP_IN_TEST = "writeVerificationData";

    @Configuration
    @EnableBatchProcessing
    @Import(TestConfigServices.class)
    static class JobConfiguration {

        @Autowired
        JobBuilderFactory jobBuilder;

        @Autowired
        StepBuilderFactory stepBuilder;

        @Autowired
        VotersSerializationDestProvider destProvider;

        @Autowired
        FingerprintGenerator fingerprintGenerator;

        @Autowired
        private JobExecutionObjectContext objectContext;

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

        @Bean
        public Step step(Tasklet tasklet) {
            return stepBuilder.get(STEP_IN_TEST).tasklet(tasklet).build();
        }

        @Bean
        public Tasklet tasklet() {
            return new WriteVerificationDataTasklet(destProvider, objectContext);
        }

        @Bean
        Job job(Step step) {
            return jobBuilder.get("job").start(step).build();
        }

        @Bean
        VotersParametersHolder holder() {
            VotersParametersHolder holder = mock(VotersParametersHolder.class);
            return holder;
        }
    }

    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;

    @Autowired
    private JobExecutionObjectContext stepExecutionObjectContext;

    @Autowired
    private VotersSerializationDestProvider destProviderMock;

    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    private ObjectMapper jsonMapper = new ObjectMapper();

    private File verificationCardSetDataExpectedFile;

    private File voteVerificationContextDataExpectedFile;

    @Test
    public void generateVerificationDataFiles() throws Exception {

        // given
        JobParameters jobParameters = getJobInputParameters();

        // when
        JobExecution jobExecution = jobLauncherTestUtils.launchStep(STEP_IN_TEST, jobParameters);

        // then (we want to know that the "new" job parameters are generated and
        // stored in the context)
        Assert.assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());

        // we have to assert that the output files of this tasklet are generated
        // and have correct data
        // the files are json files created from the following classes:
        // VerificationCardSetData, VoteVerificationContextData
        // we could go further and verify the content matches all the "test"
        // data, but it's maybe overkill
        Assert.assertTrue(verificationCardSetDataExpectedFile.exists());
        try {
            jsonMapper.readValue(verificationCardSetDataExpectedFile, VerificationCardSetData.class);
        } catch (IOException e) {
            fail("unexpected format for VerificationCardSetData");
        }

        Assert.assertTrue(voteVerificationContextDataExpectedFile.exists());
        try {
            jsonMapper.readValue(voteVerificationContextDataExpectedFile, VoteVerificationContextData.class);
        } catch (IOException e) {
            fail("unexpected format for VoteVerificationContextData");
        }
    }

    private JobParameters getJobInputParameters() {
        JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();
        jobParametersBuilder.addString(JOB_IN_PARAM_JOB_INSTANCE_ID, UUID.randomUUID().toString(), 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, "10");
        jobParametersBuilder.addString(JOB_IN_PARAM_VOTING_CARD_SET_NAME, "votingCardSetAlias");
        jobParametersBuilder.addString(JOB_IN_PARAM_VALIDITY_PERIOD_START, "2017-02-15");
        jobParametersBuilder.addString(JOB_IN_PARAM_VALIDITY_PERIOD_END, "2018-02-15");
        jobParametersBuilder.addString(JOB_IN_PARAM_KEY_FOR_PROTECTING_KEYSTORE_PW, "password");
        jobParametersBuilder.addString(JOB_IN_PARAM_BASE_PATH, "absoluteOutputPath");
        jobParametersBuilder.addString(JOB_IN_PARAM_VERIFICATIONCARDSET_ID, "verificationCardSetId");
        return jobParametersBuilder.toJobParameters();
    }

    @Before
    public void setup() throws Exception {

        verificationCardSetDataExpectedFile = temporaryFolder.newFile();
        voteVerificationContextDataExpectedFile = temporaryFolder.newFile();

        VotersParametersHolder parametersHolder = mock(VotersParametersHolder.class);
        when(parametersHolder.getEncryptionParameters()).thenReturn(new EncryptionParameters("23", "11", "2"));
        when(stepExecutionObjectContext.get(anyString(), eq(VotersParametersHolder.class)))
            .thenReturn(parametersHolder);

        ElGamalEncryptionParameters params =
            new ElGamalEncryptionParameters(new BigInteger("23"), new BigInteger("11"), new BigInteger("2"));
        ElGamalKeyPair elGamalKeyPair = new ElGamalService().getElGamalKeyPairGenerator().generateKeys(params, 1);
        KeyPair certificateKeyPair = new AsymmetricService().getKeyPairForSigning();
        CryptoAPIScytlKeyStore keyStore = new ScytlKeyStoreService().createKeyStore();

        VerificationCardSetCredentialDataPack verificationCardSetCredentialDataPack =
            mock(VerificationCardSetCredentialDataPack.class);
        when(verificationCardSetCredentialDataPack.getChoiceCodesEncryptionPublicKey())
            .thenReturn(elGamalKeyPair.getPublicKeys());
        when(verificationCardSetCredentialDataPack.getNonCombinedChoiceCodesEncryptionPublicKeys())
            .thenReturn(new ElGamalPublicKey[] {elGamalKeyPair.getPublicKeys(), elGamalKeyPair.getPublicKeys(),
                    elGamalKeyPair.getPublicKeys(), elGamalKeyPair.getPublicKeys() });
        when(verificationCardSetCredentialDataPack.getVerificationCardIssuerCert())
            .thenReturn(generateCertificate(certificateKeyPair));
        when(verificationCardSetCredentialDataPack.getSerializedKeyStore()).thenReturn("{}");
        when(stepExecutionObjectContext.get(anyString(), eq(VerificationCardSetCredentialDataPack.class)))
            .thenReturn(verificationCardSetCredentialDataPack);

        VotingCardSetCredentialDataPack votingCardSetCredentialDataPack = mock(VotingCardSetCredentialDataPack.class);
        when(votingCardSetCredentialDataPack.getVoteCastCodeSignerCertificate())
            .thenReturn(generateCertificate(certificateKeyPair));
        when(votingCardSetCredentialDataPack.getKeyStore()).thenReturn(keyStore);
        when(votingCardSetCredentialDataPack.getPassword()).thenReturn("1234567890987654".toCharArray());

        when(stepExecutionObjectContext.get(anyString(), eq(VotingCardSetCredentialDataPack.class)))
            .thenReturn(votingCardSetCredentialDataPack);

        when(destProviderMock.getVerificationCardSetData()).thenReturn(verificationCardSetDataExpectedFile.toPath());
        when(destProviderMock.getVoteVerificationContextData())
            .thenReturn(voteVerificationContextDataExpectedFile.toPath());
    }

    private CryptoAPIX509Certificate generateCertificate(KeyPair keyPair) throws Exception {

        CertificateData certificateData = new CertificateData();
        certificateData.setSubjectPublicKey(keyPair.getPublic());
        ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
        ZonedDateTime end = now.plusYears(1);
        ValidityDates validityDates = new ValidityDates(Date.from(now.toInstant()), Date.from(end.toInstant()));
        certificateData.setValidityDates(validityDates);
        X509DistinguishedName distinguishedName = new X509DistinguishedName.Builder("commonName", "ES").build();
        certificateData.setSubjectDn(distinguishedName);
        certificateData.setIssuerDn(distinguishedName);

        CryptoAPIX509Certificate cert =
            new CertificatesService().createSignX509Certificate(certificateData, keyPair.getPrivate());
        return cert;
    }
}
