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

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertTrue;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.policy.PolicySpec;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.entity.lifecycle.Lifecycle;
import org.apache.brooklyn.core.entity.trait.FailingEntity;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.core.test.entity.TestEntity;
import org.apache.brooklyn.policy.ha.HASensors.FailureDescriptor;
import org.apache.brooklyn.test.Asserts;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

public class ServiceRestarterTest extends BrooklynAppUnitTestSupport {

    private static final int TIMEOUT_MS = 10*1000;

    private TestEntity e1;
    private ServiceRestarter policy;
    private SensorEventListener<Object> eventListener;
    private List<SensorEvent<?>> events;
    
    @Override
    @BeforeMethod(alwaysRun=true)
    public void setUp() throws Exception {
        super.setUp();
        e1 = app.createAndManageChild(EntitySpec.create(TestEntity.class));
        events = Lists.newCopyOnWriteArrayList();
        eventListener = new SensorEventListener<Object>() {
            @Override public void onEvent(SensorEvent<Object> event) {
                events.add(event);
            }
        };
    }
    
    @Test
    public void testRestartsOnFailure() throws Exception {
        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
        e1.policies().add(policy);
        
        e1.sensors().emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
        
        Asserts.succeedsEventually(new Runnable() {
            @Override public void run() {
                assertEquals(e1.getCallHistory(), ImmutableList.of("restart"));
            }});
    }
    
    @Test(groups="Integration") // Has a 1 second wait
    public void testDoesNotRestartsWhenHealthy() throws Exception {
        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
        e1.policies().add(policy);
        
        e1.sensors().emit(HASensors.ENTITY_RECOVERED, new FailureDescriptor(e1, "not a failure"));
        
        Asserts.succeedsContinually(new Runnable() {
            @Override public void run() {
                assertEquals(e1.getCallHistory(), ImmutableList.of());
            }});
    }
    
    @Test
    public void testEmitsFailureEventWhenRestarterFails() throws Exception {
        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
                .configure(FailingEntity.FAIL_ON_RESTART, true));
        app.subscriptions().subscribe(e2, ServiceRestarter.ENTITY_RESTART_FAILED, eventListener);

        policy = new ServiceRestarter(new ConfigBag().configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
        e2.policies().add(policy);

        e2.sensors().emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e2, "simulate failure"));
        
        Asserts.succeedsEventually(new Runnable() {
            @Override public void run() {
                assertEquals(Iterables.getOnlyElement(events).getSensor(), ServiceRestarter.ENTITY_RESTART_FAILED, "events="+events);
                assertEquals(Iterables.getOnlyElement(events).getSource(), e2, "events="+events);
                assertEquals(((FailureDescriptor)Iterables.getOnlyElement(events).getValue()).getComponent(), e2, "events="+events);
            }});
        
        assertEquals(e2.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
    }
    
    @Test
    public void testDoesNotSetOnFireOnFailure() throws Exception {
        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
                .configure(FailingEntity.SET_SERVICE_DOWN_ON_FAILURE, false)
                .configure(FailingEntity.FAIL_ON_RESTART, true));
        app.subscriptions().subscribe(e2, ServiceRestarter.ENTITY_RESTART_FAILED, eventListener);

        policy = new ServiceRestarter(new ConfigBag()
                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED)
                .configure(ServiceRestarter.SET_ON_FIRE_ON_FAILURE, false));
        e2.policies().add(policy);

        e2.sensors().emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e2, "simulate failure"));
        
        Asserts.succeedsContinually(new Runnable() {
            @Override public void run() {
                assertNotEquals(e2.getAttribute(Attributes.SERVICE_STATE_ACTUAL), Lifecycle.ON_FIRE);
            }});
    }
    
    // Previously RestarterPolicy called entity.restart inside the event-listener thread.
    // That caused all other events for that entity's subscriptions to be queued until that
    // entity's single event handler thread was free again.
    @Test
    public void testRestartDoesNotBlockOtherSubscriptions() throws Exception {
        final CountDownLatch inRestartLatch = new CountDownLatch(1);
        final CountDownLatch continueRestartLatch = new CountDownLatch(1);
        
        final FailingEntity e2 = app.createAndManageChild(EntitySpec.create(FailingEntity.class)
                .configure(FailingEntity.FAIL_ON_RESTART, true)
                .configure(FailingEntity.EXEC_ON_FAILURE, new Function<Object, Void>() {
                    @Override public Void apply(Object input) {
                        inRestartLatch.countDown();
                        try {
                            continueRestartLatch.await();
                        } catch (InterruptedException e) {
                            throw Exceptions.propagate(e);
                        }
                        return null;
                    }}));
        
        e2.policies().add(PolicySpec.create(ServiceRestarter.class)
                .configure(ServiceRestarter.FAILURE_SENSOR_TO_MONITOR, HASensors.ENTITY_FAILED));
        e2.subscriptions().subscribe(e2, TestEntity.SEQUENCE, eventListener);

        // Cause failure, and wait for entity.restart to be blocking
        e2.sensors().emit(HASensors.ENTITY_FAILED, new FailureDescriptor(e1, "simulate failure"));
        assertTrue(inRestartLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
        
        // Expect other notifications to continue to get through
        e2.sensors().set(TestEntity.SEQUENCE, 1);
        Asserts.succeedsEventually(new Runnable() {
            @Override public void run() {
                assertEquals(Iterables.getOnlyElement(events).getValue(), 1);
            }});

        // Allow restart to finish
        continueRestartLatch.countDown();
    }
}
