/*
 * 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.plugins.mysql;

import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertNotNull;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;

import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.JAXBElement;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeSuite;

import org.rhq.core.clientapi.agent.metadata.PluginMetadataManager;
import org.rhq.core.clientapi.descriptor.AgentPluginDescriptorUtil;
import org.rhq.core.clientapi.descriptor.configuration.ConfigurationProperty;
import org.rhq.core.clientapi.descriptor.plugin.MetricDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.PluginDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.ResourceDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.ServerDescriptor;
import org.rhq.core.clientapi.descriptor.plugin.ServiceDescriptor;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.MeasurementDataNumeric;
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.MeasurementSchedule;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.domain.resource.ProcessScan;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.pc.PluginContainer;
import org.rhq.core.pc.PluginContainerConfiguration;
import org.rhq.core.pc.availability.AvailabilityContextImpl;
import org.rhq.core.pc.content.ContentContextImpl;
import org.rhq.core.pc.event.EventContextImpl;
import org.rhq.core.pc.event.EventManager;
import org.rhq.core.pc.inventory.InventoryContextImpl;
import org.rhq.core.pc.inventory.ResourceContainer;
import org.rhq.core.pc.operation.OperationContextImpl;
import org.rhq.core.pluginapi.availability.AvailabilityContext;
import org.rhq.core.pluginapi.configuration.ConfigurationFacet;
import org.rhq.core.pluginapi.content.ContentContext;
import org.rhq.core.pluginapi.event.EventContext;
import org.rhq.core.pluginapi.inventory.DiscoveredResourceDetails;
import org.rhq.core.pluginapi.inventory.InventoryContext;
import org.rhq.core.pluginapi.inventory.PluginContainerDeployment;
import org.rhq.core.pluginapi.inventory.ProcessScanResult;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.inventory.ResourceDiscoveryComponent;
import org.rhq.core.pluginapi.inventory.ResourceDiscoveryContext;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.pluginapi.operation.OperationContext;
import org.rhq.core.system.ProcessInfo;
import org.rhq.core.system.SystemInfo;
import org.rhq.core.system.SystemInfoFactory;
import org.rhq.core.system.pquery.ProcessInfoQuery;

/**
 * Base class for RHQ Component Testing.
 * Initializes a plugin configuration.
 *
 * Methods to override:
 *
 * {@link #setConfiguration(Configuration, ResourceType)}
 */
public abstract class ComponentTest {

    /**
     * Logging component.
     */
    protected final Log log = LogFactory.getLog(getClass());

    private static File temp = new File(System.getProperty("java.io.tmpdir"));

    /**
     * Associates a resource component with a resource.
     */
    protected Map<ResourceComponent, Resource> components = new LinkedHashMap<ResourceComponent, Resource>();

    /**
     * Associates a name of a resource with a resource descriptor.
     */
    protected Map<String, ResourceDescriptor> descriptors = new LinkedHashMap<String, ResourceDescriptor>();

    /**
     * Associates a name of a resource with a type.
     * This is useful for manually adding resources.
     *
     * @see #manuallyAdd(ResourceType, Configuration)
     */
    protected Map<String, ResourceType> resourceTypes = new LinkedHashMap<String, ResourceType>();

    /**
     * Event manager; used to obtain events.
     */
    private EventManager eventManager;

    private PluginContainer pluginContainer = PluginContainer.getInstance();

    private SystemInfo systemInfo = SystemInfoFactory.createSystemInfo();

    /**
     * Scan all processes before starting; false will disable this feature.
     * Disabling is suggested for running tests against a remote instance.
     */
    private boolean processScan = true;

    // TODO

    private final PluginMetadataManager pmm = new PluginMetadataManager();
    private final File temporaryDirectory = temp;
    private final File dataDirectory = temp;
    private final String pluginContainerName = "rhq";
    private final OperationContext operationContext = new OperationContextImpl(0);
    private final ContentContext contentContext = new ContentContextImpl(0);
    private PluginContainerDeployment pluginContainerDeployment = null;
    private Resource platform;
    private ResourceContainer platformContainer;
    private List<ProcessInfo> processInfo = Collections.emptyList();
    private PluginDescriptor pluginDescriptor;

    /**
     * Constructs a new component test.
     */
    protected ComponentTest() {
    }

    /**
     * Initializes the plugin container.
     * This is run before {@link #before()}.
     */
    @BeforeSuite
    protected void beforeSuite() {
        // Speed up propagation of events by adjusting delay/period to 1 second
        PluginContainerConfiguration pcc = new PluginContainerConfiguration();
        pcc.setEventSenderInitialDelay(1);
        pcc.setEventSenderPeriod(1);
        pluginContainer.setConfiguration(pcc);
        pluginContainer.initialize();
        eventManager = pluginContainer.getEventManager();
        platform = pluginContainer.getInventoryManager().getPlatform();
        platformContainer = pluginContainer.getInventoryManager().getResourceContainer(platform);
        if (platformContainer == null) {
            platformContainer = new ResourceContainer(platform, getClass().getClassLoader());
        }
    }

    /**
     * Initializes all plugins defined in the system; using auto-discovery where possible.
     * This is run once per test class.
     */
    @BeforeClass
    protected void before() throws Exception {
        if (processScan) {
            processInfo = getProcessInfos();
            if (processInfo == null)
                processInfo = Collections.emptyList();
            log.debug("Process Info " + processInfo);
            for (ProcessInfo i : processInfo) {
                log.debug(i.getBaseName() + " " + Arrays.toString(i.getCommandLine()));
            }
        }
        Enumeration<URL> e = getClass().getClassLoader().getResources("META-INF/rhq-plugin.xml");
        List<URL> l = Collections.list(e);
        Collections.sort(l, new Comparator<URL>() {
            @Override
            public int compare(URL u1, URL u2) {
                return u2.toString().compareTo(u1.toString());
            }
        });
        for (URL url : l) {
            log.debug("parse " + url);
            InputStream is = url.openStream();
            PluginDescriptor pd = AgentPluginDescriptorUtil.parsePluginDescriptor(is);
            processPluginDescriptor(pd);
            log.debug("pmm names " + pmm.getPluginNames());
            buildDesc(pd.getServers());
            buildDesc(pd.getServices());
            is.close();
        }
    }

    @AfterSuite
    protected void afterSuite() {
        pluginContainer.shutdown();
    }

    /**
     * Process a plugin descriptor.
     */
    private void processPluginDescriptor(PluginDescriptor pd) throws Exception {
        this.pluginDescriptor = pd;
        Set<ResourceType> types = pmm.loadPlugin(pd);
        mapResourceTypeNames(types);
        log.info("Resource types: " + resourceTypes);
        resources(types, platform, platformContainer.getResourceComponent(), platformContainer.getResourceContext());
        log.info("ResourceComponent map: " + components);
    }

    private void mapResourceTypeNames(Set<ResourceType> types) {
        for (ResourceType type : types) {
            this.resourceTypes.put(type.getName(), type);
            mapResourceTypeNames(type.getChildResourceTypes());
        }
    }

    /**
     * Manually create a component by name.
     */
    public ResourceComponent manuallyAdd(String name) throws Exception {
        ResourceType resourceType = resourceTypes.get(name);
        if (resourceType == null)
            throw new IllegalStateException("no type " + name);
        Configuration configuration = resourceType.getPluginConfigurationDefinition().getDefaultTemplate().createConfiguration();
        setConfiguration(configuration, resourceType);
        return manuallyAdd(resourceType, configuration);
    }

    /**
     * Manually create a component by resource type.
     */
    public ResourceComponent manuallyAdd(ResourceType type, Configuration configuration) throws Exception {
        return manuallyAdd(type, configuration, platformContainer.getResourceComponent());
    }

    /**
     * Manually create a component by resource type, configuration, parent.
     */
    public ResourceComponent manuallyAdd(ResourceType type, Configuration configuration, ResourceComponent parent) throws Exception {
        DiscoveredResourceDetails drd = new DiscoveredResourceDetails(type,
                "key", "name", "ver", "desc", configuration, (ProcessInfo) null);
        ResourceDiscoveryComponent c = null;
        return createChild(drd, platform, configuration, parent, c);
    }

    private ResourceComponent createChild(DiscoveredResourceDetails drd,
            Resource resource,
            Configuration configuration,
            ResourceComponent parentComponent,
            ResourceDiscoveryComponent rdc) throws Exception
    {
        ResourceType type = pmm.getType(drd.getResourceType());

        Resource cresource = new Resource();
        cresource.setResourceType(type);
        cresource.setPluginConfiguration(configuration);
        cresource.setResourceKey(drd.getResourceKey());
        cresource.setParentResource(resource);
        cresource.setName(drd.getResourceName());
        cresource.setVersion(drd.getResourceVersion());

        String rclassname = pmm.getComponentClass(type);
        ResourceComponent component = (ResourceComponent) Class.forName(rclassname).newInstance();

        AvailabilityContext availContext = new AvailabilityContextImpl(cresource, pluginContainer.getInventoryManager());
        InventoryContext inventoryContext = new InventoryContextImpl(cresource, pluginContainer.getInventoryManager());

        EventContext eventContext = new EventContextImpl(resource, eventManager);
        ResourceContext context = new ResourceContext(cresource, parentComponent,
                null, rdc, systemInfo, temporaryDirectory, dataDirectory,
                pluginContainerName, eventContext, operationContext, contentContext,
                availContext, inventoryContext,pluginContainerDeployment);

        component.start(context);
        components.put(component, cresource);
        resources(type.getChildResourceTypes(), cresource, component, context);
        return component;
    }

    private void resources(Set<ResourceType> types, Resource parent, ResourceComponent component, ResourceContext context) throws Exception {
        for (ResourceType type : types) {
            String s = pmm.getDiscoveryClass(type);
            if (s == null) {
                throw new NullPointerException("no discovery " + type);
            }
            ResourceDiscoveryComponent rdc = (ResourceDiscoveryComponent) Class.forName(s).newInstance();
            log.debug("rdc=" + rdc);
            List<Configuration> configList = new ArrayList<Configuration>();
            ResourceDiscoveryContext resourceDiscoveryContext = new ResourceDiscoveryContext(type, component,
                    context, systemInfo,
                    performProcessScans(type), configList,
                    pluginContainerName, pluginContainerDeployment);
            Set<DiscoveredResourceDetails> drds = rdc.discoverResources(resourceDiscoveryContext);
            for (DiscoveredResourceDetails drd : drds) {
                log.debug("discovered " + drd);
                ResourceType resourceType = drd.getResourceType();
                setConfiguration(drd.getPluginConfiguration(), resourceType);
                createChild(drd, parent, drd.getPluginConfiguration(), component, rdc);
            }
            if (drds.isEmpty()) {
                log.warn("not discovered " + type);
                context.getPluginConfiguration();
            }
        }

    }

    /**
     * Called before the configuration is processed; override to set specific plugin parameters.
     */
    protected void setConfiguration(Configuration configuration, ResourceType resourceType) {
    }

    /**
     * Stops all components, stops the plugin container.
     */
    @AfterTest
    protected void after() throws Exception {
        for (ResourceComponent c : components.keySet())
            c.stop();
    }

    /**
     * Returns a measurement report.
     */
    public MeasurementReport getMeasurementReport(ResourceComponent component) throws Exception {
        Resource resource = this.components.get(component);
        ResourceType type = resource.getResourceType();
        MeasurementReport report = new MeasurementReport();
        Set<MeasurementScheduleRequest> s = new HashSet<MeasurementScheduleRequest>();
        for (MeasurementDefinition md : type.getMetricDefinitions())
            s.add(new MeasurementScheduleRequest(new MeasurementSchedule(md, resource)));
        ((MeasurementFacet)component).getValues(report, s);
        return report;
    }

    /**
     * Returns the first resource component by resource name, then looks by matching resource type name,
     * then asserts failure if not found.
     */
    public ResourceComponent getComponent(String name) {
        for (Map.Entry<ResourceComponent, Resource> c : components.entrySet())
            if (c.getValue().getName().equals(name))
                return c.getKey();
        for (Map.Entry<ResourceComponent, Resource> c : components.entrySet())
            if (c.getValue().getResourceType().getName().equals(name))
                return c.getKey();
        fail("component not found " + name + " in " + components.entrySet());
        return null;
    }

    /**
     * Returns a resource matching this component.
     */
    public Resource getResource(ResourceComponent rc) {
        Resource r = components.get(rc);
        if (r == null)
            throw new IllegalStateException();
        return r;
    }

    /**
     * Returns a resource matching this name.
     */
    public Resource getResource(String name) {
        return getResource(getComponent(name));
    }

    /**
     * Builds a new configuration for a resource type.
     */
    public Configuration getConfiguration(ResourceType resourceType) {
        Configuration configuration = resourceType.getPluginConfigurationDefinition().getDefaultTemplate().createConfiguration();
        setConfiguration(configuration, resourceType);
        return configuration;
    }

    // ASSERT METHOD

    /**
     * From a measurement report, returns a measurement value, or asserts failure if no such value exists.
     */
    public static Double getValue(MeasurementReport report, String name) {
        for (MeasurementDataNumeric m: report.getNumericData()) {
            if (m.getName().equals(name)) {
                return m.getValue();
            }
        }
        fail("report does not incude " + name + " report " + report.getNumericData());
        return null;
    }

    /**
     * Asserts the resource component is available.
     */
    public static void assertUp(ResourceComponent component) {
        assertEquals("up " + component, AvailabilityType.UP, component.getAvailability());
    }

    /**
     * Asserts the resource component is unavailable.
     */
    public static void assertDown(ResourceComponent component) {
        assertEquals("down " + component, AvailabilityType.DOWN, component.getAvailability());
    }

    /**
     * Sets a configuration option.
     */
    public static void set(Configuration config, String name, String value) {
        PropertySimple s = config.getSimple(name);
        if (s == null) {
            s = new PropertySimple(name, value);
            config.put(s);
        } else {
            s.setStringValue(value);
        }
    }

    private List<ProcessScanResult> performProcessScans(ResourceType serverType) {
        List<ProcessScanResult> scanResults = new ArrayList<ProcessScanResult>();
        Set<ProcessScan> processScans = serverType.getProcessScans();
        log.debug("Executing process scans for server type " + serverType + "...");
        ProcessInfoQuery piq = new ProcessInfoQuery(processInfo);
        for (ProcessScan processScan : processScans) {
            List<ProcessInfo> queryResults = piq.query(processScan.getQuery());
            for (ProcessInfo autoDiscoveredProcess : queryResults) {
                scanResults.add(new ProcessScanResult(processScan, autoDiscoveredProcess));
                log.info("Process scan auto-detected new server resource: scan=[" + processScan
                        + "], discovered-process=[" + autoDiscoveredProcess + "]");
            }
        }
        return scanResults;
    }

    /**
     * AutoDiscoveryExecutor method.
     */
    private List<ProcessInfo> getProcessInfos() {
        SystemInfo systemInfo = SystemInfoFactory.createSystemInfo();
        log.debug("Retrieving process table...");
        long startTime = System.currentTimeMillis();
        List<ProcessInfo> processInfos = null;
        try {
            processInfos = systemInfo.getAllProcesses();
        } catch (UnsupportedOperationException uoe) {
            log.debug("Cannot perform process scan - not supported on this platform. (" + systemInfo.getClass() + ")");
        }
        long elapsedTime = System.currentTimeMillis() - startTime;
        log.debug("Retrieval of process table took " + elapsedTime + " ms.");
        return processInfos;
    }

    /**
     * Returns the plugin descriptor.
     */
    public PluginDescriptor getPluginDescriptor() {
        return pluginDescriptor;
    }

    /**
     * Returns the plugin descriptor.
     */
    public ResourceDescriptor getResourceDescriptor(String name) {
        ResourceDescriptor rd = descriptors.get(name);
        if (rd == null)
            throw new IllegalStateException("no descriptor " + name + " in " + descriptors.keySet());
        return rd;
    }

    private void buildDesc(List<? extends ResourceDescriptor> l) {
        for (ResourceDescriptor rd : l) {
            descriptors.put(rd.getName(), rd);
            if (rd instanceof ServerDescriptor) {
                buildDesc(((ServerDescriptor)rd).getServers());
                buildDesc(((ServerDescriptor)rd).getServices());
            }
            if (rd instanceof ServiceDescriptor) {
                buildDesc(((ServiceDescriptor)rd).getServices());
                buildDesc(((ServiceDescriptor)rd).getServices());
            }
        }
    }

    /**
     * Asserts that all measurements in the report are present
     * according to the resource descriptor.
     *
     * @see #getResourceDescriptor(String) for obtaining this.
     * @param report
     */
    public static void assertAll(MeasurementReport report, ResourceDescriptor l) {
        HashMap<String, MetricDescriptor> map = new HashMap<String, MetricDescriptor>();
        for (MetricDescriptor md : l.getMetric()) {
            map.put(md.getProperty(), md);
        }
        for (MeasurementDataNumeric n : report.getNumericData()) {
            map.remove(n.getName());
        }
        for (MeasurementDataTrait n : report.getTraitData()) {
            map.remove(n.getName());
        }
        assertTrue("Measurements not found " + map.keySet(), map.isEmpty());
    }

    /**
     * Returns the event manager.
     */
    public EventManager getEventManager() {
        return eventManager;
    }

    /**
     * Set to false to avoid scanning local machine processes to speed up testing.
     */
    public void setProcessScan(boolean processScan) {
        this.processScan = processScan;
    }

    public void assertAll(ConfigurationFacet cf, ResourceDescriptor rd) throws Exception {
        Configuration config = cf.loadResourceConfiguration();
        List<JAXBElement<? extends ConfigurationProperty>> templates = rd.getResourceConfiguration().getConfigurationProperty();
        for (JAXBElement<? extends ConfigurationProperty> template : templates) {
            String name = template.getValue().getName();
            // Property property = config.get(name);
            assertNotNull("config contains " + name, config.get(name));
            Object value = config.getSimpleValue(name, null);
            assertNotNull("value for " + name, value);
            log.debug("config found " + name + " value " + value );
        }
    }

}
