/*
 * RHQ Management Platform
 * Copyright (C) 2005-2010 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.server.configuration;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;

import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import org.rhq.core.clientapi.agent.PluginContainerException;
import org.rhq.core.clientapi.agent.configuration.ConfigurationAgentService;
import org.rhq.core.clientapi.agent.configuration.ConfigurationUpdateRequest;
import org.rhq.core.clientapi.agent.discovery.DiscoveryAgentService;
import org.rhq.core.clientapi.agent.discovery.InvalidPluginConfigurationClientException;
import org.rhq.core.clientapi.server.configuration.ConfigurationUpdateResponse;
import org.rhq.core.clientapi.server.discovery.InventoryReport;
import org.rhq.core.communications.command.annotation.Asynchronous;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.ConfigurationUpdateStatus;
import org.rhq.core.domain.configuration.PluginConfigurationUpdate;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.ResourceConfigurationUpdate;
import org.rhq.core.domain.configuration.group.GroupPluginConfigurationUpdate;
import org.rhq.core.domain.discovery.AvailabilityReport;
import org.rhq.core.domain.discovery.MergeResourceResponse;
import org.rhq.core.domain.discovery.ResourceSyncInfo;
import org.rhq.core.domain.measurement.Availability;
import org.rhq.core.domain.resource.Agent;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.domain.resource.group.ResourceGroup;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageOrdering;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.enterprise.server.authz.PermissionException;
import org.rhq.enterprise.server.resource.ResourceManagerLocal;
import org.rhq.enterprise.server.test.AbstractEJB3Test;
import org.rhq.enterprise.server.test.TestServerCommunicationsService;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.util.SessionTestHelper;

/**
 * Tests the configuration manager.
 */
@Test
public class ConfigurationManagerBeanTest extends AbstractEJB3Test {
    private static final boolean ENABLE_TESTS = true;

    private ConfigurationManagerLocal configurationManager;
    private ResourceManagerLocal resourceManager;
    private Resource newResource1;
    private Resource newResource2;
    private ResourceGroup compatibleGroup;
    private PageControl configUpdatesPageControl;
    private Agent agent;
    private Subject overlord;

    /**
     * Prepares things for the entire test class.
     */
    @BeforeClass
    public void beforeClass() {
        // Make sure page control sorts so the latest config update is last (the default is for the latest to be first).
        configUpdatesPageControl = PageControl.getUnlimitedInstance();
        // (ips, 04/01/10): Use createdTime, rather than id, to order by, since the id's are not guaranteed to be
        //                  ordered sequentially (this is because, dbsetup configures our sequences to pre-create
        //                  and cache sequence values 10 at a time.
        configUpdatesPageControl.addDefaultOrderingField("cu.createdTime", PageOrdering.ASC);

        configurationManager = LookupUtil.getConfigurationManager();
        resourceManager = LookupUtil.getResourceManager();
        overlord = LookupUtil.getSubjectManager().getOverlord();

        TestServerCommunicationsService agentServiceContainer = prepareForTestAgents();
        TestServices testServices = new TestServices();
        agentServiceContainer.configurationService = testServices;
        agentServiceContainer.discoveryService = testServices;
    }

    @AfterClass
    public void afterClass() throws Exception {
        unprepareForTestAgents();
    }

    @BeforeMethod
    public void beforeMethod() throws Exception {
        prepareScheduler();

        getTransactionManager().begin();
        EntityManager em = getEntityManager();
        try {
            compatibleGroup = SessionTestHelper.createNewCompatibleGroupForRole(em, null, // no role necessary here
                "compat");

            newResource1 = SessionTestHelper.createNewResourceForGroup(em, compatibleGroup, "res"
                + System.currentTimeMillis());
            newResource2 = SessionTestHelper.createNewResourceForGroup(em, compatibleGroup, "res"
                + System.currentTimeMillis());

            // set one resource as the child of another, so that they don't both look like platforms under the agent
            newResource1.addChildResource(newResource2);

            agent = SessionTestHelper.createNewAgent(em, "agent-" + getClass().getSimpleName());
            newResource1.setAgent(agent);
            newResource2.setAgent(agent);

            getTransactionManager().commit();
        } catch (Exception e) {
            try {
                System.out.println(e);
                getTransactionManager().rollback();
            } catch (Exception ignore) {
            }

            throw e;
        } finally {
            em.close();
        }
    }

    @AfterMethod
    public void afterMethod() throws Exception {
        try {
            // perform in-band and out-of-band work in quick succession
            // only need to delete newResource1, which will delete his child newResource2 as well as the agent
            List<Integer> deletedIds = resourceManager.uninventoryResource(overlord, newResource1.getId());
            for (Integer deletedResourceId : deletedIds) {
                resourceManager.uninventoryResourceAsyncWork(overlord, deletedResourceId);
            }
        } catch (Exception e) {
            System.out.println(e);
            throw e;
        }

        try {
            getTransactionManager().begin();
            EntityManager em = getEntityManager();

            try {
                ResourceGroup group = em.find(ResourceGroup.class, compatibleGroup.getId());
                em.remove(group);

                getTransactionManager().commit();
            } catch (Exception e) {
                try {
                    System.out.println(e);
                    getTransactionManager().rollback();
                } catch (Exception ignore) {
                }

                throw e;
            } finally {
                em.close();
            }

            getTransactionManager().begin();
            em = getEntityManager();
            try {
                ResourceType type = em.find(ResourceType.class, newResource1.getResourceType().getId());

                em.remove(type);

                getTransactionManager().commit();
            } catch (Exception e) {
                try {
                    System.out.println(e);
                    getTransactionManager().rollback();
                } catch (Exception ignore) {
                }

                throw e;
            } finally {
                em.close();
            }

        } finally {
            unprepareScheduler();
        }
    }

    @Test(enabled = ENABLE_TESTS)
    public void testLatestConfiguration() throws Exception {
        int resourceId = newResource1.getId();

        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        // create a config, then another that spends some time in INPROGRESS before succeeding
        Configuration configuration1 = new Configuration();
        configuration1.put(new PropertySimple("myboolean", "true"));

        Configuration configuration2 = new Configuration();
        configuration2.put(new PropertySimple("myboolean", "false"));
        configuration2.put(new PropertySimple("mysleep", "7000"));

        configurationManager.updateResourceConfiguration(overlord, resourceId, configuration1);
        Thread.sleep(2000); // wait for the test agent to complete the request

        ResourceConfigurationUpdate history1;
        history1 = configurationManager.getLatestResourceConfigurationUpdate(overlord, resourceId);
        assert history1 != null;
        PropertySimple myprop = history1.getConfiguration().getSimple("myboolean");
        assert myprop != null;
        assert "true".equals(myprop.getStringValue());

        // now update to that second config - the "agent" will sleep for a bit before it completes
        // so we will have an INPROGRESS configuration for a few seconds before it goes to SUCCESS
        ResourceConfigurationUpdate history2 = null;
        boolean inProgress = false;
        boolean inProgressTested = false;

        configurationManager.updateResourceConfiguration(overlord, resourceId, configuration2);

        do {
            history2 = configurationManager.getLatestResourceConfigurationUpdate(overlord, resourceId);
            inProgress = configurationManager.isResourceConfigurationUpdateInProgress(overlord, resourceId);

            if (inProgress) {
                // history2 should be history1 since the update is not complete                
                assert history2 != null;
                assert history2.getId() == history1.getId();
                myprop = history2.getConfiguration().getSimple("myboolean");
                assert myprop != null;
                assert "true".equals(myprop.getStringValue());
                myprop = history2.getConfiguration().getSimple("mysleep"); // this wasn't in the first config
                assert myprop == null;
                // record that this test case ran, we expect it will if the agent delay is there 
                inProgressTested = true;
            } else {
                // update is complete, history 2 should be different
                history2 = configurationManager.getLatestResourceConfigurationUpdate(overlord, resourceId);
                assert history2 != null;
                assert history2.getId() != history1.getId();
                myprop = history2.getConfiguration().getSimple("myboolean");
                assert myprop != null;
                assert "false".equals(myprop.getStringValue());
                myprop = history2.getConfiguration().getSimple("mysleep");
                assert myprop.getLongValue() != null;
                assert myprop.getLongValue().longValue() == 7000L;
            }
        } while (inProgress);

        assertTrue(inProgressTested);
    }

    @Test(enabled = ENABLE_TESTS)
    public void testDeleteType() throws Exception {
        // the purpose of this little test is to test an error condition I'm getting when attempting to delete
        // a resource type - just forces a run with before/afterMethod
        return;
    }

    @Test(enabled = ENABLE_TESTS)
    public void testPluginConfigurationUpdateCallbackSuccess() throws Exception {
        Resource resource = newResource1;

        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        Configuration configuration1 = new Configuration();
        configuration1.put(new PropertySimple("foo", "1"));

        Configuration configuration2 = new Configuration();
        configuration2.put(new PropertySimple("bar", "2"));

        Configuration current;

        current = configurationManager.getPluginConfiguration(overlord, resource.getId());
        assert current != null;
        assert current.getProperties().size() == 0 : current; // there is no plugin config settings yet

        configurationManager.updatePluginConfiguration(overlord, resource.getId(), configuration1);
        current = configurationManager.getPluginConfiguration(overlord, resource.getId());
        assert current != null;
        assert current.getProperties().size() == 1 : current;
        assert current.getSimple("foo").getIntegerValue() == 1 : current;

        configurationManager.updatePluginConfiguration(overlord, resource.getId(), configuration2);
        current = configurationManager.getPluginConfiguration(overlord, resource.getId());
        assert current != null;
        assert current.getProperties().size() == 1 : current;
        assert current.getSimple("bar").getIntegerValue() == 2 : current;
        assert current.getSimple("foo") == null : current; // this is gone now, we overrode it

        assert current.remove("foo") == null : current; // secondary test - just exercise the remove method
        assert current.remove("bar") != null : current; // again just test the remove method
        current.put(new PropertySimple("hello", "3"));
        configurationManager.updatePluginConfiguration(overlord, resource.getId(), current);
        current = configurationManager.getPluginConfiguration(overlord, resource.getId());
        assert current != null;
        assert current.getProperties().size() == 1 : current;
        assert current.getSimple("hello").getIntegerValue() == 3 : current;
        assert current.getSimple("foo") == null : current; // this is gone now
        assert current.getSimple("bar") == null : current; // this is gone now
    }

    @Test(enabled = ENABLE_TESTS)
    public void testPluginConfigurationUpdateCallbackFailure() throws Exception {
        Resource resource = newResource1;

        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        Configuration configuration1 = new Configuration();
        configuration1.put(new PropertySimple("foo", "1"));
        configuration1.put(new PropertySimple("fail", "true"));

        Configuration current;

        current = configurationManager.getPluginConfiguration(overlord, resource.getId());
        assert current != null;
        assert current.getProperties().size() == 0 : current; // there is no plugin config settings yet

        PluginConfigurationUpdate update = configurationManager.updatePluginConfiguration(overlord, resource.getId(),
            configuration1);
        assert update.getErrorMessage() != null : "We should have simulated a failure, "
            + "but instead received no error.";

        // Even if a plugin container exception occurs, the plugin configuration should still be updated.
        current = configurationManager.getPluginConfiguration(overlord, resource.getId());
        assert current != null;
        assert current.getProperties().size() != 0 : current.toString();
    }

    @Test(enabled = false)
    public void testGroupPluginConfigurationUpdateWorkflowSuccess() throws Exception {
        // TODO (joseph): Fix and then re-enable this test.
        //testGroupPluginConfigurationUpdateWorkflowHelper(false);
    }

    @Test(enabled = false)
    public void testGroupPluginConfigurationUpdateWorkflowFailure() throws Exception {
        // TODO (joseph): Fix and then re-enable this test.
        //testGroupPluginConfigurationUpdateWorkflowHelper(true);
    }

    @Test(enabled = false)
    public void testGroupPluginConfigurationUpdateMergeAlgorithmPerformance() throws Exception {
        Configuration configuration = new Configuration();
        configuration.put(new PropertySimple("foo1", "1"));
        configuration.put(new PropertySimple("foo2", "2"));
        configuration.put(new PropertySimple("foo3", "3"));
        configuration.put(new PropertySimple("foo4", "4"));
        configuration.put(new PropertySimple("foo5", "5"));

        /*
         * for a configuration object that has 5 simple properties, baseline for groupSize of 100K was 165ms (Dec 6,
         * 2007);
         *
         * that means it took 0.000165ms per merge into the group configuration;
         */
        final int TEST_SIZE = 100000;
        final double BASELINE = 0.00165;
        final double TOLERANCE = 3.0; // a number, representing percentage
        final long EXPECTED = (long) (BASELINE * TEST_SIZE * (1 + TOLERANCE));

        List<Configuration> timingSet = new ArrayList<Configuration>();
        for (int i = 0; i < TEST_SIZE; i++) {
            timingSet.add(configuration);
        }

        /*
         * getGroupConfiguration is the most expensive part of the algorithm that helps to render a ResourceGroup's
         * inventory tab
         *
         * so let's run timings on uber groups to make sure it's speedy
         */
        long before = System.currentTimeMillis();
        //GroupPluginConfigurationUpdate.getGroupConfiguration(timingSet);
        long after = System.currentTimeMillis();

        long millisTaken = after - before;

        if (millisTaken < EXPECTED) {
            if ((millisTaken + 5000) < EXPECTED) {
                assert false : "Merging GroupPluginConfigurationUpdates is too slow: " + "It took " + millisTaken
                    + " millis for " + TEST_SIZE + " resources; " + "We wanted less than " + EXPECTED + "ms";
            } else {
                System.out.println("Merging GroupPluginConfigurationUpdates is a bit slow: " + "It took " + millisTaken
                    + " millis for " + TEST_SIZE + " resources; " + "We wanted less than " + EXPECTED + "ms");
            }
        }
    }

    private void testGroupPluginConfigurationUpdateWorkflowHelper(boolean failOnChildUpdates) throws Exception {
        getTransactionManager().begin();
        EntityManager em = getEntityManager();

        Resource resource1 = newResource1;
        Resource resource2 = newResource2;

        try {
            Subject overlord = LookupUtil.getSubjectManager().getOverlord();

            Configuration configuration1 = new Configuration();
            configuration1.put(new PropertySimple("foo", "1"));
            configuration1.put(new PropertySimple("fail", "false"));

            Configuration configuration2 = new Configuration();
            configuration2.put(new PropertySimple("foo", "2"));
            configuration2.put(new PropertySimple("fail", "false"));

            /* begin simple checks */
            PluginConfigurationUpdate update1 = configurationManager.updatePluginConfiguration(overlord, resource1
                .getId(), configuration1);
            assert update1.getErrorMessage() == null : "We weren't expecting a failure here";

            PluginConfigurationUpdate update2 = configurationManager.updatePluginConfiguration(overlord, resource2
                .getId(), configuration2);
            assert update2.getErrorMessage() == null : "We weren't expecting a failure here";

            Configuration updatedConfiguration1 = configurationManager.getPluginConfiguration(overlord, resource1
                .getId());
            Configuration updatedConfiguration2 = configurationManager.getPluginConfiguration(overlord, resource2
                .getId());

            assert updatedConfiguration1.equals(configuration1) : "configuration1 was: " + updatedConfiguration1 + ", "
                + "expected was: " + configuration1;

            assert updatedConfiguration2.equals(configuration2) : "configuration2 was: " + updatedConfiguration2 + ", "
                + "expected was: " + configuration2;
            /*  end simple checks */

            /* begin group configuration creation checks */
            Configuration expectedGroupConfiguration = new Configuration();
            expectedGroupConfiguration
                .put(new PropertySimple("foo", GroupPluginConfigurationUpdate.MIXED_VALUES_MARKER));
            expectedGroupConfiguration.put(new PropertySimple("fail", "false"));

            List<Configuration> updatedConfigurations = Arrays.asList(new Configuration[] { updatedConfiguration1,
                updatedConfiguration2 });

            Configuration groupConfiguration = null;
            /*
            Configuration groupConfiguration = GroupPluginConfigurationUpdate
                .getGroupConfiguration(updatedConfigurations);
             */

            assert groupConfiguration.equals(expectedGroupConfiguration) : "group configuration was: "
                + groupConfiguration + ", " + "expected was: " + expectedGroupConfiguration;
            /*  end group configuration creation checks */

            /* begin group modification */
            Configuration groupConfigurationOverride = new Configuration();
            PropertySimple propertySimple1 = new PropertySimple("foo", "3");
            PropertySimple propertySimple2 = new PropertySimple("fail", "true");
            groupConfigurationOverride.put(propertySimple1);
            groupConfigurationOverride.put(propertySimple2);

            // regardless of failures, semantics dictate that the new configuration should be persisted to the resource
            Configuration expectedGroupConfigurationResults = new Configuration();
            expectedGroupConfigurationResults.put(new PropertySimple("foo", "3")); // from groupConfigurationOverride
            propertySimple1.setOverride(Boolean.TRUE);

            if (failOnChildUpdates) {
                expectedGroupConfigurationResults.put(new PropertySimple("fail", "true")); // from groupConfigurationOverride
                propertySimple2.setOverride(Boolean.TRUE); // will make TestServices fail
            } else {
                expectedGroupConfigurationResults.put(new PropertySimple("fail", "false")); // from both resource's current configuration
                propertySimple2.setOverride(Boolean.FALSE); // false is default, but setting explicitly for test clarity
            }

            Map<Integer, Configuration> memberConfigs = new HashMap<Integer, Configuration>();
            memberConfigs.put(resource1.getId(), configuration1);
            memberConfigs.put(resource2.getId(), configuration2);
            int groupUpdateId = configurationManager.scheduleGroupPluginConfigurationUpdate(overlord, compatibleGroup
                .getId(), memberConfigs);

            // instead of sleeping, let's directly execute what would normally be scheduled
            //configurationManager.completeGroupPluginConfigurationUpdate(groupUpdateId);

            GroupPluginConfigurationUpdate update = configurationManager.getGroupPluginConfigurationById(groupUpdateId);

            assert update != null : "Group plugin configuration update should not have been null";

            int i = 0;
            for (PluginConfigurationUpdate childUpdate : update.getConfigurationUpdates()) {
                Configuration childUpdateConfiguration = childUpdate.getConfiguration();
                assert childUpdateConfiguration.equals(expectedGroupConfigurationResults) : "new updateChildConfig["
                    + i + "] was: " + childUpdateConfiguration + ", " + "expected was: "
                    + expectedGroupConfigurationResults;
                i++;
            }

            Configuration configurationAfterGroupUpdate1 = configurationManager.getPluginConfiguration(overlord,
                resource1.getId());
            Configuration configurationAfterGroupUpdate2 = configurationManager.getPluginConfiguration(overlord,
                resource2.getId());

            ConfigurationUpdateStatus expectedResultStatus = null;

            if (failOnChildUpdates) {
                expectedResultStatus = ConfigurationUpdateStatus.FAILURE;
            } else {
                expectedResultStatus = ConfigurationUpdateStatus.SUCCESS;
            }

            assert configurationAfterGroupUpdate1.equals(expectedGroupConfigurationResults) : "new config1 was: "
                + configurationAfterGroupUpdate1 + ", " + "expected was: " + expectedGroupConfigurationResults;

            assert configurationAfterGroupUpdate2.equals(expectedGroupConfigurationResults) : "new config2 was: "
                + configurationAfterGroupUpdate2 + ", " + "expected was: " + expectedGroupConfigurationResults;

            assert update.getStatus() == expectedResultStatus : "Group plugin configuration update "
                + "should have been marked as " + expectedResultStatus + ": " + update;

            /*  end group modification */
        } catch (Exception e) {
            e.printStackTrace();
            assert false : "Failed test with this exception: " + e;
        } finally {
            getTransactionManager().rollback();
            try {
                em.close();
            } catch (Exception e) {
            }
        }
    }

    @Test(enabled = ENABLE_TESTS)
    public void testResourceConfigurationUpdateCallbackFailure() throws Exception {
        Resource resource = newResource1;

        try {
            Subject overlord = LookupUtil.getSubjectManager().getOverlord();

            // this is simulating what the UI would be doing, build the config and call the server-side API
            // we'll pretend the user is the overlord - another test will check a real user to see permission errors
            Configuration configuration = new Configuration();
            configuration.put(new PropertySimple("myboolean", "invalid-bool"));

            ResourceConfigurationUpdate history = new ResourceConfigurationUpdate(resource, configuration, "dummyuser");

            assert history.getStatus().equals(ConfigurationUpdateStatus.INPROGRESS);

            configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration);

            Thread.sleep(2000); // wait for the test agent to complete the request

            // our test service pretends the agent got an error - it will set some errors and the call to
            // completedConfigurationUpdate is made inline (in the real code, this would be asynchronous)
            // at this point in time, the round trip messaging is done and we have the agent response
            getTransactionManager().begin();
            EntityManager em = getEntityManager();
            try {
                resource = em.find(Resource.class, resource.getId());

                history = resource.getResourceConfigurationUpdates().get(0);

                assert history.getStatus().equals(ConfigurationUpdateStatus.FAILURE) : "Status was "
                    + history.getStatus();
                assert history.getErrorMessage() != null;
                assert history.getErrorMessage().indexOf("This simulates a failed update") > 0;
                assert history.getConfiguration() != null;
                PropertySimple mybool = history.getConfiguration().getSimple("myboolean");
                assert mybool != null;
                assert mybool.getStringValue().equals("invalid-bool");
                assert mybool.getErrorMessage().indexOf("Not a valid boolean") > 0;
            } finally {
                getTransactionManager().rollback();
                try {
                    em.close();
                } catch (Exception e) {
                }
            }

            assert configurationManager.getLatestResourceConfigurationUpdate(overlord, resource.getId()) != null : "Resource wasn't configured yet - but we should have populated it with live values";

            // purging a non-existing request is a no-op
            configurationManager.purgeResourceConfigurationUpdate(overlord, Integer.MIN_VALUE, false);

            // delete the request now
            configurationManager.purgeResourceConfigurationUpdate(overlord, history.getId(), false);

            getTransactionManager().begin();
            em = getEntityManager();
            try {
                resource = em.find(Resource.class, resource.getId());
                assert resource.getResourceConfigurationUpdates().size() == 1; // the initial built for us for free with the live config
            } finally {
                getTransactionManager().rollback();
                try {
                    em.close();
                } catch (Exception e) {
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            assert false : "Failed test with this exception: " + e;
        }
    }

    @Test(enabled = ENABLE_TESTS)
    public void testResourceConfigurationUpdateCallbackSuccess() throws Exception {
        Resource resource = newResource1;
        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        // this is simulating what the UI would be doing, build the config and call the server-side API
        // we'll pretend the user is the overlord - another test will check a real user to see permission errors
        Configuration configuration = new Configuration();
        configuration.put(new PropertySimple("myboolean", "true"));

        ResourceConfigurationUpdate request = new ResourceConfigurationUpdate(resource, configuration, "dummyuser");

        assert request.getStatus().equals(ConfigurationUpdateStatus.INPROGRESS);

        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration);

        Thread.sleep(2000); // wait for the test agent to complete the request

        // at this point in time, the round trip messaging is done and we have the agent response
        List<ResourceConfigurationUpdate> requests;

        requests = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert requests.size() == 1;
        assert requests.get(0) != null;

        request = requests.get(0);

        assert request.getStatus().equals(ConfigurationUpdateStatus.SUCCESS);
        assert request.getErrorMessage() == null;
        assert request.getConfiguration() != null;
        PropertySimple mybool = request.getConfiguration().getSimple("myboolean");
        assert mybool != null;
        assert mybool.getStringValue().equals("true");
        assert mybool.getErrorMessage() == null;

        ResourceConfigurationUpdate current;
        current = configurationManager.getLatestResourceConfigurationUpdate(overlord, resource.getId());

        assert current != null;
        assert current.getId() == request.getId();
        assert current.getResource().equals(resource);
        assert current.getStatus().equals(request.getStatus());
        assert current.getSubjectName().equals(request.getSubjectName());
        assert current.getCreatedTime() == request.getCreatedTime();
        assert current.getModifiedTime() == request.getModifiedTime();
        assert current.getConfiguration().getId() == request.getConfiguration().getId();

        Configuration live = configurationManager.getLiveResourceConfiguration(overlord, resource.getId(), false);
        assert live != null;
        mybool = live.getSimple("myboolean");
        assert mybool != null;
        assert "true".equals(mybool.getStringValue());
        assert mybool.getErrorMessage() == null;

        // purging a non-existing request is a no-op
        configurationManager.purgeResourceConfigurationUpdate(overlord, Integer.MIN_VALUE, false);

        // delete the request now
        System.out.println("REQUEST WAS: " + request.toString());
        configurationManager.purgeResourceConfigurationUpdate(overlord, request.getId(), false);

        requests = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert requests.size() == 1; // it will create one for us from the "live" configuration
    }

    @Test(enabled = ENABLE_TESTS)
    public void testConfigurationRollback() throws Exception {
        Resource resource = newResource1;

        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        // create a couple configs in history
        Configuration configuration1 = new Configuration();
        configuration1.put(new PropertySimple("myboolean", "true"));

        Configuration configuration2 = new Configuration();
        configuration2.put(new PropertySimple("myboolean", "false"));

        Configuration configuration3 = new Configuration();
        configuration3.put(new PropertySimple("myboolean", "TRUE"));

        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration1);
        Thread.sleep(2000); // wait for the test agent to complete the request
        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration2);
        Thread.sleep(2000); // wait for the test agent to complete the request
        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration3);
        Thread.sleep(2000); // wait for the test agent to complete the request

        List<ResourceConfigurationUpdate> history;
        history = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert history != null;
        assert history.size() == 3;

        Configuration currentConfiguration = history.get(2).getConfiguration();
        PropertySimple mybool = currentConfiguration.getSimple("myboolean");
        assert mybool != null;
        assert "TRUE".equals(mybool.getStringValue()) : "actual: " + mybool.getStringValue();

        // now grab one of the earlier configurations and rollback to it
        Configuration rollbackToHere = history.get(1).getConfiguration(); // the "false" one
        mybool = rollbackToHere.getSimple("myboolean");
        assert mybool != null;
        assert "false".equals(mybool.getStringValue()) : "actual: " + mybool.getStringValue();

        configurationManager.updateResourceConfiguration(overlord, resource.getId(), rollbackToHere);
        Thread.sleep(2000); // wait for the test agent to complete the request

        history = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);
        assert history != null;
        assert history.size() == 4;
        ResourceConfigurationUpdate newConfigUpdate = history.get(3); // the last one is the new one
        Configuration newConfiguration = newConfigUpdate.getConfiguration();
        assert newConfiguration.getId() != rollbackToHere.getId();
        mybool = newConfiguration.getSimple("myboolean");
        assert mybool != null;
        assert "false".equals(mybool.getStringValue());
        assert mybool.getErrorMessage() == null;

        ResourceConfigurationUpdate current = configurationManager.getLatestResourceConfigurationUpdate(overlord,
            resource.getId());
        assert current != null;
        assert current.getId() == newConfigUpdate.getId();
        assert current.getResource().equals(resource);
        assert current.getStatus().equals(newConfigUpdate.getStatus());
        assert current.getSubjectName().equals(newConfigUpdate.getSubjectName());
        assert current.getCreatedTime() == newConfigUpdate.getCreatedTime();
        assert current.getModifiedTime() == newConfigUpdate.getModifiedTime();
        assert current.getConfiguration().getId() == newConfigUpdate.getConfiguration().getId();
    }

    @Test(enabled = ENABLE_TESTS)
    public void testPurgeConfigurationHistorySame() throws Exception {
        Resource resource = newResource1;

        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        // create a couple configs in history
        Configuration configuration1 = new Configuration();
        configuration1.put(new PropertySimple("myboolean", "true"));

        Configuration configuration2 = new Configuration();
        configuration2.put(new PropertySimple("myboolean", "true"));

        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration1);
        Thread.sleep(2000); // wait for the test agent to complete the request
        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration2);
        Thread.sleep(2000); // wait for the test agent to complete the request

        // at this point in time, the round trip messaging is done and we have the agent response
        List<ResourceConfigurationUpdate> requests;

        requests = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert requests != null;
        assert requests.size() == 1 : "Got " + requests.size() + " config update requests - expected 1.";
    }

    @Test(enabled = true)
    public void testPurgeConfigurationHistoryDifferent() throws Exception {
        Resource resource = newResource1;

        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        // create a couple configs in history
        Configuration configuration1 = new Configuration();
        configuration1.put(new PropertySimple("myboolean", "true"));

        Configuration configuration2 = new Configuration();
        configuration2.put(new PropertySimple("myboolean", "false"));

        ResourceConfigurationUpdate up;
        up = configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration1);
        Thread.sleep(2000); // wait for the test agent to complete the request
        assert configuration1.equals(configurationManager.getResourceConfiguration(resource.getId()));
        up = configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration2);
        Thread.sleep(2000); // wait for the test agent to complete the request
        assert configuration2.equals(configurationManager.getResourceConfiguration(resource.getId()));

        // at this point in time, the round trip messaging is done and we have the agent response
        List<ResourceConfigurationUpdate> requests;

        requests = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert requests != null;
        assert requests.size() == 2 : "Got " + requests.size() + " config update requests - expected 2.";

        // delete the first one
        ResourceConfigurationUpdate doomedRequest = requests.get(0);
        ResourceConfigurationUpdate savedRequest = requests.get(1);

        configurationManager.purgeResourceConfigurationUpdates(overlord, new int[] { doomedRequest.getId() }, false);

        // now get the current configs/requests and
        // make sure we deleted just the one configuration, leaving one left
        requests = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert requests.size() == 1;
        assert requests.get(0).getId() == savedRequest.getId();
    }

    @Test(enabled = ENABLE_TESTS)
    public void testPurgeConfigurationHistoryWithFailedUpdateRequest() throws Exception {
        Resource resource = newResource1;

        Subject overlord = LookupUtil.getSubjectManager().getOverlord();

        // create a couple update/requests in history - one request will fail, so only a single config in history will be there
        Configuration configuration1 = new Configuration();
        configuration1.put(new PropertySimple("myboolean", "invalid-boolean"));

        Configuration configuration2 = new Configuration();
        configuration2.put(new PropertySimple("myboolean", "true"));

        Configuration activeConfigurationBefore = configurationManager.getResourceConfiguration(resource.getId());

        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration1);
        Thread.sleep(2000); // wait for the test agent to complete the request

        Configuration activeConfigurationAfter = configurationManager.getResourceConfiguration(resource.getId());
        assert activeConfigurationBefore.equals(activeConfigurationAfter) : "ActiveResourceConfiguration was not supposed to change for a failed update -- old was: "
            + activeConfigurationBefore + ", new was: " + activeConfigurationAfter;

        configurationManager.updateResourceConfiguration(overlord, resource.getId(), configuration2);
        Thread.sleep(2000); // wait for the test agent to complete the request

        Configuration activeConfiguration = configurationManager.getResourceConfiguration(resource.getId());
        assert activeConfiguration != null : "ActiveResourceConfiguration was not updated with configuration2";
        Map<String, PropertySimple> activeProperties = activeConfiguration.getSimpleProperties();
        assert activeProperties.size() == 1;
        assert activeProperties.containsKey("myboolean");
        PropertySimple activeProperty = activeProperties.get("myboolean");
        assert activeProperty.getName().equals("myboolean");
        assert "true".equals(activeProperty.getStringValue());

        // at this point in time, the round trip messaging is done and we have the agent response
        List<ResourceConfigurationUpdate> requests;

        requests = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert requests != null;
        assert requests.size() == 2; // one succeeded and one failed

        assert requests.get(0).getStatus() == ConfigurationUpdateStatus.FAILURE : "actual: " + requests.get(0).getStatus();
        assert requests.get(1).getStatus() == ConfigurationUpdateStatus.SUCCESS : "actual: " + requests.get(1).getStatus();

        ResourceConfigurationUpdate savedRequest = requests.get(0); // this is the one that failed
        ResourceConfigurationUpdate doomedRequest = requests.get(1); // this is the one that succeeded

        configurationManager.purgeResourceConfigurationUpdate(overlord, doomedRequest.getId(), false);

        // now get the current configs/requests and
        // make sure we deleted the only one configuration that succeeded, leaving one update record
        requests = configurationManager.findResourceConfigurationUpdates(overlord, resource.getId(), null, null, false,
                configUpdatesPageControl);

        assert requests.size() == 1;

        ResourceConfigurationUpdate request = requests.get(0);
        assert request.getId() == savedRequest.getId();
        assert request.getStatus().equals(ConfigurationUpdateStatus.FAILURE);
        assert request.getErrorMessage() != null;
        assert request.getErrorMessage().indexOf("This simulates a failed update") > 0;
        assert request.getConfiguration() != null;
        PropertySimple mybool = request.getConfiguration().getSimple("myboolean");
        assert mybool != null;
        assert mybool.getStringValue().equals("invalid-boolean");
        assert mybool.getErrorMessage().indexOf("Not a valid boolean") > 0;
    }

    @Test(enabled = ENABLE_TESTS)
    public void testNoPermissionCallback() throws Exception {
        Subject superuser = LookupUtil.getSubjectManager().getOverlord();
        Subject noPermSubject = new Subject("userWithNoPermissions", true, false);
        Resource resource = newResource1;

        try {
            noPermSubject = LookupUtil.getSubjectManager().createSubject(superuser, noPermSubject);
            createSession(noPermSubject);

            try {
                configurationManager.updateResourceConfiguration(noPermSubject, resource.getId(), new Configuration());
                assert false : "Should not have been updated - user didn't have permissions";
            } catch (PermissionException expected) {
                System.out.println("This was expected and OK:\n" + expected);
            }

            try {
                configurationManager.updatePluginConfiguration(noPermSubject, resource.getId(), new Configuration());
                assert false : "Should not have been updated - user didn't have permissions";
            } catch (PermissionException expected) {
                System.out.println("This was expected and OK:\n" + expected);
                expected.printStackTrace();
            }
        } finally {
            LookupUtil.getSubjectManager().deleteUsers(LookupUtil.getSubjectManager().getOverlord(),
                new int[] { noPermSubject.getId() });
        }
    }

    @Test(enabled = ENABLE_TESTS)
    public void testInvalidUpdateCallback() throws Exception {
        ConfigurationUpdateResponse response = new ConfigurationUpdateResponse(Integer.MAX_VALUE, new Configuration(),
            ConfigurationUpdateStatus.SUCCESS, null);

        try {
            configurationManager.completeResourceConfigurationUpdate(response);
            assert false : "Should not have been able to process an unknown request - we didn't persist it yet";
        } catch (Exception expected) {
            System.out.println("This was expected and OK:\n" + expected);
        }
    }

    void persist(Object entity) throws Exception {
        EntityManager entityMgr = getEntityManager();
        getEntityManager().persist(entity);
        entityMgr.flush();
        entityMgr.clear();
    }

    void delete(Configuration configuration) {
        EntityManager entityMgr = getEntityManager();
        Configuration managedConfig = entityMgr.find(Configuration.class, configuration.getId());
        entityMgr.remove(managedConfig);
    }

    private class TestServices implements ConfigurationAgentService, DiscoveryAgentService {
        private Configuration savedConfiguration = null;

        public ConfigurationUpdateResponse executeUpdateResourceConfigurationImmediately(
            ConfigurationUpdateRequest request) throws PluginContainerException {
            return null;
        }

        public AvailabilityReport executeAvailabilityScanImmediately(boolean changedOnlyReport) {
            return null;
        }

        public void updateResourceConfiguration(final ConfigurationUpdateRequest request) {
            Thread thread = new Thread() {
                @Override
                public void run() {
                    try {
                        PropertySimple mysleep = request.getConfiguration().getSimple("mysleep");
                        PropertySimple mybool = request.getConfiguration().getSimple("myboolean");
                        assert mybool != null;

                        ConfigurationUpdateResponse response;

                        if (mybool.getStringValue().equalsIgnoreCase("true")) {
                            savedConfiguration = request.getConfiguration();

                            // tests need us to sleep AFTER we save the config
                            if (mysleep != null) {
                                Thread.sleep(mysleep.getLongValue());
                            }

                            response = new ConfigurationUpdateResponse(request.getConfigurationUpdateId(), null,
                                ConfigurationUpdateStatus.SUCCESS, null);
                        } else if (mybool.getStringValue().equalsIgnoreCase("false")) {
                            savedConfiguration = request.getConfiguration();

                            // tests need us to sleep AFTER we save the config
                            if (mysleep != null) {
                                Thread.sleep(mysleep.getLongValue());
                            }

                            response = new ConfigurationUpdateResponse(request.getConfigurationUpdateId(), null,
                                ConfigurationUpdateStatus.SUCCESS, null);
                        } else {
                            mybool.setErrorMessage(ThrowableUtil.getStackAsString(new IllegalArgumentException(
                                "Not a valid boolean")));
                            response = new ConfigurationUpdateResponse(request.getConfigurationUpdateId(), request
                                .getConfiguration(), new NullPointerException("This simulates a failed update"));
                        }

                        LookupUtil.getConfigurationManager().completeResourceConfigurationUpdate(response);
                    } catch (Throwable t) {
                        t.printStackTrace();
                        throw new RuntimeException(t);
                    }
                }
            };

            thread.start();
        }

        public void updatePluginConfiguration(int resourceId, Configuration newPluginConfiguration)
            throws InvalidPluginConfigurationClientException, PluginContainerException {
            PropertySimple mybool = newPluginConfiguration.getSimple("fail");
            if ((mybool != null) && mybool.getBooleanValue()) {
                throw new PluginContainerException("Simulates a failure to update plugin config");
            }

            return;
        }

        public ResourceConfigurationUpdate createResource(Resource parent, ResourceType resourceType,
            ResourceConfigurationUpdate configuration) throws PluginContainerException {
            return null;
        }

        public void deleteResource(Resource resource) throws PluginContainerException {
        }

        public Configuration loadResourceConfiguration(int resourceId) throws PluginContainerException {
            return savedConfiguration;
        }

        public Configuration loadResourceConfiguration(int resourceId, boolean fromStructured)
            throws PluginContainerException {
            return savedConfiguration;
        }

        public Configuration merge(Configuration configuration, int resourceId, boolean fromStructured)
            throws PluginContainerException {
            return null;
        }

        public void disableServiceScans(int serverResourceId) {
        }

        public void enableServiceScans(int serverResourceId, Configuration config) {
        }

        public InventoryReport executeServerScanImmediately() throws PluginContainerException {
            return null;
        }

        public InventoryReport executeServiceScanImmediately() throws PluginContainerException {
            return null;
        }

        public void executeServiceScanDeferred() {
            return;
        }

        public Availability getCurrentAvailability(Resource resource) {
            return null;
        }

        public MergeResourceResponse manuallyAddResource(ResourceType resourceType, int parentResourceId,
            Configuration pluginConfiguration, int ownerSubjectId) throws InvalidPluginConfigurationClientException,
            PluginContainerException {
            return null;
        }

        public Resource getPlatform() {
            return null;
        }

        public InventoryReport getPlatformScanResults(long jobId) {
            return null;
        }

        public long startPlatformScan(Configuration config) {
            return 0;
        }

        public boolean stopPlatformScan(long jobId) {
            return false;
        }

        public void uninventoryResource(int resourceId) {
        }

        @Asynchronous(guaranteedDelivery = true)
        public void synchronizeInventory(ResourceSyncInfo syncInfo) {
        }

        public Configuration validate(Configuration configuration, int resourceId, boolean isStructured)
            throws PluginContainerException {
            return null;
        }
    }
}
