/*
 * 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.entity;

import static com.google.common.base.Preconditions.checkNotNull;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.util.List;
import java.util.Set;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.core.entity.EntityAutomanagedTest.RecordingCollectionChangeListener.ChangeEvent;
import org.apache.brooklyn.core.entity.EntityAutomanagedTest.RecordingCollectionChangeListener.ChangeType;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.core.test.entity.TestApplicationImpl;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.test.Asserts;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;

public class EntityAutomanagedTest extends BrooklynAppUnitTestSupport {

    // TODO Want to use a RecordingCollectionChangeListener to ensure on auto-manage
    // we are notified of the entity being added etc, but compilation fails on command line,
    // running `mvn clean install` (using 1.7.0_65, build 24.65-b04)!
    //  - when written "properly", it complains about the @Override annotation on onItemAdded(Entity item)
    //  - when that is removed, it complains about the call to mgmt.addEntitySetListener not being compatible
    //  - when try stripping out generics, it complains that "cannot find symbol" for the declaration of this class
    //
    // In your IDE (at least in Eclipse), you can uncomment the bits beside the TODO and run it 
    // with those insertions!
    
    protected RecordingCollectionChangeListener listener;
    
    @Override
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        super.setUp();
        listener = new RecordingCollectionChangeListener();
        
        // TODO Compiler problems - see comment at top of class
        //mgmt.addEntitySetListener((CollectionChangeListener)listener);
    }

    
    //////////////////////////////////////
    // Variants of addChild(EntitySpec) //
    //////////////////////////////////////
    
    @Test
    public void testAddedChildSpec() throws Exception {
        TestEntity e = app.addChild(EntitySpec.create(TestEntity.class));
        assertTrue(Entities.isManaged(e));
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, e)));
    }

    @Test
    public void testAddedChildHierarchySpec() throws Exception {
        TestEntity e = app.addChild(EntitySpec.create(TestEntity.class)
                .child(EntitySpec.create(TestEntity.class)
                        .child(EntitySpec.create(TestEntity.class))));
        TestEntity e2 = (TestEntity) Iterables.getOnlyElement(e.getChildren());
        TestEntity e3 = (TestEntity) Iterables.getOnlyElement(e2.getChildren());
        
        assertTrue(Entities.isManaged(e));
        assertTrue(Entities.isManaged(e2));
        assertTrue(Entities.isManaged(e3));
        assertEquals(e.getParent(), app);
        assertEquals(e2.getParent(), e);
        assertEquals(e3.getParent(), e2);
        listener.assertEventsEqualsEventually(ImmutableList.of(
                new ChangeEvent(ChangeType.ADDED, e),
                new ChangeEvent(ChangeType.ADDED, e2),
                new ChangeEvent(ChangeType.ADDED, e3)));
    }

    @Test
    public void testNewEntityWithParent() throws Exception {
        TestEntity e = app.addChild(EntitySpec.create(TestEntity.class)
                .parent(app));
        assertTrue(Entities.isManaged(e));
        assertEquals(e.getParent(), app);
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, e)));
    }

    @Test
    public void testNewEntityHierarchyWithParent() throws Exception {
        TestEntity e = mgmt.getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
                .child(EntitySpec.create(TestEntity.class)
                        .child(EntitySpec.create(TestEntity.class)))
                .parent(app));
        TestEntity e2 = (TestEntity) Iterables.getOnlyElement(e.getChildren());
        TestEntity e3 = (TestEntity) Iterables.getOnlyElement(e2.getChildren());
        
        assertTrue(Entities.isManaged(e));
        assertTrue(Entities.isManaged(e2));
        assertTrue(Entities.isManaged(e3));
        assertEquals(e.getParent(), app);
        assertEquals(e2.getParent(), e);
        assertEquals(e3.getParent(), e2);
        listener.assertEventsEqualsEventually(ImmutableList.of(
                new ChangeEvent(ChangeType.ADDED, e),
                new ChangeEvent(ChangeType.ADDED, e2),
                new ChangeEvent(ChangeType.ADDED, e3)));
    }
    
    @Test
    public void testAddingSameChildAgainIsNoop() throws Exception {
        TestEntity e = app.addChild(EntitySpec.create(TestEntity.class)
                .parent(app));
        
        app.addChild(e);
        assertTrue(Entities.isManaged(e));
        assertEquals(e.getParent(), app);
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, e)));
    }
    
    
    //////////////////////////////////////
    // Variants of createEntity for app //
    //////////////////////////////////////

    @Test
    public void testNewApp() throws Exception {
        TestApplication app2 = mgmt.getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
        assertTrue(Entities.isManaged(app2));
        
        assertTrue(mgmt.getApplications().contains(app2), "app="+app2+"; apps="+mgmt.getApplications());
        app.addChild(app2);
        assertTrue(Entities.isManaged(app2));
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, app2)));
    }
    
    
    ////////////////////////////////////////////////////////////////
    // Variants of Entities.startManagement and Entities.manage() //
    ////////////////////////////////////////////////////////////////

    @Test
    public void testManageIsNoop() throws Exception {
        TestEntity child = mgmt.getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
                .parent(app));
        
        Entities.manage(child);
        assertTrue(Entities.isManaged(child));
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, child)));
    }
    
    @Test
    public void testStartManagementIsNoop() throws Exception {
        TestApplication app2 = mgmt.getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
        assertTrue(Entities.isManaged(app2));
        
        Entities.startManagement(app2, mgmt);
        assertTrue(Entities.isManaged(app2));
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, app2)));
    }
    
    @Test
    public void testStartManagementOfEntityIsNoop() throws Exception {
        Entity app2 = mgmt.getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
        assertTrue(Entities.isManaged(app2));
        
        Entities.startManagement(app2);
        assertTrue(Entities.isManaged(app2));
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, app2)));
    }
    
    @Test
    public void testStartManagementFailsIfAppDeleted() throws Exception {
        TestApplication app2 = mgmt.getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
        Entities.unmanage(app2);
        
        try {
            Entities.startManagement(app2, mgmt);
            fail("Managed deleted app "+app2+" in "+mgmt);
        } catch (IllegalStateException e) {
            if (!(e.toString().contains("No concrete entity known"))) throw e;
        }
    }

    @Test
    public void testManageFailsIfEntityDeleted() throws Exception {
        TestEntity child = mgmt.getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
                .parent(app));
        Entities.unmanage(child);
        
        try {
            Entities.manage(child);
            fail("Managed deleted entity "+child+" in "+mgmt);
        } catch (IllegalArgumentException e) {
            if (!(e.toString().contains("Can't manage"))) throw e;
        }
    }
    

    ///////////////////////////////////////////
    // Variants of createEntity for non-apps //
    ///////////////////////////////////////////

    // TODO Controversial? Should it be based on reachability from parent? Can entities be (temporarily) top-level?
    // But management model is simpler if it becomes managed immediately.
    @Test
    public void testNewOrphanedEntityIsManaged() throws Exception {
        TestEntity e = mgmt.getEntityManager().createEntity(EntitySpec.create(TestEntity.class));
        assertTrue(Entities.isManaged(e));
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, e)));
        
        // Check that orphaned entity doesn't interfere with getApplications
        Asserts.assertEqualsIgnoringOrder(mgmt.getApplications(), ImmutableList.of(app)); 
    }

    @Test
    public void testOrphanedEntityHierarchyIsManaged() throws Exception {
        TestEntity e = mgmt.getEntityManager().createEntity(EntitySpec.create(TestEntity.class)
                .child(EntitySpec.create(TestEntity.class)
                        .child(EntitySpec.create(TestEntity.class))));
        TestEntity e2 = (TestEntity) Iterables.getOnlyElement(e.getChildren());
        TestEntity e3 = (TestEntity) Iterables.getOnlyElement(e2.getChildren());
        
        assertTrue(Entities.isManaged(e));
        assertTrue(Entities.isManaged(e2));
        assertTrue(Entities.isManaged(e3));
        assertEquals(e.getParent(), null);
        assertEquals(e2.getParent(), e);
        assertEquals(e3.getParent(), e2);
        listener.assertEventsEqualsEventually(ImmutableList.of(
                new ChangeEvent(ChangeType.ADDED, e),
                new ChangeEvent(ChangeType.ADDED, e2),
                new ChangeEvent(ChangeType.ADDED, e3)));
    }
    
    @Test
    public void testNewOrphanedEntityCanBeAddedToChild() throws Exception {
        TestEntity e = mgmt.getEntityManager().createEntity(EntitySpec.create(TestEntity.class));
        
        app.addChild(e);
        assertTrue(Entities.isManaged(e));
        listener.assertEventsEqualsEventually(ImmutableList.of(new ChangeEvent(ChangeType.ADDED, e)));
    }

    @Test
    public void testEntityNotEqualNull() {
        assertNotEquals(new TestApplicationImpl(), null);
    }

    // TODO Compiler problems - see comment at top of class
    public static class RecordingCollectionChangeListener { // FIXME implements CollectionChangeListener<Entity> {
        public enum ChangeType {
            ADDED, REMOVED;
        }
        public static class ChangeEvent {
            public final ChangeType type;
            public final Entity entity;
            
            ChangeEvent(ChangeType type, Entity entity) {
                this.type = checkNotNull(type, "type");
                this.entity = checkNotNull(entity, "entity");
            }
            
            @Override
            public int hashCode() {
                return Objects.hashCode(type, entity);
            }
            
            @Override
            public boolean equals(Object obj) {
                if (!(obj instanceof ChangeEvent)) return false;
                ChangeEvent o = (ChangeEvent) obj;
                return type.equals(o.type) && entity.equals(o.entity);
            }
            
            @Override
            public String toString() {
                return MoreObjects.toStringHelper(this).add("type", type).add("entity", entity).toString();
            }
        }
        
        private final List<ChangeEvent> events = Lists.newCopyOnWriteArrayList();
        private final Set<Entity> items = Sets.newConcurrentHashSet();

        public void assertEventsEqualsEventually(final Iterable<? extends ChangeEvent> expected) {
            // TODO Compiler problems - see comment at top of class
//            Asserts.succeedsEventually(new Runnable() {
//                public void run() {
//                    assertEquals(events, ImmutableList.copyOf(expected));
//                }});
        }

        public void assertItemsEqualsEventually(final Iterable<? extends Entity> expected) {
            // TODO Compiler problems - see comment at top of class
//            Asserts.succeedsEventually(new Runnable() {
//                public void run() {
//                    assertEquals(items, ImmutableSet.copyOf(expected));
//                }});
        }

        // TODO Want to include @Override; compiler problems - see comment at top of class
        public void onItemAdded(Entity item) {
            items.add(item);
            events.add(new ChangeEvent(ChangeType.ADDED, item));
        }

        // TODO Want to include @Override; compiler problems - see comment at top of class
        public void onItemRemoved(Entity item) {
            items.remove(item);
            events.add(new ChangeEvent(ChangeType.REMOVED, item));
        }
    }
}
