/*
 * Copyright 2006-2022 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.communication.nodeproperties.internal;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

import java.util.Arrays;
import java.util.Collection;
import java.util.Map;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import de.rcenvironment.core.communication.api.NodeIdentifierService;
import de.rcenvironment.core.communication.common.IdentifierException;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.common.NodeIdentifierTestUtils;
import de.rcenvironment.core.communication.configuration.NodeConfigurationService;
import de.rcenvironment.core.communication.nodeproperties.NodePropertiesService;
import de.rcenvironment.core.communication.nodeproperties.NodeProperty;
import de.rcenvironment.core.communication.nodeproperties.spi.NodePropertiesChangeListener;
import de.rcenvironment.core.communication.nodeproperties.spi.RawNodePropertiesChangeListener;
import de.rcenvironment.core.communication.testutils.AbstractVirtualInstanceTest;
import de.rcenvironment.core.communication.testutils.TestConfiguration;
import de.rcenvironment.core.communication.testutils.VirtualInstance;
import de.rcenvironment.core.communication.transport.virtual.VirtualTransportTestConfiguration;
import de.rcenvironment.core.utils.common.StringUtils;

/**
 * Tests {@link NodePropertiesService} update propagation between two {@link VirtualInstance}s.
 * 
 * @author Robert Mischke
 */
public class NodePropertiesServiceImplTest extends AbstractVirtualInstanceTest {

    private static final String TEST_PROPERTY_KEY_A = "a";

    /**
     * A wait time im msec that must be larger than the time over which node property updates are aggegated in the tested service.
     */
    private static final int WAIT_TIME_FOR_UPDATE_AGGREGATION = 400;

    /**
     * Common setup.
     */
    @Before
    public void setup() {
        NodeIdentifierTestUtils.resetTestNodeIdentifierService(false);
    }

    /**
     * Common teardown.
     */
    @After
    public void teardown() {
        NodeIdentifierTestUtils.resetTestNodeIdentifierService(true);
    }

    /**
     * Verifies that display name mappings are separated between virtual instances, and name associations are registered on both sides when
     * connecting.
     * 
     * @throws Exception none expected
     */
    @Test
    public void testDisplayNamePropagationOnConnect() throws Exception {
        setupInstances(2, true, true);

        final VirtualInstance instanceA = testTopology.getInstance(0);
        final VirtualInstance instanceB = testTopology.getInstance(1);

        final NodeIdentifierService nodeIdentifierServiceA = instanceA.getService(NodeIdentifierService.class);
        final NodeIdentifierService nodeIdentifierServiceB = instanceB.getService(NodeIdentifierService.class);

        nodeIdentifierServiceA.printAllNameAssociations(System.out, "Name mappings of instance A:");
        nodeIdentifierServiceB.printAllNameAssociations(System.out, "Name mappings of instance B:");

        assertNull(rawDisplayNameOfFirstAsSeenFromSecond(instanceA, instanceB));
        assertNull(rawDisplayNameOfFirstAsSeenFromSecond(instanceB, instanceA));

        log.debug("Connnecting virtual test instances");
        testTopology.connectAndWait(0, 1, DEFAULT_NODE_REACHABILITY_TIMEOUT);

        nodeIdentifierServiceA.printAllNameAssociations(System.out, "Name mappings of instance A:");
        nodeIdentifierServiceB.printAllNameAssociations(System.out, "Name mappings of instance B:");

        assertEquals(getDisplayNameOf(instanceA), rawDisplayNameOfFirstAsSeenFromSecond(instanceA, instanceB));
        assertEquals(getDisplayNameOf(instanceB), rawDisplayNameOfFirstAsSeenFromSecond(instanceB, instanceA));
    }

    /**
     * Verifies that property deletions/removals are properly propagated, and are correctly combined with following updates.
     * 
     * @throws Exception on unexpected errors
     */
    @Test
    public void testNullHandlingInUpdatePropagation() throws Exception {
        setupInstances(2, true, true);

        final VirtualInstance instanceA = testTopology.getInstance(0);
        final VirtualInstance instanceB = testTopology.getInstance(1);
        final NodePropertiesService serviceA = instanceA.getNodePropertiesService();
        final NodePropertiesService serviceB = instanceB.getNodePropertiesService();

        serviceB.addRawNodePropertiesChangeListener(new RawNodePropertiesChangeListener() {

            @Override
            public void onRawNodePropertiesAddedOrModified(Collection<? extends NodeProperty> newProperties) {
                log.debug("'RawNodePropertiesAddedOrModified' listener callback: " + Arrays.toString(newProperties.toArray()));
            }
        });

        instanceB.injectService(NodePropertiesChangeListener.class, new NodePropertiesChangeListener() {

            @Override
            public void onReachableNodePropertiesChanged(Collection<? extends NodeProperty> addedProperties,
                Collection<? extends NodeProperty> updatedProperties, Collection<? extends NodeProperty> removedProperties) {
                log.debug(StringUtils.format("Added: %d,  Updated: %d, Removed: %d", addedProperties.size(), updatedProperties.size(),
                    removedProperties.size()));
            }

            @Override
            public void onNodePropertyMapsOfNodesChanged(Map<InstanceNodeSessionId, Map<String, String>> updatedPropertyMaps) {
                log.debug("Node property maps of these nodes changed: " + updatedPropertyMaps.keySet());
            }
        });

        Map<InstanceNodeSessionId, Map<String, String>> allPropertiesAtB;
        final InstanceNodeSessionId nodeIdOfA = instanceA.getInstanceNodeSessionId();

        log.debug("Connnecting virtual test instances");
        testTopology.connectAndWait(0, 1, DEFAULT_NODE_REACHABILITY_TIMEOUT);

        log.info("Setup complete, beginning actual node property tests");

        serviceA.addOrUpdateLocalNodeProperty(TEST_PROPERTY_KEY_A, "1");
        Thread.sleep(WAIT_TIME_FOR_UPDATE_AGGREGATION);
        allPropertiesAtB = serviceB.getAllNodeProperties();
        assertEquals("1", allPropertiesAtB.get(nodeIdOfA).get(TEST_PROPERTY_KEY_A));

        serviceA.addOrUpdateLocalNodeProperty(TEST_PROPERTY_KEY_A, "2");
        serviceA.addOrUpdateLocalNodeProperty(TEST_PROPERTY_KEY_A, null);
        Thread.sleep(WAIT_TIME_FOR_UPDATE_AGGREGATION);
        allPropertiesAtB = serviceB.getAllNodeProperties();
        assertEquals(null, allPropertiesAtB.get(nodeIdOfA).get(TEST_PROPERTY_KEY_A));
        // TODO review: should the behavior be changed to remove keys on null value? - misc_ro
        // assertFalse(allPropertiesAtB.get(nodeIdOfA).containsKey(TEST_PROPERTY_KEY_A));

        serviceA.addOrUpdateLocalNodeProperty(TEST_PROPERTY_KEY_A, "3");
        serviceA.addOrUpdateLocalNodeProperty(TEST_PROPERTY_KEY_A, null);
        serviceA.addOrUpdateLocalNodeProperty(TEST_PROPERTY_KEY_A, "4");
        Thread.sleep(WAIT_TIME_FOR_UPDATE_AGGREGATION);
        allPropertiesAtB = serviceB.getAllNodeProperties();
        assertEquals("4", allPropertiesAtB.get(nodeIdOfA).get(TEST_PROPERTY_KEY_A));
    }

    @Override
    protected TestConfiguration defineTestConfiguration() {
        return new VirtualTransportTestConfiguration(true);
    }

    private String getDisplayNameOf(VirtualInstance instance) {
        return instance.getService(NodeConfigurationService.class).getInitialNodeInformation().getDisplayName();
    }

    private String rawDisplayNameOfFirstAsSeenFromSecond(VirtualInstance virtualInstanceX, VirtualInstance virtualInstanceY)
        throws IdentifierException {
        return virtualInstanceY.getService(NodeIdentifierService.class)
            .parseInstanceNodeSessionIdString(virtualInstanceX.getInstanceNodeSessionId().getInstanceNodeSessionIdString())
            .getRawAssociatedDisplayName();
    }
}
