/*
 * 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.ei.services.domain.service.ballotbox;

import static com.scytl.products.ov.ei.services.domain.model.util.Constants.BALLOT_BOX_ID;
import static com.scytl.products.ov.ei.services.domain.model.util.Constants.ELECTION_EVENT_ID;
import static com.scytl.products.ov.ei.services.domain.model.util.Constants.TENANT_ID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.json.Json;
import javax.json.JsonObject;
import javax.naming.InvalidNameException;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.slf4j.Logger;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.products.ov.commons.beans.Ballot;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxIdImpl;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.vote.CiphertextEncryptedVote;
import com.scytl.products.ov.commons.beans.domain.model.vote.EncryptedVote;
import com.scytl.products.ov.commons.beans.exceptions.CleansedBallotBoxServiceException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.crypto.CertificateChainRepository;
import com.scytl.products.ov.commons.crypto.PrivateKeyForObjectRepository;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
import com.scytl.products.ov.commons.sign.CryptoTestData;
import com.scytl.products.ov.commons.sign.CryptolibPayloadSigner;
import com.scytl.products.ov.commons.sign.PayloadSigner;
import com.scytl.products.ov.commons.sign.SignedTestVotingDataService;
import com.scytl.products.ov.commons.sign.TestCertificateGenerator;
import com.scytl.products.ov.commons.signature.SignatureFactory;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxInformation;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxInformationRepository;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.CleansedBallotBox;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.CleansedBallotBoxRepository;
import com.scytl.products.ov.ei.services.domain.model.content.ElectoralAuthorityEntity;
import com.scytl.products.ov.ei.services.domain.model.content.ElectoralAuthorityRepository;
import com.scytl.products.ov.ei.services.domain.model.tenant.EiTenantSystemKeys;
import com.scytl.products.ov.ei.services.infrastructure.persistence.CleansedBallotBoxAccess;
import com.scytl.products.ov.ei.services.infrastructure.persistence.SuccessfulVotesAccess;

@RunWith(MockitoJUnitRunner.class)
public class CleansedBallotBoxServiceImplIntegrationTest {

    private static final short ORIGINAL_VOTE_SET_MAX_SIZE = CleansedBallotBox.CHUNK_SIZE;

    private static final short TESTING_VOTE_SET_MAX_SIZE = 10;

    private static final String BALLOT_JSON = "ballot.json";

    private static final BallotBoxIdImpl ballotBoxId = new BallotBoxIdImpl(TENANT_ID, ELECTION_EVENT_ID, BALLOT_BOX_ID);

    private static final ElectoralAuthorityRepository electoralAuthorityRepository =
        mock(ElectoralAuthorityRepository.class);

    private static final CleansedBallotBoxRepository cleansedBallotBoxRepository =
        mock(CleansedBallotBoxRepository.class);

    private static final BallotBoxInformationRepository ballotBoxInformationRepository =
        mock(BallotBoxInformationRepository.class);

    private static Field maxSizeField;

    private static ElGamalKeyPair electoralAuthorityKeyPair;

    private static TestCertificateGenerator testCertificateGenerator;

    private static AsymmetricServiceAPI asymmetricService;

    private static EiTenantSystemKeys eiTenantSystemKeys = mock(EiTenantSystemKeys.class);

    @Mock
    private SignatureFactory signatureFactory;

    @Mock
    private CleansedBallotBoxAccess cleansedBallotBoxAccess;

    @Mock
    private SuccessfulVotesAccess successfulVotesAccess;

    @Mock
    private Logger logger;

    @Mock
    private PrivateKeyForObjectRepository privateKeyRepository;

    @Mock
    private CertificateChainRepository certificateChainRepository;

    @Spy
    private static PayloadSigner payloadSigner;

    @InjectMocks
    private final CleansedBallotBoxServiceImpl sut = new CleansedBallotBoxServiceImpl();

    @BeforeClass
    public static void setUpAll()
            throws GeneralCryptoLibException, IOException, InvalidNameException, ResourceNotFoundException,
            URISyntaxException {
        addSecurityProvider();

        // Initialise actual services.
        asymmetricService = new AsymmetricService();
        payloadSigner = new CryptolibPayloadSigner(asymmetricService);
        testCertificateGenerator = TestCertificateGenerator.createDefault();

        // Initialise mocks.
        setUpEiTenantSystemKeysMock();
        setUpBallotBoxInformationRepositoryMock();
        setUpElectoralAuthorityRepositoryMock();
    }

    @BeforeClass
    public static void reduceVoteSetMaxValueForTesting() throws NoSuchFieldException, IllegalAccessException {
        // Get the private static final field.
        maxSizeField = CleansedBallotBox.class.getDeclaredField("CHUNK_SIZE");
        // Make it accessible.
        maxSizeField.setAccessible(true);
        // Make it non-final.
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(maxSizeField, maxSizeField.getModifiers() & ~Modifier.FINAL);
        // Alter its value.
        maxSizeField.setShort(null, TESTING_VOTE_SET_MAX_SIZE);
    }

    @AfterClass
    public static void restoreVoteSetMaxValue() throws IllegalAccessException {
        // Restore the vote set's original maximum size value.
        maxSizeField.setShort(null, ORIGINAL_VOTE_SET_MAX_SIZE);
    }

    @Test
    public void testPayloadWithValidVoteSet()
            throws PayloadSignatureException, ResourceNotFoundException, GeneralCryptoLibException, IOException,
            InvalidNameException, CleansedBallotBoxServiceException {
        assertEquals(TESTING_VOTE_SET_MAX_SIZE, CleansedBallotBox.CHUNK_SIZE);

        SignedTestVotingDataService votingDataService =
            new SignedTestVotingDataService(getBallotDefinition(), asymmetricService, testCertificateGenerator);

        List<Ciphertext> votes = votingDataService.generateVotes(TESTING_VOTE_SET_MAX_SIZE);
        Stream<EncryptedVote> voteStream = votes.stream().map(CiphertextEncryptedVote::new);
        when(cleansedBallotBoxRepository.getVoteSet(any(), anyInt(), anyInt())).thenReturn(voteStream);

        List<MixingPayload> mixingPayloads = sut.getMixingPayloads(ballotBoxId).collect(Collectors.toList());

        // Ensure there is one payload.
        assertEquals(1, mixingPayloads.size());
        // Ensure the payload contains the expected number of votes.
        assertEquals(TESTING_VOTE_SET_MAX_SIZE, mixingPayloads.get(0).getVotes().size());
    }

    @Test
    public void testEmptyVoteSetPayload()
            throws PayloadSignatureException, ResourceNotFoundException, URISyntaxException, IOException, 
            CleansedBallotBoxServiceException {
        assertEquals(TESTING_VOTE_SET_MAX_SIZE, CleansedBallotBox.CHUNK_SIZE);

        when(cleansedBallotBoxRepository.getVoteSet(any(), anyInt(), anyInt())).thenReturn(Stream.empty());

        List<MixingPayload> mixingPayloads = sut.getMixingPayloads(ballotBoxId).collect(Collectors.toList());

        // Ensure there is one payload.
        assertEquals(1, mixingPayloads.size());
        // Ensure the payload contains no votes.
        assertEquals(Collections.emptyList(), mixingPayloads.get(0).getVotes());
    }

    /**
     * Get a ballot definition from a JSON file.
     *
     * @return the ballot definition
     */
    private static Ballot getBallotDefinition() throws IOException {
        return new ObjectMapper().readValue(
            CleansedBallotBoxServiceImplIntegrationTest.class.getClassLoader().getResourceAsStream(BALLOT_JSON),
            Ballot.class);
    }

    private static void addSecurityProvider() {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static void setUpEiTenantSystemKeysMock() throws InvalidNameException, GeneralCryptoLibException {

        KeyPair signingKeyPair = asymmetricService.getKeyPairForSigning();
        X509Certificate signingCertificate =
            testCertificateGenerator.createCACertificate(signingKeyPair, "Signing certificate");

        // Set up the certificate chain for signing.
        X509Certificate[] certificateChain = {signingCertificate, testCertificateGenerator.getRootCertificate() };
        when(eiTenantSystemKeys.getSigningCertificateChain(anyString())).thenReturn(certificateChain);

        // Set up the signing key.
        when(eiTenantSystemKeys.getSigningPrivateKey(anyString())).thenReturn(signingKeyPair.getPrivate());
    }

    private static void setUpBallotBoxInformationRepositoryMock()
            throws ResourceNotFoundException, URISyntaxException, IOException {
        String jsonString;
        try (InputStream is =
            Thread.currentThread().getContextClassLoader().getResourceAsStream("ballot_box_information-empty.json");
                Scanner scanner = new Scanner(is);) {
            assertNotNull(is);
            jsonString = scanner.useDelimiter("\\A").next();
            assertFalse(jsonString.isEmpty());
        }
        BallotBoxInformation bbi = mock(BallotBoxInformation.class);
        when(bbi.getJson()).thenReturn(jsonString);
        when(ballotBoxInformationRepository.findByTenantIdElectionEventIdBallotBoxId(anyString(), anyString(),
            anyString())).thenReturn(bbi);
    }

    private static void setUpElectoralAuthorityRepositoryMock()
            throws GeneralCryptoLibException, ResourceNotFoundException, IOException {
        electoralAuthorityKeyPair = CryptoTestData.generateElGamalKeyPair(1);

        String elGamalPublicKeyJson = electoralAuthorityKeyPair.getPublicKeys().toJson();
        String base64PublicKey =
            Base64.getEncoder().encodeToString(elGamalPublicKeyJson.getBytes(StandardCharsets.UTF_8));

        JsonObject jsonObject = Json.createObjectBuilder().add("publicKey", base64PublicKey).build();

        ElectoralAuthorityEntity entity = mock(ElectoralAuthorityEntity.class);
        when(entity.getJson()).thenReturn(jsonObject.toString());
        when(electoralAuthorityRepository.findByTenantIdElectionEventIdElectoralAuthorityId(anyString(), anyString(),
            anyString())).thenReturn(entity);

    }
}
