package meerkat.bulletinboard;

import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ByteString;
import com.google.protobuf.Timestamp;
import meerkat.comm.CommunicationException;
import meerkat.crypto.concrete.ECDSASignature;
import meerkat.crypto.concrete.SHA256Digest;
import meerkat.protobuf.BulletinBoardApi.*;
import meerkat.protobuf.Crypto;
import meerkat.util.BulletinBoardMessageComparator;
import meerkat.util.BulletinBoardMessageGenerator;
import meerkat.util.BulletinBoardUtils;
import meerkat.bulletinboard.AsyncBulletinBoardClient.BatchIdentifier;

import java.io.IOException;
import java.io.InputStream;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.*;
import java.util.concurrent.Semaphore;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.number.OrderingComparison.greaterThanOrEqualTo;
import static org.junit.Assert.*;

/**
 * Created by Arbel Deutsch Peled on 05-Dec-15.
 */
public class GenericBulletinBoardClientTester {

    // Signature resources

    private BulletinBoardSignature signers[];
    private ByteString[] signerIDs;

    private static String KEYFILE_EXAMPLE = "/certs/enduser-certs/user1-key-with-password-secret.p12";
    private static String KEYFILE_EXAMPLE3 = "/certs/enduser-certs/user3-key-with-password-shh.p12";

    private static String KEYFILE_PASSWORD1 = "secret";
    private static String KEYFILE_PASSWORD3 = "shh";

    private static String CERT1_PEM_EXAMPLE = "/certs/enduser-certs/user1.crt";
    private static String CERT3_PEM_EXAMPLE = "/certs/enduser-certs/user3.crt";

    // Client and callbacks

    private AsyncBulletinBoardClient bulletinBoardClient;

    private PostCallback postCallback;

    private RedundancyCallback redundancyCallback;
    private ReadCallback readCallback;

    // Sync and misc

    private Semaphore jobSemaphore;
    private Vector<Throwable> thrown;
    private Random random;
    private BulletinBoardMessageGenerator generator;

    private BulletinBoardDigest digest;

    // Constructor

    public GenericBulletinBoardClientTester(AsyncBulletinBoardClient bulletinBoardClient, int seed){

        this.bulletinBoardClient = bulletinBoardClient;

        signers = new GenericBulletinBoardSignature[2];
        signerIDs = new ByteString[signers.length];
        signers[0] = new GenericBulletinBoardSignature(new ECDSASignature());
        signers[1] = new GenericBulletinBoardSignature(new ECDSASignature());

        InputStream keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE);
        char[] password = KEYFILE_PASSWORD1.toCharArray();

        KeyStore.Builder keyStoreBuilder;
        try {
            keyStoreBuilder = signers[0].getPKCS12KeyStoreBuilder(keyStream, password);

            signers[0].loadSigningCertificate(keyStoreBuilder);

            signers[0].loadVerificationCertificates(getClass().getResourceAsStream(CERT1_PEM_EXAMPLE));

            keyStream = getClass().getResourceAsStream(KEYFILE_EXAMPLE3);
            password = KEYFILE_PASSWORD3.toCharArray();

            keyStoreBuilder = signers[1].getPKCS12KeyStoreBuilder(keyStream, password);
            signers[1].loadSigningCertificate(keyStoreBuilder);

            signers[1].loadVerificationCertificates(getClass().getResourceAsStream(CERT3_PEM_EXAMPLE));

            for (int i = 0 ; i < signers.length ; i++) {
                signerIDs[i] = signers[i].getSignerID();
            }

        } catch (IOException e) {
            System.err.println("Failed reading from signature file " + e.getMessage());
            fail("Failed reading from signature file " + e.getMessage());
        } catch (CertificateException e) {
            System.err.println("Failed reading certificate " + e.getMessage());
            fail("Failed reading certificate " + e.getMessage());
        } catch (KeyStoreException e) {
            System.err.println("Failed reading keystore " + e.getMessage());
            fail("Failed reading keystore " + e.getMessage());
        } catch (NoSuchAlgorithmException e) {
            System.err.println("Couldn't find signing algorithm " + e.getMessage());
            fail("Couldn't find signing algorithm " + e.getMessage());
        } catch (UnrecoverableKeyException e) {
            System.err.println("Couldn't find signing key " + e.getMessage());
            fail("Couldn't find signing key " + e.getMessage());
        }

        this.random = new Random(seed);
        this.generator = new BulletinBoardMessageGenerator(random);
        this.digest = new GenericBulletinBoardDigest(new SHA256Digest());

    }

    // Callback definitions

    protected void genericHandleFailure(Throwable t){
        System.err.println(t.getCause() + " " + t.getMessage());
        thrown.add(t);
        jobSemaphore.release();
    }

    private class PostCallback implements FutureCallback<Boolean>{

        private boolean isAssert;
        private boolean assertValue;

        public PostCallback() {
            this(false);
        }

        public PostCallback(boolean isAssert) {
            this(isAssert,true);
        }

        public PostCallback(boolean isAssert, boolean assertValue) {
            this.isAssert = isAssert;
            this.assertValue = assertValue;
        }

        @Override
        public void onSuccess(Boolean msg) {

            System.err.println("Post operation completed");

            if (isAssert) {
                if (assertValue && !msg) {
                    genericHandleFailure(new AssertionError("Post operation failed"));
                } else if (!assertValue && msg){
                    genericHandleFailure(new AssertionError("Post operation succeeded unexpectedly"));
                } else {
                    jobSemaphore.release();
                }
            } else {
                jobSemaphore.release();
            }

        }

        @Override
        public void onFailure(Throwable t) {
            genericHandleFailure(t);
        }
    }

    private class RedundancyCallback implements FutureCallback<Float>{

        private float minRedundancy;

        public RedundancyCallback(float minRedundancy) {
            this.minRedundancy = minRedundancy;
        }

        @Override
        public void onSuccess(Float redundancy) {
            System.err.println("Redundancy found is: " + redundancy);
            jobSemaphore.release();
            assertThat(redundancy, greaterThanOrEqualTo(minRedundancy));
        }

        @Override
        public void onFailure(Throwable t) {
            genericHandleFailure(t);
        }
    }

    private class ReadCallback implements FutureCallback<List<BulletinBoardMessage>>{

        private List<BulletinBoardMessage> expectedMsgList;

        public ReadCallback(List<BulletinBoardMessage> expectedMsgList) {
            this.expectedMsgList = expectedMsgList;
        }

        @Override
        public void onSuccess(List<BulletinBoardMessage> messages) {

            System.err.println(messages);
            jobSemaphore.release();

            BulletinBoardMessageComparator msgComparator = new BulletinBoardMessageComparator();

            assertThat(messages.size(), is(expectedMsgList.size()));

            Iterator<BulletinBoardMessage> expectedMessageIterator = expectedMsgList.iterator();
            Iterator<BulletinBoardMessage> receivedMessageIterator = messages.iterator();

            while (expectedMessageIterator.hasNext()) {
                assertThat(msgComparator.compare(expectedMessageIterator.next(), receivedMessageIterator.next()), is(0));
            }

        }

        @Override
        public void onFailure(Throwable t) {
            genericHandleFailure(t);
        }
    }

    private class ReadBatchCallback implements FutureCallback<BulletinBoardMessage>{

        private BulletinBoardMessage expectedMsg;

        public ReadBatchCallback(BulletinBoardMessage expectedMsg) {
            this.expectedMsg = expectedMsg;
        }

        @Override
        public void onSuccess(BulletinBoardMessage msg) {

            BulletinBoardMessageComparator msgComparator = new BulletinBoardMessageComparator();

            if (msgComparator.compare(msg, expectedMsg) != 0) {
                genericHandleFailure(new AssertionError("Batch read returned different message.\nExpected:" + expectedMsg + "\nRecieved:" + msg + "\n"));
            } else {
                jobSemaphore.release();
            }

        }

        @Override
        public void onFailure(Throwable t) {
            genericHandleFailure(t);
        }
    }

    // Test methods

    /**
     * Takes care of initializing the client and the test resources
     */
    public void init(){

        random = new Random(0); // We use insecure randomness in tests for repeatability

        postCallback = new PostCallback();
        redundancyCallback = new RedundancyCallback((float) 1.0);

        thrown = new Vector<>();
        jobSemaphore = new Semaphore(0);

    }

    /**
     * Closes the client and makes sure the test fails when an exception occurred in a separate thread
     */

    public void close() {

        if (thrown.size() > 0) {

            for (Throwable t : thrown){
                System.err.println(t.getMessage());
            }

            assert false;

        }

    }

    /**
     * Tests the standard post, redundancy and read methods
     */
    public void testPost() {

        byte[] b1 = {(byte) 1, (byte) 2, (byte) 3, (byte) 4};
        byte[] b2 = {(byte) 11, (byte) 12, (byte) 13, (byte) 14};
        byte[] b3 = {(byte) 21, (byte) 22, (byte) 23, (byte) 24};

        BulletinBoardMessage msg;

        MessageFilterList filterList;
        List<BulletinBoardMessage> msgList;

        MessageID messageID;

        msg = BulletinBoardMessage.newBuilder()
                .setMsg(UnsignedBulletinBoardMessage.newBuilder()
                        .addTag("Signature")
                        .addTag("Trustee")
                        .setData(ByteString.copyFrom(b1))
                        .setTimestamp(Timestamp.newBuilder()
                                .setSeconds(20)
                                .setNanos(30)
                                .build())
                        .build())
                .addSig(Crypto.Signature.newBuilder()
                        .setType(Crypto.SignatureType.DSA)
                        .setData(ByteString.copyFrom(b2))
                        .setSignerId(ByteString.copyFrom(b3))
                        .build())
                .addSig(Crypto.Signature.newBuilder()
                        .setType(Crypto.SignatureType.ECDSA)
                        .setData(ByteString.copyFrom(b3))
                        .setSignerId(ByteString.copyFrom(b2))
                        .build())
                .build();

        messageID = bulletinBoardClient.postMessage(msg,postCallback);

        try {
            jobSemaphore.acquire();
        } catch (InterruptedException e) {
            System.err.println(e.getCause() + " " + e.getMessage());
        }

        bulletinBoardClient.getRedundancy(messageID,redundancyCallback);

        filterList = MessageFilterList.newBuilder()
                .addFilter(
                        MessageFilter.newBuilder()
                                .setType(FilterType.TAG)
                                .setTag("Signature")
                                .build()
                )
                .addFilter(
                        MessageFilter.newBuilder()
                                .setType(FilterType.TAG)
                                .setTag("Trustee")
                                .build()
                )
                .build();

        msgList = new LinkedList<>();
        msgList.add(msg);

        readCallback = new ReadCallback(msgList);

        bulletinBoardClient.readMessages(filterList, readCallback);
        try {
            jobSemaphore.acquire(2);
        } catch (InterruptedException e) {
            System.err.println(e.getCause() + " " + e.getMessage());
        }

    }

    /**
     * Tests posting a batch by parts
     * @throws CommunicationException, SignatureException, InterruptedException
     */
    public void testBatchPost() throws CommunicationException, SignatureException, InterruptedException {

        final int BATCH_LENGTH = 100;
        final int CHUNK_SIZE = 10;
        final int TAG_NUM = 10;

        final Timestamp timestamp = Timestamp.newBuilder()
                .setSeconds(141515)
                .setNanos(859018)
                .build();

        final BulletinBoardMessage msg = generator.generateRandomMessage(signers, timestamp, BATCH_LENGTH, TAG_NUM);

        // Begin batch

        bulletinBoardClient.beginBatch(msg.getMsg().getTagList(), new FutureCallback<BatchIdentifier>() {

            @Override
            public void onSuccess(final BatchIdentifier identifier) {

                bulletinBoardClient.postBatchData(identifier, BulletinBoardUtils.breakToBatch(msg, CHUNK_SIZE), new FutureCallback<Boolean>() {

                    @Override
                    public void onSuccess(Boolean result) {

                        bulletinBoardClient.closeBatch(identifier, msg.getMsg().getTimestamp(), msg.getSigList(), new FutureCallback<Boolean>() {
                            @Override
                            public void onSuccess(Boolean result) {
                                jobSemaphore.release();
                            }

                            @Override
                            public void onFailure(Throwable t) {
                                genericHandleFailure(t);
                            }
                        });

                    }

                    @Override
                    public void onFailure(Throwable t) {
                        genericHandleFailure(t);
                    }

                });

            }

            @Override
            public void onFailure(Throwable t) {
                genericHandleFailure(t);
            }

        });

        jobSemaphore.acquire();

        digest.reset();
        digest.update(msg);

        bulletinBoardClient.readMessage(digest.digestAsMessageID(), new ReadBatchCallback(msg));

        jobSemaphore.acquire();

    }

    /**
     * Posts a complete batch message
     * Checks reading of the message in two parts
     * @throws CommunicationException, SignatureException, InterruptedException
     */
    public void testCompleteBatchPost() throws CommunicationException, SignatureException, InterruptedException {

        final int BATCH_LENGTH = 100;
        final int CHUNK_SIZE = 99;
        final int TAG_NUM = 8;

        final Timestamp timestamp = Timestamp.newBuilder()
                .setSeconds(7776151)
                .setNanos(252616)
                .build();

        final BulletinBoardMessage msg = generator.generateRandomMessage(signers, timestamp, BATCH_LENGTH, TAG_NUM);

        // Post batch

        MessageID msgID = bulletinBoardClient.postAsBatch(msg, CHUNK_SIZE, postCallback);

        jobSemaphore.acquire();

        // Read batch

        MessageFilterList filterList = MessageFilterList.newBuilder()
                .addFilter(MessageFilter.newBuilder()
                        .setType(FilterType.MSG_ID)
                        .setId(msgID.getID())
                        .build())
                .build();

        bulletinBoardClient.readMessages(filterList, new FutureCallback<List<BulletinBoardMessage>>() {

            @Override
            public void onSuccess(List<BulletinBoardMessage> msgList) {

                if (msgList.size() != 1) {

                    genericHandleFailure(new AssertionError("Wrong number of stubs returned. Expected: 1; Found: " + msgList.size()));

                } else {

                    BulletinBoardMessage retrievedMsg = msgList.get(0);
                    bulletinBoardClient.readBatchData(retrievedMsg, new ReadBatchCallback(msg));

                }

            }

            @Override
            public void onFailure(Throwable t) {
                genericHandleFailure(t);
            }

        });

        jobSemaphore.acquire();

    }

}
