/*
 * 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.sdm.application.service;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.mockito.Matchers.anyMapOf;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PublicKey;
import java.security.Security;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.commons.beans.BallotBox;
import com.scytl.products.ov.commons.beans.BallotBoxContextData;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;
import com.scytl.products.ov.commons.path.PathResolver;
import com.scytl.products.ov.commons.verify.JSONVerifier;
import com.scytl.products.ov.sdm.domain.common.SignedObject;
import com.scytl.products.ov.sdm.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.sdm.domain.model.status.Status;
import com.scytl.products.ov.sdm.domain.model.status.SynchronizeStatus;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;
import com.scytl.products.ov.utils.ConfigObjectMapper;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

@RunWith(MockitoJUnitRunner.class)
public class BallotBoxServiceTest {

    @Rule
    public ExpectedException expectedException = ExpectedException.none();

    @InjectMocks
    private BallotBoxService ballotBoxService = new BallotBoxService();

    private String ballotBoxREADY =
        "{\"result\": [{\"id\": \"96e\", \"ballot\" : { " + "\"id\": \"96f\"}, \"status\": \"READY\"}]}";

    final String orUrl = "http://localhost:%s/ag-ws-rest/or";

    private final static String BB_LIST = "{\"result\":[{\"id\":\"123\", \"status\":\"SIGNED\"}]}";

    private static int httpPort;

    @Mock
    private BallotBoxRepository ballotBoxRepositoryMock;

    @Mock
    private PathResolver pathResolver;

    @Mock
    private ConfigurationEntityStatusService statusServiceMock;

    @Mock
    private KeyStoreService keyStoreServiceMock;

    private ConfigObjectMapper mapper;

    @BeforeClass
    public static void beforeClass() {
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }
    
    @Before
    public void init() {
        mapper = new ConfigObjectMapper();
    }

    @Test
    public void sign() throws IOException, GeneralCryptoLibException, ResourceNotFoundException {

        ballotBoxService.init();
        String electionEventId = "989";
        String ballotId = "96f";
        String ballotBoxId = "96e";

        when(ballotBoxRepositoryMock.list(anyMapOf(String.class, Object.class))).thenReturn(ballotBoxREADY);
        when(pathResolver.resolve(Matchers.<String> anyVararg()))
            .thenReturn(Paths.get("src/test/resources/ballotboxservice/" + electionEventId
                + "/ONLINE/electionInformation/ballots/" + ballotId + "/ballotBoxes/" + ballotBoxId));
        when(statusServiceMock.updateWithSynchronizedStatus(Status.SIGNED.name(), ballotBoxId, ballotBoxRepositoryMock,
            SynchronizeStatus.PENDING)).thenReturn("");

        ballotBoxService.sign(electionEventId, ballotBoxId, SigningTestData.PRIVATE_KEY_PEM);

        Path outputPath = Paths.get("src/test/resources/ballotboxservice/989/ONLINE/electionInformation" + "/ballots/"
            + ballotId + "/ballotBoxes/" + ballotBoxId);
        Path signedBallotBox = Paths.get(outputPath.toString(), ConfigConstants.CONFIG_DIR_NAME_SIGNED_BALLOTBOX_JSON);
        Path signedBallotBoxContextData =
            Paths.get(outputPath.toString(), ConfigConstants.CONFIG_DIR_NAME_SIGNED_BALLOTBOX_CONTEXT_DATA_JSON);

        assertThat(Files.exists(signedBallotBox), is(true));
        assertThat(Files.exists(signedBallotBoxContextData), is(true));

        SignedObject signedBallotBoxObject = mapper.fromJSONFileToJava(signedBallotBox.toFile(), SignedObject.class);
        String signatureBallotBox = signedBallotBoxObject.getSignature();

        SignedObject signedBallotBoxContextDataObject =
            mapper.fromJSONFileToJava(signedBallotBoxContextData.toFile(), SignedObject.class);
        String signatureBallotBoxContextData = signedBallotBoxContextDataObject.getSignature();

        JSONVerifier verifier = new JSONVerifier();
        PublicKey publicKey = PemUtils.publicKeyFromPem(SigningTestData.PUBLIC_KEY_PEM);

        verifier.verify(publicKey, signatureBallotBox, BallotBox.class);
        verifier.verify(publicKey, signatureBallotBoxContextData, BallotBoxContextData.class);

        Files.deleteIfExists(signedBallotBox);
        Files.deleteIfExists(signedBallotBoxContextData);

    }

    @SuppressWarnings("unchecked")
    @Test
    public void throw_an_exception_when_ballot_box_is_locked()
            throws IOException, GeneralCryptoLibException, ResourceNotFoundException {

        ballotBoxService.init();
        String electionEventId = "989";
        String ballotBoxId = "96e";

        when(ballotBoxRepositoryMock.list(anyMapOf(String.class, Object.class)))
            .thenThrow(ResourceNotFoundException.class);

        expectedException.expect(ResourceNotFoundException.class);
        ballotBoxService.sign(electionEventId, ballotBoxId, SigningTestData.PRIVATE_KEY_PEM);

    }

    @Test
    public void updateMixingStatus() throws RetrofitException, IOException, GeneralCryptoLibException {
        setupHttpServer();

        ReflectionTestUtils.setField(ballotBoxService, "tenantId", "100");

        String finalUrl = String.format(orUrl, httpPort);
        ReflectionTestUtils.setField(ballotBoxService, "orchestratorUrl", finalUrl);

        when(keyStoreServiceMock.getPrivateKey())
            .thenReturn(new AsymmetricService().getKeyPairForSigning().getPrivate());

        when(ballotBoxRepositoryMock.list(Mockito.anyMap())).thenReturn(BB_LIST);

        String electionEventId = "abcd";
        ballotBoxService.updateBallotBoxesMixingStatus(electionEventId);

        Mockito.verify(ballotBoxRepositoryMock).update("{\"id\":\"123\",\"status\":\"MIXED\"}");
    }

    public void setupHttpServer() {
        try {
            setPorts();

            // setup the socket address
            InetSocketAddress address = new InetSocketAddress(httpPort);

            // initialise the HTTP server
            HttpServer httpServer = HttpServer.create(address, 0);
            httpServer.createContext("/", new MyHttpHandler());
            httpServer.setExecutor(null); // creates a default executor
            httpServer.start();

        } catch (Exception exception) {
            System.out.println("Failed to create HTTP server on port 8080 of localhost");
            exception.printStackTrace();

        }
    }

    private static void setPorts() throws IOException {
        httpPort = discoverFreePorts(50000, 60000);
    }

    private static int discoverFreePorts(int from, int to) throws IOException {
        int result = 0;
        ServerSocket tempServer = null;

        for (int i = from; i <= to; i++) {
            try {
                tempServer = new ServerSocket(i);
                result = tempServer.getLocalPort();
                break;

            } catch (IOException ex) {
                continue; // try next port
            }
        }

        if (result == 0) {
            throw new IOException("no free port found");
        }

        tempServer.close();
        return result;
    }

    public static class MyHttpHandler implements HttpHandler {
        @Override
        public void handle(HttpExchange t) throws IOException {
            String response = "{\"status\":\"MIXED\"}";
            t.getResponseHeaders().add("Content-Type", "application/json");
            t.sendResponseHeaders(200, response.length());
            OutputStream os = t.getResponseBody();
            os.write(response.getBytes());
            os.close();
        }
    }
}
