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

import static org.apache.brooklyn.core.sensor.DependentConfiguration.attributeWhenReady;
import static org.apache.brooklyn.core.sensor.DependentConfiguration.transform;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.location.SimulatedLocation;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.time.Duration;
import org.apache.brooklyn.util.time.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Stopwatch;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

/** tests effector invocation and a variety of sensor accessors and subscribers */
public class LocalEntitiesTest extends BrooklynAppUnitTestSupport {
    
    public static final Logger log = LoggerFactory.getLogger(LocalEntitiesTest.class);
    
    private SimulatedLocation loc;
            
    @BeforeMethod(alwaysRun=true)
    @Override
    public void setUp() throws Exception {
        super.setUp();
        loc = new SimulatedLocation();
    }

    @Test
    public void testEffectorUpdatesAttributeSensor() {
        HelloEntity h = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        app.start(ImmutableList.of(loc));
        
        h.setAge(5);
        assertEquals((Integer)5, h.getAttribute(HelloEntity.AGE));
    }

    //REVIEW 1459 - new test
    //subscriptions get notified in separate thread
    @Test
    public void testEffectorEmitsAttributeSensor() throws Exception {
        final HelloEntity h = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        app.start(ImmutableList.of(loc));
        
        final AtomicReference<SensorEvent<?>> evt = new AtomicReference<SensorEvent<?>>();
        final CountDownLatch latch = new CountDownLatch(1);
        
        app.subscriptions().subscribe(h, HelloEntity.AGE, new SensorEventListener<Integer>() {
            @Override public void onEvent(SensorEvent<Integer> event) {
                evt.set(event);
                latch.countDown();
            }});
        
        long startTime = System.currentTimeMillis();
        
        h.invoke(HelloEntity.SET_AGE, ImmutableMap.of("age", 5));
        assertTrue(latch.await(5000, TimeUnit.MILLISECONDS));

        // observed intermittent failure 2 May 2012. and 14 Jun "after 2 ms". spurious wakeups.
        // code added above to guard against this. (if problem does not recur, remove these comments!)
        assertNotNull(evt.get(), "null response after "+(System.currentTimeMillis()-startTime)+" ms");
        assertEquals(HelloEntity.AGE, evt.get().getSensor());
        assertEquals(h, evt.get().getSource());
        assertEquals(5, evt.get().getValue());
        assertTrue(System.currentTimeMillis() - startTime < 5000);  //shouldn't have blocked for all 5s
    }
    
    //REVIEW 1459 - new test
    @Test
    public void testEffectorEmitsTransientSensor() throws Exception {
        HelloEntity h = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        app.start(ImmutableList.of(loc));
        
        final AtomicReference<SensorEvent<?>> evt = new AtomicReference<SensorEvent<?>>();
        app.subscriptions().subscribe(h, HelloEntity.ITS_MY_BIRTHDAY, new SensorEventListener<Object>() {
            @Override public void onEvent(SensorEvent<Object> event) {
                evt.set(event);
                synchronized (evt) {
                    evt.notifyAll();
                }
            }});
        
        long startTime = System.currentTimeMillis();
        synchronized (evt) {
            h.setAge(5);
            evt.wait(5000);
        }
        assertNotNull(evt.get());
        assertEquals(HelloEntity.ITS_MY_BIRTHDAY, evt.get().getSensor());
        assertEquals(h, evt.get().getSource());
        assertNull(evt.get().getValue());
        assertTrue(System.currentTimeMillis() - startTime < 5000);  //shouldn't have blocked for all 5s
    }

    @Test
    public void testSendMultipleInOrderThenUnsubscribe() throws Exception {
        HelloEntity h = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        app.start(ImmutableList.of(loc));

        final List<Integer> data = Lists.newArrayList();
        final CountDownLatch latch = new CountDownLatch(5);
        
        app.subscriptions().subscribe(h, HelloEntity.AGE, new SensorEventListener<Integer>() {
            @Override public void onEvent(SensorEvent<Integer> event) {
                data.add(event.getValue());
                Time.sleep((int)(20*Math.random()));
                log.info("Thread "+Thread.currentThread()+" notify on subscription received for "+event.getValue()+", data is "+data);
                latch.countDown();
            }});
        
        Stopwatch stopwatch = Stopwatch.createStarted();
        for (int i = 1; i <= 5; i++) {
            h.setAge(i);
        }
        assertTrue(latch.await(5000, TimeUnit.MILLISECONDS));

        app.subscriptions().unsubscribeAll();
        h.setAge(6);
        long totalTime = stopwatch.elapsed(TimeUnit.MILLISECONDS);
        
        Asserts.continually(
            Suppliers.ofInstance(data), Predicates.<Object>equalTo(ImmutableList.of(1,2,3,4,5)),
            Duration.millis(50), null, null);
        assertTrue(totalTime < 2000, "totalTime="+totalTime);  //shouldn't have blocked for anywhere close to 2s (unless build machine v v slow eg BuildHive)
    }

    @Test
    public void testConfigSetFromAttribute() {
        app.config().set(HelloEntity.MY_NAME, "Bob");
        
        HelloEntity dad = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        HelloEntity son = dad.addChild(EntitySpec.create(HelloEntity.class));
        
        //config is inherited
        assertEquals("Bob", app.getConfig(HelloEntity.MY_NAME));
        assertEquals("Bob", dad.getConfig(HelloEntity.MY_NAME));
        assertEquals("Bob", son.getConfig(HelloEntity.MY_NAME));
        
        //attributes are not
        app.sensors().set(HelloEntity.FAVOURITE_NAME, "Carl");
        assertEquals("Carl", app.getAttribute(HelloEntity.FAVOURITE_NAME));
        assertEquals(null, dad.getAttribute(HelloEntity.FAVOURITE_NAME));
    }
    @Test
    public void testConfigSetFromAttributeWhenReady() throws Exception {
        app.config().set(HelloEntity.MY_NAME, "Bob");
        
        final HelloEntity dad = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        final HelloEntity son = dad.addChild(EntitySpec.create(HelloEntity.class)
                .configure(HelloEntity.MY_NAME, attributeWhenReady(dad, HelloEntity.FAVOURITE_NAME
                        /* third param is closure; defaults to groovy truth (see google), but could be e.g.
                           , { it!=null && it.length()>0 && it!="Jebediah" }
                         */ )));
        
        app.start(ImmutableList.of(loc));
         
        final Semaphore s1 = new Semaphore(0);
        final Object[] sonsConfig = new Object[1];
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                log.info("started");
                s1.release();
                log.info("getting config "+sonsConfig[0]);
                sonsConfig[0] = son.getConfig(HelloEntity.MY_NAME);
                log.info("got config {}", sonsConfig[0]);
                s1.release();
            }});
                
        log.info("starting");
        long startTime = System.currentTimeMillis();
        t.start();
        log.info("waiting {}", System.identityHashCode(sonsConfig));
        if (!s1.tryAcquire(2, TimeUnit.SECONDS)) fail("race mismatch, missing permits");
        
        //thread should be blocking on call to getConfig
        assertTrue(t.isAlive());
        assertTrue(System.currentTimeMillis() - startTime < 1500);
        synchronized (sonsConfig) {
            assertEquals(null, sonsConfig[0]);
            for (Task<?> tt : ((EntityInternal)dad).getExecutionContext().getTasks()) { log.info("task at dad:  {}, {}", tt, tt.getStatusDetail(false)); }
            for (Task<?> tt : ((EntityInternal)son).getExecutionContext().getTasks()) { log.info("task at son:  {}, {}", tt, tt.getStatusDetail(false)); }
            dad.sensors().set(HelloEntity.FAVOURITE_NAME, "Dan");
            if (!s1.tryAcquire(2, TimeUnit.SECONDS)) fail("race mismatch, missing permits");
        }
        log.info("dad: "+dad.getAttribute(HelloEntity.FAVOURITE_NAME));
        log.info("son: "+son.getConfig(HelloEntity.MY_NAME));
        
        //shouldn't have blocked for very long at all
        assertTrue(System.currentTimeMillis() - startTime < 1500);
        //and sons config should now pick up the dad's attribute
        assertEquals(sonsConfig[0], "Dan");
    }
    
    @Test
    public void testConfigSetFromAttributeWhenReadyTransformations() {
        app.config().set(HelloEntity.MY_NAME, "Bob");
        
        HelloEntity dad = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        HelloEntity son = dad.addChild(EntitySpec.create(HelloEntity.class)
                .configure(HelloEntity.MY_NAME, transform(attributeWhenReady(dad, HelloEntity.FAVOURITE_NAME), new Function<String,String>() {
                    @Override
                    public String apply(String input) {
                        return input+input.charAt(input.length()-1)+"y";
                    }})));
        
        app.start(ImmutableList.of(loc));
        dad.sensors().set(HelloEntity.FAVOURITE_NAME, "Dan");
        assertEquals(son.getConfig(HelloEntity.MY_NAME), "Danny");
    }
    
    @Test
    public void testConfigSetFromAttributeWhenReadyNullTransformations() {
        app.config().set(HelloEntity.MY_NAME, "Bob");
        
        HelloEntity dad = app.createAndManageChild(EntitySpec.create(HelloEntity.class));
        // the unnecessary (HelloEntity) cast is required as a work-around to an IntelliJ issue that prevents Brooklyn from launching from the IDE
        HelloEntity son = (HelloEntity) dad.addChild(EntitySpec.create(HelloEntity.class)
                .configure(HelloEntity.MY_NAME, transform(attributeWhenReady(dad, HelloEntity.FAVOURITE_NAME, (Predicate<String>)null), new Function<String,String>() {
                    @Override
                    public String apply(String input) {
                        return input+input.charAt(input.length()-1)+"y";
                    }})));
        
        app.start(ImmutableList.of(loc));
        dad.sensors().set(HelloEntity.FAVOURITE_NAME, "Dan");
        assertEquals(son.getConfig(HelloEntity.MY_NAME), "Danny");
    }

}
