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

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

import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.brooklyn.api.entity.Application;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.StartableApplication;
import org.apache.brooklyn.core.entity.factory.ApplicationBuilder;
import org.apache.brooklyn.core.objs.proxy.EntityProxy;
import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.entity.stock.BasicApplication;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

public class ApplicationBuilderOverridingTest {

    private static final Logger LOG = LoggerFactory.getLogger(ApplicationBuilderOverridingTest.class);
    
    private static final long TIMEOUT_MS = 10*1000;
    
    private ManagementContext spareManagementContext;
    private Application app;
    private ExecutorService executor;
    
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        spareManagementContext = LocalManagementContextForTests.newInstance();
        executor = Executors.newCachedThreadPool();
    }
    
    @AfterMethod(alwaysRun=true)
    public void tearDown() {
        if (app != null) Entities.destroyAll(app.getManagementContext());
        app = null;
        if (spareManagementContext != null) Entities.destroyAll(spareManagementContext);
        spareManagementContext = null;
    }

    @Test
    public void testUsesDefaultBasicApplicationClass() {
        app = new ApplicationBuilder() {
            @Override public void doBuild() {}
        }.manage();
        
        assertEquals(app.getEntityType().getName(), BasicApplication.class.getCanonicalName());
        assertIsProxy(app);
    }
    
    @Test
    public void testUsesSuppliedApplicationClass() {
        app = new ApplicationBuilder(EntitySpec.create(TestApplication.class)) {
            @Override public void doBuild() {}
        }.manage();
        
        assertEquals(app.getEntityType().getName(), TestApplication.class.getName());
    }

    @Test
    public void testUsesSuppliedManagementContext() {
        app = new ApplicationBuilder() {
            @Override public void doBuild() {}
        }.manage(spareManagementContext);
        
        assertEquals(app.getManagementContext(), spareManagementContext);
    }

    @Test
    public void testCreatesChildEntity() {
        final AtomicReference<TestEntity> expectedChild = new AtomicReference<TestEntity>();
        app = new ApplicationBuilder() {
            @Override public void doBuild() {
                expectedChild.set(addChild(EntitySpec.create(TestEntity.class)));
            }
        }.manage();
        
        assertIsProxy(expectedChild.get());
        assertEquals(ImmutableSet.copyOf(app.getChildren()), ImmutableSet.of(expectedChild.get()));
        assertEquals(expectedChild.get().getParent(), app);
    }

    @Test(enabled=false)
    public void testAppHierarchyIsManaged() {
        app = new ApplicationBuilder() {
            @Override public void doBuild() {
                Entity entity = addChild(EntitySpec.create(TestEntity.class));
            }
        }.manage();
        
        assertIsManaged(app);
        assertIsManaged(Iterables.get(app.getChildren(), 0));
    }

    // TODO Can't assert the child added in doBuild is unmanaged
    @Test(enabled=false)
    public void testEntityAddedInDoBuildIsUnmanagedUntilAppIsManaged() {
        app = new ApplicationBuilder() {
            @Override public void doBuild() {
                Entity entity = addChild(EntitySpec.create(TestEntity.class));
                assertFalse(getManagementContext().getEntityManager().isManaged(entity));
            }
        }.manage();
        
        assertIsManaged(app);
        assertIsManaged(Iterables.get(app.getChildren(), 0));
    }

    @Test(expectedExceptions=IllegalStateException.class)
    public void testRentrantCallToManageForbidden() {
        ManagementContext secondManagementContext = LocalManagementContextForTests.newInstance();
        try {
            app = new ApplicationBuilder() {
                @Override public void doBuild() {
                    manage(spareManagementContext);
                }
            }.manage(secondManagementContext);
        } finally {
            Entities.destroyAll(secondManagementContext);
        }
    }

    @Test(expectedExceptions=IllegalStateException.class)
    public void testMultipleCallsToManageForbidden() {
        ApplicationBuilder appBuilder = new ApplicationBuilder() {
            @Override public void doBuild() {
            }
        };
        app = appBuilder.manage();
        
        appBuilder.manage(spareManagementContext);
    }

    @Test(expectedExceptions=IllegalStateException.class)
    public void testCallToConfigureAfterManageForbidden() {
        ApplicationBuilder appBuilder = new ApplicationBuilder() {
            @Override public void doBuild() {
            }
        };
        app = appBuilder.manage();
        appBuilder.configure(ImmutableMap.of());
    }

    @Test(expectedExceptions=IllegalStateException.class)
    public void testCallToSetDisplayNameAfterManageForbidden() {
        ApplicationBuilder appBuilder = new ApplicationBuilder() {
            @Override public void doBuild() {
            }
        };
        app = appBuilder.manage(spareManagementContext);
        appBuilder.appDisplayName("myname");
    }

    @Test
    public void testConcurrentCallToManageForbidden() throws Exception {
        final AtomicReference<Throwable> err = new AtomicReference<Throwable>();
        final CountDownLatch inbuildLatch = new CountDownLatch(1);
        final CountDownLatch continueLatch = new CountDownLatch(1);
        final ApplicationBuilder builder = new ApplicationBuilder() {
            @Override public void doBuild() {
                inbuildLatch.countDown();
                try {
                    assertTrue(continueLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
                } catch (InterruptedException e) {
                    throw Exceptions.propagate(e);
                }
            }
        };
        Future<StartableApplication> future = executor.submit(new Callable<StartableApplication>() {
            @Override
            public StartableApplication call() {
                try {
                    return builder.manage();
                } catch (Throwable t) {
                    LOG.error("Problem in simple ApplicationBuilder", t);
                    err.set(t);
                    inbuildLatch.countDown();
                    throw Exceptions.propagate(t);
                }
            }
        });
        
        inbuildLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
        if (err.get() != null) {
            throw Exceptions.propagate(err.get());
        }
        
        try {
            app = builder.manage(spareManagementContext);
            fail();
        } catch (IllegalStateException e) {
            // expected
        }
        
        continueLatch.countDown();
        app = future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
    }

    private void assertIsProxy(Entity e) {
        assertFalse(e instanceof AbstractEntity, "e="+e+";e.class="+e.getClass());
        assertTrue(e instanceof EntityProxy, "e="+e+";e.class="+e.getClass());
    }
    
    private void assertIsManaged(Entity e) {
        assertTrue(((EntityInternal)e).getManagementSupport().isDeployed(), "e="+e);
    }
}
