/*
 * 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.commons.dto.mixing;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
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.Mockito.mock;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignature;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.Ignore;
import org.junit.Test;

public class MixingPayloadImplITest {

    private static final ExecutorService executor = Executors
            .newFixedThreadPool(Runtime.getRuntime().availableProcessors());

    @Test
    public void allowUnsignedContent() throws GeneralCryptoLibException {
        MixingPayload sut = new MixingPayloadMockBuilder().build();

        assertNull(sut.getSignature());
    }

    @Test
    public void allowReducedDataForInitialPayloads() throws GeneralCryptoLibException {
        MixingPayload sut = getInitialPayload();

        assertNotNull(sut.getVoteEncryptionKey());
        assertNotNull(sut.getVoteSetId());
        assertNotNull(sut.getVotes());
        assertNull(sut.getDecryptionProofs());
        assertNull(sut.getShuffleProof());
        assertNull(sut.getShuffledVotes());

        assertNull(sut.getCommitmentParameters());
        assertNull(sut.getDecryptionProofs());
        assertNull(sut.getShuffledVotes());
        assertNull(sut.getShuffleProof());
        assertNull(sut.getPreviousVotes());
        assertNull(sut.getPreviousVoteEncryptionKey());
    }

    @Test
    public void requireFullDataForOutputPayloads() throws GeneralCryptoLibException {
        MixingPayload initialPayload = getInitialPayload();
        MixingPayload sut = new MixingPayloadMockBuilder().withPayload(initialPayload)
                .withSignature(mock(PayloadSignature.class)).build();
        // Simulate shuffle. Do not actually use shuffle as there is the chance
        // it might leave the list as it is.
        Collections.reverse(sut.getVotes());

        assertNotNull(sut.getCommitmentParameters());
        assertNotNull(sut.getVoteEncryptionKey());

        assertNotEquals(sut.getVoteEncryptionKey(), initialPayload.getVoteEncryptionKey());
        assertEquals(sut.getPreviousVoteEncryptionKey(), initialPayload.getVoteEncryptionKey());

        assertNotNull(sut.getVotes());
        assertNotEquals(sut.getVotes(), initialPayload.getVotes());
        assertEquals(sut.getPreviousVotes(), initialPayload.getVotes());

        assertNotNull(sut.getDecryptionProofs());
        assertNotNull(sut.getShuffleProof());
        assertNotNull(sut.getShuffledVotes());
    }

    @Test
    @Ignore // The Jackson-deserialised payload does not produce the same signable content!
    public void generateSameSignatureAfterDeserialisingFromJSON()
            throws GeneralCryptoLibException, IOException {
        MixingPayload sut = new MixingPayloadMockBuilder().withPayload(getInitialPayload())
                .withSignature(MixingPayloadMockBuilder.getMockSignature()).build();

        ObjectMapper objectMapper = new ObjectMapper().enableDefaultTyping();
        byte[] originalSignableContent = getInputStreamContents(sut.getSignableContent());

        // Serialize
        String serialised = objectMapper.writeValueAsString(sut);
        // Read back
        MixingPayload deserialised = objectMapper.readValue(serialised, MixingPayloadImpl.class);

        comparePayloads(sut, deserialised);

        // Retrieve signable content.
        byte[] deserialisedSignableContent
                = getInputStreamContents(deserialised.getSignableContent());

        // The signable content should be the same after deserialisation.
        assertThat(originalSignableContent.length, not(equalTo(0)));
        assertThat(deserialisedSignableContent.length, equalTo(originalSignableContent.length));
        assertThat(deserialisedSignableContent, equalTo(originalSignableContent));
    }

    @Test
    public void generateSameSignatureAfterDeserialising()
            throws GeneralCryptoLibException, IOException, ClassNotFoundException {
        MixingPayload sut = new MixingPayloadMockBuilder().withPayload(getInitialPayload())
                .withSignature(MixingPayloadMockBuilder.getMockSignature()).build();

        byte[] originalSignableContent = getInputStreamContents(sut.getSignableContent());

        // Serialize
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        new ObjectOutputStream(baos).writeObject(sut);
        // Read back
        byte[] serialised = baos.toByteArray();
        MixingPayload deserialised = (MixingPayload) new ObjectInputStream(
                new ByteArrayInputStream(serialised)).readObject();

        comparePayloads(sut, deserialised);

        // Retrieve signable content.
        byte[] deserialisedSignableContent
                = getInputStreamContents(deserialised.getSignableContent());

        // The signable content should be the same after deserialisation.
        assertThat(originalSignableContent.length, not(equalTo(0)));
        assertThat(deserialisedSignableContent.length, equalTo(originalSignableContent.length));
        assertThat(deserialisedSignableContent, equalTo(originalSignableContent));
    }

    @Test
    public void testNativeSerialization()
            throws GeneralCryptoLibException, IOException, ClassNotFoundException {
        MixingPayload original = getInitialPayload();
        MixingPayload sut = new MixingPayloadMockBuilder().withPayload(original)
                .withSignature(MixingPayloadMockBuilder.getMockSignature()).build();

        // Serialise the object.
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                ObjectOutput oo = new ObjectOutputStream(baos)) {
            oo.writeObject(sut);
            // Deserialise the byte array back into an object.
            try (InputStream is = new ByteArrayInputStream(baos.toByteArray());
                    ObjectInput oi = new ObjectInputStream(is)) {
                MixingPayload deserialised = (MixingPayload) oi.readObject();

                assertThat(deserialised.getVoteSetId(), equalTo(sut.getVoteSetId()));
            }
        }
    }

    @Test
    @Ignore("Performance test - should not be part of the general build")
    public void shouldWorkWithMultipleMixingPayloadsConcurrently()
            throws GeneralCryptoLibException, IOException, InterruptedException {
        final int PAYLOAD_COUNT = 10000;
        CountDownLatch latch = new CountDownLatch(PAYLOAD_COUNT);
        for (int i = 0; i < PAYLOAD_COUNT; i++) {
            executor.execute(() -> {
                try {
                    MixingPayload sut = new MixingPayloadMockBuilder()
                            .withPayload(getInitialPayload())
                            .withSignature(MixingPayloadMockBuilder.getMockSignature()).build();
                    // Serialise the object.
                    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            ObjectOutput oo = new ObjectOutputStream(baos)) {
                        oo.writeObject(sut);
                        // Deserialise the byte array back into an object.
                        try (InputStream is = new ByteArrayInputStream(baos.toByteArray());
                                ObjectInput oi = new ObjectInputStream(is)) {
                            MixingPayload deserialised = (MixingPayload) oi.readObject();

                            // Make sure the mixing payloads are serialised correctly.
                            assertThat(deserialised.getVoteSetId(), equalTo(sut.getVoteSetId()));
                            latch.countDown();
                        }
                    } catch (IOException | ClassNotFoundException e) {
                        throw new RuntimeException(e);
                    }
                } catch (GeneralCryptoLibException ex) {
                    Logger.getLogger(MixingPayloadImplITest.class.getName()).log(Level.SEVERE, null, ex);
                }
            });
        }

        System.out.println("All sent!");

        latch.await();

        System.out.println("All done!");
    }

    private void comparePayloads(MixingPayload onePayload, MixingPayload anotherPayload)
            throws GeneralCryptoLibException {
        assertEquals(onePayload.getVoteEncryptionKey().toJson(),
                anotherPayload.getVoteEncryptionKey().toJson());
        assertEquals(onePayload.getVoteSetId(), anotherPayload.getVoteSetId());
        assertEquals(onePayload.getVotes(), anotherPayload.getVotes());
        assertEquals(onePayload.getShuffledVotes(), anotherPayload.getShuffledVotes());
        assertEquals(onePayload.getShuffleProof(), anotherPayload.getShuffleProof());
        assertEquals(onePayload.getElectoralAuthorityId(),
                anotherPayload.getElectoralAuthorityId());
        assertEquals(onePayload.getCommitmentParameters(),
                anotherPayload.getCommitmentParameters());
        assertEquals(onePayload.getEncryptionParameters(),
                anotherPayload.getEncryptionParameters());
        assertEquals(onePayload.getPreviousVotes(), anotherPayload.getPreviousVotes());
        assertEquals(onePayload.getPreviousVoteEncryptionKey().toJson(),
                anotherPayload.getPreviousVoteEncryptionKey().toJson());
    }

    private static byte[] getInputStreamContents(InputStream is) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int bytesRead;
        byte[] data = new byte[1024];
        while ((bytesRead = is.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, bytesRead);
        }

        buffer.flush();
        return buffer.toByteArray();
    }

    private MixingPayload getInitialPayload() throws GeneralCryptoLibException {
        return new MixingPayloadMockBuilder().withSignature(mock(PayloadSignature.class)).build();
    }
}
