package meerkat.pollingstation;

import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ByteString;
import meerkat.crypto.DigitalSignatureGenerator;
import meerkat.crypto.concrete.ECDSADeterministicSignature;
import meerkat.protobuf.Crypto;
import meerkat.protobuf.PollingStation;
import meerkat.protobuf.PollingStation.ScannedData;
import meerkat.protobuf.Voting;
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.TimeUnit;

import static org.junit.Assert.*;


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

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

    PollingStation.ConnectionServerData serverData = null;
    DigitalSignatureGenerator clientSigner;


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

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

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


    @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!");

        scanner = new PollingStationWebScanner(0, SCAN_CONTEXT_PATH);
        scanner.subscribe(new ScanHandler());

        try {
            serverData = scanner.start(true);
        } catch (Exception e) {
            fail("Could not start server: " + e.getMessage());
        }

        logger.info("Started scanner web service. API endpoint: {}", serverData.getServerUrl());

    }

    PollingStationScanner.ScannerClient connectClient() throws InterruptedException {

        PollingStationScanner.ScannerClient scannerClient =
                new ScannerClientAPI(clientSigner);

        boolean connectOk = scannerClient.connect(serverData);

        assertTrue("Connection to Scanner server failed!", connectOk);

        return scannerClient;
    }


    void checkResult(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 checkResult(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());
    }


    @Test
    public void testSuccessfulScan() throws Exception {

        byte[] data = {(byte) 1, (byte) 2};

        PollingStation.ScannedBallot ballot = PollingStation.ScannedBallot.newBuilder()
                .setChannel(ByteString.copyFrom(data))
                .setSignedEncryptedBallot(Voting.SignedEncryptedBallot.newBuilder()
                        .setEncryptedBallot(Voting.EncryptedBallot.newBuilder()
                                .setSerialNumber(0)
                                .setData(Crypto.RerandomizableEncryptedMessage.newBuilder()
                                        .setData(ByteString.copyFromUtf8("Testing"))
                                )
                        )
                )
                .build();

        PollingStationScanner.ScannerClient client = connectClient();

        boolean scanOk = client.newScan(ballot);

        assertTrue("Scan transmission failed", scanOk);

        checkResult(ballot, TIMEOUT);
    }

    @Test
    public void testErroneousScan() throws Exception {
        PollingStation.ScanError errorMsg = PollingStation.ScanError.newBuilder()
                .setMsg("!Error Message!")
                .build();

        PollingStationScanner.ScannerClient client = connectClient();
        boolean scanOk = client.reportError(errorMsg);
        assertTrue("Error report transmission failed", scanOk);

        checkResult(errorMsg, TIMEOUT);
    }

    @After
    public void shutDown() {
        logger.debug("{} shutting down...", scanner.getClass().getCanonicalName());

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

    }

}