package meerkat.voting;


import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ByteString;
import meerkat.crypto.DigitalSignatureGenerator;
import meerkat.crypto.concrete.ECDSADeterministicSignature;
import meerkat.pollingstation.PollingStationScanner;
import meerkat.pollingstation.PollingStationWebScanner;
import meerkat.protobuf.Crypto.RerandomizableEncryptedMessage;
import meerkat.protobuf.Crypto.Signature;
import meerkat.protobuf.Crypto.SignatureType;
import meerkat.protobuf.PollingStation;
import meerkat.protobuf.PollingStation.ScannedData;
import meerkat.protobuf.Voting.EncryptedBallot;
import meerkat.protobuf.Voting.PlaintextBallot;
import meerkat.protobuf.Voting.SignedEncryptedBallot;
import meerkat.voting.controller.callbacks.ControllerCallback;
import meerkat.voting.output.NetworkVirtualPrinter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigInteger;
import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static org.junit.Assert.*;

/**
 * A test for the NetworkVirtualPrinter
 * coded with too much effort by Hai, based on the PollingStationWebScannerTest coded by Arbel
 */



public class NetworkVirtualPrinterTest {
    final Logger logger = LoggerFactory.getLogger(getClass());
    private static final String SCAN_CONTEXT_PATH = "/scan";

    /**
     * Waiting time for transmission in ms.
     */
    final static long TIMEOUT = 500;

    private PollingStationScanner.PollingStationServer scannerServer;


    private NetworkVirtualPrinter networkPrinter;

    PollingStation.ConnectionServerData serverData = null;
    DigitalSignatureGenerator clientSigner;


    BlockingQueue<ScannedData> results = new ArrayBlockingQueue<ScannedData>(1);

    class CommandCallbackHandler extends ControllerCallback<Void> {
        boolean ok;
        CountDownLatch isFlagSet;

        public CommandCallbackHandler(int requestId, long serial) {
            super(requestId, serial,null);
            this.ok = false;
            isFlagSet = new CountDownLatch(1);
        }

        private void done(boolean isOk) {
            ok = isOk;
            isFlagSet.countDown();
        }
        public boolean isOk(long timeout) throws InterruptedException {
            isFlagSet.await(timeout, TimeUnit.MILLISECONDS);
            return ok;
        }

        @Override
        public void onSuccess(Void result) { done(true); }

        @Override
        public void onFailure(Throwable t) {
            logger.error("Commit to ballot failed " + t.getMessage());
            done(false);
        }
    }

    private class ScanHandler implements FutureCallback<ScannedData> {
        @Override
        public void onSuccess(ScannedData result) {
            results.add(result);
        }

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

    void checkScanResult(PollingStation.ScannedBallot ballot, long timeout) throws Exception {
        ScannedData arrivedData = results.poll(timeout, TimeUnit.MILLISECONDS);
        assertEquals("Expecting ballot in received scan", ScannedData.DataCase.BALLOT, arrivedData.getDataCase());
        assertEquals("Received scan does not match transmission", ballot, arrivedData.getBallot());
    }

    void checkSCanResult(PollingStation.ScanError error, long timeout) throws Exception {
        ScannedData arrivedData = results.poll(timeout, TimeUnit.MILLISECONDS);
        assertEquals("Expecting error in received scan", ScannedData.DataCase.ERROR, arrivedData.getDataCase());
        assertEquals("Received scan does not match transmission", error, arrivedData.getError());
    }

    @Before
    public void init() {

        clientSigner = new ECDSADeterministicSignature();
        clientSigner.generateSigningCertificate(BigInteger.ONE, new Date(System.currentTimeMillis()),
                new Date(System.currentTimeMillis() + 1000*60*60*24*365), "testing");

        logger.debug("Setting up Scanner WebApp!");

        scannerServer = new PollingStationWebScanner(0, SCAN_CONTEXT_PATH);

        scannerServer.subscribe(new ScanHandler());

        try {
            serverData = scannerServer.start(true);
            logger.info("Scanner server started at {}", serverData.getServerUrl());
        } catch (Exception e) {
            fail("Could not start server: " + e.getMessage());
        }

        networkPrinter = new NetworkVirtualPrinter(serverData, clientSigner);
        Thread outputThread = new Thread(networkPrinter);
        outputThread.setName("Meerkat VB-Output Thread");
        outputThread.start();
    }

    @Test
    public void testSuccessfulScan() throws Exception {

        // create scannedData

        byte[] channel = {(byte) 1, (byte) 2};
        byte[] encMessageData = {(byte) 50, (byte) 51, (byte) 52};
        byte[] signatureData = {(byte) 93, (byte) 95, (byte) 95};
        byte[] signerId = {(byte) 17, (byte) 18, (byte) 19};
        int serialNumber = 17;

        PlaintextBallot plaintextBallot = PlaintextBallot.newBuilder()
                .setChannelIdentifier(ByteString.copyFrom(channel))
                .setSerialNumber(serialNumber)
                .build();

        RerandomizableEncryptedMessage encMessage = RerandomizableEncryptedMessage.newBuilder()
                .setData(ByteString.copyFrom(encMessageData))
                .build();

        EncryptedBallot encryptedBallot = EncryptedBallot.newBuilder()
                .setSerialNumber(serialNumber)
                .setData(encMessage)
                .build();

        Signature signature = Signature.newBuilder()
                .setType(SignatureType.ECDSA)
                .setData(ByteString.copyFrom(signatureData))
                .setSignerId(ByteString.copyFrom(signerId))
                .build();

        SignedEncryptedBallot signedEncryptedBallot = SignedEncryptedBallot.newBuilder()
                .setEncryptedBallot(encryptedBallot)
                .setSignature(signature)
                .build();

        PollingStation.ScannedBallot scannedBallot = PollingStation.ScannedBallot.newBuilder()
                .setChannel(ByteString.copyFrom(channel))
                .setSignedEncryptedBallot(signedEncryptedBallot)
                .build();



        //Send scan

        CommandCallbackHandler commitHandler = new CommandCallbackHandler(0, 1);
        networkPrinter.commitToBallot(plaintextBallot, signedEncryptedBallot, commitHandler);

        CommandCallbackHandler castHandler = new CommandCallbackHandler(1, 1);
        networkPrinter.castBallot(castHandler);

        // Make sure the response was valid
        assertTrue("Commit to ballot callback did not receive success", commitHandler.isOk(TIMEOUT));
        assertTrue("Cast ballot callback did not receive success", castHandler.isOk(TIMEOUT));

        checkScanResult(scannedBallot, TIMEOUT);
    }

    @After
    public void close() {

        logger.debug("Scanner WebApp shutting down...");

        try {
            scannerServer.stop();
        } catch (Exception e) {
            fail("Could not stop scan server: " + e.getMessage());
        }

        networkPrinter.callShutDown();

    }

}