/*
 * 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.channel.cc.orchestrator.ui.ws.rs.operation;

import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import java.io.InputStream;
import java.math.BigInteger;
import java.security.Security;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.transaction.UserTransaction;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.apache.commons.io.IOUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jboss.arquillian.extension.rest.client.ArquillianResteasyResource;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.jboss.resteasy.client.jaxrs.internal.ClientResponse;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

import com.scytl.cryptolib.api.elgamal.ElGamalServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalEncrypter;
import com.scytl.cryptolib.elgamal.cryptoapi.CryptoAPIElGamalKeyPairGenerator;
import com.scytl.cryptolib.elgamal.service.ElGamalService;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.channel.cc.orchestrator.choicecodes.domain.model.computedvalues.ComputedValues;
import com.scytl.products.ov.channel.cc.orchestrator.ui.ws.rs.config.SystemPropertiesLoader;
import com.scytl.products.ov.commons.dto.ChoiceCodeGenerationReqPayload;
import com.scytl.products.ov.commons.dto.ElGamalComputationsValuesCodec;
import com.scytl.products.ov.commons.dto.ElGamalComputationsValuesCodecImpl;

/**
 * WARNING: This test can only be run as part of the MessageBrokerTestSuite.
 */
@RunWith(Arquillian.class)
public class ChoiceCodesResourceArquillian {

    private static final String TRACK_ID_HEADER = "X-Request-ID";

    private static final String TEST_TRACK_ID = "TestTrackingId";

    private static final int TEST_CHUNK_ID = 0;

    private static final int TEST_CHUNK_COUNT = 1;

    private static final String TEST_VERIFICATION_CARD_SET_ID = "2adf5a4df5as4df51a5sf1gh5657846h";

    private static final String TEST_ELECTION_EVENT_ID = "5adfa4sdf581sdf8d4fasdfbhgh349k8";

    protected static final String TEST_BALLOT_BOX_ID = "7f59b097dcc6407e933fdfa94b9812b7";

    private static final String TEST_TENANT_ID = "100";

    private static final BigInteger OPTION_0 = new BigInteger("1");

    private static final BigInteger OPTION_1 = new BigInteger("5");

    private static final BigInteger OPTION_2 = new BigInteger("7");

    private static final BigInteger COMPUTED_VALUE_0 = new BigInteger("1");

    private static final BigInteger COMPUTED_VALUE_1 = new BigInteger("2");

    private static final BigInteger COMPUTED_VALUE_2 = new BigInteger("3");

    private static final String VC_ID_1 = "79c6c49af4044b1abb5434413304748f";

    private static final String VC_ID_2 = "39c6c49af4044b1abb5434413304748f";

    private static final List<BigInteger> BALLOT_OPTIONS_LIST = asList(OPTION_0, OPTION_1, OPTION_2);

    private static final Map<String, String> VERIFICATION_CARD_IDS_TO_BCK = new HashMap<>();

    private static final String BALLOT_CASTING_KEY1 = "2";

    private static final String BALLOT_CASTING_KEY2 = "3";

    private static final String ELECTION_EVENT_TEMPLATE = "electionEventId";

    private static final String TENANT_TEMPLATE = "tenantId";

    private static final String BALLOT_BOX_TEMPLATE = "ballotBoxId";

    private static final String VERIFICATION_CARD_SET_TEMPLATE = "verificationCardSetId";

    private static final String CHUNK_TEMPLATE = "chunkId";

    private static final String CHUNK_COUNT_TEMPLATE = "chunkCount";

    private static final SystemPropertiesLoader spl = new SystemPropertiesLoader();

    private static final ElGamalComputationsValuesCodec CODEC = ElGamalComputationsValuesCodecImpl.getInstance();

    private static Map<String, Object> templates;

    private static ElGamalKeyPair keyPair;

    private static String encryptedRepresentations;

    private static ElGamalServiceAPI elGamalService;

    private static final String COMPUTED_VALUES_JSON = "computedValuesJson";

    @PersistenceContext(unitName = "persistenceUnitJdbc")
    EntityManager entityManager;

    @Resource
    UserTransaction userTransaction;

    @BeforeClass
    public static void beforeClass() throws GeneralCryptoLibException {
        spl.setProperties();

        templates = new HashMap<>();
        templates.put(TENANT_TEMPLATE, TEST_TENANT_ID);
        templates.put(ELECTION_EVENT_TEMPLATE, TEST_ELECTION_EVENT_ID);
        templates.put(VERIFICATION_CARD_SET_TEMPLATE, TEST_VERIFICATION_CARD_SET_ID);
        templates.put(CHUNK_TEMPLATE, TEST_CHUNK_ID);
        templates.put(CHUNK_COUNT_TEMPLATE, TEST_CHUNK_COUNT);
        templates.put(BALLOT_BOX_TEMPLATE, TEST_BALLOT_BOX_ID);

        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }

        elGamalService = new ElGamalService();

        CryptoAPIElGamalKeyPairGenerator generator = elGamalService.getElGamalKeyPairGenerator();

        ElGamalEncryptionParameters encryptionParameters =
            new ElGamalEncryptionParameters(BigInteger.valueOf(23), BigInteger.valueOf(11), BigInteger.valueOf(2));
        keyPair = generator.generateKeys(encryptionParameters, 1);

        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(keyPair.getPublicKeys());
        List<ElGamalComputationsValues> ciphertexts = new ArrayList<>();
        for (BigInteger option : BALLOT_OPTIONS_LIST) {
            ZpGroupElement message = new ZpGroupElement(option, (ZpSubgroup) encryptionParameters.getGroup());
            ElGamalComputationsValues ciphertext =
                encrypter.encryptGroupElements(singletonList(message)).getComputationValues();
            ciphertexts.add(ciphertext);
        }
        encryptedRepresentations = CODEC.encodeList(ciphertexts);
    }

    @Before
    public void init() throws Exception {
        // CLEANING DATABASE BETWEEN TESTS
        userTransaction.begin();
        Query query = entityManager.createQuery("DELETE FROM " + ComputedValues.class.getSimpleName());
        query.executeUpdate();
        userTransaction.commit();

        VERIFICATION_CARD_IDS_TO_BCK.put(VC_ID_1, BALLOT_CASTING_KEY1);
        VERIFICATION_CARD_IDS_TO_BCK.put(VC_ID_2, BALLOT_CASTING_KEY2);
    }

    @Test
    public void testComputeRequestHappyPath(@ArquillianResteasyResource("") ResteasyWebTarget webTarget) {
        ChoiceCodeGenerationReqPayload payload = new ChoiceCodeGenerationReqPayload(TEST_TENANT_ID,
            TEST_ELECTION_EVENT_ID, TEST_VERIFICATION_CARD_SET_ID, TEST_CHUNK_ID);

        ClientResponse response =
            (ClientResponse) webTarget.path(getPath(ChoiceCodesResource.PATH_COMPUTE_GENERATION_CONTRIBUTIONS_REQUEST))
                .resolveTemplates(templates).request(MediaType.APPLICATION_JSON).header(TRACK_ID_HEADER, TEST_TRACK_ID)
                .post(Entity.entity(payload, MediaType.APPLICATION_JSON));

        assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));
    }

    @Test
    public void testGetComputationStatusHappyPath(@ArquillianResteasyResource("") ResteasyWebTarget webTarget)
            throws Exception {

        populateDB();

        ClientResponse response = (ClientResponse) webTarget
            .path(getPath(ChoiceCodesResource.PATH_COMPUTE_GENERATION_CONTRIBUTIONS_STATUS))
            .queryParam(ChoiceCodesResource.QUERY_PARAMETER_CHUNK_COUNT, TEST_CHUNK_COUNT).resolveTemplates(templates)
            .request(MediaType.APPLICATION_JSON).header(TRACK_ID_HEADER, TEST_TRACK_ID).get();

        assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));
        assertThat(response.readEntity(String.class), is("{\"status\":\"COMPUTED\"}"));
    }

    @Test
    public void testComputeRetrieveHappyPath(@ArquillianResteasyResource("") ResteasyWebTarget webTarget)
            throws Exception {

        populateDB();

        ClientResponse response = (ClientResponse) webTarget
            .path(getPath(ChoiceCodesResource.PATH_COMPUTE_GENERATION_CONTRIBUTIONS_RETRIEVAL))
            .resolveTemplates(templates).request(MediaType.APPLICATION_OCTET_STREAM)
            .header(TRACK_ID_HEADER, TEST_TRACK_ID).get();

        assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode()));

        InputStream inputStream = response.readEntity(InputStream.class);
        byte[] inputStreamBytes = IOUtils.toByteArray(inputStream);
        String json = new String(inputStreamBytes);

        assertThat(json, is(COMPUTED_VALUES_JSON));
    }

    private static String getPath(String segment) {
        return ChoiceCodesResource.RESOURCE_PATH + "/" + segment;
    }

    private void populateDB() throws Exception {

        List<ElGamalComputationsValues> encryptedOptions = CODEC.decodeList(encryptedRepresentations);

        CryptoAPIElGamalEncrypter encrypter = elGamalService.createEncrypter(keyPair.getPublicKeys());
        ZpSubgroup group = keyPair.getPublicKeys().getGroup();
        // Use same contribution for all verification card ids and single node
        // configuration as test data
        Map<ElGamalComputationsValues, ElGamalComputationsValues> persistedOptionRepresentationContribution =
            new HashMap<>();
        ZpGroupElement element = new ZpGroupElement(COMPUTED_VALUE_0, group);
        ElGamalComputationsValues encryptedComputedValue =
            encrypter.encryptGroupElements(singletonList(element)).getComputationValues();
        persistedOptionRepresentationContribution.put(encryptedOptions.get(0), encryptedComputedValue);
        element = new ZpGroupElement(COMPUTED_VALUE_1, group);
        encryptedComputedValue = encrypter.encryptGroupElements(singletonList(element)).getComputationValues();
        persistedOptionRepresentationContribution.put(encryptedOptions.get(1), encryptedComputedValue);
        element = new ZpGroupElement(COMPUTED_VALUE_2, group);
        encryptedComputedValue = encrypter.encryptGroupElements(singletonList(element)).getComputationValues();
        persistedOptionRepresentationContribution.put(encryptedOptions.get(2), encryptedComputedValue);

        ComputedValues computedValues = new ComputedValues();
        computedValues.setTenantId(TEST_TENANT_ID);
        computedValues.setElectionEventId(TEST_ELECTION_EVENT_ID);
        computedValues.setVerificationCardSetId(TEST_VERIFICATION_CARD_SET_ID);
        computedValues.setChunkId(TEST_CHUNK_ID);

        computedValues.setJson(COMPUTED_VALUES_JSON);

        saveEntity(computedValues);
    }

    private <T> void saveEntity(T entityToSave) throws Exception {

        userTransaction.begin();
        entityManager.persist(entityToSave);
        userTransaction.commit();
    }
}
