/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.agent;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.prefs.BackingStoreException;
import javax.management.MBeanServer;
import javax.management.MBeanServerInvocationHandler;

import org.rhq.core.util.stream.StreamUtil;
import org.rhq.enterprise.communications.ServiceContainerMetricsMBean;
import org.rhq.enterprise.communications.command.client.ClientCommandSenderMetrics;

/**
 * The class that tests can use to configure and create agents. This class also provides the output of the agent as well
 * as allowing input to the agent.
 *
 * @author John Mazzitelli
 */
public class AgentTestClass {
    /**
     * The default configuration file this test class will use if one isn't specified.
     */
    public static final String DEFAULT_CONFIG_FILE = "test-agent-configuration.xml";

    /**
     * The default preference node used by the configuration file - this is used if the node name isn't specified.
     */
    public static final String DEFAULT_CONFIG_PREF_NODE = "test";

    private String m_configFilePath = DEFAULT_CONFIG_FILE;
    private String m_preferenceNodeName = DEFAULT_CONFIG_PREF_NODE;
    private Properties m_configOverrides = null;
    private List m_inputCommands = null;
    private File m_outputFile = null;
    private File m_inputFile = null;
    private boolean m_captureOutput = false;
    private AgentMain m_agent = null;

    /**
     * Constructor for {@link AgentTestClass}.
     */
    public AgentTestClass() {
    }

    /**
     * Sets the location of the configuration file and the name of the preferences node that is in the configuration
     * file. The file path can be a path in the classloader or an absolute file system path.
     *
     * @param file_path      the preferences configuration file
     * @param pref_node_name the preferences node name that corresponds to the node name in the configuration file
     */
    public void setConfigurationFile(String file_path, String pref_node_name) {
        assert file_path != null : "File path must not be null";
        assert pref_node_name != null : "Preference node name must not be null";

        m_configFilePath = file_path;
        m_preferenceNodeName = pref_node_name;
    }

    /**
     * Sets some properties that can be used to override those properties in the
     * {@link #setConfigurationFile(String, String) configuration file}. This is useful when a series of tests want to
     * use mostly the same configuration with one or two config properties that vary.
     *
     * @param props properties that override those found in the configuration file
     */
    public void setConfigurationOverrides(Properties props) {
        m_configOverrides = props;
    }

    /**
     * This sets a list of commands that will be input to the agent - the commands will be stored to a temporary file
     * and passed to the agent via its "-i" argument.
     *
     * @param commands set of commands for the agent to execute
     */
    public void setInputCommands(List commands) {
        m_inputCommands = commands;
    }

    /**
     * Sets the flag to indicate if the agent's output should be captured. If a test wants to be able to get the
     * {@link #getAgentOutput() agent output}, it must set this flag to <code>true</code>.
     *
     * @param flag if <code>true</code>, the agent's output will be redirected to a file that will later be read in via
     *             {@link #getAgentOutput()}.
     */
    public void setCaptureOutput(boolean flag) {
        m_captureOutput = flag;
    }

    /**
     * This creates an agent with the configuration specified by the
     * {@link #setConfigurationFile(String, String) config file} and
     * {@link #setConfigurationOverrides(Properties) override properties}.
     *
     * @param  start_it if <code>true</code>, the agent will be started before it is returned
     *
     * @return the newly created agent instance
     *
     * @throws Exception if an error occurred while created or starting the agent
     */
    public AgentMain createAgent(boolean start_it) throws Exception {
        // make sure we clean up any previously created agent
        if (m_agent != null) {
            m_agent.shutdown();
            m_agent = null;
        }

        assert m_configFilePath != null : "No configuration file has been specified for the agent";

        ArrayList<String> args = new ArrayList<String>();

        if (m_captureOutput) {
            m_outputFile = createOutputFile();
            args.add("-o");
            args.add(m_outputFile.getAbsolutePath());
        }

        if ((m_inputCommands != null) && (m_inputCommands.size() > 0)) {
            m_inputFile = createInputFile();
            args.add("-i");
            args.add(m_inputFile.getAbsolutePath());
        }

        args.add("-d"); // put it daemon mode, otherwise, the agent main input loop thread never dies in our tests
        args.add("-l"); // clear out any old preferences
        args.add("-c");
        args.add(m_configFilePath);
        args.add("-p");
        args.add(m_preferenceNodeName);

        if ((m_configOverrides != null) && (m_configOverrides.size() > 0)) {
            for (Iterator iter = m_configOverrides.entrySet().iterator(); iter.hasNext();) {
                Map.Entry entry = (Map.Entry) iter.next();
                args.add("-D" + entry.getKey() + "=" + entry.getValue());
            }
        }

        m_agent = new AgentMain(args.toArray(new String[0]));

        if (start_it) {
            // start the agent *and* allow it to send messages immediately
            m_agent.start();
            m_agent.getClientCommandSender().getRemoteCommunicator().setInitializeCallback(null);
            m_agent.getClientCommandSender().startSending();
        }

        return m_agent;
    }

    /**
     * Returns the agent that was created by this class. If this class hasn't created an agent yet, <code>null</code> is
     * returned.
     *
     * @return the created agent or <code>null</code>
     */
    public AgentMain getAgent() {
        return m_agent;
    }

    /**
     * Returns a proxy to the server metrics MBean.
     *
     * @return metrics mbean that contains server-side metrics
     *
     * @throws RuntimeException if can't create the MBean proxy
     */
    public ServiceContainerMetricsMBean getServerMetrics() {
        MBeanServer mbs = getAgent().getServiceContainer().getMBeanServer();
        ServiceContainerMetricsMBean metrics;

        try {
            metrics = (ServiceContainerMetricsMBean) MBeanServerInvocationHandler.newProxyInstance(mbs,
                ServiceContainerMetricsMBean.OBJECTNAME_METRICS, ServiceContainerMetricsMBean.class, false);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return metrics;
    }

    /**
     * Returns the set of metrics for the agent's client sender.
     *
     * @return metrics
     */
    public ClientCommandSenderMetrics getClientMetrics() {
        return getAgent().getClientCommandSender().getMetrics();
    }

    /**
     * This will clear any and all agent configuration preferences. After this method returns, only default values will
     * remain in effect. An exception is thrown if an agent has not yet been {@link #createAgent(boolean) created}.
     *
     * @throws RuntimeException if the agent hasn't been created yet or if the configuration failed to get cleared
     */
    public void clearAgentConfiguration() {
        if (m_agent == null) {
            throw new RuntimeException("Cannot clear configuration - the agent has not yet been created");
        }

        try {
            m_agent.getConfiguration().getPreferences().clear();
        } catch (BackingStoreException e) {
            throw new RuntimeException("Failed to clear the agent configuration", e);
        }
    }

    /**
     * Call this when you want to delete the temporary input and output files this class created.
     */
    public void cleanUpFiles() {
        if (m_outputFile != null) {
            getAgent().getOut().close();
            m_outputFile.delete();
        }

        if (m_inputFile != null) {
            m_inputFile.delete();
        }

        return;
    }

    /**
     * This returns the agent's previous output.
     *
     * @return agent output as one big string
     *
     * @throws FileNotFoundException if the agent was not told to capture output or the output file cannot be opened for
     *                               some reason
     */
    public String getAgentOutput() throws FileNotFoundException {
        String output = null;

        if (m_outputFile != null) {
            FileInputStream fis = new FileInputStream(m_outputFile);
            output = new String(StreamUtil.slurp(fis));
        } else {
            throw new FileNotFoundException("The agent was not told to capture its output - no output available");
        }

        return output;
    }

    /**
     * Creates the input file with the set of {@link #setInputCommands(List) input commands}. The file will be created
     * in a temporary location and will be deleted on exit.
     *
     * @return the file that was created
     *
     * @throws Exception if failed to create the file
     */
    private File createInputFile() throws Exception {
        File file = File.createTempFile("input-commands", null);
        file.deleteOnExit();

        PrintWriter pw = new PrintWriter(new FileWriter(file));

        for (Iterator iter = m_inputCommands.iterator(); iter.hasNext();) {
            String cmd = (String) iter.next();
            pw.println(cmd);
        }

        pw.close();

        return file;
    }

    /**
     * Creates the output file where the agent will dump its output. The file will be created in a temporary location
     * and will be deleted on exit.
     *
     * @return the file that was created
     *
     * @throws Exception if failed to create the file
     */
    private File createOutputFile() throws Exception {
        File file = File.createTempFile("agent-output", null);
        file.deleteOnExit();

        return file;
    }
}