/*
 * 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.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.mgmt.rebind.RebindContext;
import org.apache.brooklyn.api.mgmt.rebind.RebindSupport;
import org.apache.brooklyn.api.mgmt.rebind.mementos.LocationMemento;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.location.AbstractLocation;
import org.apache.brooklyn.core.mgmt.rebind.BasicLocationRebindSupport;
import org.apache.brooklyn.core.mgmt.rebind.RebindEntityTest.MyEntity;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

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

public class RebindLocationTest extends RebindTestFixtureWithApp {

    @SuppressWarnings("unused")
    private MyEntity origE;
    
    @Override
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        super.setUp();
        origE = origApp.createAndManageChild(EntitySpec.create(MyEntity.class));
    }

    @Test
    public void testSetsLocationOnEntities() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of("name", "mylocname"));
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyEntity newE = (MyEntity) Iterables.find(newApp.getChildren(), Predicates.instanceOf(MyEntity.class));

        assertEquals(newApp.getLocations().size(), 1, "locs="+newE.getLocations());
        assertTrue(Iterables.get(newApp.getLocations(), 0) instanceof MyLocation);
        
        assertEquals(newE.getLocations().size(), 1, "locs="+newE.getLocations());
        assertTrue(Iterables.get(newE.getLocations(), 0) instanceof MyLocation);
    }
    
    @Test
    public void testRestoresLocationIdAndDisplayName() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of("name", "mylocname"));
        origApp.start(ImmutableList.of(origLoc));
        
        newApp = rebind();
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);
        
        assertEquals(newLoc.getId(), origLoc.getId());
        assertEquals(newLoc.getDisplayName(), origLoc.getDisplayName());
    }
    
    @Test
    public void testCanCustomizeLocationRebind() throws Exception {
        MyLocationCustomProps origLoc = new MyLocationCustomProps(MutableMap.of("name", "mylocname", "myfield", "myval"));
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyLocationCustomProps newLoc2 = (MyLocationCustomProps) Iterables.get(newApp.getLocations(), 0);
        
        assertEquals(newLoc2.getId(), origLoc.getId());
        assertEquals(newLoc2.getDisplayName(), origLoc.getDisplayName());
        assertEquals(newLoc2.rebound, true);
        assertEquals(newLoc2.myfield, "myval");
    }
    
    @Test
    public void testRestoresFieldsWithSetFromFlag() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of("myfield", "myval"));
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);
        
        assertEquals(newLoc.myfield, "myval");
    }
    
    @Test
    public void testRestoresAtomicLongWithSetFromFlag() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of("myAtomicLong", "123"));
        origApp.start(ImmutableList.of(origLoc));

        origLoc.myAtomicLong.incrementAndGet();
        assertEquals(origLoc.myAtomicLong.get(), 124L);
        origApp.getManagementContext().getRebindManager().getChangeListener().onChanged(origLoc);
        
        newApp = rebind();
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);
        
        // should get _modified_ value, not the one in the config map
        assertEquals(newLoc.myAtomicLong.get(), 124L);
    }
    
    @Test
    public void testRestoresConfig() throws Exception {
        MyLocation origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(MyLocation.class)
                .configure(MyLocation.MY_CONFIG_WITH_SETFROMFLAG_NO_SHORT_NAME, "myVal for with setFromFlag noShortName")
                .configure(MyLocation.MY_CONFIG_WITH_SETFROMFLAG_WITH_SHORT_NAME, "myVal for setFromFlag withShortName")
                .configure(MyLocation.MY_CONFIG_WITHOUT_SETFROMFLAG, "myVal for witout setFromFlag"));
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);
        
        assertEquals(newLoc.getConfig(MyLocation.MY_CONFIG_WITH_SETFROMFLAG_NO_SHORT_NAME), "myVal for with setFromFlag noShortName");
        assertEquals(newLoc.getConfig(MyLocation.MY_CONFIG_WITH_SETFROMFLAG_WITH_SHORT_NAME), "myVal for setFromFlag withShortName");
        assertEquals(newLoc.getConfig(MyLocation.MY_CONFIG_WITHOUT_SETFROMFLAG), "myVal for witout setFromFlag");
    }
    
    @Test
    public void testIgnoresTransientFieldsNotSetFromFlag() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of());
        origLoc.myTransientFieldNotSetFromFlag = "myval";
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);

        // transient fields normally not persisted
        assertEquals(newLoc.myTransientFieldNotSetFromFlag, null);
    }
    
    @Test
    public void testIgnoresTransientFieldsSetFromFlag() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of("myTransientFieldSetFromFlag", "myval"));
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);
        
        assertEquals(newLoc.myTransientFieldSetFromFlag, null);
    }
    
    @Test
    public void testIgnoresStaticFieldsNotSetFromFlag() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of());
        MyLocation.myStaticFieldNotSetFromFlag = "myval";
        origApp.start(ImmutableList.of(origLoc));

        RebindTestUtils.stopPersistence(origApp);
        MyLocation.myStaticFieldNotSetFromFlag = "mynewval";
        newApp = (TestApplication) RebindTestUtils.rebind(mementoDir, getClass().getClassLoader());
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);
        Assert.assertEquals(newLoc, origLoc);
        
        // static fields normally not persisted (we see new value)
        assertEquals(MyLocation.myStaticFieldNotSetFromFlag, "mynewval");
    }
    
    @Test
    public void testIgnoresStaticFieldsSetFromFlag() throws Exception {
        MyLocation origLoc = new MyLocation(MutableMap.of("myStaticFieldSetFromFlag", "myval"));
        origApp.start(ImmutableList.of(origLoc));

        RebindTestUtils.stopPersistence(origApp);
        MyLocation.myStaticFieldSetFromFlag = "mynewval"; // not auto-checkpointed
        newApp = (TestApplication) RebindTestUtils.rebind(mementoDir, getClass().getClassLoader());
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);
        Assert.assertEquals(newLoc, origLoc);
        
        assertEquals(MyLocation.myStaticFieldSetFromFlag, "mynewval");
    }
    
    @Test
    public void testHandlesFieldReferencingOtherLocations() throws Exception {
        MyLocation origOtherLoc = new MyLocation();
        MyLocationReffingOthers origLoc = new MyLocationReffingOthers(MutableMap.of("otherLocs", ImmutableList.of(origOtherLoc), "myfield", "myval"));
        origOtherLoc.setParent(origLoc);
        
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyLocationReffingOthers newLoc = (MyLocationReffingOthers) Iterables.get(newApp.getLocations(), 0);
        
        assertEquals(newLoc.getChildren().size(), 1);
        assertTrue(Iterables.get(newLoc.getChildren(), 0) instanceof MyLocation, "children="+newLoc.getChildren());
        assertEquals(newLoc.otherLocs, ImmutableList.copyOf(newLoc.getChildren()));
        
        // Confirm this didn't override other values (e.g. setting other fields back to their defaults, as was once the case!)
        assertEquals(newLoc.myfield, "myval");
    }

    /**
     * @deprecated since 0.7; support for rebinding old-style locations is deprecated
     */
    @Deprecated
    @Test
    public void testHandlesOldStyleLocation() throws Exception {
        MyOldStyleLocation origLoc = new MyOldStyleLocation(ImmutableMap.of("myfield", "myval"));
        
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyOldStyleLocation newLoc = (MyOldStyleLocation) Iterables.get(newApp.getLocations(), 0);
        assertEquals(newLoc.myfield, "myval");
    }

    @Test
    public void testReboundConfigDoesNotContainId() throws Exception {
        MyLocation origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(MyLocation.class));
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        MyLocation newLoc = (MyLocation) Iterables.get(newApp.getLocations(), 0);

        assertNull(newLoc.config().getBag().getStringKey("id"));
        assertEquals(newLoc.getId(), origLoc.getId());
    }
    
    @Test
    public void testIsRebinding() throws Exception {
        LocationChecksIsRebinding origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(LocationChecksIsRebinding.class));

        rebind();
        LocationChecksIsRebinding newLoc = (LocationChecksIsRebinding) newManagementContext.getLocationManager().getLocation(origLoc.getId());

        assertTrue(newLoc.isRebindingValWhenRebinding());
        assertFalse(newLoc.isRebinding());
    }
    
    @Test
    public void testLocationTags() throws Exception {
        Location origLoc = origManagementContext.getLocationManager().createLocation(LocationSpec.create(MyLocation.class));
        origLoc.tags().addTag("foo");
        origLoc.tags().addTag(origApp);
        origApp.start(ImmutableList.of(origLoc));

        newApp = rebind();
        Location newLoc = newManagementContext.getLocationManager().getLocation(origLoc.getId());

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

    // See https://issues.apache.org/jira/browse/BROOKLYN-396
    @Test
    public void testFlagFieldsNotReturnedInConfig() throws Exception {
        MyLocation origLoc = mgmt().getLocationManager().createLocation(LocationSpec.create(MyLocation.class));
        origLoc.myfield = "myval";
        origLoc.requestPersist();

        // Check (before rebind) that the 'myfield' isn't also in the config
        assertNull(origLoc.config().getBag().getStringKey("myfield"));

        // Check after rebind that we are the same: 'myfield' isn't also in the config
        rebind();
        MyLocation newLoc = (MyLocation) mgmt().getLocationManager().getLocation(origLoc.getId());
        
        assertEquals(newLoc.myfield, "myval");
        assertNull(newLoc.config().getBag().getStringKey("myfield"));
    }
    

    public static class LocationChecksIsRebinding extends AbstractLocation {
        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 MyOldStyleLocation extends AbstractLocation {
        @SetFromFlag
        String myfield;

        public MyOldStyleLocation(Map<?,?> flags) {
            super(flags);
        }
    }
    
    public static class MyLocation extends AbstractLocation {
        @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;

        @SetFromFlag(defaultVal="1")
        AtomicLong myAtomicLong;

        @SuppressWarnings("unused")
        private final Object dummy = new Object(); // so not serializable
        
        @SetFromFlag
        transient String myTransientFieldSetFromFlag;
        
        transient String myTransientFieldNotSetFromFlag;
        
        @SetFromFlag
        static String myStaticFieldSetFromFlag;
        
        static String myStaticFieldNotSetFromFlag;
        
        public MyLocation() {
        }
        
        public MyLocation(Map<?,?> flags) {
            super(flags);
        }
        
        @Override
        public void requestPersist() {
            super.requestPersist();
        }
    }
    
    public static class MyLocationReffingOthers extends AbstractLocation {
        @SetFromFlag(defaultVal="a")
        String myfield;

        @SetFromFlag
        List<Location> otherLocs;

        @SuppressWarnings("unused")
        private final Object dummy = new Object(); // so not serializable

        public MyLocationReffingOthers() {
        }
        
        public MyLocationReffingOthers(Map<?,?> flags) {
            super(flags);
        }
    }
    
    public static class MyLocationCustomProps extends AbstractLocation {
        String myfield;
        boolean rebound;

        @SuppressWarnings("unused")
        private final Object dummy = new Object(); // so not serializable

        public MyLocationCustomProps() {
        }
        
        public MyLocationCustomProps(Map<?,?> flags) {
            super(flags);
            myfield = (String) flags.get("myfield");
        }
        
        @Override
        public RebindSupport<LocationMemento> getRebindSupport() {
            return new BasicLocationRebindSupport(this) {
                @Override public LocationMemento getMemento() {
                    return getMementoWithProperties(MutableMap.<String,Object>of("myfield", myfield));
                }
                @Override
                protected void doReconstruct(RebindContext rebindContext, LocationMemento memento) {
                    super.doReconstruct(rebindContext, memento);
                    myfield = (String) memento.getCustomField("myfield");
                    rebound = true;
                }
            };
        }
    }
}
