package stroom.util.thread;

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

public class TestConditionalWait {

    private static final Logger LOGGER = LoggerFactory.getLogger(TestConditionalWait.class);

    @Test
    public void wait_zeroTimeout() {

        assertTimeTaken(Duration.ZERO, 200L, () -> {
            ConditionalWait.Outcome outcome = ConditionalWait.wait(() -> false, Duration.ZERO);
            Assertions.assertThat(outcome).isEqualTo(ConditionalWait.Outcome.TIMED_OUT);
        });
    }

    @Test
    public void wait_2secTimeout() {

        final Duration duration = Duration.of(2, ChronoUnit.SECONDS);
        assertTimeTaken(duration, 200L, () -> {
            ConditionalWait.Outcome outcome = ConditionalWait.wait(() -> false, duration);
            Assertions.assertThat(outcome).isEqualTo(ConditionalWait.Outcome.TIMED_OUT);
        });
    }

    @Test
    public void wait_conditionCompletesBeforeTimeout() {

        final Duration waitDuration = Duration.of(2, ChronoUnit.SECONDS);
        final Duration sleepDuration = Duration.of(100, ChronoUnit.MILLIS);

        assertTimeTaken(sleepDuration, 200L, () -> {

            final AtomicBoolean atomicBoolean = new AtomicBoolean(false);
            //spawn a thread that will wait a short time then change the value used for the wait condition
            CompletableFuture.runAsync(() -> {
                try {
                    Thread.sleep(sleepDuration.toMillis());
                } catch (InterruptedException e) {
                    throw new RuntimeException("Thread interrupted", e);
                }
                atomicBoolean.set(true);
            });

            //be sure it is false before waiting
            Assertions.assertThat(atomicBoolean).isFalse();

            ConditionalWait.Outcome outcome = ConditionalWait.wait(atomicBoolean::get, waitDuration);
            Assertions.assertThat(outcome).isEqualTo(ConditionalWait.Outcome.CONDITION_MET);
        });
    }


    private void assertTimeTaken(Duration expectedDuration, long toleranceMs, Runnable test) {
        Instant startTime = Instant.now();

        test.run();

        Duration actualDuration = Duration.between(startTime, Instant.now());

        LOGGER.info("Actual: {}, expected: {}", actualDuration, expectedDuration);

        long durationDiffMs = Math.abs(actualDuration.toMillis() - expectedDuration.toMillis());
//        long toleranceMs = (long) Math.max(expectedDuration.toMillis() * 0.2, 10);

        //check it is within 5%
        Assertions.assertThat(durationDiffMs).isLessThan(toleranceMs);
    }
}
@@ -549,17 +549,17 @@
 n.ZERO, 
-2
+3
 00L, () 
@@ -911,33 +911,33 @@
 Taken(duration, 
-2
+3
 00L, () -%3E %7B%0A   
@@ -1349,16 +1349,77 @@
 MILLIS);
+%0A        final long startTimeMs = System.currentTimeMillis();
 %0A%0A      
@@ -1451,17 +1451,17 @@
 ration, 
-2
+3
 00L, () 
@@ -1740,45 +1740,203 @@
     
-Thread.sleep(sleepDuration.toMillis()
+//removing the time taken to spin up this thread%0A                    long sleepMs = sleepDuration.toMillis() - (System.currentTimeMillis() - startTimeMs);%0A                    Thread.sleep(sleepMs
 );%0A 

Increase tolerances in TestConditionalWait