package meerkat.voting;


import com.google.common.util.concurrent.FutureCallback;

import meerkat.protobuf.Crypto.*;
import meerkat.protobuf.PollingStation.*;

import com.google.protobuf.ByteString;
import meerkat.pollingstation.PollingStationScanner;
import meerkat.pollingstation.PollingStationWebScanner;

import meerkat.protobuf.Voting.*;
import meerkat.voting.controller.callbacks.OutputDeviceCommitCallback;
import meerkat.voting.controller.callbacks.OutputDeviceFinalizeCallback;
import meerkat.voting.output.NetworkVirtualPrinter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.concurrent.Semaphore;


import static org.hamcrest.MatcherAssert.assertThat;

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



public class NetworkVirtualPrinterTest {

    private PollingStationScanner.Consumer scanner;
    private static final String ADDRESS = "http://localhost";
    private static final String SUB_ADDRESS = "";
    private static final int PORT = 8080;

    private Semaphore semaphore0;
    private Semaphore semaphore1;
    private Semaphore semaphore2;
    private Throwable thrown;
    private boolean dataIsAsExpected;

    private NetworkVirtualPrinter networkPrinter;

    private class ScanHandler implements FutureCallback<ScannedData> {

        private final ScannedData expectedData;

        public ScanHandler(ScannedData expectedData) {
            this.expectedData = expectedData;
        }

        @Override
        public void onSuccess(ScannedData result) {
            dataIsAsExpected = result.equals(expectedData);
            semaphore2.release();
        }

        @Override
        public void onFailure(Throwable t) {
            dataIsAsExpected = false;
            thrown = t;
            semaphore2.release();
        }
    }

    private class CommitHandler extends OutputDeviceCommitCallback {

        private boolean success;
        public String errorMessage;

        public CommitHandler(int requestId, long serialNumber) {
            super(requestId, serialNumber, null, null);
            errorMessage = null;
            success = false;
        }

        @Override
        public void onSuccess(Void v) {
            System.out.println("CommitHandler success");
            success = true;
            semaphore0.release();
        }

        @Override
        public void onFailure(Throwable t) {
            errorMessage = "Commit to ballot failed " + t.getMessage();
            semaphore0.release();
        }

        public boolean gotSuccess() {
            return success;
        }
    }

    private class CastHandler extends OutputDeviceFinalizeCallback {

        private boolean success;
        public String errorMessage;

        public CastHandler(int requestId, long serialNumber) {
            super(requestId, serialNumber, null, null);
            errorMessage = null;
            success = false;
        }

        @Override
        public void onSuccess(Void v) {
            System.out.println("CastHandler success");
            success = true;
            semaphore1.release();
        }

        @Override
        public void onFailure(Throwable t) {
            errorMessage = "Cast to ballot failed " + t.getMessage();
            semaphore1.release();
        }

        public boolean gotSuccess() {
            return success;
        }
    }

    @Before
    public void init() {

        System.err.println("Setting up Scanner WebApp!");

        scanner = new PollingStationWebScanner(PORT, SUB_ADDRESS);

        semaphore0 = new Semaphore(0);
        semaphore1 = new Semaphore(0);
        semaphore2 = new Semaphore(0);
        thrown = null;

        try {
            scanner.start();
        } catch (Exception e) {
            assertThat("Could not start server: " + e.getMessage(), false);
        }


        networkPrinter = new NetworkVirtualPrinter(ADDRESS + ":" + PORT);
        Thread outputThread = new Thread(networkPrinter);
        outputThread.setName("Meerkat VB-Output Thread");
        outputThread.start();
    }

    @Test
    public void testSuccessfulScan() throws InterruptedException {

        // 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();

        ScannedData scannedData = ScannedData.newBuilder()
                .setChannel(ByteString.copyFrom(channel))
                .setSignedEncryptedBallot(signedEncryptedBallot)
                .build();


        scanner.subscribe(new ScanHandler(scannedData));

        //Send scan

        CommitHandler commitHandler = new CommitHandler(0, serialNumber);
        networkPrinter.commitToBallot(plaintextBallot, signedEncryptedBallot, commitHandler);

        semaphore0.acquire();

        CastHandler castHandler = new CastHandler(1, serialNumber);
        networkPrinter.castBallot(castHandler);

        semaphore1.acquire();

        semaphore2.acquire();

        // Make sure the response was valid

        assertThat("Commit to ballot callback did not receive success", commitHandler.gotSuccess());
        assertThat("Cast ballot callback did not receive success", castHandler.gotSuccess());

        assertThat("Scanner has thrown an error", thrown == null);
        assertThat("Scanned data received was incorrect", dataIsAsExpected);

    }

    @After
    public void close() {

        System.err.println("Scanner WebApp shutting down...");

        try {
            scanner.stop();
        } catch (Exception e) {
            assertThat("Could not stop server: " + e.getMessage(), false);
        }

        networkPrinter.callShutDown();

    }

}