/*
 * Copyright (c) 2019, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.synapse.unittest;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
import org.apache.synapse.config.SynapseConfiguration;
import org.apache.synapse.unittest.testcase.data.classes.RegistryResource;
import org.apache.synapse.unittest.testcase.data.classes.SynapseTestCase;
import org.apache.synapse.unittest.testcase.data.classes.TestCaseAssertionSummary;
import org.apache.synapse.unittest.testcase.data.classes.TestCaseSummary;
import org.apache.synapse.unittest.testcase.data.classes.TestSuiteSummary;
import org.apache.synapse.unittest.testcase.data.holders.ArtifactData;
import org.apache.synapse.unittest.testcase.data.holders.MockServiceData;
import org.apache.synapse.unittest.testcase.data.holders.TestCaseData;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.AbstractMap;
import java.util.Map;

/**
 * Class is implements with Runnable.
 * Handle multiple requests from the client
 * Process receive message and response to the particular client
 */
public class RequestHandler implements Runnable {

    private static Logger log = Logger.getLogger(UnitTestingExecutor.class.getName());

    private Socket socket;
    private boolean isTransportPassThroughPortChecked = false;
    private String exception;
    private TestSuiteSummary testSuiteSummary = new TestSuiteSummary();

    /**
     * Initializing RequestHandler withe the client socket connection.
     */
    RequestHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            log.info("Start processing test-case handler");
            checkTransportPassThroughPortAvailability();

            String receivedData = readData();

            SynapseConfiguration synapseConfiguration = UnitTestingExecutor.getExecuteInstance().
                    getSynapseConfiguration();
            synapseConfiguration.setProperty(Constants.IS_RUNNING_AS_UNIT_TEST, "true");

            SynapseTestCase synapseTestCases = preProcessingData(receivedData);

            if (synapseTestCases != null) {
                runTestingAgent(synapseTestCases);
                clearRegistryAndConnectorResources(synapseTestCases);
            } else {
                log.error("Reading Synapse testcase data failed");
                testSuiteSummary.setDescription("Failed while reading synapseTestCase data");
                testSuiteSummary.setDeploymentStatus(Constants.SKIPPED_KEY);
                testSuiteSummary.setDeploymentException(exception);
            }

            writeData(testSuiteSummary);
            MockServiceCreator.stopServices();
            log.info("End processing test-case handler");
        } catch (Exception e) {
            log.error("Error while running client request in test agent", e);
        } finally {
            closeSocket();
        }
    }

    /**
     * Read input data from the unit testing client.
     *
     * @return read data from the client
     */
    private String readData() {
        String inputFromClient = null;

        try {
            InputStream inputStream = socket.getInputStream();
            ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
            inputFromClient = (String) objectInputStream.readObject();

        } catch (Exception e) {
            log.error("Failed to get input stream from TCP connection", e);
        }

        //receiving message from the client
        return inputFromClient;

    }

    /**
     * Processed received message data and stores those data in relevant data holders.
     * Uses configModifier if there are some mock services to start
     *
     * @param receivedMessage received synapseTestcase data message as String
     * @return SynapaseTestCase object contains artifact, test cases and mock services data
     */
    private SynapseTestCase preProcessingData(String receivedMessage) {

        try {
            //Read relevant data from the received message and add to the relevant data holders
            SynapseTestcaseDataReader synapseTestcaseDataReader = new SynapseTestcaseDataReader(receivedMessage);

            ArtifactData readArtifactData = synapseTestcaseDataReader.readAndStoreArtifactData();
            TestCaseData readTestCaseData = synapseTestcaseDataReader.readAndStoreTestCaseData();
            MockServiceData readMockServiceData = synapseTestcaseDataReader.readAndStoreMockServiceData();

            //configure connector resources if exists
            if (!readArtifactData.getConnectorResources().isEmpty()) {
                ConnectorDeployer.deployConnectorResources(readArtifactData.getConnectorResources());
            }

            //store registry resources into mock registry in synapse configuration
            if (!readArtifactData.getRegistryResources().isEmpty()) {
                addRegistryResourcesToMockRegistry(readArtifactData.getRegistryResources());
            }

            //configure the artifact if there are mock-services to append
            String exceptionWhileMocking = null;
            if (readMockServiceData.getMockServicesCount() > 0) {
                exceptionWhileMocking = ConfigModifier.mockServiceLoader(readMockServiceData);
            }

            //check is there any error occurred while mocking endpoints if yes stop the testing and return the exception
            if (exceptionWhileMocking != null) {
                exception = exceptionWhileMocking;
                return null;
            }

            //wrap the artifact data, testcase data and mock service data as one
            SynapseTestCase synapseTestCases = new SynapseTestCase();
            synapseTestCases.setArtifacts(readArtifactData);
            synapseTestCases.setTestCases(readTestCaseData);

            return synapseTestCases;

        } catch (Exception e) {
            log.error("Error while reading data from received message", e);
            exception = CommonUtils.stackTraceToString(e);
            return null;
        }

    }

    /**
     * Execute test agent for artifact deployment and mediation using receiving JSON message.
     *
     * @param synapseTestCase test cases data received from client
     */
    private void runTestingAgent(SynapseTestCase synapseTestCase) {

        TestingAgent agent = new TestingAgent();
        Map.Entry<Boolean, TestSuiteSummary> supportiveArtifactDeployment = new AbstractMap.SimpleEntry<>(false, null);
        Map.Entry<Boolean, TestSuiteSummary> testArtifactDeployment = new AbstractMap.SimpleEntry<>(false, null);

        //get results of supportive-artifact deployment if exists
        if (synapseTestCase.getArtifacts().getSupportiveArtifactCount() > 0) {
            log.info("Supportive artifacts deployment started");
            supportiveArtifactDeployment = agent.processSupportiveArtifacts(synapseTestCase, testSuiteSummary);
        }

        //check supportive-artifact deployment is success or not
        if (supportiveArtifactDeployment.getKey() || synapseTestCase.getArtifacts().getSupportiveArtifactCount() == 0) {
            log.info("Main test artifact deployment started");
            testSuiteSummary.setDeploymentStatus(Constants.PASSED_KEY);
            testArtifactDeployment = agent.processTestArtifact(synapseTestCase, testSuiteSummary);

        } else if (!supportiveArtifactDeployment.getKey() &&
                supportiveArtifactDeployment.getValue().getDeploymentException() != null) {
            testSuiteSummary.setDeploymentStatus(Constants.FAILED_KEY);

        } else {
            testSuiteSummary.setDeploymentStatus(Constants.FAILED_KEY);
            testSuiteSummary.setDescription("Supportive artifact deployment failed");
        }

        //check test-artifact deployment is success or not
        if (testArtifactDeployment.getKey()) {

            log.info("Synapse testing agent ready to mediate test cases through deployments");
            //performs test cases through the deployed synapse configuration
            agent.processTestCases(synapseTestCase, testSuiteSummary);

        } else if (testArtifactDeployment.getValue() != null) {
            testSuiteSummary.setDeploymentStatus(Constants.FAILED_KEY);
        } else {
            testSuiteSummary.setDeploymentStatus(Constants.FAILED_KEY);
            testSuiteSummary.setDescription("test artifact deployment failed");
        }

        //undeploy all the deployed artifacts
        agent.artifactUndeployer();
        ConfigModifier.unitTestMockEndpointMap.clear();
        UnitTestingExecutor.getExecuteInstance().getSynapseConfiguration().
                setProperty(Constants.IS_RUNNING_AS_UNIT_TEST, "false");
    }

    /**
     * Write output data to the unit testing client.
     */
    private void writeData(TestSuiteSummary testSummary) throws IOException {
        JsonObject jsonObject = createResponseJSON(testSummary);
        OutputStream out = socket.getOutputStream();
        ObjectOutputStream o = new ObjectOutputStream(out);
        o.writeObject(jsonObject.toString());
        out.flush();
    }

    /**
     * Create a json response message including all the details of the test suite.
     *
     * @return json message
     */
    private JsonObject createResponseJSON(TestSuiteSummary testSummary) {
        JsonObject jsonResponse = new JsonObject();
        jsonResponse.addProperty(Constants.DEPLOYMENT_STATUS, testSummary.getDeploymentStatus());
        jsonResponse.addProperty(Constants.DEPLOYMENT_EXCEPTION, testSummary.getDeploymentException());
        jsonResponse.addProperty(Constants.DEPLOYMENT_DESCRIPTION, testSummary.getDescription());
        jsonResponse.addProperty(Constants.MEDIATION_STATUS, testSummary.getMediationStatus());
        jsonResponse.addProperty(Constants.CURRENT_TESTCASE, testSummary.getRecentTestCaseName());
        jsonResponse.addProperty(Constants.MEDIATION_EXCEPTION, testSummary.getMediationException());

        JsonArray jsonArray = new JsonArray();
        for (TestCaseSummary summary : testSummary.getTestCaseSumamryList()) {
            JsonObject testObject = new JsonObject();
            testObject.addProperty(Constants.TEST_CASE_NAME, summary.getTestCaseName());
            testObject.addProperty(Constants.MEDIATION_STATUS, summary.getMediationStatus());
            testObject.addProperty(Constants.ASSERTION_STATUS, summary.getAssertionStatus());
            testObject.addProperty(Constants.ASSERTION_EXCEPTION, summary.getTestException());

            JsonArray jsonFailedAssertionArray = new JsonArray();
            for (TestCaseAssertionSummary assertionFailure : summary.getTestCaseAssertionList()) {
                JsonObject failedAssertionObject = new JsonObject();
                failedAssertionObject.addProperty(Constants.ASSERTION_TYPE,
                        assertionFailure.getAssertionType());
                failedAssertionObject.addProperty(Constants.ASSERTION_EXPRESSION,
                        assertionFailure.getAssertionExpression());
                failedAssertionObject.addProperty(Constants.ASSERTION_ACTUAL,
                        assertionFailure.getAssertionActualValue());
                failedAssertionObject.addProperty(Constants.ASSERTION_EXPECTED,
                        assertionFailure.getAssertionExpectedValue());
                failedAssertionObject.addProperty(Constants.ASSERTION_DESCRIPTION,
                        assertionFailure.getAssertionDescription());
                failedAssertionObject.addProperty(Constants.ASSERTION_MESSAGE,
                        assertionFailure.getAssertionErrorMessage());

                jsonFailedAssertionArray.add(failedAssertionObject);
            }

            if (jsonFailedAssertionArray.size() > 0) {
                testObject.add(Constants.FAILURE_ASSERTIONS, jsonFailedAssertionArray);
            }
            jsonArray.add(testObject);
        }

        jsonResponse.add("testCases", jsonArray);
        return jsonResponse;
    }

    /**
     * Check transport pass through port is started or not.
     * Waits until port started to receive the request from client
     */
    private void checkTransportPassThroughPortAvailability() throws IOException {

        if (!isTransportPassThroughPortChecked) {
            log.info("Unit testing agent checks transport Pass-through HTTP Listener port");

            boolean isPassThroughPortNotOccupied = true;
            int transportPassThroughPort = Integer.parseInt(System.getProperty("http.nio.port"));
            long timeoutExpiredMs = System.currentTimeMillis() + 10000;

            while (isPassThroughPortNotOccupied) {
                long waitMillis = timeoutExpiredMs - System.currentTimeMillis();
                isPassThroughPortNotOccupied = checkPortAvailability(transportPassThroughPort);

                if (waitMillis <= 0) {
                    // timeout expired
                    throw new IOException("Connection refused for http Pass-through HTTP Listener port - "
                            + transportPassThroughPort);
                }
            }

            isTransportPassThroughPortChecked = true;
        }
    }

    /**
     * Add user sent registry resources into the mock registry.
     *
     * @param resourceMap map which has registry resources
     */
    private void addRegistryResourcesToMockRegistry(Map<String, RegistryResource> resourceMap) {
        SynapseConfiguration synapseConfiguration = UnitTestingExecutor.getExecuteInstance().getSynapseConfiguration();
        UnitTestMockRegistry mockRegistry = (UnitTestMockRegistry) synapseConfiguration.getRegistry();

        for (Map.Entry<String, RegistryResource> resource : resourceMap.entrySet()) {
            mockRegistry.addResource(resource.getKey(), resource.getValue());
        }
    }

    /**
     * Clear unit test mock registry resources and remove connector test folder in temp if exists.
     *
     * @param synapseTestCases map which has test suite data
     */
    private void clearRegistryAndConnectorResources(SynapseTestCase synapseTestCases) {
        ArtifactData readArtifactData = synapseTestCases.getArtifacts();

        try {
            //remove connector resources include test directory in temp
            if (!readArtifactData.getConnectorResources().isEmpty()) {
                String connectorDestination = System.getProperty(Constants.PRAM_TEMP_DIR) + File.separator
                        + Constants.CONNECTOR_TEST_FOLDER;
                FileUtils.deleteDirectory(new File(connectorDestination));

                log.info("Removed connector resources from the temp directory");
            }

            //clear registry resources map in mock registry
            if (!readArtifactData.getRegistryResources().isEmpty()) {
                SynapseConfiguration synapseConfiguration = UnitTestingExecutor.getExecuteInstance()
                        .getSynapseConfiguration();
                UnitTestMockRegistry mockRegistry = (UnitTestMockRegistry) synapseConfiguration.getRegistry();
                mockRegistry.clearResources();

                log.info("Clear registry resources from the UnitTestMockRegistry");
            }
        } catch (IOException e) {
            log.error("Exception while removing mock connector directory in temp", e);
        }
    }

    /**
     * Check port availability.
     *
     * @param port port which want to check availability
     * @return if available true else false
     */
    private boolean checkPortAvailability(int port) {
        boolean isPortAvailable;
        try (Socket socketTester = new Socket()) {
            socketTester.connect(new InetSocketAddress("127.0.0.1", port));
            isPortAvailable = false;
        } catch (IOException e) {
            isPortAvailable = true;
        }

        return isPortAvailable;
    }

    /**
     * close the TCP connection with client.
     */
    private void closeSocket() {
        try {
            socket.close();
        } catch (IOException e) {
            log.error("Error when closing socket connection", e);
        }
    }

}
