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

import java.util.Map;
import java.util.concurrent.Callable;

import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.entity.ImplementedBy;
import org.apache.brooklyn.api.mgmt.Task;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.AbstractEntity;
import org.apache.brooklyn.core.entity.EntityAsserts;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
import org.apache.brooklyn.feed.function.FunctionFeed;
import org.apache.brooklyn.feed.function.FunctionPollConfig;
import org.apache.brooklyn.util.core.task.DynamicTasks;
import org.apache.brooklyn.util.core.task.Tasks;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import com.google.common.base.Functions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

public class PollerTest extends BrooklynAppUnitTestSupport {

    @DataProvider(name = "specProvider")
    public Object[][] specProvider() {
        EntitySpec<FeedExceptionEntity> pollFailer = EntitySpec.create(FeedExceptionEntity.class)
                .configure(FeedExceptionEntity.POLLER, new PollFailer());
        EntitySpec<FeedExceptionEntity> taskFailer = EntitySpec.create(FeedExceptionEntity.class)
                .configure(FeedExceptionEntity.POLLER, new TaskFailer());
        return new Object[][]{{pollFailer}, {taskFailer}};
    }

    // TODO BROOKLYN-272, Disabled, because fails non-deterministically in jenkins (windows):
    //    testFeedContinuesWhenPollerThrows(org.apache.brooklyn.core.feed.PollerTest)  Time elapsed: 3.556 sec  <<< FAILURE!
    //    org.apache.brooklyn.util.exceptions.PropagatedRuntimeException: failed succeeds-eventually, 4 attempts, 105ms elapsed: AssertionError: entity=FeedExceptionEntityImpl{id=dYG0SKbP}; attribute=Sensor: flag (java.lang.Boolean) expected [false] but found [true]
    //        at org.apache.brooklyn.test.Asserts.fail(Asserts.java:721)
    //        at org.apache.brooklyn.test.Asserts.failNotEquals(Asserts.java:114)
    //        at org.apache.brooklyn.test.Asserts.assertEquals(Asserts.java:436)
    //        at org.apache.brooklyn.core.entity.EntityAsserts.assertAttributeEquals(EntityAsserts.java:54)
    //        at org.apache.brooklyn.core.entity.EntityAsserts$1.run(EntityAsserts.java:70)
    //        at org.apache.brooklyn.test.Asserts$RunnableAdapter.call(Asserts.java:1277)
    //        at org.apache.brooklyn.test.Asserts.succeedsEventually(Asserts.java:930)
    //        at org.apache.brooklyn.test.Asserts.succeedsEventually(Asserts.java:854)
    //        at org.apache.brooklyn.core.entity.EntityAsserts.assertAttributeEqualsEventually(EntityAsserts.java:67)
    //        at org.apache.brooklyn.core.feed.PollerTest.testFeedContinuesWhenPollerThrows(PollerTest.java:65)
    @Test(dataProvider = "specProvider", groups={"Broken"})
    public void testFeedContinuesWhenPollerThrows(EntitySpec<FeedExceptionEntity> spec) {
        Map<?, ?> timeoutFlags = ImmutableMap.of("timeout", "100ms");
        FeedExceptionEntity fee = app.createAndManageChild(spec);
        app.start(ImmutableList.of(app.newSimulatedLocation()));
        EntityAsserts.assertAttributeEqualsEventually(timeoutFlags, fee, FeedExceptionEntity.FLAG, true);

        fee.startThrowingPollExceptions();
        EntityAsserts.assertAttributeEqualsEventually(timeoutFlags, fee, FeedExceptionEntity.FLAG, false);
        EntityAsserts.assertAttributeEqualsContinually(timeoutFlags, fee, FeedExceptionEntity.FLAG, false);

        fee.stopThrowingPollExceptions();
        EntityAsserts.assertAttributeEqualsEventually(timeoutFlags, fee, FeedExceptionEntity.FLAG, true);
        EntityAsserts.assertAttributeEqualsContinually(timeoutFlags, fee, FeedExceptionEntity.FLAG, true);
    }

    @ImplementedBy(FeedExceptionEntityImpl.class)
    public static interface FeedExceptionEntity extends Entity {
        ConfigKey<ThrowingPoller> POLLER = ConfigKeys.newConfigKey(ThrowingPoller.class, "poller");
        AttributeSensor<Boolean> FLAG = Sensors.newBooleanSensor("flag");

        void startThrowingPollExceptions();
        void stopThrowingPollExceptions();
    }

    public static class FeedExceptionEntityImpl extends AbstractEntity implements FeedExceptionEntity {
        private ThrowingPoller poller;

        @Override
        public void init() {
            super.init();
            poller = config().get(POLLER);
            FunctionFeed.builder()
                    .entity(this)
                    .period(1L)
                    .poll(new FunctionPollConfig<Boolean, Boolean>(FLAG)
                            .callable(poller)
                            .onException(Functions.constant(false)))
                    .build();
        }

        @Override
        public void startThrowingPollExceptions() {
            this.poller.setShouldThrow(true);
        }

        @Override
        public void stopThrowingPollExceptions() {
            this.poller.setShouldThrow(false);
        }
    }

    private static class TaskFailer extends ThrowingPoller {
        @Override
        public Boolean execute(final boolean shouldThrow) {
            Task<Boolean> t = Tasks.<Boolean>builder()
                    .body(new Callable<Boolean>() {
                        @Override
                        public Boolean call() {
                            if (shouldThrow) {
                                throw new IllegalArgumentException("exception in feed task");
                            }
                            return true;
                        }
                    })
                    .build();
            return DynamicTasks.queueIfPossible(t).orSubmitAsync().asTask().getUnchecked();
        }
    }

    private static class PollFailer extends ThrowingPoller {
        @Override
        public Boolean execute(final boolean shouldThrow) {
            if (shouldThrow) {
                throw new IllegalArgumentException("exception in poller");
            }
            return true;
        }
    }

    private static abstract class ThrowingPoller implements Callable<Boolean> {
        protected final Object throwLock = new Object[0];
        boolean shouldThrow = false;

        abstract Boolean execute(boolean shouldThrow);

        @Override
        public Boolean call() throws Exception {
            synchronized (throwLock) {
                return execute(shouldThrow);
            }
        }

        public void setShouldThrow(boolean shouldThrow) {
            synchronized (throwLock) {
                this.shouldThrow = shouldThrow;
            }
        }
    }

}
