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

import static org.testng.Assert.assertTrue;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.time.Duration;
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.base.Joiner;
import com.google.common.collect.Iterables;

@Test
public class DependentConfigPollingYamlTest extends AbstractYamlTest {
    private static final Logger log = LoggerFactory.getLogger(DependentConfigPollingYamlTest.class);
    
    private ExecutorService executor;

    @BeforeMethod(alwaysRun = true)
    @Override
    public void setUp() throws Exception {
        super.setUp();
        executor = Executors.newCachedThreadPool();
    }
            
    @AfterMethod(alwaysRun = true)
    @Override
    public void tearDown() throws Exception {
        if (executor != null) executor.shutdownNow();
        super.tearDown();
    }
            
    // Test for BROOKLYN-214. Previously, the brief Tasks.resolving would cause a thread to be
    // leaked. This was because it would call into BrooklynDslDeferredSupplier.get, which would
    // wait on a synchronized block and thus not be interruptible - the thread would be consumed
    // forever, until the attributeWhenReady returned true!
    //
    // Integration test, because takes several seconds.
    @Test(groups="Integration")
    public void testResolveAttributeWhenReadyWithTimeoutDoesNotLeaveThreadRunning() throws Exception {
        String yaml = Joiner.on("\n").join(
                "services:",
                "- type: org.apache.brooklyn.core.test.entity.TestEntity",
                "  id: myentity",
                "  brooklyn.config:",
                "    test.confName: $brooklyn:entity(\"myentity\").attributeWhenReady(\"mysensor\")");
        
        final Entity app = createAndStartApplication(yaml);
        final TestEntity entity = (TestEntity) Iterables.getOnlyElement(app.getChildren());

        // Cause a thread to block, getting the config - previousy (before fixing 214) this would be in
        // the synchronized block if BrooklynDslDeferredSupplier.get().
        // The sleep is to ensure we really did get into the locking code.
        executor.submit(new Callable<Object>() {
            @Override
            public Object call() {
                return entity.config().get(TestEntity.CONF_NAME);
            }});
        Thread.sleep(100);
        
        // Try to resolve the value many times, each in its own task, but with a short timeout for each.
        final int numIterations = 20;
        final int preNumThreads = Thread.activeCount();
        
        for (int i = 0; i < numIterations; i++) {
            // Same as RestValueResolver.getImmediateValue
            Tasks.resolving(entity.config().getRaw(TestEntity.CONF_NAME).get())
                    .as(Object.class)
                    .defaultValue("UNRESOLVED")
                    .timeout(Duration.millis(100))
                    .context(entity)
                    .swallowExceptions()
                    .get();
        }

        // Confirm we haven't left threads behind.
        Asserts.succeedsEventually(new Runnable() {
            @Override
            public void run() {
                int postNumThreads = Thread.activeCount();
                String msg = "pre="+preNumThreads+"; post="+postNumThreads+"; iterations="+numIterations;
                log.info(msg);
                assertTrue(postNumThreads < preNumThreads + (numIterations / 2), msg);
            }});
    }

    @Override
    protected Logger getLogger() {
        return log;
    }
}
