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

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.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.brooklyn.util.repeat.Repeater;
import org.apache.brooklyn.util.time.Duration;
import org.testng.annotations.Test;

import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.Callables;

public class RepeaterTest {

    @Test
    public void sanityTest() {
        new Repeater("Sanity test")
            .until(Callables.returning(true))
            .every(Duration.millis(10));
    }

    @Test
    public void sanityTestDescription() {
        new Repeater()
            .until(Callables.returning(true))
            .every(Duration.millis(10));
    }

    @Test
    public void sanityTestBuilder() {
        Repeater.create("Sanity test")
            .until(Callables.returning(true))
            .every(Duration.millis(10));
    }

    @Test
    public void sanityTestBuilderDescription() {
        Repeater.create()
            .until(Callables.returning(true))
            .every(Duration.millis(10));
    }

    @Test
    public void repeatSucceedsIfClosureIsNonNull() {
        new Repeater("repeatSucceedsIfClosureIsNonNull").repeat(Callables.returning(true));
    }

    @Test(expectedExceptions = { NullPointerException.class, IllegalArgumentException.class })
    public void untilFailsIfClosureIsNull() {
        new Repeater("untilFailsIfClosureIsNull").until(null);
        fail("Expected exception was not thrown");
    }

    @Test
    public void untilSucceedsIfClosureIsNonNull() {
        new Repeater("untilSucceedsIfClosureIsNonNull")
            .until(Callables.returning(true));
    }

    @Test(expectedExceptions = { NullPointerException.class, IllegalArgumentException.class })
    public void everyFailsIfPeriodIsZero() {
        new Repeater("everyFailsIfPeriodIsZero").every(Duration.ZERO);
        fail("Expected exception was not thrown");
    }

    @Test(expectedExceptions = { NullPointerException.class, IllegalArgumentException.class })
    public void everyFailsIfPeriodIsNegative() {
        new Repeater("everyFailsIfPeriodIsNegative").every(Duration.millis(-1));
        fail("Expected exception was not thrown");
    }

    @Test(expectedExceptions = { NullPointerException.class, IllegalArgumentException.class })
    public void everyFailsIfUnitsIsNull() {
        new Repeater("everyFailsIfUnitsIsNull").every(10, null);
        fail("Expected exception was not thrown");
    }

    @Test
    public void everySucceedsIfPeriodIsPositiveAndUnitsIsNonNull() {
        new Repeater("repeatSucceedsIfClosureIsNonNull").every(Duration.millis(10));
    }

    @Test(expectedExceptions = { NullPointerException.class, IllegalArgumentException.class })
    public void limitTimeToFailsIfPeriodIsZero() {
        new Repeater("limitTimeToFailsIfPeriodIsZero").limitTimeTo(0, TimeUnit.MILLISECONDS);
        fail("Expected exception was not thrown");
    }

    @Test(expectedExceptions = { NullPointerException.class, IllegalArgumentException.class })
    public void limitTimeToFailsIfPeriodIsNegative() {
        new Repeater("limitTimeToFailsIfPeriodIsNegative").limitTimeTo(-1, TimeUnit.MILLISECONDS);
        fail("Expected exception was not thrown");
    }

    @Test(expectedExceptions = { NullPointerException.class, IllegalArgumentException.class })
    public void limitTimeToFailsIfUnitsIsNull() {
        new Repeater("limitTimeToFailsIfUnitsIsNull").limitTimeTo(10, null);
        fail("Expected exception was not thrown");
    }

    @Test
    public void limitTimeToSucceedsIfPeriodIsPositiveAndUnitsIsNonNull() {
        new Repeater("limitTimeToSucceedsIfClosureIsNonNull").limitTimeTo(10, TimeUnit.MILLISECONDS);
    }

    @Test
    public void everyAcceptsDuration() {
        new Repeater("everyAcceptsDuration").every(Duration.ONE_SECOND);
    }

    @Test
    public void everyAcceptsTimeUnit() {
        new Repeater("everyAcceptsTimeUnit").every(1000000L, TimeUnit.MICROSECONDS);
    }

    @Test
    public void runReturnsTrueIfExitConditionIsTrue() {
        assertTrue(new Repeater("runReturnsTrueIfExitConditionIsTrue")
            .every(Duration.millis(1))
            .until(Callables.returning(true))
            .run());
    }

    @Test
    public void runRespectsMaximumIterationLimitAndReturnsFalseIfReached() {
        final AtomicInteger iterations = new AtomicInteger();
        assertFalse(new Repeater("runRespectsMaximumIterationLimitAndReturnsFalseIfReached")
            .repeat(new Runnable() {
                @Override public void run() { iterations.incrementAndGet(); }
            })
            .every(Duration.millis(1))
            .until(Callables.returning(false))
            .limitIterationsTo(5)
            .run());
        assertEquals(iterations.get(), 5);
    }

    /**
     * Check that the {@link Repeater} will stop after a time limit.
     *
     * The repeater is configured to run every 100ms and never stop until the limit is reached.
     * This is given as {@link Repeater#limitTimeTo(org.apache.brooklyn.util.time.Duration)} and the execution time
     * is then checked to ensure it is between 100% and 400% of the specified value. Due to scheduling
     * delays and other factors in a non RTOS system it is expected that the repeater will take much
     * longer to exit occasionally.
     *
     * @see #runRespectsMaximumIterationLimitAndReturnsFalseIfReached()
     */
    @Test(groups="Integration")
    public void runRespectsTimeLimitAndReturnsFalseIfReached() {
        final long LIMIT = 2000l;
        Repeater repeater = new Repeater("runRespectsTimeLimitAndReturnsFalseIfReached")
            .every(Duration.millis(100))
            .until(Callables.returning(false))
            .limitTimeTo(LIMIT, TimeUnit.MILLISECONDS);

        Stopwatch stopwatch = Stopwatch.createStarted();
        boolean result = repeater.run();
        stopwatch.stop();

        assertFalse(result);

        long difference = stopwatch.elapsed(TimeUnit.MILLISECONDS);
        assertTrue(difference >= LIMIT, "Difference was: " + difference);
        assertTrue(difference < 4 * LIMIT, "Difference was: " + difference);
    }

    @Test(expectedExceptions = { NullPointerException.class })
    public void runFailsIfUntilWasNotSet() {
        new Repeater("runFailsIfUntilWasNotSet")
            .every(Duration.millis(10))
            .run();
        fail("Expected exception was not thrown");
    }

    @Test(expectedExceptions = { NullPointerException.class })
    public void runFailsIfEveryWasNotSet() {
        new Repeater("runFailsIfEveryWasNotSet")
            .until(Callables.returning(true))
            .run();
        fail("Expected exception was not thrown");
    }

    @Test(expectedExceptions = { UnsupportedOperationException.class })
    public void testRethrowsException() {
        new Repeater("throwRuntimeException")
            .every(Duration.millis(10))
            .until(callableThrowingUnsupportedFail())
            .rethrowException()
            .limitIterationsTo(2)
            .run();
        fail("Expected exception was not thrown");
    }

    @Test
    public void testNoRethrowsException() {
        try {
            boolean result = new Repeater("throwRuntimeException")
                .every(Duration.millis(10))
                .until(callableThrowingUnsupportedFail())
                .limitIterationsTo(2)
                .run();
            assertFalse(result);
        } catch (RuntimeException re) {
            fail("Exception should not have been thrown: " + re.getMessage());
        }
    }
    
    private static Callable<Boolean> callableThrowingUnsupportedFail() {
        return new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                throw new UnsupportedOperationException("fail");
            }
        };
    }
    
}
