/*
 * RHQ Management Platform
 * Copyright (C) 2005-2014 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

package org.rhq.core.plugin.testutil;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mockito.Mockito;

import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.jboss.arquillian.testng.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.importer.ZipImporter;
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import org.jboss.shrinkwrap.resolver.api.maven.MavenResolverSystem;
import org.jboss.shrinkwrap.resolver.api.maven.ScopeType;
import org.jboss.shrinkwrap.resolver.api.maven.strategy.AcceptScopesStrategy;

import org.rhq.core.clientapi.agent.PluginContainerException;
import org.rhq.core.clientapi.agent.configuration.ConfigurationUpdateRequest;
import org.rhq.core.clientapi.server.configuration.ConfigurationUpdateResponse;
import org.rhq.core.clientapi.server.discovery.InventoryReport;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.measurement.MeasurementDefinition;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.domain.operation.OperationDefinition;
import org.rhq.core.domain.resource.InventoryStatus;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.domain.util.ResourceFilter;
import org.rhq.core.domain.util.ResourceTypeUtility;
import org.rhq.core.domain.util.ResourceUtility;
import org.rhq.core.domain.util.TypeAndKeyResourceFilter;
import org.rhq.core.pc.PluginContainer;
import org.rhq.core.pc.PluginContainerConfiguration;
import org.rhq.core.pc.configuration.ConfigurationManager;
import org.rhq.core.pc.inventory.InventoryManager;
import org.rhq.core.pc.inventory.ResourceContainer;
import org.rhq.core.pc.util.FacetLockType;
import org.rhq.core.pluginapi.availability.AvailabilityFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.util.maven.MavenArtifactNotFoundException;
import org.rhq.core.util.maven.MavenArtifactProperties;
import org.rhq.test.arquillian.BeforeDiscovery;
import org.rhq.test.arquillian.FakeServerInventory;
import org.rhq.test.arquillian.MockingServerServices;
import org.rhq.test.shrinkwrap.RhqAgentPluginArchive;

/**
 * The base class for an Agent plugin integration test class.
 *
 * @author Ian Springer
 */
public abstract class AbstractAgentPluginTest extends Arquillian {

    private static final Log log = LogFactory.getLog(AbstractAgentPluginTest.class);

    @ArquillianResource
    protected MockingServerServices serverServices;

    @ArquillianResource
    protected PluginContainerConfiguration pluginContainerConfiguration;

    @ArquillianResource
    protected PluginContainer pluginContainer;

    private FakeServerInventory serverInventory;

    private static String PLATFORM_PLUGIN_VERSION = "4.12.0.JON330GA"; //hard code for now.

    @Deployment(name = "platform", order = 1)
    protected static RhqAgentPluginArchive getPlatformPlugin() throws Exception {
        MavenResolverSystem mavenDependencyResolver = Maven.resolver();
        //check for system property specifiers before going to classpath
        String rhqPlatformPluginVersion = System.getProperty("rhq-platform-plugin.version");
        rhqPlatformPluginVersion = PLATFORM_PLUGIN_VERSION; //hard code for now.
        //complete maven gav construction.
        String platformPluginArtifact = "org.rhq:rhq-platform-plugin:jar:";
        if ((rhqPlatformPluginVersion == null) || rhqPlatformPluginVersion.trim().isEmpty()) {
            platformPluginArtifact += getRhqVersion();//got to classpath and MavenArtifactProperties
        } else {
            platformPluginArtifact += rhqPlatformPluginVersion;
        }

        return mavenDependencyResolver.offline().loadPomFromFile("pom.xml").resolve(platformPluginArtifact)
            .withoutTransitivity().asSingle(RhqAgentPluginArchive.class);
    }

    @Deployment(name = "pluginUnderTest", order = 2)
    protected static RhqAgentPluginArchive getPluginUnderTest() throws Exception {
        // This is the jar that was just built during the Maven package phase, just prior to this test getting run
        // during the Maven integration-test phase. This is exactly what we want, because it's the real Maven-produced
        // jar, freshly assembled from the classes being tested.
        File pluginJarFile = getPluginJarFile();
        System.out.println("Using plugin jar [" + pluginJarFile + "]...");
        MavenResolverSystem mavenDependencyResolver = Maven.resolver();
        // Pull in any required plugins from our pom's dependencies.

        Collection<RhqAgentPluginArchive> requiredPlugins = Arrays.asList(mavenDependencyResolver
            .loadPomFromFile("pom.xml").importRuntimeAndTestDependencies(new AcceptScopesStrategy(ScopeType.PROVIDED))
            .as(RhqAgentPluginArchive.class));

        return ShrinkWrap.create(ZipImporter.class, pluginJarFile.getName()).importFrom(pluginJarFile)
            .as(RhqAgentPluginArchive.class).withRequiredPluginsFrom(requiredPlugins);
    }

    protected static File getPluginJarFile() {
        File targetDir = new File("target").getAbsoluteFile();
        log.info("Target dir for plugin file [" + targetDir.getAbsolutePath() + "]");
        File[] testFiles = targetDir.listFiles();
        String list = "File list: [";
        if (testFiles.length == 0) {
            list += "]";
        } else {
            for (File aFile : testFiles) {
                list += aFile.getName() + ",\n";
            }
            list += "]";
        }
        log.info("Plugin candidates [" + list + "]");
        File[] files = targetDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                boolean locatedPlugin = name.endsWith("-" + getRhqVersion() + ".jar");
                if (!locatedPlugin) {//try again to look for -redhat-*.jar
                    if (((name.indexOf(getRhqVersion() + "-redhat-")) > -1) && (name.endsWith("*.jar"))) {
                        locatedPlugin = true;
                    }
                }
                return locatedPlugin;
            }
        });
        return files[0];
    }

    /**
     * Superclass can override this to add additional mocked services to those already
     * defined in this abstract base class.  The base class will call for injection each
     * time the mocks are reset.
     *
     * @param serverServices
     */
    protected void injectMocks(MockingServerServices serverServices) {
        return;
    }

    /**
     * Set up our fake server discovery ServerService, which will auto-import all Resources in reports it receives.
     *
     * @throws Exception if an error occurs
     */
    @BeforeDiscovery
    protected void resetServerServices() throws Exception {
        System.out.println("\n=== Resetting fake Server prior to running discovery scan...");

        this.serverInventory = new FakeServerInventory();

        try {
            this.serverServices.resetMocks();
            Mockito.when(
                this.serverServices.getDiscoveryServerService()
                    .mergeInventoryReport(Mockito.any(InventoryReport.class))).then(
                serverInventory.mergeInventoryReport(InventoryStatus.COMMITTED));
            Mockito.when(serverServices.getDiscoveryServerService().getResourceSyncInfo(Mockito.any(int.class))).then(
                serverInventory.getResourceSyncInfo());

            injectMocks(this.serverServices);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * This waits until the discovered tree size stabilizes.
     * </p>
     * This is equivalent to {{waitForAsyncDiscoveryToStabilize(root, 5000L, 7)}}. As such it will wait
     * for at least 35s.
     */
    protected void waitForAsyncDiscoveryToStabilize(Resource root) {
        waitForAsyncDiscoveryToStabilize(root, 5000L, 7);
    }

    /**
     * This waits until the discovered tree size stabilizes.
     *
     * @param root
     * @param checkInterval how long between checks of the tree size
     * @param stableCount how many checks must be the same before we're convinced we're stable
     */
    protected void waitForAsyncDiscoveryToStabilize(Resource root, long checkInterval, int stableCount) {
        int startResCount = 0;
        int endResCount = getResCount(root);
        int numStableChecks = 0;
        log.info("waitForAsyncDiscoveryToStabilize under [" + root.getName() + ":" + root.getResourceKey()
            + "] ResourceCount Start=" + endResCount);
        do {
            startResCount = endResCount;
            try {
                Thread.sleep(checkInterval);
            } catch (InterruptedException e) {
                //
            }
            endResCount = getResCount(root);

            if (startResCount == endResCount) {
                ++numStableChecks;
            } else {
                numStableChecks = 0;
            }
        } while (startResCount < endResCount || numStableChecks < stableCount);
        log.info("waitForAsyncDiscoveryToStabilize: ResourceCount under [" + root.getName() + ":"
            + root.getResourceKey() + "] Stable at=" + endResCount);
    }

    static protected int getResCount(Resource resource) {
        int size = 1;
        Set<Resource> children = resource.getChildResources();
        if (null != children && !children.isEmpty()) {
            HashSet<Resource> safeChildren = new HashSet<Resource>(children);
            for (Resource r : safeChildren) {
                size += getResCount(r);
            }
        }
        return size;
    }

    /**
     * Convenience, same as calling {{waitForResourceByTypeAndKey(platform, parent, type, key, 600)}}.
     * @see #waitForResourceByTypeAndKey(Resource, Resource, ResourceType, String, int)
     */
    protected Resource waitForResourceByTypeAndKey(Resource platform, Resource parent, final ResourceType type,
        String key) {
        return waitForResourceByTypeAndKey(platform, parent, type, key, 600);
    }

    /**
     * A method that looks for a child resource, given the parent, and will wait for the resource to
     * show up (and be committed) under the parent if it isn't there already - on the assumption that
     * discovery is still taking place.  If the resource still does not show up after the timeout period, an assertion
     * failure is generated.
     *
     * @param parent
     * @param type
     * @param key
     * @param timeoutInSeconds minimum timeout is 10s.
     * @return
     */
    protected Resource waitForResourceByTypeAndKey(Resource platform, Resource parent, final ResourceType type,
        String key, int timeoutInSeconds) {
        int maxTries = (timeoutInSeconds / 10) + 1;
        // Discovery is not enough, it needs to be imported and started for test success
        Resource result = getResourceByTypeAndKey(parent, type, key, true);

        for (int numTries = 1; (null == result && numTries <= maxTries); ++numTries) {
            try {
                log.info("Waiting 10s for resource [" + key + "] to be discovered. Current Tree Size ["
                    + getResCount(platform) + "] Number of tries remaining [" + (maxTries - numTries) + "]");
                Thread.sleep(10000L);
            } catch (InterruptedException e) {
                // just keep trying
            }
            result = getResourceByTypeAndKey(parent, type, key, true);
        }

        assertNotNull(
            result,
            type.getName() + " Resource with key [" + key + "] not found in inventory - child "
                + type.getCategory().getDisplayName() + "s that were discovered: "
                + ResourceUtility.getChildResources(parent, new ResourceFilter() {
                    public boolean accept(Resource resource) {
                        return (resource.getResourceType().getCategory() == type.getCategory());
                    }
                }));

        return result;
    }

    protected Resource getResourceByTypeAndKey(Resource parent, final ResourceType type, String key, boolean isCommitted) {
        Set<Resource> childResources = ResourceUtility.getChildResources(parent,
            new TypeAndKeyResourceFilter(type, key));
        if (childResources.size() > 1) {
            throw new IllegalStateException(parent + " has more than one child Resource with same type (" + type
                + ") and key (" + key + ").");
        }
        Resource serverResource = (childResources.isEmpty()) ? null : childResources.iterator().next();

        if (null == serverResource) {
            return null;
        }
        if (isCommitted && (0 == serverResource.getId())) {
            return null;
        }

        InventoryManager im = this.pluginContainer.getInventoryManager();
        ResourceContainer container = im.getResourceContainer(serverResource);
        if (null == container) {
            return null;
        }

        try {
            im.activateResource(serverResource, container, false);
        } catch (Exception e) {
            //
        }
        return serverResource;
    }

    /**
     * Get availability for a Resource synchronously, with a 5 second timeout.
     *
     * @param resource the Resource
     *
     * @return the report containing the collected data
     */
    @NotNull
    protected AvailabilityType getAvailability(Resource resource) throws PluginContainerException {
        ResourceContainer resourceContainer = this.pluginContainer.getInventoryManager().getResourceContainer(resource);
        long timeoutMillis = 5000;
        AvailabilityFacet availFacet = resourceContainer.createResourceComponentProxy(AvailabilityFacet.class,
            FacetLockType.READ, timeoutMillis, false, false, false);
        AvailabilityType avail;
        try {
            avail = availFacet.getAvailability();
        } catch (Exception e) {
            System.out.println("====== Error occurred during availability check on " + resource + ": " + e);
            throw new RuntimeException("Error occurred during availability check on " + resource + ": " + e);
        }
        return avail;
    }

    /**
     * Execute an operation on a Resource synchronously, with the same timeout that the PC would use.
     *
     * @param resource the Resource
     * @param operationName the name of the operation
     * @param params parameters to pass to the operation; may be null if the operation does not define any parameters
     *
     * @return the result of the operation
     */
    @NotNull
    protected OperationResult invokeOperation(Resource resource, String operationName, @Nullable
    Configuration params) throws PluginContainerException {
        ResourceType resourceType = resource.getResourceType();
        OperationDefinition operationDefinition = ResourceTypeUtility.getOperationDefinition(resourceType,
            operationName);
        assertNotNull(operationDefinition, "No operation named [" + operationName + "] is defined for ResourceType {"
            + resourceType.getPlugin() + "}" + resourceType.getName() + ".");

        long timeout = getDefaultTimeout(resource.getResourceType(), operationName);
        System.out.println("=== Invoking operation [" + operationName + "] with parameters ["
            + ((params != null) ? params.toString(true) : params) + "] on " + resource + "...");
        ResourceContainer resourceContainer = this.pluginContainer.getInventoryManager().getResourceContainer(resource);
        long timeoutMillis = timeout * 1000;
        OperationFacet operationFacet = resourceContainer.createResourceComponentProxy(OperationFacet.class,
            FacetLockType.WRITE, timeoutMillis, false, false, false);
        OperationResult operationResult;
        try {
            operationResult = operationFacet.invokeOperation(operationName, params);
        } catch (Exception e) {
            String paramsString = (params != null) ? params.toString(true) : String.valueOf(params);
            System.out.println("====== Error occurred during invocation of operation [" + operationName
                + "] with parameters [" + paramsString + "] on " + resource + ": " + e);
            e.printStackTrace(System.out);
            throw new RuntimeException("Error occurred during invocation of operation [" + operationName
                + "] with parameters [" + paramsString + "] on " + resource + ".", e);
        }
        return operationResult;
    }

    @NotNull
    protected Configuration loadResourceConfiguration(Resource resource) throws Exception {
        ResourceType resourceType = resource.getResourceType();
        ConfigurationDefinition resourceConfigDef = resourceType.getResourceConfigurationDefinition();
        assertNotNull(resourceConfigDef, "No resource config is defined for ResourceType " + resourceType + ".");
        System.out.println("=== Loading Resource config for " + resource + "...");
        ResourceContainer resourceContainer = this.pluginContainer.getInventoryManager().getResourceContainer(resource);
        long timeoutMillis = 5000;
        ConfigurationFacet configurationFacet = resourceContainer.createResourceComponentProxy(
            ConfigurationFacet.class, FacetLockType.READ, timeoutMillis, false, false, false);
        return configurationFacet.loadResourceConfiguration();
    }

    @NotNull
    protected ConfigurationUpdateReport updateResourceConfiguration(Resource resource, Configuration resourceConfig)
        throws Exception {
        ResourceType resourceType = resource.getResourceType();
        ConfigurationDefinition resourceConfigDef = resourceType.getResourceConfigurationDefinition();
        assertNotNull(resourceConfigDef, "No resource config is defined for ResourceType " + resourceType + ".");
        System.out.println("=== Updating Resource config for " + resource + "...");
        ResourceContainer resourceContainer = this.pluginContainer.getInventoryManager().getResourceContainer(resource);
        long timeoutMillis = 5000;
        ConfigurationFacet configurationFacet = resourceContainer.createResourceComponentProxy(
            ConfigurationFacet.class, FacetLockType.WRITE, timeoutMillis, false, false, false);
        ConfigurationUpdateReport report = new ConfigurationUpdateReport(resourceConfig);
        configurationFacet.updateResourceConfiguration(report);
        return report;
    }





    protected OperationResult invokeOperationAndAssertSuccess(Resource resource, String operationName, @Nullable
    Configuration params) throws PluginContainerException {
        OperationResult result = invokeOperation(resource, operationName, params);
        assertOperationSucceeded(operationName, params, result);
        return result;
    }

    private long getDefaultTimeout(ResourceType resourceType, String operationName) {
        OperationDefinition operationDefinition = ResourceTypeUtility.getOperationDefinition(resourceType,
            operationName);
        // Note: The PC's default timeout is 10 minutes.
        return (operationDefinition.getTimeout() != null) ? operationDefinition.getTimeout()
            : this.pluginContainerConfiguration.getOperationInvocationTimeout();
    }

    private static String getRhqVersion() {
        MavenArtifactProperties rhqPluginContainerPom = null;
        try {
            //check for system property specifiers before going to classpath
            String rhqVersion = System.getProperty("rhq.version");
            if ((rhqVersion != null) && !(rhqVersion.trim().isEmpty())) {
                return rhqVersion;
            }
            rhqPluginContainerPom = MavenArtifactProperties.getInstance("org.rhq", "rhq-core-plugin-container");
        } catch (MavenArtifactNotFoundException e) {
            throw new RuntimeException(e);
        }
        return PLATFORM_PLUGIN_VERSION; //hard code for now.
        //        return rhqPluginContainerPom.getVersion();
    }

    protected void assertOperationSucceeded(String operationName, Configuration params, OperationResult result) {
        String paramsString = (params != null) ? params.toString(true) : String.valueOf(params);
        assertNull(result.getErrorMessage(), "Operation [" + operationName + "] with parameters " + paramsString
            + " returned an error: " + result.getErrorMessage());
    }

    protected abstract String getPluginName();

    protected FakeServerInventory getServerInventory() {
        return serverInventory;
    }

    /**
     * Test that loads a resource configuration and then immediately updates the resource
     * with the exact same loaded settings.
     *
     * Notes:
     * 1) load/update is not executed on the root resource provided.
     * 2) if a resource is ignored then all of subresource of that resources are ignored
     *
     * @param rootResource root resource
     * @param ignoredResources resources to be ignored
     * @return number of errors
     * @throws InterruptedException
     * @throws PluginContainerException
     */
    protected int loadUpdateConfigChildResources(Resource rootResource, List<String> ignoredResources)
        throws InterruptedException, PluginContainerException {

        ignoredResources = (ignoredResources == null) ? new ArrayList<String>() : ignoredResources;

        ConfigurationManager configManager = this.pluginContainer.getConfigurationManager();
        Thread.sleep(10 * 1000L);

        Queue<Resource> unparsedResources = new LinkedList<Resource>();
        addCommitedChildrenToCollection(unparsedResources, rootResource, ignoredResources);

        int errorCount = 0;

        while (!unparsedResources.isEmpty()) {
            Resource resourceUnderTest = unparsedResources.poll();

            addCommitedChildrenToCollection(unparsedResources, resourceUnderTest, ignoredResources);

            if (resourceUnderTest.getResourceType().getResourceConfigurationDefinition() != null) {
                Configuration configUnderTest = configManager.loadResourceConfiguration(resourceUnderTest.getId());

                ConfigurationUpdateRequest updateRequest = new ConfigurationUpdateRequest(1, configUnderTest,
                    resourceUnderTest.getId());
                ConfigurationUpdateResponse updateResponse = configManager
                    .executeUpdateResourceConfigurationImmediately(updateRequest);

                if (updateResponse == null) {
                    errorCount++;
                    log.error("------------------------------");
                    log.error(resourceUnderTest);
                    log.error("Update Response is NULL!!!!");
                    log.error("------------------------------\n");
                }
                if (updateResponse.getErrorMessage() != null) {
                    errorCount++;
                    log.error("------------------------------");
                    log.error(resourceUnderTest);
                    log.error(updateResponse.getErrorMessage());
                    log.error("------------------------------\n");
                }
            }
        }

        return errorCount;
    }

    /**
     * Test that executes all the no arg operations for all the subresources of a provided resource.
     * Notes:
     * 1) no operations are executed on the root resource provided.
     * 2) if a resource is ignored then all of subresource of that resources are ignored
     *
     * @param rootResource root resource
     * @param ignoredResources resources to be ignored
     * @param ignoredOperations operations to be ignored
     * @throws PluginContainerException
     */
    protected void executeNoArgOperations(Resource rootResource, List<String> ignoredResources,
        List<String> ignoredOperations) throws PluginContainerException {

        ignoredResources = (ignoredResources == null) ? new ArrayList<String>() : ignoredResources;
        ignoredOperations = (ignoredOperations == null) ? new ArrayList<String>() : ignoredOperations;

        Queue<Resource> unparsedResources = new LinkedList<Resource>();
        addCommitedChildrenToCollection(unparsedResources, rootResource, ignoredResources);

        while (!unparsedResources.isEmpty()) {
            Resource resourceUnderTest = unparsedResources.poll();

            addCommitedChildrenToCollection(unparsedResources, resourceUnderTest, ignoredResources);

            for (OperationDefinition operationUnderTest : resourceUnderTest.getResourceType().getOperationDefinitions()) {
                if (!ignoredOperations.contains(operationUnderTest.getName())) {
                    if (operationUnderTest.getParametersConfigurationDefinition() == null
                        || operationUnderTest.getParametersConfigurationDefinition().getPropertyDefinitions().isEmpty()) {
                        this.invokeOperationAndAssertSuccess(resourceUnderTest, operationUnderTest.getName(),
                            new Configuration());
                    }
                }
            }
        }
    }

    /**
     * Adds direct subresources of resources to a collection.
     *
     * @param accumulatorCollection accumulator collection
     * @param rootResource root resource
     * @param ignoredResources resources to be ignored
     */
    private void addCommitedChildrenToCollection(Collection<Resource> accumulatorCollection, Resource rootResource,
        List<String> ignoredResources) {
        for (Resource childResource : rootResource.getChildResources()) {
            if (childResource.getInventoryStatus().equals(InventoryStatus.COMMITTED)) {
                if (!ignoredResources.contains(childResource.getResourceType().getName())) {
                    accumulatorCollection.add(childResource);
                }
            } else {
                log.info("Resource NOT COMMITTED --> not added to collection!! - " + childResource + " - "
                    + childResource.getInventoryStatus());
            }
        }
    }

    @Nullable
    protected String collectTrait(Resource resource, String traitName) throws Exception {
        System.out.println("=== Collecting trait [" + traitName + "] for " + resource + "...");
        MeasurementReport report = collectMetric(resource, traitName);

        String value;
        if (report.getTraitData().isEmpty()) {
            assertEquals(
                report.getNumericData().size(),
                0,
                "Metric [" + traitName + "] for Resource type " + resource.getResourceType()
                    + " is defined as a trait, but the plugin returned one or more numeric metrics!: "
                    + report.getNumericData());
            assertEquals(
                report.getCallTimeData().size(),
                0,
                "Metric [" + traitName + "] for Resource type " + resource.getResourceType()
                    + " is defined as a trait, but the plugin returned one or more call-time metrics!: "
                    + report.getCallTimeData());
            value = null;
        } else {
            assertEquals(report.getTraitData().size(), 1,
                "Requested a single trait, but plugin returned more than one datum: " + report.getTraitData());
            MeasurementDataTrait datum = report.getTraitData().iterator().next();
            assertEquals(datum.getName(), traitName,
                "Trait [" + traitName + "] for Resource type " + resource.getResourceType()
                    + " was requested, but the plugin returned a trait with name [" + datum.getName() + "] and value ["
                    + datum.getValue() + "]!");
            value = datum.getValue();
        }
        System.out.println("====== Collected trait [" + traitName + "] with value of [" + value + "] for " + resource
            + ".");

        return value;
    }

    /**
     * Collect a metric for a Resource synchronously, with a 7 second timeout.
     *
     * @param resource the Resource
     * @param metricName the name of the metric
     *
     * @return the report containing the collected data
     */
    @NotNull
    protected MeasurementReport collectMetric(Resource resource, String metricName) throws Exception {
        ResourceType resourceType = resource.getResourceType();
        MeasurementDefinition measurementDefinition = ResourceTypeUtility.getMeasurementDefinition(resourceType,
            metricName);
        assertNotNull(measurementDefinition, "No metric named [" + metricName + "] is defined for ResourceType {"
            + resourceType.getPlugin() + "}" + resourceType.getName() + ".");

        ResourceContainer resourceContainer = this.pluginContainer.getInventoryManager().getResourceContainer(resource);
        long timeoutMillis = 5000;
        if (resourceContainer.getResourceComponentState() != ResourceContainer.ResourceComponentState.STARTED) {
            throw new IllegalStateException("Resource component for " + resource + " has not yet been started.");
        }
        MeasurementFacet measurementFacet = resourceContainer.createResourceComponentProxy(MeasurementFacet.class,
            FacetLockType.READ, timeoutMillis, false, false, false);
        MeasurementReport report = new MeasurementReport();
        MeasurementScheduleRequest request = new MeasurementScheduleRequest(-1, metricName, -1, true,
            measurementDefinition.getDataType(), measurementDefinition.getRawNumericType());
        Set<MeasurementScheduleRequest> requests = new HashSet<MeasurementScheduleRequest>();
        requests.add(request);
        try {
            measurementFacet.getValues(report, requests);
        } catch (Exception e) {
            System.out.println("====== Error occurred during collection of metric [" + metricName + "] on " + resource
                + ": " + e);
            throw new RuntimeException("Error occurred during collection of metric [" + metricName + "] on " + resource
                + ": " + e);
        }
        return report;
    }

}
