/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */
package de.rcenvironment.core.communication.testutils.templates;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.List;

import org.easymock.Capture;
import org.easymock.CaptureType;
import org.easymock.EasyMock;
import org.easymock.IAnswer;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.Timeout;

import de.rcenvironment.core.communication.channel.MessageChannelState;
import de.rcenvironment.core.communication.channel.ServerContactPoint;
import de.rcenvironment.core.communication.common.NodeIdentifierTestUtils;
import de.rcenvironment.core.communication.configuration.IPWhitelistConnectionFilter;
import de.rcenvironment.core.communication.model.InitialNodeInformation;
import de.rcenvironment.core.communication.model.NetworkContactPoint;
import de.rcenvironment.core.communication.model.NetworkRequest;
import de.rcenvironment.core.communication.model.NetworkResponse;
import de.rcenvironment.core.communication.model.impl.InitialNodeInformationImpl;
import de.rcenvironment.core.communication.model.impl.NetworkResponseImpl;
import de.rcenvironment.core.communication.protocol.MessageMetaData;
import de.rcenvironment.core.communication.protocol.NetworkRequestFactory;
import de.rcenvironment.core.communication.protocol.ProtocolConstants;
import de.rcenvironment.core.communication.testutils.AbstractTransportBasedTest;
import de.rcenvironment.core.communication.transport.spi.BrokenMessageChannelListener;
import de.rcenvironment.core.communication.transport.spi.MessageChannel;
import de.rcenvironment.core.communication.transport.spi.MessageChannelEndpointHandler;
import de.rcenvironment.core.communication.transport.spi.MessageChannelResponseHandler;
import de.rcenvironment.core.communication.utils.MessageUtils;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;

/**
 * A common base class that defines common tests to verify proper transport operation. Subclasses implement
 * {@link #defineTestConfiguration()} to create a transport-specific test.
 * 
 * @author Robert Mischke
 */
public abstract class AbstractTransportLowLevelTest extends AbstractTransportBasedTest {

    private static final int DEFAULT_REQUEST_TIMEOUT = 1000;

    private static final long WAIT_FOR_INBOUND_CHANNEL_CLOSING_EVENT_MSEC = 1000;

    /**
     * This sets a conservative timeout for all derived tests; individual test may set stricter timeouts. - misc_ro
     */
    @Rule
    public Timeout globalTimeout = new Timeout(DEFAULT_SAFEGUARD_TEST_TIMEOUT);

    /**
     * This test verifies the basic functions of a network transport. It covers {@link ServerContactPoint} startup, connection initiation,
     * initial node information handshake, and the basic request/response loop.
     * 
     * @author Robert Mischke
     * @throws Exception on uncaught exceptions
     */
    @Test
    public void basicTransportOperation() throws Exception {

        int messageRepetitions = 10;

        // create mock server config
        InitialNodeInformationImpl mockServerNodeInformation =
            new InitialNodeInformationImpl(NodeIdentifierTestUtils.createTestInstanceNodeSessionId());
        mockServerNodeInformation.setDisplayName("Mock Server");
        // configure mock endpoint handler
        MessageChannelEndpointHandler serverEndpointHandler = EasyMock.createMock(MessageChannelEndpointHandler.class);
        // configure handshake response
        EasyMock.expect(serverEndpointHandler.exchangeNodeInformation(EasyMock.anyObject(InitialNodeInformation.class))).andReturn(
            mockServerNodeInformation);
        // expect passive connection event (if applicable)
        if (transportProvider.supportsRemoteInitiatedConnections()) {
            serverEndpointHandler.onRemoteInitiatedChannelEstablished(EasyMock.anyObject(MessageChannel.class),
                EasyMock.anyObject(ServerContactPoint.class));
        }
        EasyMock.replay(serverEndpointHandler);
        BrokenMessageChannelListener brokenConnectionListener = EasyMock.createMock(BrokenMessageChannelListener.class);
        EasyMock.replay(brokenConnectionListener);
        // create server contact point
        NetworkContactPoint ncp = contactPointGenerator.createContactPoint();
        IPWhitelistConnectionFilter ipFilter = new IPWhitelistConnectionFilter();
        // allow test connections from IPv4 localhost; adapt if necessary
        ipFilter.configure(Arrays.asList(new String[] { "127.0.0.1" }));
        ServerContactPoint scp =
            new ServerContactPoint(transportProvider, ncp, ProtocolConstants.PROTOCOL_COMPATIBILITY_VERSION, serverEndpointHandler,
                ipFilter);
        // start it
        assertFalse(scp.isAcceptingMessages());
        scp.start();
        assertTrue(scp.isAcceptingMessages());

        // create mock client config
        InitialNodeInformationImpl clientNodeInformation =
            new InitialNodeInformationImpl(NodeIdentifierTestUtils.createTestInstanceNodeSessionIdWithDisplayName("clientNode"));
        // connect
        // (allows duplex connections, but omits client endpoint handler as it should not be used)
        MessageChannel channel =
            transportProvider.connect(ncp, clientNodeInformation, ProtocolConstants.PROTOCOL_COMPATIBILITY_VERSION, true, null,
                brokenConnectionListener);

        assertEquals(MessageChannelState.ESTABLISHED, channel.getState());
        assertTrue(channel.isReadyToUse());

        // verify server side of handshake
        EasyMock.verify(serverEndpointHandler);
        // verify client side of handshake
        assertNotNull(channel.getChannelId());
        assertNotNull(channel.getRemoteNodeInformation());
        assertEquals(mockServerNodeInformation, channel.getRemoteNodeInformation());

        logThreadPoolState("after connecting");

        // define server response behavior
        final String requestString = "Hi world";
        final String responseSuffix = "#response"; // arbitrary
        EasyMock.reset(serverEndpointHandler);
        serverEndpointHandler.onRawRequestReceived(EasyMock.isA(NetworkRequest.class), EasyMock.isA(String.class));

        EasyMock.expectLastCall().andAnswer(new IAnswer<NetworkResponse>() {

            @Override
            public NetworkResponse answer() throws Throwable {
                try {
                    NetworkRequest request = (NetworkRequest) EasyMock.getCurrentArguments()[0];
                    String responseString = request.getDeserializedContent().toString() + responseSuffix;
                    byte[] responseBytes = MessageUtils.serializeSafeObject(responseString);
                    return new NetworkResponseImpl(responseBytes, MessageMetaData.create().getInnerMap());
                } catch (RuntimeException e) {
                    log.warn("RTE in mock", e);
                    return null;
                }
            }
        }).times(messageRepetitions);

        // set up mock client response handler
        MessageChannelResponseHandler responseHandler = EasyMock.createMock(MessageChannelResponseHandler.class);
        // define expected callback
        Capture<NetworkResponse> responseCapture = Capture.newInstance(CaptureType.ALL);
        responseHandler.onResponseAvailable(EasyMock.capture(responseCapture));
        EasyMock.expectLastCall().times(messageRepetitions);

        // enter test mode
        EasyMock.replay(serverEndpointHandler, responseHandler);
        // send request(s)
        for (int i = 0; i < messageRepetitions; i++) {
            byte[] contentBytes = MessageUtils.serializeSafeObject(requestString);
            // note: message type is arbitrary, but must be valid
            NetworkRequest request =
                NetworkRequestFactory.createNetworkRequest(contentBytes, ProtocolConstants.VALUE_MESSAGE_TYPE_HEALTH_CHECK,
                    clientNodeInformation.getInstanceNodeSessionId(),
                    mockServerNodeInformation.getInstanceNodeSessionId());
            channel.sendRequest(request, responseHandler, DEFAULT_REQUEST_TIMEOUT);
        }
        // TODO improve; quick&dirty hack to test larger message repetition counts
        Thread.sleep(testConfiguration.getDefaultTrafficWaitTimeout() + messageRepetitions * 10);

        // first, verify that the endpoint handler was called
        EasyMock.verify(serverEndpointHandler);
        // then, verify that the response handler was called
        EasyMock.verify(responseHandler);
        // verify response content
        List<NetworkResponse> responses = responseCapture.getValues();
        assertEquals(messageRepetitions, responses.size());
        for (NetworkResponse response : responses) {
            assertEquals(requestString + responseSuffix, response.getDeserializedContent());
        }

        // close channel and verify that the remote endpoint handler is notified
        EasyMock.reset(serverEndpointHandler);
        serverEndpointHandler.onInboundChannelClosing(channel.getChannelId());
        EasyMock.replay(serverEndpointHandler);
        channel.close();
        Thread.sleep(WAIT_FOR_INBOUND_CHANNEL_CLOSING_EVENT_MSEC);
        EasyMock.verify(serverEndpointHandler);

        scp.shutDown();
        assertFalse(scp.isAcceptingMessages());

        logThreadPoolState("after server shutdown");
    }

    private void logThreadPoolState(String timeDescription) {
        log.debug("Thread pool state " + timeDescription + ":\n"
            + ConcurrencyUtils.getThreadPoolManagement().getFormattedStatistics(true));
    }

}
