/*
 * 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 java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.math.BigInteger;
import java.security.KeyPair;
import java.util.Map;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Matchers;
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.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.derivation.cryptoapi.CryptoAPIPBKDFDeriver;
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.products.ov.choices.codes.BallotCastingKeyGenerator;
import com.scytl.products.ov.choices.codes.ChoicesCodesGenerationAPI;
import com.scytl.products.ov.choices.codes.ChoicesCodesMappingEntry;
import com.scytl.products.ov.commons.beans.EncryptionParameters;
import com.scytl.products.ov.commons.dto.ElGamalComputationsValuesCodec;
import com.scytl.products.ov.commons.dto.ElGamalComputationsValuesCodecImpl;
import com.scytl.products.ov.config.actions.ExtendedAuthenticationService;
import com.scytl.products.ov.config.commands.voters.JobExecutionObjectContext;
import com.scytl.products.ov.config.commands.voters.VotersGenerationTaskStaticContentProvider;
import com.scytl.products.ov.config.commands.voters.VotersParametersHolder;
import com.scytl.products.ov.config.commands.voters.datapacks.beans.VerificationCardCredentialDataPack;
import com.scytl.products.ov.config.commands.voters.datapacks.beans.VerificationCardSetCredentialDataPack;
import com.scytl.products.ov.config.commands.voters.datapacks.beans.VotingCardCredentialDataPack;
import com.scytl.products.ov.config.commands.voters.datapacks.beans.VotingCardCredentialInputDataPack;
import com.scytl.products.ov.config.commands.voters.datapacks.beans.VotingCardSetCredentialDataPack;
import com.scytl.products.ov.config.commands.voters.datapacks.generators.VerificationCardCredentialDataPackGenerator;
import com.scytl.products.ov.config.commands.voters.datapacks.generators.VotingCardCredentialDataPackGenerator;
import com.scytl.products.ov.config.model.authentication.ExtendedAuthInformation;
import com.scytl.products.ov.config.model.authentication.service.StartVotingKeyService;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.datapacks.beans.VerificationCardIdComputedValues;

/**
 * To be honest, this test does so muck "mocking" that i don't know if we are
 * really testing anything. Maybe try to run in "real" Spring batch context
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
public class VotingCardGeneratorTest {
    private static final String P =
        "27839315951550607954908947302805005270748306495150332019506497627681949106676211603621655834342899312853752944034571664077675893773681536386846966667523575670806834175256557051826034202665533443825675341545392523192426729654012224826614092343340720174718101364493840811991904431138360523983907332811743305613997816019580431245599135029769534308919329503387684189735998288344293295738991424137467298627689337334152522616643588240444981657026169355687112876037117176846382595626492699708769169931274950646847052618070904638628820367215645566336974910524181637499593007713578628942182502228080597082761613515206700523109";

    private static final String Q = "72672713147406890228989244889596588007200985967243596643124233889542687254499";

    private static final String G =
        "4267702297229469673790018703734877296807681845981859915554469538283570502856417708468191328245393782101264664502289657179067509176893351929373100181771085681303723016005109429862508282653479146065243990330272150374977895870396872387540216688889850476152205361728439710042818398798929578066878456091896533097188888338823697398469244803574755568190747136605054794708632236694927928994592574083595309695961503955530493578418229984920184090088732231205782707337157009325887375886691582508754748112217552487874508954018581071754592385039406544226588417197430102130453807486352982973717703392982010821660259571513106072633";

    @Autowired
    VotingCardGenerator sut;

    @Autowired
    JobExecutionObjectContext jobExecutionContextMock;

    @Autowired
    VotingCardGenerationJobExecutionContext jobContext;

    @Autowired
    ChoicesCodesGenerationAPI codesGeneratorMock;

    @Autowired
    ExtendedAuthenticationService extendedAuthenticationServiceMock;

    @Autowired
    StartVotingKeyService startVotingKeyServiceMock;

    @Autowired
    VotingCardCredentialDataPackGenerator votingCardCredentialDataPackGeneratorMock;

    @Autowired
    VerificationCardCredentialDataPackGenerator verificationCardCredentialDataPackGeneratorMock;

    @Autowired
    VotersParametersHolder parametersHolderMock;

    @Autowired
    VerificationCardIdComputedValues verificationCardIdComputedValuesMock;

    @Autowired
    VotingCardGenerationJobExecutionContext cardGenerationJobExecutionContextMock;

    @Autowired
    CryptoAPIPBKDFDeriver cryptoAPIPBKDFDeriverMock;

    @Autowired
    VotersGenerationTaskStaticContentProvider staticContentProviderMock;

    @Autowired
    AsymmetricServiceAPI asymmetricService;

    @Autowired
    ElGamalServiceAPI elGamalService;

    @Autowired
    ElGamalComputationsValuesCodec codec;

    @Autowired
    ElGamalKeyPair primeEncryptionKeys;

    @Test
    public void generatesVotingCardOutput() throws Exception {

        // given
        setupMocks();

        // when
        final GeneratedVotingCardOutput output = sut.process(verificationCardIdComputedValuesMock);

        // then
        Assert.assertNotNull(output);
        Assert.assertNotNull(output.getBallotBoxId());
        Assert.assertNotNull(output.getBallotId());
        Assert.assertNotNull(output.getCredentialId());
        Assert.assertNotNull(output.getElectionEventId());
        Assert.assertNull(output.getError());
        Assert.assertNotNull(output.getExtendedAuthInformation());
        Assert.assertNotNull(output.getStartVotingKey());
        Assert.assertNotNull(output.getVerificationCardCodesDataPack());
        Assert.assertNotNull(output.getVerificationCardCredentialDataPack());
        Assert.assertNotNull(output.getVerificationCardId());
        // Assert.assertNotNull(output.getVerificationCardSetId());
        Assert.assertNotNull(output.getVoterCredentialDataPack());
        Assert.assertNotNull(output.getVotingCardId());
        Assert.assertNotNull(output.getVotingCardSetId());
        Assert.assertEquals(Constants.NUM_DIGITS_BALLOT_CASTING_KEY_FINAL,
            output.getVerificationCardCodesDataPack().getBallotCastingKey().length());

    }

    private void setupMocks() throws Exception {
        VerificationCardSetCredentialDataPack verificationCardSetCredentialDataPack =
            mock(VerificationCardSetCredentialDataPack.class);

        KeyPair keyPair = asymmetricService.getKeyPairForSigning();
        when(verificationCardSetCredentialDataPack.getVerificationCardIssuerKeyPair()).thenReturn(keyPair);
        when(jobExecutionContextMock.get(anyString(), eq(VerificationCardSetCredentialDataPack.class)))
            .thenReturn(verificationCardSetCredentialDataPack);

        VotingCardSetCredentialDataPack votingCardSetCredentialDataPack = mock(VotingCardSetCredentialDataPack.class);
        when(votingCardSetCredentialDataPack.getVoteCastCodeSignerKeyPair()).thenReturn(keyPair);
        when(jobExecutionContextMock.get(anyString(), eq(VotingCardSetCredentialDataPack.class)))
            .thenReturn(votingCardSetCredentialDataPack);

        // ---
        when(codesGeneratorMock.generateVoteCastingCode()).thenReturn("1");
        when(codesGeneratorMock.generateShortChoiceCode()).thenReturn("1");
        when(codesGeneratorMock.generateLongChoiceCode(any(), any(), any(), any())).thenReturn(new byte[] {});
        when(codesGeneratorMock.generateMapping(any(), any(), any()))
            .thenReturn(new ChoicesCodesMappingEntry(new byte[] {0 }, new byte[] {1 }));

        // --
        when(extendedAuthenticationServiceMock.create(any(), any())).thenReturn(mock(ExtendedAuthInformation.class));

        // --
        when(startVotingKeyServiceMock.generateStartVotingKey()).thenReturn("1");

        // --
        when(votingCardCredentialDataPackGeneratorMock.generate(any(), any(), any(), anyString(), anyString(), any(),
            any(), Matchers.<CryptoAPIX509Certificate> anyVararg()))
                .thenReturn(mock(VotingCardCredentialDataPack.class));

        // --
        ElGamalEncryptionParameters params =
            new ElGamalEncryptionParameters(new BigInteger(P), new BigInteger(Q), new BigInteger(G));
        ElGamalKeyPair elGamalKeyPair = new ElGamalService().getElGamalKeyPairGenerator().generateKeys(params, 1);
        VerificationCardCredentialDataPack verificationCardCredentialDataPack =
            mock(VerificationCardCredentialDataPack.class);
        EncryptionParameters encryptionParameters = new EncryptionParameters(P, Q, G);
        when(verificationCardCredentialDataPack.getVerificationCardKeyPair()).thenReturn(elGamalKeyPair);
        when(verificationCardCredentialDataPackGeneratorMock.generate(any(), any(), any(), any(), any(), any(), any(), any()))
            .thenReturn(verificationCardCredentialDataPack);

        // --
        when(parametersHolderMock.getVotingCardCredentialInputDataPack())
            .thenReturn(mock(VotingCardCredentialInputDataPack.class));
        when(parametersHolderMock.getVotingCardCredentialInputDataPack().getEeid()).thenReturn("1");
        when(parametersHolderMock.getBallot().getElectionEvent().getId()).thenReturn("1");
        when(parametersHolderMock.getEncryptionParameters()).thenReturn(encryptionParameters);

        when(verificationCardIdComputedValuesMock.getId()).thenReturn("1");

        ZpSubgroup group = primeEncryptionKeys.getPrivateKeys().getGroup();
        ZpGroupElement prime = new ZpGroupElement(BigInteger.valueOf(2), group);
        ZpGroupElement powerOfPrime = new ZpGroupElement(BigInteger.valueOf(4), group);
        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(primeEncryptionKeys.getPublicKeys());
        ElGamalComputationsValues key = encrypter.encryptGroupElements(singletonList(prime)).getComputationValues();
        ElGamalComputationsValues value =
            encrypter.encryptGroupElements(singletonList(powerOfPrime)).getComputationValues();
        Map<ElGamalComputationsValues, ElGamalComputationsValues> map = singletonMap(key, value);
        when(verificationCardIdComputedValuesMock.getBallotVotingOption2prePartialChoiceCodes())
            .thenReturn(codec.encodeMap(map));

        ZpGroupElement bck = new ZpGroupElement(BigInteger.valueOf(2), group);
        ElGamalComputationsValues encryptedBck =
            encrypter.encryptGroupElements(singletonList(bck)).getComputationValues();
        when(verificationCardIdComputedValuesMock.getEncryptedBallotCastingKey())
            .thenReturn(codec.encodeSingle(encryptedBck));
        ZpGroupElement computedBck = new ZpGroupElement(BigInteger.valueOf(4), group);
        ElGamalComputationsValues encryptedComputedBck =
            encrypter.encryptGroupElements(singletonList(computedBck)).getComputationValues();
        when(verificationCardIdComputedValuesMock.getComputedBallotCastingKey())
            .thenReturn(codec.encodeSingle(encryptedComputedBck));

        // --
        when(cardGenerationJobExecutionContextMock.getVerificationCardSetId()).thenReturn("1");
        when(cardGenerationJobExecutionContextMock.getVotingCardSetId()).thenReturn("1");
        when(cardGenerationJobExecutionContextMock.getNumberOfVotingCards()).thenReturn(1);
        when(cardGenerationJobExecutionContextMock.getSaltPin()).thenReturn("1");
        when(cardGenerationJobExecutionContextMock.getBallotId()).thenReturn("1");
        when(cardGenerationJobExecutionContextMock.getBallotBoxId()).thenReturn("1");
        when(cardGenerationJobExecutionContextMock.getVotingCardSetId()).thenReturn("1");
        when(cardGenerationJobExecutionContextMock.getJobInstanceId()).thenReturn("1");
        when(cardGenerationJobExecutionContextMock.getElectionEventId()).thenReturn("1");

        // --
        when(cryptoAPIPBKDFDeriverMock.deriveKey(any(), any())).thenReturn(() -> "1".getBytes());

        // --
        when(staticContentProviderMock.getZpsubgroup())
            .thenReturn(new ZpSubgroup(new BigInteger(G), new BigInteger(P), new BigInteger(Q)));
        when(staticContentProviderMock.getRepresentationsWithCorrectness())
            .thenReturn(singletonMap(BigInteger.valueOf(2), singletonList("correctness")));

    }

    @Configuration
    @Import(TestConfigServices.class)
    static class PrivateConfiguration {

        @Bean
        JobExecutionObjectContext executionObjectContext(AsymmetricServiceAPI asymmetricService) throws Exception {
            final JobExecutionObjectContext context = mock(JobExecutionObjectContext.class);
            return context;
        }

        @Bean
        ChoicesCodesGenerationAPI codesGenerationAPI() {
            final ChoicesCodesGenerationAPI generator = mock(ChoicesCodesGenerationAPI.class);
            return generator;
        }

        @Bean
        BallotCastingKeyGenerator ballotCastingKeyGeneratorAPI() {
            final BallotCastingKeyGenerator generator = mock(BallotCastingKeyGenerator.class);
            return generator;
        }

        @Bean
        ExtendedAuthenticationService extendedAuthenticationService() {
            final ExtendedAuthenticationService service = mock(ExtendedAuthenticationService.class);
            return service;
        }

        @Bean
        StartVotingKeyService startVotingKeyService() throws Exception {
            final StartVotingKeyService service = mock(StartVotingKeyService.class);
            return service;
        }

        @Bean
        VotingCardCredentialDataPackGenerator votingCardCredentialDataPackGenerator() throws Exception {
            final VotingCardCredentialDataPackGenerator generator = mock(VotingCardCredentialDataPackGenerator.class);
            return generator;
        }

        @Bean
        VerificationCardCredentialDataPackGenerator verificationCardCredentialDataPackGenerator() throws Exception {
            final VerificationCardCredentialDataPackGenerator generator =
                mock(VerificationCardCredentialDataPackGenerator.class);
            return generator;
        }

        @Bean
        AsymmetricServiceAPI asymmetricServiceAPI() throws Exception {
            return new AsymmetricService();
        }

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

        @Bean
        VerificationCardIdComputedValues verificationCardIdComputedValues() {
            final VerificationCardIdComputedValues verificationCardIdComputedValues =
                mock(VerificationCardIdComputedValues.class);
            return verificationCardIdComputedValues;
        }

        @Bean
        VotingCardGenerationJobExecutionContext jobContext(ElGamalKeyPair keys) throws GeneralCryptoLibException {
            final VotingCardGenerationJobExecutionContext context = mock(VotingCardGenerationJobExecutionContext.class);
            when(context.getPrimeEncryptionPrivateKeyJson()).thenReturn(keys.getPrivateKeys().toJson());
            return context;
        }

        @Bean
        ElGamalKeyPair primeEncryptionKeys(ElGamalServiceAPI elGamalService) throws GeneralCryptoLibException {
            CryptoAPIElGamalKeyPairGenerator generator = elGamalService.getElGamalKeyPairGenerator();
            ElGamalEncryptionParameters parameters =
                new ElGamalEncryptionParameters(new BigInteger(VotingCardGeneratorTest.P),
                    new BigInteger(VotingCardGeneratorTest.Q), new BigInteger(VotingCardGeneratorTest.G));
            return generator.generateKeys(parameters, 1);
        }

        @Bean
        CryptoAPIPBKDFDeriver deriver() throws Exception {
            final CryptoAPIPBKDFDeriver deriver = mock(CryptoAPIPBKDFDeriver.class);
            return deriver;
        }

        @Bean
        VotersGenerationTaskStaticContentProvider contentProvider() throws Exception {
            final VotersGenerationTaskStaticContentProvider provider =
                mock(VotersGenerationTaskStaticContentProvider.class);
            return provider;
        }

        @Bean
        ElGamalServiceAPI elGamalService() throws GeneralCryptoLibException {
            return new ElGamalService();
        }

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

        @Bean
        VotingCardGenerator votingCardGenerator(VotersParametersHolder holder,
                VotingCardGenerationJobExecutionContext jobContext, CryptoAPIPBKDFDeriver deriver,
                VotersGenerationTaskStaticContentProvider contentProvider) {
            return new VotingCardGenerator(holder, jobContext, deriver, contentProvider);
        }
    }
}
