/*
 * 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 org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.Resource;
import javax.inject.Inject;
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 javax.ws.rs.core.Response.Status;

import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;

import com.scytl.products.ov.channel.cc.orchestrator.mixdec.domain.model.BallotBoxStatus;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.domain.model.MixDecBallotBoxStatus;
import com.scytl.products.ov.channel.cc.orchestrator.mixdec.domain.services.MixDecBallotBoxService;
import com.scytl.products.ov.channel.cc.orchestrator.ui.ws.rs.config.MockMixingNode;
import com.scytl.products.ov.channel.cc.orchestrator.ui.ws.rs.config.SystemPropertiesLoader;
import com.scytl.products.ov.commons.beans.domain.model.EntityId;
import com.scytl.products.ov.commons.dto.mixing.MixingPayloadMockBuilder;
import com.scytl.products.ov.commons.messaging.MessageListener;
import com.scytl.products.ov.commons.messaging.MessagingException;
import com.scytl.products.ov.commons.messaging.MessagingService;

public abstract class MixDecArquillianTestBase {

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

    private static final String TEST_TRACK_ID = "TestTrackingId";

    private static final String TENANT_ID_PLACEHOLDER = "{tenantId}";

    private static final String ELECTION_EVENT_ID_PLACEHOLDER = "{electionEventId}";

    private static final String BALLOT_BOX_ID_PLACEHOLDER = "{ballotBoxId}";

    private List<MockMixingNode> mixingNodes = new ArrayList<>();

    static final String BALLOT_BOX_ID = "BB1";

    private static final SystemPropertiesLoader spl = new SystemPropertiesLoader();

    static ControlComponentNodeLayout nodeLayout;

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

    @Resource
    private UserTransaction userTransaction;

    @Inject
    private MixDecBallotBoxService mixDecBallotBoxService;

    @Inject
    MessagingService messagingService;

    @BeforeClass
    public static void init() {
        spl.setProperties();

        nodeLayout = createNodeLayout();
    }

    @Before
    public void cleanDatabase() throws Exception {
        userTransaction.begin();
        Query query = entityManager.createQuery("DELETE FROM " + MixDecBallotBoxStatus.class.getSimpleName());
        query.executeUpdate();
        userTransaction.commit();
    }

    @Before
    public void initTest() throws MessagingException {
        mixDecBallotBoxService.startup();
    }

    /**
     * Start up a few mock mixing nodes as defined by {@code nodeLayout}.
     */
    @Before
    public void startMixingNodes() throws MessagingException {
        // Start up the mixing nodes.
        for (String nodeName : nodeLayout.getNodeNames()) {
            String requestQueueName = nodeLayout.getRequestQueueName(nodeName);
            String responseQueueName = nodeLayout.getResponseQueueName(nodeName);

            MessageListener listener = createListener(nodeName, requestQueueName, responseQueueName);

            mixingNodes
                .add(new MockMixingNode(messagingService, nodeName, requestQueueName, responseQueueName, listener));
        }
    }

    @After
    public void stopMixingNodes() {
        mixingNodes.forEach(MockMixingNode::close);
    }

    @After
    public void endTest() throws MessagingException {
        mixDecBallotBoxService.shutdown();
    }

    /**
     * Create a listener for the requested node name.
     *
     * @param nodeName
     *            the name of the node the listener is for
     * @param requestQueueName
     *            the queue the listener will get messages from
     * @param responseQueueName
     *            the queue the listener will put messages in
     * @return the listener
     */
    protected abstract MessageListener createListener(String nodeName, String requestQueueName,
            String responseQueueName);

    /**
     * @return the control component infrastructure the tests will use.
     */
    private static ControlComponentNodeLayout createNodeLayout() {
        ControlComponentNodeLayout nodeLayout = new ControlComponentNodeLayout();
        nodeLayout.addNode("m1", "md-mixdec-m1-req", "md-mixdec-m1-res");
        nodeLayout.addNode("m2", "md-mixdec-m2-req", "md-mixdec-m2-res");
        nodeLayout.addNode("m3", "md-mixdec-m3-req", "md-mixdec-m3-res");

        return nodeLayout;
    }

    /**
     * Performs the request to start mixing a ballot box
     *
     * @param webTarget
     *            the web target provided by Arquillian
     * @param ballotBoxIds
     *            the identifier of the ballot boxes to be mixed
     * @return the ballot box statuses
     */
    List<BallotBoxStatus> startMixing(ResteasyWebTarget webTarget, String... ballotBoxIds) {
        // Prepare the request URL
        String mixDecStartURL =
            MixDecBallotBoxesResource.RESOURCE_PATH.replace(TENANT_ID_PLACEHOLDER, MixingPayloadMockBuilder.TENANT_ID)
                .replace(ELECTION_EVENT_ID_PLACEHOLDER, MixingPayloadMockBuilder.ELECTION_EVENT_ID);

        // Perform the request
        Response response =
            webTarget.path(mixDecStartURL).request(MediaType.APPLICATION_JSON).header(TRACK_ID_HEADER, TEST_TRACK_ID)
                .post(Entity.entity(buildEntityIdList(ballotBoxIds), MediaType.APPLICATION_JSON));

        // Get the list of accepted ballot boxes.
        List<LinkedHashMap> results = response.readEntity(List.class);
        List<BallotBoxStatus> bbStatuses = results.stream().map(result -> {
            BallotBoxStatus bbStatus = new BallotBoxStatus();
            bbStatus.setProcessStatus((String) result.get("processStatus"));
            bbStatus.setErrorMessage((String) result.get("errorMessage"));
            bbStatus.setBallotBoxId((String) result.get("ballotBoxId"));

            return bbStatus;
        }).collect(Collectors.toList());

        // Ensure the request worked.
        assertThat(response.getStatus(), is(Status.OK.getStatusCode()));

        // Free the connection for the next request.
        response.close();

        return bbStatuses;
    }

    /**
     * Performs the request to retrieve the status of a ballot box.
     * <p>
     * Please note that the connection must be closed manually, so that the
     * response entity is available in the caller context.
     *
     * @param webTarget
     *            the web target provided by Arquillian
     * @param ballotBoxId
     *            the identifier of the ballot box in question
     * @return the response
     */
    String getBallotBoxStatus(ResteasyWebTarget webTarget, String ballotBoxId) {
        // Prepare the request URL
        String mixDecStatusURL = (MixDecBallotBoxResource.RESOURCE_PATH + "/" + MixDecBallotBoxResource.PATH_STATUS)
            .replace(TENANT_ID_PLACEHOLDER, MixingPayloadMockBuilder.TENANT_ID)
            .replace(ELECTION_EVENT_ID_PLACEHOLDER, MixingPayloadMockBuilder.ELECTION_EVENT_ID)
            .replace(BALLOT_BOX_ID_PLACEHOLDER, ballotBoxId);

        Response response = webTarget.path(mixDecStatusURL).request(MediaType.APPLICATION_JSON)
            .header(TRACK_ID_HEADER, TEST_TRACK_ID).get();

        // Ensure the request worked.
        assertThat(response.getStatus(), is(Status.OK.getStatusCode()));

        // Get the status string from the entity.
        String ballotBoxStatus;
        try {
            ballotBoxStatus = (String) response.readEntity(Map.class).values().toArray()[0];
        } catch (Exception e) {
            ballotBoxStatus = null;
        }

        // Free the connection for the next request.
        response.close();

        return ballotBoxStatus;
    }

    /**
     * Performs the request to download the results of mixing a ballot box
     *
     * @param webTarget
     *            the web target provided by Arquillian
     * @param ballotBoxId
     *            the identifier of the ballot box in question
     * @return the response
     */
    Response downloadMixingResults(ResteasyWebTarget webTarget, String ballotBoxId) {
        // Prepare the request URL
        String downloadURL =
            MixDecBallotBoxResource.RESOURCE_PATH.replace(TENANT_ID_PLACEHOLDER, MixingPayloadMockBuilder.TENANT_ID)
                .replace(ELECTION_EVENT_ID_PLACEHOLDER, MixingPayloadMockBuilder.ELECTION_EVENT_ID)
                .replace(BALLOT_BOX_ID_PLACEHOLDER, ballotBoxId);

        // Check the ballot boxes' statuses
        Response response = webTarget.path(downloadURL).request().header(TRACK_ID_HEADER, TEST_TRACK_ID).get();

        // Free the connection for the next request.
        response.close();

        return response;
    }

    /**
     * Creates a list of entity IDs from a number of ballot box IDs.
     *
     * @param ballotBoxIds
     *            the IDs to add to the list
     * @return a list of entity IDs
     */
    private List<EntityId> buildEntityIdList(String... ballotBoxIds) {
        List<EntityId> entityIds = new ArrayList<>(ballotBoxIds.length);

        Arrays.stream(ballotBoxIds).forEach(id -> {
            EntityId entityId = new EntityId();
            entityId.setId(id);
            entityIds.add(entityId);
        });

        return entityIds;
    }

    /**
     * Start up the mixing nodes defined in {@code nodeLayout}.
     */
    void startUpMixingNodes() throws MessagingException {
        // Start up the mixing nodes.
        for (String nodeName : nodeLayout.getNodeNames()) {
            String requestQueueName = nodeLayout.getRequestQueueName(nodeName);
            String responseQueueName = nodeLayout.getResponseQueueName(nodeName);

            mixingNodes.add(new MockMixingNode(messagingService, nodeName, requestQueueName, responseQueueName,
                createListener(nodeName, requestQueueName, responseQueueName)));
        }
    }

    /**
     * A class that stores a control component node layout, along with its
     * queues, in a conveniently accessible structure.
     */
    static class ControlComponentNodeLayout {
        private enum QueueType {
            REQUEST, RESPONSE
        }

        private final Map<String, Map<QueueType, String>> map;

        public ControlComponentNodeLayout() {
            map = new HashMap<>();
        }

        public void addNode(String nodeName, String requestQueueName, String responseQueueName) {
            Map<QueueType, String> queues = new HashMap<>();
            queues.put(QueueType.REQUEST, requestQueueName);
            queues.put(QueueType.RESPONSE, responseQueueName);
            map.put(nodeName, queues);
        }

        public Set<String> getNodeNames() {
            return map.keySet();
        }

        public String getResponseQueueName(String nodeName) {
            return map.get(nodeName).get(QueueType.RESPONSE);
        }

        public String getRequestQueueName(String nodeName) {
            return map.get(nodeName).get(QueueType.REQUEST);
        }
    }
}
