/*
 * 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, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * 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 and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser 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.enterprise.server.configuration;

import static org.rhq.core.domain.authz.Permission.CONFIGURE_READ;
import static org.rhq.core.domain.authz.Permission.CONFIGURE_WRITE;
import static org.rhq.test.AssertUtils.*;
import static org.rhq.core.domain.configuration.ConfigurationUpdateStatus.*;
import static org.testng.Assert.*;

import org.rhq.core.domain.authz.Permission;
import org.rhq.core.util.exception.ThrowableUtil;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.resource.ResourceManagerLocal;
import org.rhq.test.JMockTest;
import org.rhq.test.jmock.PropertyMatcher;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.ResourceConfigurationUpdate;
import org.rhq.core.domain.configuration.RawConfiguration;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.definition.ConfigurationDefinition;
import org.rhq.core.domain.configuration.definition.ConfigurationFormat;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.Agent;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.clientapi.agent.configuration.ConfigurationAgentService;
import org.rhq.core.clientapi.agent.configuration.ConfigurationUpdateRequest;
import org.rhq.enterprise.server.core.AgentManagerLocal;
import org.rhq.enterprise.server.agentclient.AgentClient;
import org.rhq.enterprise.server.authz.AuthorizationManagerLocal;
import org.testng.annotations.Test;
import org.testng.annotations.BeforeMethod;
import org.jmock.Expectations;
import org.jmock.Sequence;
import org.hamcrest.Matcher;

import javax.persistence.EntityManager;
import java.lang.reflect.Field;

public class ConfigurationManagerBeanUnitTest extends JMockTest {

    static final boolean FROM_STRUCTURED = true;
    static final boolean FROM_RAW = false;
    static final Subject OVERLORD = new Subject("overlord", true, true);

    ConfigurationManagerLocal configurationMgrLocal;

    ResourceManagerLocal resourceMgr;

    SubjectManagerLocal subjectMgr;

    AgentManagerLocal agentMgr;

    AuthorizationManagerLocal authorizationMgr;

    EntityManager entityMgr;

    ConfigurationManagerBean configurationMgr;

    @BeforeMethod
    public void setup() throws Exception {
        configurationMgr = new ConfigurationManagerBean();

        configurationMgrLocal = context.mock(ConfigurationManagerLocal.class);
        setField(configurationMgr, "configurationManager", configurationMgrLocal);

        agentMgr = context.mock(AgentManagerLocal.class);
        setField(configurationMgr, "agentManager", agentMgr);

        authorizationMgr = context.mock(AuthorizationManagerLocal.class);
        setField(configurationMgr, "authorizationManager", authorizationMgr);

        entityMgr = context.mock(EntityManager.class);
        setField(configurationMgr, "entityManager", entityMgr);

        resourceMgr = context.mock(ResourceManagerLocal.class);
        setField(configurationMgr, "resourceManager", resourceMgr);

        subjectMgr = context.mock(SubjectManagerLocal.class);
        setField(configurationMgr, "subjectManager", subjectMgr);
    }

    static void setField(Object src, String fieldName, Object value) {
        try {
            Field field = src.getClass().getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(src, value);
        }
        catch (SecurityException e) {
            throw new RuntimeException(e);
        }
        catch (IllegalArgumentException e) {
            throw new RuntimeException(e);
        }
        catch (NoSuchFieldException e) {
            String msg = "The field <" + fieldName + "> does not exist for " +
                src.getClass().getName();
            throw new RuntimeException(e);
        }
        catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }

    }

    @Test
    public void updateStructuredResourceConfigShouldUpdateAuditTrailAndSendUpdateToAgent() throws Exception {
        final ResourceConfigUpdateFixture fixture = newStructuredResourceConfigUpdateFixture();

        final ResourceConfigurationUpdate expectedUpdate = new ResourceConfigurationUpdate(fixture.resource,
            fixture.configuration, fixture.subject.getName());

        final AgentClient agentClient = context.mock(AgentClient.class);
        final ConfigurationAgentService configAgentService = context.mock(ConfigurationAgentService.class);

        final ConfigurationUpdateRequest expectedUpdateRequest = new ConfigurationUpdateRequest(expectedUpdate.getId(),
            expectedUpdate.getConfiguration(), expectedUpdate.getResource().getId());

        context.checking(new Expectations() {{
            allowing(subjectMgr).getOverlord(); will(returnValue(OVERLORD));

            allowing(resourceMgr).getResource(OVERLORD, fixture.resourceId);
            will(returnValue(fixture.resource));

            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_WRITE, fixture.resourceId);
            will(returnValue(true));

            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));
            
            oneOf(configurationMgrLocal).persistResourceConfigurationUpdateInNewTransaction(fixture.subject,
                fixture.resourceId, fixture.configuration, INPROGRESS, fixture.subject.getName(),
                fixture.isPartOfGroupUpdate);
            will(returnValue(expectedUpdate));

            allowing(agentMgr).getAgentClient(expectedUpdate.getResource().getAgent()); will(returnValue(agentClient));

            allowing(agentClient).getConfigurationAgentService(); will(returnValue(configAgentService));

            oneOf(configAgentService).updateResourceConfiguration(with(matchingUpdateRequest(expectedUpdateRequest)));
        }});

        ResourceConfigurationUpdate actualUpdate = configurationMgr.updateResourceConfiguration(fixture.subject,
            fixture.resourceId, fixture.configuration);

        assertSame(actualUpdate, expectedUpdate, "Expected to get back the persisted configuration update");
    }

    @Test
    public void updateRawResourceConfigShouldUpdateAuditTrailAndSendUpdateToAgent() throws Exception {
        final ResourceConfigUpdateFixture fixture = newRawResourceConfigUpdateFixture();

        final ResourceConfigurationUpdate expectedUpdate = new ResourceConfigurationUpdate(fixture.resource,
            fixture.configuration, fixture.subject.getName());

        final AgentClient agentClient = context.mock(AgentClient.class);
        final ConfigurationAgentService configAgentService = context.mock(ConfigurationAgentService.class);

        final ConfigurationUpdateRequest expectedUpdateRequest = new ConfigurationUpdateRequest(expectedUpdate.getId(),
            expectedUpdate.getConfiguration(), expectedUpdate.getResource().getId());

        context.checking(new Expectations() {{
            allowing(subjectMgr).getOverlord(); will(returnValue(OVERLORD));

            allowing(resourceMgr).getResource(OVERLORD, fixture.resourceId);
            will(returnValue(fixture.resource));

            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));

            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_WRITE, fixture.resourceId);
            will(returnValue(true));

            oneOf(configurationMgrLocal).persistResourceConfigurationUpdateInNewTransaction(fixture.subject,
                fixture.resourceId, fixture.configuration, INPROGRESS, fixture.subject.getName(),
                fixture.isPartOfGroupUpdate);
            will(returnValue(expectedUpdate));

            allowing(agentMgr).getAgentClient(expectedUpdate.getResource().getAgent()); will(returnValue(agentClient));

            allowing(agentClient).getConfigurationAgentService(); will(returnValue(configAgentService));

            oneOf(configAgentService).updateResourceConfiguration(with(matchingUpdateRequest(expectedUpdateRequest)));
        }});

        ResourceConfigurationUpdate actualUpdate = configurationMgr.updateResourceConfiguration(fixture.subject,
            fixture.resourceId, fixture.configuration);

        assertSame(actualUpdate, expectedUpdate, "Expected to get back the persisted configuration update");
    }

    @Test(expectedExceptions = ConfigurationUpdateNotSupportedException.class)
    public void exceptionShouldBeThrownWhenCallingWrongMethodForConfigThatSupportsStructuredAndRaw() throws Exception {
        final ResourceConfigUpdateFixture fixture = newStructuredAndRawResourceConfigUpdateFixture();

        context.checking(new Expectations() {{
            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));
        }});

        configurationMgr.updateResourceConfiguration(fixture.subject, fixture.resourceId, fixture.configuration);
    }

    @Test
    public void updateResourceConfigShouldNotTranslateStructuredWhenRawNotSupported() throws Exception {
        final ResourceConfigUpdateFixture fixture = newStructuredResourceConfigUpdateFixture();

        final ResourceConfigurationUpdate expectedUpdate = new ResourceConfigurationUpdate(fixture.resource,
            fixture.configuration, fixture.subject.getName());
        expectedUpdate.setId(-1);

        final AgentClient agentClient = context.mock(AgentClient.class);
        final ConfigurationAgentService configAgentService = context.mock(ConfigurationAgentService.class);

        final ConfigurationUpdateRequest expectedUpdateRequest = new ConfigurationUpdateRequest(expectedUpdate.getId(),
            expectedUpdate.getConfiguration(), expectedUpdate.getResource().getId());

        final Sequence configUdpate = context.sequence("structured-config-update");

        context.checking(new Expectations() {{
            oneOf(authorizationMgr).canViewResource(fixture.subject, fixture.resourceId);
            will(returnValue(true));

            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_WRITE, fixture.resourceId);
            will(returnValue(true));

            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));

            allowing(agentMgr).getAgentClient(expectedUpdate.getResource().getAgent()); will(returnValue(agentClient));

            allowing(agentClient).getConfigurationAgentService(); will(returnValue(configAgentService));

            oneOf(configAgentService).validate(fixture.configuration, fixture.resourceId, FROM_STRUCTURED);
            inSequence(configUdpate);

            oneOf(configurationMgrLocal).persistResourceConfigurationUpdateInNewTransaction(fixture.subject,
                fixture.resourceId, fixture.configuration, INPROGRESS, fixture.subject.getName(),
                fixture.isPartOfGroupUpdate);
            will(returnValue(expectedUpdate));
            inSequence(configUdpate);

            oneOf(configAgentService).updateResourceConfiguration(with(matchingUpdateRequest(expectedUpdateRequest)));
            inSequence(configUdpate);
        }});

        ResourceConfigurationUpdate actualUpdate = configurationMgr.updateStructuredOrRawConfiguration(fixture.subject,
            fixture.resourceId, fixture.configuration, FROM_STRUCTURED);

        assertSame(actualUpdate, expectedUpdate, "Expected to get back the persisted configuration update");
    }

    @Test
    public void updateResourceConfigShouldTranslateRawWhenRawConfigHasEdits() throws Exception {
        final ResourceConfigUpdateFixture fixture = newStructuredAndRawResourceConfigUpdateFixture();
        fixture.configuration.addRawConfiguration(new RawConfiguration());

        final Configuration translatedConfig = fixture.configuration.deepCopy();
        translatedConfig.put(new PropertySimple("x", "y"));

        final ResourceConfigurationUpdate expectedUpdate = new ResourceConfigurationUpdate(fixture.resource, translatedConfig,
            fixture.subject.getName());
        expectedUpdate.setId(-1);

        final AgentClient agentClient = context.mock(AgentClient.class);
        final ConfigurationAgentService configAgentService = context.mock(ConfigurationAgentService.class);

        final ConfigurationUpdateRequest expectedUpdateRequest = new ConfigurationUpdateRequest(expectedUpdate.getId(),
            expectedUpdate.getConfiguration(), expectedUpdate.getResource().getId());

        final Sequence configUdpate = context.sequence("raw-config-update");

        context.checking(new Expectations() {{
            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_READ, fixture.resourceId);
            will(returnValue(true));

            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_WRITE, fixture.resourceId);
            will(returnValue(true));

            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));

            allowing(authorizationMgr).canViewResource(fixture.subject, fixture.resourceId); will(returnValue(true));

            oneOf(configAgentService).merge(fixture.configuration, fixture.resourceId, FROM_RAW);
            will(returnValue(translatedConfig)); inSequence(configUdpate);

            oneOf(configurationMgrLocal).persistResourceConfigurationUpdateInNewTransaction(fixture.subject,
                fixture.resourceId, translatedConfig, INPROGRESS, fixture.subject.getName(),
                fixture.isPartOfGroupUpdate);
            will(returnValue(expectedUpdate));
            inSequence(configUdpate);

            allowing(agentMgr).getAgentClient(expectedUpdate.getResource().getAgent()); will(returnValue(agentClient));

            allowing(agentClient).getConfigurationAgentService(); will(returnValue(configAgentService));

            oneOf(configAgentService).validate(fixture.configuration, fixture.resourceId, FROM_RAW);

            oneOf(configAgentService).updateResourceConfiguration(with(matchingUpdateRequest(expectedUpdateRequest)));
            inSequence(configUdpate);
        }});

        ResourceConfigurationUpdate actualUpdate = configurationMgr.updateStructuredOrRawConfiguration(fixture.subject,
            fixture.resourceId, fixture.configuration, FROM_RAW);

        assertSame(actualUpdate, expectedUpdate, "Expected to get back the persisted configuration update");
    }

    @Test
    public void updateResourceConfigShouldMarkUpdateAsFailureWhenExceptionOccurs() throws Exception {
        final ResourceConfigUpdateFixture fixture = newStructuredResourceConfigUpdateFixture();

        final RuntimeException exception = new RuntimeException();

        final ResourceConfigurationUpdate expectedUpdate = new ResourceConfigurationUpdate(fixture.resource,
            fixture.configuration, fixture.subject.getName());
        expectedUpdate.setId(-1);
        expectedUpdate.setStatus(FAILURE);
        expectedUpdate.setErrorMessage(ThrowableUtil.getStackAsString(exception));

        context.checking(new Expectations() {{
            allowing(subjectMgr).getOverlord(); will(returnValue(OVERLORD));

            allowing(resourceMgr).getResource(OVERLORD, fixture.resourceId);
            will(returnValue(fixture.resource));

            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));

            allowing(entityMgr).find(ResourceConfigurationUpdate.class, expectedUpdate.getId());
            will(returnValue(expectedUpdate));

            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_WRITE, fixture.resourceId);
            will(returnValue(true));
            
            oneOf(configurationMgrLocal).persistResourceConfigurationUpdateInNewTransaction(fixture.subject, fixture.resourceId,
                fixture.configuration, INPROGRESS, fixture.subject.getName(), fixture.isPartOfGroupUpdate);
            will(returnValue(expectedUpdate));

            oneOf(configurationMgrLocal).mergeConfigurationUpdate(expectedUpdate);

            allowing(agentMgr).getAgentClient(expectedUpdate.getResource().getAgent());
            will(throwException(exception));
        }});

        ResourceConfigurationUpdate actualUpdate = configurationMgr.updateResourceConfiguration(fixture.subject, 
            fixture.resourceId, fixture.configuration);

        assertPropertiesMatch(expectedUpdate, actualUpdate, "Expected to get back a failure update");
    }

    @Test
    public void translatingFromStructuredToRawShouldReturnModifiedConfig() throws Exception {
        final ConfigTranslationFixture fixture = newStructuredAndRawTranslationFixture();

        final AgentClient agentClient = context.mock(AgentClient.class);
        final ConfigurationAgentService configAgentService = context.mock(ConfigurationAgentService.class);

        final Configuration expectedConfig = new Configuration();

        context.checking(new Expectations() {{
            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));

            allowing(agentMgr).getAgentClient(fixture.resource.getAgent()); will(returnValue(agentClient));

            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_READ, fixture.resourceId);
            will(returnValue(true));

            allowing(agentClient).getConfigurationAgentService(); will(returnValue(configAgentService));

            oneOf(configAgentService).merge(fixture.configuration, fixture.resourceId, FROM_STRUCTURED);
            will(returnValue(expectedConfig));
        }});

        Configuration translatedConfig = configurationMgr.translateResourceConfiguration(fixture.subject,
            fixture.resourceId, fixture.configuration, FROM_STRUCTURED);

        assertSame(
            translatedConfig,
            expectedConfig,
            "Expected to get back the configuration translated by the " + ConfigurationAgentService.class.getSimpleName());
    }

    @Test
    public void translatingFromRawToStructuredShouldReturnModifiedConfig() throws Exception {
        final ConfigTranslationFixture fixture = newStructuredAndRawTranslationFixture();

        final AgentClient agentClient = context.mock(AgentClient.class);
        final ConfigurationAgentService configAgentService = context.mock(ConfigurationAgentService.class);

        final Configuration expectedConfig = new Configuration();

        context.checking(new Expectations() {{
            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));

            oneOf(authorizationMgr).hasResourcePermission(fixture.subject, CONFIGURE_READ, fixture.resourceId);
            will(returnValue(true));

            allowing(agentMgr).getAgentClient(fixture.resource.getAgent()); will(returnValue(agentClient));

            allowing(agentClient).getConfigurationAgentService(); will(returnValue(configAgentService));

            oneOf(configAgentService).merge(fixture.configuration, fixture.resourceId, FROM_RAW);
            will(returnValue(expectedConfig));
        }});

        Configuration translatedConfig = configurationMgr.translateResourceConfiguration(fixture.subject,
            fixture.resourceId, fixture.configuration, FROM_RAW);

        assertSame(
            translatedConfig,
            expectedConfig,
            "Expected to get back the configuration translated by the " + ConfigurationAgentService.class.getSimpleName());
    }

    @Test(expectedExceptions = TranslationNotSupportedException.class)
    public void exceptionShouldBeThrownWhenTryingToTranslateStructuredOnlyConfig() throws Exception {
        final ConfigTranslationFixture fixture = newStructuredTranslationFixture();

        context.checking(new Expectations() {{
            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));    
        }});

        configurationMgr.translateResourceConfiguration(fixture.subject, fixture.resourceId, fixture.configuration,
            FROM_STRUCTURED);
    }

    @Test(expectedExceptions = TranslationNotSupportedException.class)
    public void exceptionShouldBeThrownWhenTryingToTranslateRawOnlyConfig() throws Exception {
        final ConfigTranslationFixture fixture = newRawTranslationFixture();

        context.checking(new Expectations() {{
            allowing(entityMgr).find(Resource.class, fixture.resourceId); will(returnValue(fixture.resource));
        }});

        configurationMgr.translateResourceConfiguration(fixture.subject, fixture.resourceId, fixture.configuration,
            FROM_RAW);                
    }

    public static Matcher<ConfigurationUpdateRequest> matchingUpdateRequest(ConfigurationUpdateRequest expected) {
        return new PropertyMatcher<ConfigurationUpdateRequest>(expected);
    }

    ResourceConfigUpdateFixture newStructuredResourceConfigUpdateFixture() {
        ResourceConfigUpdateFixture fixture = new ResourceConfigUpdateFixture();
        fixture.resource = createResourceWithStructuredConfig(fixture.resourceId);

        return fixture;
    }

    ResourceConfigUpdateFixture newRawResourceConfigUpdateFixture() {
        ResourceConfigUpdateFixture fixture = new ResourceConfigUpdateFixture();
        fixture.resource = createResourceWithRawConfig(fixture.resourceId);

        return fixture;
    }

    ResourceConfigUpdateFixture newStructuredAndRawResourceConfigUpdateFixture() {
        ResourceConfigUpdateFixture fixture = new ResourceConfigUpdateFixture();
        fixture.resource = createResourceWithStructuredAndRawConfig(fixture.resourceId);

        return fixture;
    }

    ConfigTranslationFixture newStructuredAndRawTranslationFixture() {
        ConfigTranslationFixture fixture = new ConfigTranslationFixture();
        fixture.resource = createResourceWithStructuredAndRawConfig(fixture.resourceId);

        return fixture;
    }

    ConfigTranslationFixture newStructuredTranslationFixture() {
        ConfigTranslationFixture fixture = new ConfigTranslationFixture();
        fixture.resource = createResourceWithStructuredConfig(fixture.resourceId);

        return fixture;
    }

    ConfigTranslationFixture newRawTranslationFixture() {
        ConfigTranslationFixture fixture = new ConfigTranslationFixture();
        fixture.resource = createResourceWithRawConfig(fixture.resourceId);

        return fixture;
    }

    Resource createResource(int resourceId) {
        Resource resource = new Resource(resourceId);
        resource.setAgent(new Agent("test-agent", "localhost", 7080, null, null));
        return resource;
    }

    Resource createResourceWithStructuredConfig(int resourceId) {
        Resource resource = createResource(resourceId);
        resource.setResourceType(createStructurerdOnlyResourceType());

        return resource;
    }

    Resource createResourceWithStructuredAndRawConfig(int resourceId) {
        Resource resource = createResource(resourceId);
        resource.setResourceType(createStructuredAndRawResourceType());

        return resource;
    }

    Resource createResourceWithRawConfig(int resourceId) {
        Resource resource = createResource(resourceId);
        resource.setResourceType(createRawResourceType());

        return resource;
    }

    ResourceType createStructuredAndRawResourceType() {
        ConfigurationDefinition configDef = new ConfigurationDefinition("structured-and-raw", "structured and raw");
        configDef.setConfigurationFormat(ConfigurationFormat.STRUCTURED_AND_RAW);

        ResourceType resourceType = new ResourceType();
        resourceType.setResourceConfigurationDefinition(configDef);

        return resourceType;
    }

    ResourceType createStructurerdOnlyResourceType() {
        ConfigurationDefinition configDef = new ConfigurationDefinition("structred-config-def", "structured config def");
        configDef.setConfigurationFormat(ConfigurationFormat.STRUCTURED);

        ResourceType resourceType = new ResourceType();
        resourceType.setResourceConfigurationDefinition(configDef);

        return resourceType;
    }

    ResourceType createRawResourceType() {
        ConfigurationDefinition configDef = new ConfigurationDefinition("raw-config-def", "raw config def");
        configDef.setConfigurationFormat(ConfigurationFormat.RAW);

        ResourceType resourceType = new ResourceType();
        resourceType.setResourceConfigurationDefinition(configDef);

        return resourceType;
    }

    static class ResourceConfigUpdateFixture {
        Subject subject = new Subject("rhqadmin", true, true);
        int resourceId = -1;
        Configuration configuration = new Configuration();
        boolean isPartOfGroupUpdate = false;
        Resource resource;
    }

    static class ConfigTranslationFixture {
        Subject subject = new Subject("rhqadmin", true, true);
        int resourceId = -1;
        Configuration configuration = new Configuration();
        Resource resource;
    }

}
