// Copyright 2019 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.node.admin.nodeagent;

import org.junit.Test;

import java.time.Clock;
import java.time.Duration;
import java.util.Optional;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

/**
 * @author freva
 */
public class NodeAgentContextManagerTest {

    private final Clock clock = Clock.systemUTC();
    private final NodeAgentContext initialContext = generateContext();
    private final NodeAgentContextManager manager = new NodeAgentContextManager(clock, initialContext);

    @Test(timeout = 1000)
    public void returns_immediately_if_next_context_is_ready() throws InterruptedException {
        NodeAgentContext context1 = generateContext();
        manager.scheduleTickWith(context1);

        assertSame(initialContext, manager.currentContext());
        assertSame(context1, manager.nextContext());
        assertSame(context1, manager.currentContext());
    }

    @Test(timeout = 1000)
    public void blocks_in_nextContext_until_one_is_scheduled() throws InterruptedException {
        AsyncExecutor<NodeAgentContext> async = new AsyncExecutor<>(manager::nextContext);
        assertFalse(async.response.isPresent());
        Thread.sleep(10);
        assertFalse(async.response.isPresent());

        NodeAgentContext context1 = generateContext();
        manager.scheduleTickWith(context1);

        async.awaitResult();
        assertEquals(Optional.of(context1), async.response);
        assertFalse(async.exception.isPresent());
    }

    @Test(timeout = 1000)
    public void blocks_in_nextContext_until_interrupt() throws InterruptedException {
        AsyncExecutor<NodeAgentContext> async = new AsyncExecutor<>(manager::nextContext);
        assertFalse(async.response.isPresent());
        Thread.sleep(10);
        assertFalse(async.response.isPresent());

        manager.interrupt();

        async.awaitResult();
        assertEquals(Optional.of(InterruptedException.class), async.exception.map(Exception::getClass));
        assertFalse(async.response.isPresent());
    }

    @Test(timeout = 1000)
    public void setFrozen_does_not_block_with_no_timeout() throws InterruptedException {
        assertFalse(manager.setFrozen(false, Duration.ZERO));

        // Generate new context and get it from the supplier, this completes the unfreeze
        NodeAgentContext context1 = generateContext();
        manager.scheduleTickWith(context1);
        assertSame(context1, manager.nextContext());

        assertTrue(manager.setFrozen(false, Duration.ZERO));
    }

    @Test(timeout = 1000)
    public void setFrozen_blocks_at_least_for_duration_of_timeout() {
        long wantedDurationMillis = 100;
        long start = clock.millis();
        assertFalse(manager.setFrozen(false, Duration.ofMillis(wantedDurationMillis)));
        long actualDurationMillis = clock.millis() - start;

        assertTrue(actualDurationMillis >= wantedDurationMillis);
    }

    @Test(timeout = 1000)
    public void setFrozen_is_successful_if_converged_in_time() throws InterruptedException {
        AsyncExecutor<Boolean> async = new AsyncExecutor<>(() -> manager.setFrozen(false, Duration.ofMillis(500)));

        assertFalse(async.response.isPresent());

        NodeAgentContext context1 = generateContext();
        manager.scheduleTickWith(context1);
        assertSame(context1, manager.nextContext());

        async.awaitResult();
        assertEquals(Optional.of(true), async.response);
        assertFalse(async.exception.isPresent());
    }

    private static NodeAgentContext generateContext() {
        return new NodeAgentContextImpl.Builder("container-123.domain.tld").build();
    }

    private class AsyncExecutor<T> {
        private final Object monitor = new Object();
        private final Thread thread;
        private volatile Optional<T> response = Optional.empty();
        private volatile Optional<Exception> exception = Optional.empty();
        private boolean completed = false;

        private AsyncExecutor(ThrowingSupplier<T> supplier) {
            this.thread = new Thread(() -> {
                try {
                    response = Optional.of(supplier.get());
                } catch (Exception e) {
                    exception = Optional.of(e);
                }
                synchronized (monitor) {
                    completed = true;
                    monitor.notifyAll();
                }
            });
            this.thread.start();
        }

        private void awaitResult() {
            synchronized (monitor) {
                while (!completed) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException ignored) { }
                }
            }
        }
    }

    private interface ThrowingSupplier<T> {
        T get() throws Exception;
    }
}
@@ -514,16 +514,64 @@
 Test %7B%0A%0A
+    private static final int TIMEOUT = 10_000;%0A%0A
     priv
@@ -609,24 +609,24 @@
 ystemUTC();%0A
-
     private 
@@ -801,36 +801,39 @@
 @Test(timeout = 
-1000
+TIMEOUT
 )%0A    public voi
@@ -1201,36 +1201,39 @@
 @Test(timeout = 
-1000
+TIMEOUT
 )%0A    public voi
@@ -1786,36 +1786,39 @@
 @Test(timeout = 
-1000
+TIMEOUT
 )%0A    public voi
@@ -2337,36 +2337,39 @@
 @Test(timeout = 
-1000
+TIMEOUT
 )%0A    public voi
@@ -2829,36 +2829,39 @@
 @Test(timeout = 
-1000
+TIMEOUT
 )%0A    public voi
@@ -3240,12 +3240,15 @@
 t = 
-1000
+TIMEOUT
 )%0A  

Increase timeout to 10s