/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.brooklyn.core.mgmt.rebind;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;

import java.util.Map;

import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.entity.Group;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.mgmt.rebind.mementos.BrooklynMementoManifest;
import org.apache.brooklyn.api.policy.Policy;
import org.apache.brooklyn.api.policy.PolicySpec;
import org.apache.brooklyn.api.sensor.EnricherSpec;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.enricher.AbstractEnricher;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.location.Locations;
import org.apache.brooklyn.core.mgmt.rebind.RebindEnricherTest.MyEnricher;
import org.apache.brooklyn.core.policy.AbstractPolicy;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.entity.group.BasicGroup;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.collections.MutableSet;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.testng.annotations.Test;

import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

public class RebindPolicyTest extends RebindTestFixtureWithApp {

    /*
     * FIXME Need to decide what to do about policy mementos and restoring.
     * Lots of places register anonymous inner class policies.
     * (e.g. AbstractController registering a AbstractMembershipTrackingPolicy)
     * Also, the entity constructor often re-creates the policy.
     * 
     * See RebindManagerImpl.CheckpointingChangeListener.onChanged(Entity) and
     * MementosGenerator.newEntityMementoBuilder()
     */
    
    @Test
    public void testRestoresSimplePolicyFromConstructor() throws Exception {
        MyPolicy origPolicy = new MyPolicy(MutableMap.of("myfield", "myFieldVal", "myconfigkey", "myConfigVal"));
        origApp.policies().add(origPolicy);
        runRestoresSimplePolicy();
    }

    @Test
    public void testRestoresDeprecatedPolicyFromConstructorWithoutNoArgs() throws Exception {
        MyPolicyWithoutNoArgConstructor origPolicy = new MyPolicyWithoutNoArgConstructor(MutableMap.of("myfield", "myFieldVal", "myconfigkey", "myConfigVal"));
        origApp.policies().add(origPolicy);
        runRestoresSimplePolicy();
    }

    @Test
    public void testRestoresSimplePolicyFromPolicySpec() throws Exception {
        origApp.policies().add(PolicySpec.create(MyPolicy.class)
                .configure("myfield", "myFieldVal")
                .configure(MyPolicy.MY_CONFIG, "myConfigVal"));
        runRestoresSimplePolicy();
    }
    
    protected void runRestoresSimplePolicy() throws Exception {
        MyPolicy origPolicy = (MyPolicy) Iterables.getOnlyElement(origApp.policies());
        assertTrue(origPolicy.isRunning());
        assertTrue(origPolicy.initCalled);
        assertFalse(origPolicy.rebindCalled);
        
        newApp = rebind();
        MyPolicy newPolicy = (MyPolicy) Iterables.getOnlyElement(newApp.policies());
        
        assertEquals(newPolicy.myfield, "myFieldVal");
        assertEquals(newPolicy.getConfig(MyPolicy.MY_CONFIG), "myConfigVal");
        assertTrue(newPolicy.isRunning());
        assertFalse(newPolicy.initCalled);
        assertTrue(newPolicy.rebindCalled);
    }

    @Test
    public void testRestoresConfig() throws Exception {
        origApp.policies().add(PolicySpec.create(MyPolicy.class)
                .displayName("My Policy")
                .uniqueTag("tagU")
                .tag("tag1").tag("tag2")
                .configure(MyPolicy.MY_CONFIG_WITH_SETFROMFLAG_NO_SHORT_NAME, "myVal for with setFromFlag noShortName")
                .configure(MyPolicy.MY_CONFIG_WITH_SETFROMFLAG_WITH_SHORT_NAME, "myVal for setFromFlag withShortName")
                .configure(MyPolicy.MY_CONFIG_WITHOUT_SETFROMFLAG, "myVal for witout setFromFlag"));

        newApp = rebind();
        MyPolicy newPolicy = (MyPolicy) Iterables.getOnlyElement(newApp.policies());
        
        assertEquals(newPolicy.getDisplayName(), "My Policy");
        
        assertEquals(newPolicy.getUniqueTag(), "tagU");
        assertEquals(newPolicy.tags().getTags(), MutableSet.of("tagU", "tag1", "tag2"));
        
        assertEquals(newPolicy.getConfig(MyPolicy.MY_CONFIG_WITH_SETFROMFLAG_NO_SHORT_NAME), "myVal for with setFromFlag noShortName");
        assertEquals(newPolicy.getConfig(MyPolicy.MY_CONFIG_WITH_SETFROMFLAG_WITH_SHORT_NAME), "myVal for setFromFlag withShortName");
        assertEquals(newPolicy.getConfig(MyPolicy.MY_CONFIG_WITHOUT_SETFROMFLAG), "myVal for witout setFromFlag");
    }

    @Test
    public void testExpungesOnEntityUnmanaged() throws Exception {
        Location loc = origManagementContext.getLocationRegistry().getLocationManaged("localhost");
        TestEntity entity = origApp.createAndManageChild(EntitySpec.create(TestEntity.class));
        MyPolicy policy = entity.policies().add(PolicySpec.create(MyPolicy.class));
        MyEnricher enricher = entity.enrichers().add(EnricherSpec.create(MyEnricher.class));

        RebindTestUtils.waitForPersisted(origApp);

        Entities.unmanage(entity);
        Locations.unmanage(loc);
        RebindTestUtils.stopPersistence(origApp);
        
        BrooklynMementoManifest manifest = loadMementoManifest();
        assertFalse(manifest.getEntityIdToManifest().containsKey(entity.getId()));
        assertFalse(manifest.getPolicyIdToType().containsKey(policy.getId()));
        assertFalse(manifest.getEnricherIdToType().containsKey(enricher.getId()));
        assertFalse(manifest.getLocationIdToType().containsKey(loc.getId()));
    }

    @Test
    public void testExpungesOnPolicyRemoved() throws Exception {
        TestEntity entity = origApp.createAndManageChild(EntitySpec.create(TestEntity.class));
        MyPolicy policy = entity.policies().add(PolicySpec.create(MyPolicy.class));
        MyEnricher enricher = entity.enrichers().add(EnricherSpec.create(MyEnricher.class));

        RebindTestUtils.waitForPersisted(origApp);

        entity.policies().remove(policy);
        entity.enrichers().remove(enricher);
        RebindTestUtils.stopPersistence(origApp);
        
        BrooklynMementoManifest manifest = loadMementoManifest();
        assertFalse(manifest.getPolicyIdToType().containsKey(policy.getId()));
        assertFalse(manifest.getEnricherIdToType().containsKey(enricher.getId()));
    }

    @Test
    public void testReboundConfigDoesNotContainId() throws Exception {
        MyPolicy policy = origApp.policies().add(PolicySpec.create(MyPolicy.class));
        
        newApp = rebind();
        MyPolicy newPolicy = (MyPolicy) Iterables.getOnlyElement(newApp.policies());

        assertNull(newPolicy.getConfig(ConfigKeys.newStringConfigKey("id")));
        assertEquals(newPolicy.getId(), policy.getId());
    }
    
    @Test
    public void testReconfigurePolicyPersistsChange() throws Exception {
        MyPolicyReconfigurable policy = origApp.policies().add(PolicySpec.create(MyPolicyReconfigurable.class)
                .configure(MyPolicyReconfigurable.MY_CONFIG, "oldval"));
        policy.config().set(MyPolicyReconfigurable.MY_CONFIG, "newval");
        
        newApp = rebind();
        MyPolicyReconfigurable newPolicy = (MyPolicyReconfigurable) Iterables.getOnlyElement(newApp.policies());

        assertEquals(newPolicy.getConfig(MyPolicyReconfigurable.MY_CONFIG), "newval");
    }

    @Test
    public void testIsRebinding() throws Exception {
        origApp.policies().add(PolicySpec.create(PolicyChecksIsRebinding.class));

        newApp = rebind();
        PolicyChecksIsRebinding newPolicy = (PolicyChecksIsRebinding) Iterables.getOnlyElement(newApp.policies());

        assertTrue(newPolicy.isRebindingValWhenRebinding());
        assertFalse(newPolicy.isRebinding());
    }
    
    @Test
    public void testPolicyTags() throws Exception {
        Policy origPolicy = origApp.policies().add(PolicySpec.create(MyPolicy.class));
        origPolicy.tags().addTag("foo");
        origPolicy.tags().addTag(origApp);

        newApp = rebind();
        Policy newPolicy = Iterables.getOnlyElement(newApp.policies());

        Asserts.assertEqualsIgnoringOrder(newPolicy.tags().getTags(), ImmutableSet.of("foo", newApp));
    }

    // Previously, policy+enricher was added to entity as part of entity.reconstitute, so other entities might not
    // have been initialised and the relationships not set. If a policy immediately looked at entity's children or
    // at another entity, then it might find those entities' state uninitialised.
    //
    // Longer term, we may want to force policies+enrichers to be inactive until the entity really is managed.
    // However, currently some policies inject an onEvent message during their `setEntity` method (because 
    // their subscription does not give them the current value - only changed values. Doing that sudo-onEvent is
    // a bad idea because even on normal startup the entity might still be in its init method, so we shouldn't be
    // kicking off actions at that point.
    @Test
    public void testPolicyAddedWhenEntityRelationshipsSet() throws Exception {
        BasicGroup origGroup = origApp.createAndManageChild(EntitySpec.create(BasicGroup.class));
        TestEntity origEntity = origApp.createAndManageChild(EntitySpec.create(TestEntity.class));
        origGroup.addMember(origEntity);
        
        EnricherChecksEntityHierarchy origEnricher = origApp.enrichers().add(EnricherSpec.create(EnricherChecksEntityHierarchy.class));
        PolicyChecksEntityHierarchy origPolicy = origApp.policies().add(PolicySpec.create(PolicyChecksEntityHierarchy.class));
        assertTrue(origEnricher.success);
        assertTrue(origPolicy.success);
        
        newApp = rebind();
        EnricherChecksEntityHierarchy newEnricher = (EnricherChecksEntityHierarchy) Iterables.getOnlyElement(newApp.enrichers());
        PolicyChecksEntityHierarchy newPolicy = (PolicyChecksEntityHierarchy) Iterables.getOnlyElement(newApp.policies());

        assertTrue(newEnricher.success);
        assertTrue(newPolicy.success);
    }
    public static class PolicyChecksEntityHierarchy extends AbstractPolicy {
        transient volatile boolean success;
        @Override
        public void setEntity(EntityLocal entity) {
            super.setEntity(entity);
            assertTrue(entity instanceof TestApplication);
            assertEquals(entity.getChildren().size(), 2);
            assertEquals(((Group)Iterables.find(entity.getChildren(), Predicates.instanceOf(Group.class))).getMembers().size(), 1);
            success = true;
        }
    }
    public static class EnricherChecksEntityHierarchy extends AbstractEnricher {
        transient volatile boolean success;
        @Override
        public void setEntity(EntityLocal entity) {
            super.setEntity(entity);
            assertTrue(entity instanceof TestApplication);
            assertEquals(entity.getChildren().size(), 2);
            assertEquals(((Group)Iterables.find(entity.getChildren(), Predicates.instanceOf(Group.class))).getMembers().size(), 1);
            success = true;
        }
    }
    
    public static class PolicyChecksIsRebinding extends AbstractPolicy {
        boolean isRebindingValWhenRebinding;
        
        public boolean isRebindingValWhenRebinding() {
            return isRebindingValWhenRebinding;
        }
        @Override public boolean isRebinding() {
            return super.isRebinding();
        }
        @Override public void rebind() {
            super.rebind();
            isRebindingValWhenRebinding = isRebinding();
        }
    }
    
    public static class MyPolicy extends AbstractPolicy {
        public static final ConfigKey<String> MY_CONFIG = ConfigKeys.newStringConfigKey("myconfigkey");
        
        @SetFromFlag
        public static final ConfigKey<String> MY_CONFIG_WITH_SETFROMFLAG_NO_SHORT_NAME = ConfigKeys.newStringConfigKey("myconfig.withSetfromflag.noShortName");

        @SetFromFlag("myConfigWithSetFromFlagWithShortName")
        public static final ConfigKey<String> MY_CONFIG_WITH_SETFROMFLAG_WITH_SHORT_NAME = ConfigKeys.newStringConfigKey("myconfig.withSetfromflag.withShortName");

        public static final ConfigKey<String> MY_CONFIG_WITHOUT_SETFROMFLAG = ConfigKeys.newStringConfigKey("myconfig.withoutSetfromflag");

        @SetFromFlag
        String myfield;

        @SuppressWarnings("unused")
        private final Object dummy = new Object(); // so not serializable
        
        public volatile boolean initCalled;
        public volatile boolean rebindCalled;
        
        public MyPolicy() {
        }
        
        public MyPolicy(Map<?,?> flags) {
            super(flags);
        }
        
        @Override
        public void init() {
            super.init();
            initCalled = true;
        }
        
        @Override
        public void rebind() {
            super.rebind();
            rebindCalled = true;
        }
    }
    
    public static class MyPolicyWithoutNoArgConstructor extends MyPolicy {
        public MyPolicyWithoutNoArgConstructor(Map<?,?> flags) {
            super(flags);
        }
    }
    
    public static class MyPolicyReconfigurable extends AbstractPolicy {
        public static final ConfigKey<String> MY_CONFIG = ConfigKeys.newStringConfigKey("myconfig");
        
        public MyPolicyReconfigurable() {
            super();
        }
        
        @Override
        protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
            if (MY_CONFIG.equals(key)) {
                // we'd do here whatever reconfig meant; caller will set actual new val
            } else {
                super.doReconfigureConfig(key, val);
            }
        }
    }
}
