/*
 * Copyright (C) 2009 The Guava Authors
 *
 * Licensed 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 com.google.common.util.concurrent;

import static com.google.common.util.concurrent.InterruptionUtil.repeatedlyInterruptTestThread;
import static com.google.common.util.concurrent.Uninterruptibles.getUninterruptibly;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertThrows;

import com.google.common.testing.TearDown;
import com.google.common.testing.TearDownStack;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeoutException;
import junit.framework.TestCase;
import org.jspecify.annotations.NullUnmarked;

// TODO(cpovirk): Should this be merged into UninterruptiblesTest?
/**
 * Unit test for {@link Uninterruptibles#getUninterruptibly}
 *
 * @author Kevin Bourrillion
 * @author Chris Povirk
 */
@NullUnmarked
public class UninterruptibleFutureTest extends TestCase {
  private SleepingRunnable sleeper;
  private Future<Boolean> delayedFuture;

  private final TearDownStack tearDownStack = new TearDownStack();

  @Override
  protected void setUp() {
    ExecutorService executor = newSingleThreadExecutor();
    tearDownStack.addTearDown(
        new TearDown() {
          @Override
          public void tearDown() {
            executor.shutdownNow();
          }
        });
    sleeper = new SleepingRunnable(1000);
    delayedFuture = executor.submit(sleeper, true);

    tearDownStack.addTearDown(
        new TearDown() {
          @Override
          public void tearDown() {
            Thread.interrupted();
          }
        });
  }

  @Override
  protected void tearDown() {
    tearDownStack.runTearDown();
  }

  /**
   * This first test doesn't test anything in Uninterruptibles, just demonstrates some normal
   * behavior of futures so that you can contrast the next test with it.
   */
  public void testRegularFutureInterrupted() throws ExecutionException {

    /*
     * Here's the order of events that we want.
     *
     * 1. The client thread begins to block on a get() call to a future.
     * 2. The client thread is interrupted sometime before the result would be
     *   available.
     * 3. We expect the client's get() to throw an InterruptedException.
     * 4. We expect the client thread's interrupt state to be false.
     * 5. The client thread again makes a blocking call to get().
     * 6. Now the result becomes available.
     * 7. We expect get() to return this result.
     * 8. We expect the test thread's interrupt state to be false.
     */
    InterruptionUtil.requestInterruptIn(200, MILLISECONDS);

    assertFalse(Thread.interrupted());
    try {
      delayedFuture.get(20000, MILLISECONDS);
      fail("expected to be interrupted");
    } catch (InterruptedException expected) {
    } catch (TimeoutException e) {
      throw new RuntimeException(e);
    }

    // we were interrupted, but it's been cleared now
    assertFalse(Thread.interrupted());

    assertFalse(sleeper.completed);
    try {
      assertTrue(delayedFuture.get());
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    assertTrue(sleeper.completed);
  }

  public void testMakeUninterruptible_timeoutPreservedThroughInterruption()
      throws ExecutionException {

    repeatedlyInterruptTestThread(100, tearDownStack);

    assertThrows(
        TimeoutException.class, () -> getUninterruptibly(delayedFuture, 500, MILLISECONDS));
    assertTrue(Thread.interrupted()); // clears the interrupt state, too

    assertFalse(sleeper.completed);
    assertTrue(getUninterruptibly(delayedFuture));

    assertTrue(Thread.interrupted()); // clears the interrupt state, too
    assertTrue(sleeper.completed);
  }

  private static class SleepingRunnable implements Runnable {
    final int millis;
    volatile boolean completed;

    SleepingRunnable(int millis) {
      this.millis = millis;
    }

    @Override
    public void run() {
      try {
        Thread.sleep(millis);
      } catch (InterruptedException wontHappen) {
        throw new AssertionError();
      }
      completed = true;
    }
  }

  public void testMakeUninterruptible_untimed_uninterrupted() throws Exception {
    runUntimedInterruptsTest(0);
  }

  public void testMakeUninterruptible_untimed_interrupted() throws Exception {
    runUntimedInterruptsTest(1);
  }

  public void testMakeUninterruptible_untimed_multiplyInterrupted() throws Exception {
    runUntimedInterruptsTest(38);
  }

  public void testMakeUninterruptible_timed_uninterrupted() throws Exception {
    runTimedInterruptsTest(0);
  }

  public void testMakeUninterruptible_timed_interrupted() throws Exception {
    runTimedInterruptsTest(1);
  }

  public void testMakeUninterruptible_timed_multiplyInterrupted() throws Exception {
    runTimedInterruptsTest(38);
  }

  private static void runUntimedInterruptsTest(int times)
      throws InterruptedException, ExecutionException, TimeoutException {
    SettableFuture<String> future = SettableFuture.create();
    FutureTask<Boolean> interruptReporter = untimedInterruptReporter(future, false);

    runNInterruptsTest(times, future, interruptReporter);
  }

  private static void runTimedInterruptsTest(int times)
      throws InterruptedException, ExecutionException, TimeoutException {
    SettableFuture<String> future = SettableFuture.create();
    FutureTask<Boolean> interruptReporter = timedInterruptReporter(future);

    runNInterruptsTest(times, future, interruptReporter);
  }

  private static void runNInterruptsTest(
      int times, SettableFuture<String> future, FutureTask<Boolean> interruptReporter)
      throws InterruptedException, ExecutionException, TimeoutException {
    Thread waitingThread = new Thread(interruptReporter);
    waitingThread.start();
    for (int i = 0; i < times; i++) {
      waitingThread.interrupt();
    }

    future.set(RESULT);

    assertEquals(times > 0, (boolean) interruptReporter.get(20, SECONDS));
  }

  /**
   * Confirms that the test code triggers {@link InterruptedException} in a standard {@link Future}.
   */
  public void testMakeUninterruptible_plainFutureSanityCheck() throws Exception {
    SettableFuture<String> future = SettableFuture.create();
    FutureTask<Boolean> wasInterrupted = untimedInterruptReporter(future, true);

    Thread waitingThread = new Thread(wasInterrupted);
    waitingThread.start();
    waitingThread.interrupt();
    ExecutionException expected =
        assertThrows(ExecutionException.class, () -> wasInterrupted.get());
    assertTrue(expected.getCause().toString(), expected.getCause() instanceof InterruptedException);
  }

  public void testMakeUninterruptible_timedGetZeroTimeoutAttempted()
      throws TimeoutException, ExecutionException {
    SettableFuture<String> future = SettableFuture.create();
    future.set(RESULT);
    /*
     * getUninterruptibly should call the timed get method once with a
     * wait of 0 seconds (and it should succeed, since the result is already
     * available).
     */
    assertEquals(RESULT, getUninterruptibly(future, 0, SECONDS));
  }

  public void testMakeUninterruptible_timedGetNegativeTimeoutAttempted()
      throws TimeoutException, ExecutionException {
    SettableFuture<String> future = SettableFuture.create();
    future.set(RESULT);
    /*
     * The getUninterruptibly should call the timed get method once with a
     * wait of -1 seconds (and it should succeed, since the result is already
     * available).
     */
    assertEquals(RESULT, getUninterruptibly(future, -1, SECONDS));
  }

  private static FutureTask<Boolean> untimedInterruptReporter(
      Future<?> future, boolean allowInterruption) {
    return new FutureTask<>(
        new Callable<Boolean>() {
          @Override
          public Boolean call() throws Exception {
            Object actual;
            if (allowInterruption) {
              actual = future.get();
            } else {
              actual = getUninterruptibly(future);
            }
            assertEquals(RESULT, actual);
            return Thread.interrupted();
          }
        });
  }

  private static FutureTask<Boolean> timedInterruptReporter(Future<?> future) {
    return new FutureTask<>(
        new Callable<Boolean>() {
          @Override
          public Boolean call() throws Exception {
            assertEquals(RESULT, getUninterruptibly(future, 10, MINUTES));
            return Thread.interrupted();
          }
        });
  }

  private static final String RESULT = "result";
}
