/*
 * 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.channel.cc.mixing;

import static java.nio.file.Files.readAllLines;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.KeyException;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.PropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.channel.cc.commons.spring.CryptolibConfig;
import com.scytl.products.ov.channel.cc.mixing.service.InternalMixDecryptServiceImpl;
import com.scytl.products.ov.channel.cc.mixing.service.KeyRepositoryImpl;
import com.scytl.products.ov.channel.cc.mixing.service.MixingDecryptServiceImpl;
import com.scytl.products.ov.channel.cc.mixing.service.MixingPropertiesProviderImpl;
import com.scytl.products.ov.channel.cc.mixing.spring.MixingConfig;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxId;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxIdImpl;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignature;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignatureException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadVerificationException;
import com.scytl.products.ov.commons.beans.domain.model.vote.EncryptedVote;
import com.scytl.products.ov.commons.beans.domain.model.vote.StringEncryptedVote;
import com.scytl.products.ov.commons.beans.domain.model.vote.VoteSetId;
import com.scytl.products.ov.commons.beans.domain.model.vote.VoteSetIdImpl;
import com.scytl.products.ov.commons.dto.MixingDTO;
import com.scytl.products.ov.commons.dto.MixingDTOImpl;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
import com.scytl.products.ov.commons.dto.mixing.MixingPayloadImpl;
import com.scytl.products.ov.commons.messaging.MessagingException;
import com.scytl.products.ov.commons.messaging.MessagingService;
import com.scytl.products.ov.commons.messaging.Queue;
import com.scytl.products.ov.commons.sign.PayloadSigner;
import com.scytl.products.ov.commons.verify.PayloadVerifier;
import com.scytl.products.ov.mixnet.commons.configuration.file.FileBasedApplicationConfig;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationConstants;
import com.scytl.products.ov.mixnet.commons.constants.Constants;

@RunWith(SpringJUnit4ClassRunner.class)
public class MixingMixDecQueueTest {

    private static final String REQUEST_ID = "requestId";

    private static final int VOTESET_INDEX = 0;

    private static final String ELECTORAL_AUTHORITY_ID = "electoralAuthorityId";

    private static final byte[] SIGNATURE_CONTENTS = "signatureContents".getBytes(StandardCharsets.UTF_8);

    @Value("${queues.md-mixdec.req}")
    private String queueName;

    private static final String BALLOT_BOX_ID_VALUE = "ballotBoxId";

    private static final String ELECTION_EVENT_ID_VALUE = "electionEventId";

    private static final String TENANT_ID_VALUE = "100";

    @Autowired
    private MessagingService messagingService;

    @Autowired
    private MixingDecryptServiceImpl service;

    @BeforeClass
    public static void setSecurityProvider() {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }

    @Test
    public void testMixingHappyPath() throws GeneralCryptoLibException, KeyException, IOException, MessagingException {
        ArgumentCaptor<MixingDTO> outputDTOs = ArgumentCaptor.forClass(MixingDTO.class);
        MixingDTO originalDTO = createTestMixingDTO();
        MixingPayload originalPayload = originalDTO.getPayload();

        service.onMessage(originalDTO);

        verify(messagingService).send(any(Queue.class), outputDTOs.capture());
        assertEquals(1, outputDTOs.getAllValues().size());

        MixingDTO processedDTO = outputDTOs.getValue();
        assertNull(processedDTO.getError());

        MixingPayload processedPayload = processedDTO.getPayload();
        assertNotEquals(originalPayload.getVotes(), processedPayload.getVotes());
        assertEquals(originalPayload.getVotes(), processedPayload.getPreviousVotes());
        assertEquals(originalDTO.getRequestId(), processedDTO.getRequestId());
        assertEquals(originalPayload.getEncryptionParameters(), processedPayload.getEncryptionParameters());
        assertNotNull(originalPayload.getVoteEncryptionKey());
        assertNotNull(processedPayload.getCommitmentParameters());
        assertNotNull(processedPayload.getEncryptionParameters());
        assertNotNull(processedPayload.getVotes());
        assertNotNull(processedPayload.getDecryptionProofs());
        assertNotNull(processedPayload.getShuffledVotes());
        assertNotNull(processedPayload.getShuffleProof());
    }

    private MixingDTO createTestMixingDTO() throws GeneralCryptoLibException, KeyException, IOException {
        ObjectMapper mapper = new ObjectMapper();
        TestDataProvider tdp = new TestDataProvider();
        String createTestDataConfig = tdp.createTestDataConfig();

        FileBasedApplicationConfig config =
            mapper.readValue(new File(createTestDataConfig), FileBasedApplicationConfig.class);
        Map<String, String> input = config.getMixer().getInput();

        List<EncryptedVote> votes = new ArrayList<>();
        try (BufferedReader br = new BufferedReader(
            new FileReader(input.get(LocationConstants.ENCRYPTED_BALLOTS_JSON_TAG) + Constants.CSV_FILE_EXTENSION))) {
            for (String line; (line = br.readLine()) != null;) {
                votes.add(new StringEncryptedVote(line));
            }
        }

        String encryptionParametersJson =
            readAsString(input.get(LocationConstants.ENCRYPTION_PARAMETERS_JSON_TAG) + Constants.JSON_FILE_EXTENSION);
        ZpSubgroup encryptionParameters = ZpSubgroup.fromJson(encryptionParametersJson);

        String voteEncryptionKeyJson =
            readAsString(input.get(LocationConstants.PUBLIC_KEY_JSON_TAG) + Constants.JSON_FILE_EXTENSION);
        ElGamalPublicKey voteEncryptionKey = ElGamalPublicKey.fromJson(voteEncryptionKeyJson);

        BallotBoxId ballotBoxId = new BallotBoxIdImpl(TENANT_ID_VALUE, ELECTION_EVENT_ID_VALUE, BALLOT_BOX_ID_VALUE);
        VoteSetId voteSetId = new VoteSetIdImpl(ballotBoxId, VOTESET_INDEX);
        MixingPayload payload =
            new MixingPayloadImpl(voteEncryptionKey, voteSetId, votes, ELECTORAL_AUTHORITY_ID, encryptionParameters);

        PayloadSignature mockSignature = mock(PayloadSignature.class);
        when(mockSignature.getSignatureContents()).thenReturn(SIGNATURE_CONTENTS);
        payload.setSignature(mockSignature);

        return new MixingDTOImpl(REQUEST_ID, Stream.of(queueName).collect(Collectors.toSet()), payload);
    }

    private String readAsString(String value) throws IOException {
        StringJoiner joiner = new StringJoiner(System.lineSeparator());
        readAllLines(Paths.get(value)).forEach(joiner::add);
        return joiner.toString();
    }

    @Configuration
    @Import({MixingDecryptServiceImpl.class, InternalMixDecryptServiceImpl.class, KeyRepositoryImpl.class,
            KeyManagerMockConfig.class, MixingPropertiesProviderImpl.class, MixingConfig.class, CryptolibConfig.class })
    @PropertySource("classpath:/application.properties")
    static class Config {
        @Bean
        public MessagingService messagingService() {
            return mock(MessagingService.class);
        }

        @Bean
        PayloadVerifier payloadVerifier() throws PayloadVerificationException {
            PayloadVerifier payloadVerifier = mock(PayloadVerifier.class);
            when(payloadVerifier.isValid(any(), any())).thenReturn(true);

            return payloadVerifier;
        }

        @Bean
        PayloadSigner payloadSigner() throws PayloadSignatureException {
            PayloadSigner payloadSigner = mock(PayloadSigner.class);
            PayloadSignature mockSignature = mock(PayloadSignature.class);
            when(mockSignature.getSignatureContents()).thenReturn(SIGNATURE_CONTENTS);
            when(payloadSigner.sign(any(), any(), any())).thenReturn(mockSignature);

            return payloadSigner;
        }
    }
}
