/*
 * Copyright (C) 2011 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.joinUninterruptibly;
import static com.google.common.util.concurrent.Uninterruptibles.putUninterruptibly;
import static com.google.common.util.concurrent.Uninterruptibles.takeUninterruptibly;
import static com.google.common.util.concurrent.Uninterruptibles.tryAcquireUninterruptibly;
import static java.util.concurrent.TimeUnit.MILLISECONDS;

import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.testing.NullPointerTester;
import com.google.common.testing.TearDown;
import com.google.common.testing.TearDownStack;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;
import junit.framework.TestCase;

/**
 * Tests for {@link Uninterruptibles}.
 *
 * @author Anthony Zana
 */

public class UninterruptiblesTest extends TestCase {
  private static final String EXPECTED_TAKE = "expectedTake";

  /**
   * Timeout to use when we don't expect the timeout to expire.
   */
  private static final long LONG_DELAY_MS = 2500;
  private static final long SLEEP_SLACK = 2;

  private final TearDownStack tearDownStack = new TearDownStack();

  // NOTE: All durations in these tests are expressed in milliseconds
  @Override
  protected void setUp() {
    // Clear any previous interrupt before running the test.
    if (Thread.currentThread().isInterrupted()) {
      throw new AssertionError("Thread interrupted on test entry. "
          + "Some test probably didn't clear the interrupt state");
    }

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

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

  public void testNull() throws Exception {
    new NullPointerTester()
        .setDefault(CountDownLatch.class, new CountDownLatch(0))
        .setDefault(Semaphore.class, new Semaphore(999))
        .testAllPublicStaticMethods(Uninterruptibles.class);
  }

  // IncrementableCountDownLatch.await() tests

  // CountDownLatch.await() tests

  // BlockingQueue.put() tests
  public void testPutWithNoWait() {
    Stopwatch stopwatch = Stopwatch.createStarted();
    BlockingQueue<String> queue = new ArrayBlockingQueue<String>(999);
    putUninterruptibly(queue, "");
    assertTimeNotPassed(stopwatch, LONG_DELAY_MS);
    assertEquals("", queue.peek());
  }

  public void testPutNoInterrupt() {
    TimedPutQueue queue = TimedPutQueue.createWithDelay(20);
    queue.putSuccessfully();
    assertNotInterrupted();
  }

  public void testPutSingleInterrupt() {
    TimedPutQueue queue = TimedPutQueue.createWithDelay(50);
    requestInterruptIn(10);
    queue.putSuccessfully();
    assertInterrupted();
  }

  public void testPutMultiInterrupt() {
    TimedPutQueue queue = TimedPutQueue.createWithDelay(100);
    repeatedlyInterruptTestThread(20, tearDownStack);
    queue.putSuccessfully();
    assertInterrupted();
  }

  // BlockingQueue.take() tests
  public void testTakeWithNoWait() {
    Stopwatch stopwatch = Stopwatch.createStarted();
    BlockingQueue<String> queue = new ArrayBlockingQueue<String>(1);
    assertTrue(queue.offer(""));
    assertEquals("", takeUninterruptibly(queue));
    assertTimeNotPassed(stopwatch, LONG_DELAY_MS);
  }

  public void testTakeNoInterrupt() {
    TimedTakeQueue queue = TimedTakeQueue.createWithDelay(20);
    queue.takeSuccessfully();
    assertNotInterrupted();
  }

  public void testTakeSingleInterrupt() {
    TimedTakeQueue queue = TimedTakeQueue.createWithDelay(50);
    requestInterruptIn(10);
    queue.takeSuccessfully();
    assertInterrupted();
  }

  public void testTakeMultiInterrupt() {
    TimedTakeQueue queue = TimedTakeQueue.createWithDelay(100);
    repeatedlyInterruptTestThread(20, tearDownStack);
    queue.takeSuccessfully();
    assertInterrupted();
  }

  // join() tests
  public void testJoinWithNoWait() throws InterruptedException {
    Stopwatch stopwatch = Stopwatch.createStarted();
    Thread thread = new Thread(new JoinTarget(15));
    thread.start();
    thread.join();
    assertFalse(thread.isAlive());

    joinUninterruptibly(thread);
    joinUninterruptibly(thread, 0, MILLISECONDS);
    joinUninterruptibly(thread, -42, MILLISECONDS);
    joinUninterruptibly(thread, LONG_DELAY_MS, MILLISECONDS);
    assertTimeNotPassed(stopwatch, LONG_DELAY_MS);
  }

  public void testJoinNoInterrupt() {
    TimedThread thread = TimedThread.createWithDelay(20);
    thread.joinSuccessfully();
    assertNotInterrupted();
  }

  public void testJoinTimeoutNoInterruptNotExpired() {
    TimedThread thread = TimedThread.createWithDelay(20);
    thread.joinSuccessfully(LONG_DELAY_MS);
    assertNotInterrupted();
  }

  public void testJoinTimeoutNoInterruptExpired() {
    TimedThread thread = TimedThread.createWithDelay(LONG_DELAY_MS);
    thread.joinUnsuccessfully(30);
    assertNotInterrupted();
  }

  public void testJoinSingleInterrupt() {
    TimedThread thread = TimedThread.createWithDelay(50);
    requestInterruptIn(10);
    thread.joinSuccessfully();
    assertInterrupted();
  }

  public void testJoinTimeoutSingleInterruptNoExpire() {
    TimedThread thread = TimedThread.createWithDelay(50);
    requestInterruptIn(10);
    thread.joinSuccessfully(LONG_DELAY_MS);
    assertInterrupted();
  }

  public void testJoinTimeoutSingleInterruptExpired() {
    TimedThread thread = TimedThread.createWithDelay(LONG_DELAY_MS);
    requestInterruptIn(10);
    thread.joinUnsuccessfully(50);
    assertInterrupted();
  }

  public void testJoinMultiInterrupt() {
    TimedThread thread = TimedThread.createWithDelay(100);
    repeatedlyInterruptTestThread(20, tearDownStack);
    thread.joinSuccessfully();
    assertInterrupted();
  }

  public void testJoinTimeoutMultiInterruptNoExpire() {
    TimedThread thread = TimedThread.createWithDelay(100);
    repeatedlyInterruptTestThread(20, tearDownStack);
    thread.joinSuccessfully(LONG_DELAY_MS);
    assertInterrupted();
  }

  public void testJoinTimeoutMultiInterruptExpired() {
    /*
     * We don't "need" to schedule a thread completion at all here, but by doing
     * so, we come the closest we can to testing that the wait time is
     * appropriately decreased on each progressive join() call.
     */
    TimedThread thread = TimedThread.createWithDelay(LONG_DELAY_MS);
    repeatedlyInterruptTestThread(20, tearDownStack);
    thread.joinUnsuccessfully(70);
    assertInterrupted();
  }

  // sleep() Tests
  public void testSleepNoInterrupt() {
    sleepSuccessfully(10);
  }

  public void testSleepSingleInterrupt() {
    requestInterruptIn(10);
    sleepSuccessfully(50);
    assertInterrupted();
  }

  public void testSleepMultiInterrupt() {
    repeatedlyInterruptTestThread(10, tearDownStack);
    sleepSuccessfully(100);
    assertInterrupted();
  }

  // Semaphore.tryAcquire() tests
  public void testTryAcquireWithNoWait() {
    Stopwatch stopwatch = Stopwatch.createStarted();
    Semaphore semaphore = new Semaphore(99);
    assertTrue(tryAcquireUninterruptibly(semaphore, 0, MILLISECONDS));
    assertTrue(tryAcquireUninterruptibly(semaphore, -42, MILLISECONDS));
    assertTrue(tryAcquireUninterruptibly(semaphore, LONG_DELAY_MS, MILLISECONDS));
    assertTimeNotPassed(stopwatch, LONG_DELAY_MS);
  }

  public void testTryAcquireTimeoutNoInterruptNotExpired() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(20);
    semaphore.tryAcquireSuccessfully(LONG_DELAY_MS);
    assertNotInterrupted();
  }

  public void testTryAcquireTimeoutNoInterruptExpired() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(LONG_DELAY_MS);
    semaphore.tryAcquireUnsuccessfully(30);
    assertNotInterrupted();
  }

  public void testTryAcquireTimeoutSingleInterruptNoExpire() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(50);
    requestInterruptIn(10);
    semaphore.tryAcquireSuccessfully(LONG_DELAY_MS);
    assertInterrupted();
  }

  public void testTryAcquireTimeoutSingleInterruptExpired() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(LONG_DELAY_MS);
    requestInterruptIn(10);
    semaphore.tryAcquireUnsuccessfully(50);
    assertInterrupted();
  }

  public void testTryAcquireTimeoutMultiInterruptNoExpire() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(100);
    repeatedlyInterruptTestThread(20, tearDownStack);
    semaphore.tryAcquireSuccessfully(LONG_DELAY_MS);
    assertInterrupted();
  }

  public void testTryAcquireTimeoutMultiInterruptExpired() {
    /*
     * We don't "need" to schedule a release() call at all here, but by doing
     * so, we come the closest we can to testing that the wait time is
     * appropriately decreased on each progressive tryAcquire() call.
     */
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(LONG_DELAY_MS);
    repeatedlyInterruptTestThread(20, tearDownStack);
    semaphore.tryAcquireUnsuccessfully(70);
    assertInterrupted();
  }

  public void testTryAcquireWithNoWaitMultiPermit() {
    Stopwatch stopwatch = Stopwatch.createStarted();
    Semaphore semaphore = new Semaphore(99);
    assertTrue(tryAcquireUninterruptibly(semaphore, 10, 0, MILLISECONDS));
    assertTrue(tryAcquireUninterruptibly(semaphore, 10, -42, MILLISECONDS));
    assertTrue(tryAcquireUninterruptibly(semaphore, 10, LONG_DELAY_MS, MILLISECONDS));
    assertTimeNotPassed(stopwatch, LONG_DELAY_MS);
  }

  public void testTryAcquireTimeoutNoInterruptNotExpiredMultiPermit() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(20);
    semaphore.tryAcquireSuccessfully(10, LONG_DELAY_MS);
    assertNotInterrupted();
  }

  public void testTryAcquireTimeoutNoInterruptExpiredMultiPermit() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(LONG_DELAY_MS);
    semaphore.tryAcquireUnsuccessfully(10, 30);
    assertNotInterrupted();
  }

  public void testTryAcquireTimeoutSingleInterruptNoExpireMultiPermit() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(50);
    requestInterruptIn(10);
    semaphore.tryAcquireSuccessfully(10, LONG_DELAY_MS);
    assertInterrupted();
  }

  public void testTryAcquireTimeoutSingleInterruptExpiredMultiPermit() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(LONG_DELAY_MS);
    requestInterruptIn(10);
    semaphore.tryAcquireUnsuccessfully(10, 50);
    assertInterrupted();
  }

  public void testTryAcquireTimeoutMultiInterruptNoExpireMultiPermit() {
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(100);
    repeatedlyInterruptTestThread(20, tearDownStack);
    semaphore.tryAcquireSuccessfully(10, LONG_DELAY_MS);
    assertInterrupted();
  }

  public void testTryAcquireTimeoutMultiInterruptExpiredMultiPermit() {
    /*
     * We don't "need" to schedule a release() call at all here, but by doing
     * so, we come the closest we can to testing that the wait time is
     * appropriately decreased on each progressive tryAcquire() call.
     */
    TimedSemaphore semaphore = TimedSemaphore.createWithDelay(LONG_DELAY_MS);
    repeatedlyInterruptTestThread(20, tearDownStack);
    semaphore.tryAcquireUnsuccessfully(10, 70);
    assertInterrupted();
  }

  /**
   * Wrapper around {@link Stopwatch} which also contains an
   * "expected completion time." Creating a {@code Completion} starts the
   * underlying stopwatch.
   */
  private static final class Completion {
    final Stopwatch stopwatch;
    final long expectedCompletionWaitMillis;

    Completion(long expectedCompletionWaitMillis) {
      this.expectedCompletionWaitMillis = expectedCompletionWaitMillis;
      stopwatch = Stopwatch.createStarted();
    }

    /**
     * Asserts that the expected completion time has passed (and not "too much"
     * time beyond that).
     */
    void assertCompletionExpected() {
      assertAtLeastTimePassed(stopwatch, expectedCompletionWaitMillis);
      assertTimeNotPassed(stopwatch,
          expectedCompletionWaitMillis + LONG_DELAY_MS);
    }

    /**
     * Asserts that at least {@code timeout} has passed but the expected
     * completion time has not.
     */
    void assertCompletionNotExpected(long timeout) {
      Preconditions.checkArgument(timeout < expectedCompletionWaitMillis);
      assertAtLeastTimePassed(stopwatch, timeout);
      assertTimeNotPassed(stopwatch, expectedCompletionWaitMillis);
    }

    private static void assertAtLeastTimePassed(
        Stopwatch stopwatch, long expectedMillis) {
      long elapsedMillis = stopwatch.elapsed(MILLISECONDS);
      /*
       * The "+ 5" below is to permit, say, sleep(10) to sleep only 9 milliseconds. We see such
       * behavior sometimes when running these tests publicly as part of Guava. "+ 5" is probably
       * more generous than it needs to be.
       */
      assertTrue("Expected elapsed millis to be >= " + expectedMillis
          + " but was " + elapsedMillis, elapsedMillis + 5 >= expectedMillis);
    }
  }

  // TODO(cpovirk): Split this into separate CountDownLatch and IncrementableCountDownLatch classes.

  /**
   * Manages a {@link BlockingQueue} and associated timings for a {@code put}
   * call.
   */
  private static final class TimedPutQueue {
    final BlockingQueue<String> queue;
    final Completion completed;

    /**
     * Creates a {@link EnableWrites} which open up a spot for a {@code put} to
     * succeed in {@code countdownInMillis}.
     */
    static TimedPutQueue createWithDelay(long countdownInMillis) {
      return new TimedPutQueue(countdownInMillis);
    }

    private TimedPutQueue(long countdownInMillis) {
      this.queue = new ArrayBlockingQueue<String>(1);
      assertTrue(queue.offer("blocksPutCallsUntilRemoved"));
      this.completed = new Completion(countdownInMillis);
      scheduleEnableWrites(this.queue, countdownInMillis);
    }

    /**
     * Perform a {@code put} and assert that operation completed in the expected
     * timeframe.
     */
    void putSuccessfully() {
      putUninterruptibly(queue, "");
      completed.assertCompletionExpected();
      assertEquals("", queue.peek());
    }

    private static void scheduleEnableWrites(
        BlockingQueue<String> queue, long countdownInMillis) {
      Runnable toRun = new EnableWrites(queue, countdownInMillis);
      // TODO(cpovirk): automatically fail the test if this thread throws
      Thread enablerThread = new Thread(toRun);
      enablerThread.start();
    }
  }

  /**
   * Manages a {@link BlockingQueue} and associated timings for a {@code take}
   * call.
   */
  private static final class TimedTakeQueue {
    final BlockingQueue<String> queue;
    final Completion completed;

    /**
     * Creates a {@link EnableReads} which insert an element for a {@code take}
     * to receive in {@code countdownInMillis}.
     */
    static TimedTakeQueue createWithDelay(long countdownInMillis) {
      return new TimedTakeQueue(countdownInMillis);
    }

    private TimedTakeQueue(long countdownInMillis) {
      this.queue = new ArrayBlockingQueue<String>(1);
      this.completed = new Completion(countdownInMillis);
      scheduleEnableReads(this.queue, countdownInMillis);
    }

    /**
     * Perform a {@code take} and assert that operation completed in the
     * expected timeframe.
     */
    void takeSuccessfully() {
      assertEquals(EXPECTED_TAKE, takeUninterruptibly(queue));
      completed.assertCompletionExpected();
      assertTrue(queue.isEmpty());
    }

    private static void scheduleEnableReads(
        BlockingQueue<String> queue, long countdownInMillis) {
      Runnable toRun = new EnableReads(queue, countdownInMillis);
      // TODO(cpovirk): automatically fail the test if this thread throws
      Thread enablerThread = new Thread(toRun);
      enablerThread.start();
    }
  }

  /**
   * Manages a {@link Semaphore} and associated timings.
   */
  private static final class TimedSemaphore {
    final Semaphore semaphore;
    final Completion completed;

    /**
     * Create a {@link Release} which will release a semaphore permit in
     * {@code countdownInMillis}.
     */
    static TimedSemaphore createWithDelay(long countdownInMillis) {
      return new TimedSemaphore(countdownInMillis);
    }

    private TimedSemaphore(long countdownInMillis) {
      this.semaphore = new Semaphore(0);
      this.completed = new Completion(countdownInMillis);
      scheduleRelease(countdownInMillis);
    }

    /**
     * Requests a permit from the semaphore with a timeout and asserts that operation completed in
     * the expected timeframe.
     */
    void tryAcquireSuccessfully(long timeoutMillis) {
      assertTrue(tryAcquireUninterruptibly(semaphore, timeoutMillis, MILLISECONDS));
      completed.assertCompletionExpected();
    }

    /**
     * Requests a permit from the semaphore with a timeout and asserts that the wait returned
     * within the expected timeout.
     */
    private void tryAcquireUnsuccessfully(long timeoutMillis) {
      assertFalse(tryAcquireUninterruptibly(semaphore, timeoutMillis, MILLISECONDS));
      completed.assertCompletionNotExpected(timeoutMillis);
    }

    void tryAcquireSuccessfully(int permits, long timeoutMillis) {
      assertTrue(tryAcquireUninterruptibly(semaphore, permits, timeoutMillis, MILLISECONDS));
      completed.assertCompletionExpected();
    }

    private void tryAcquireUnsuccessfully(int permits, long timeoutMillis) {
      assertFalse(tryAcquireUninterruptibly(semaphore, permits, timeoutMillis, MILLISECONDS));
      completed.assertCompletionNotExpected(timeoutMillis);
    }

    private void scheduleRelease(long countdownInMillis) {
      DelayedActionRunnable toRun = new Release(semaphore, countdownInMillis);
      // TODO(cpovirk): automatically fail the test if this thread throws
      Thread releaserThread = new Thread(toRun);
      releaserThread.start();
    }
  }

  private abstract static class DelayedActionRunnable implements Runnable {
    private final long tMinus;

    protected DelayedActionRunnable(long tMinus) {
      this.tMinus = tMinus;
    }

    @Override
    public final void run() {
      try {
        Thread.sleep(tMinus);
      } catch (InterruptedException e) {
        throw new AssertionError(e);
      }
      doAction();
    }

    protected abstract void doAction();
  }

  private static class CountDown extends DelayedActionRunnable {
    private final CountDownLatch latch;

    public CountDown(CountDownLatch latch, long tMinus) {
      super(tMinus);
      this.latch = latch;
    }

    @Override
    protected void doAction() {
      latch.countDown();
    }
  }

  private static class EnableWrites extends DelayedActionRunnable {
    private final BlockingQueue<String> queue;

    public EnableWrites(BlockingQueue<String> queue, long tMinus) {
      super(tMinus);
      assertFalse(queue.isEmpty());
      assertFalse(queue.offer("shouldBeRejected"));
      this.queue = queue;
    }

    @Override
    protected void doAction() {
      assertNotNull(queue.remove());
    }
  }

  private static class EnableReads extends DelayedActionRunnable {
    private final BlockingQueue<String> queue;

    public EnableReads(BlockingQueue<String> queue, long tMinus) {
      super(tMinus);
      assertTrue(queue.isEmpty());
      this.queue = queue;
    }

    @Override
    protected void doAction() {
      assertTrue(queue.offer(EXPECTED_TAKE));
    }
  }

  private static final class TimedThread {
    private final Thread thread;
    private final Completion completed;

    static TimedThread createWithDelay(long countdownInMillis) {
      return new TimedThread(countdownInMillis);
    }

    private TimedThread(long expectedCompletionWaitMillis) {
      completed = new Completion(expectedCompletionWaitMillis);
      thread = new Thread(new JoinTarget(expectedCompletionWaitMillis));
      thread.start();
    }

    void joinSuccessfully() {
      Uninterruptibles.joinUninterruptibly(thread);
      completed.assertCompletionExpected();
      assertEquals(Thread.State.TERMINATED, thread.getState());
    }

    void joinSuccessfully(long timeoutMillis) {
      Uninterruptibles.joinUninterruptibly(thread, timeoutMillis, MILLISECONDS);
      completed.assertCompletionExpected();
      assertEquals(Thread.State.TERMINATED, thread.getState());
    }

    void joinUnsuccessfully(long timeoutMillis) {
      Uninterruptibles.joinUninterruptibly(thread, timeoutMillis, MILLISECONDS);
      completed.assertCompletionNotExpected(timeoutMillis);
      assertFalse(Thread.State.TERMINATED.equals(thread.getState()));
    }
  }

  private static class JoinTarget extends DelayedActionRunnable {
    public JoinTarget(long tMinus) {
      super(tMinus);
    }

    @Override
    protected void doAction() {
    }
  }

  private static class Release extends DelayedActionRunnable {
    private final Semaphore semaphore;

    public Release(Semaphore semaphore, long tMinus) {
      super(tMinus);
      this.semaphore = semaphore;
    }

    @Override
    protected void doAction() {
      semaphore.release(10);
    }
  }

  private static void sleepSuccessfully(long sleepMillis) {
    Completion completed = new Completion(sleepMillis - SLEEP_SLACK);
    Uninterruptibles.sleepUninterruptibly(sleepMillis, MILLISECONDS);
    completed.assertCompletionExpected();
  }

  private static void assertTimeNotPassed(Stopwatch stopwatch,
      long timelimitMillis) {
    long elapsedMillis = stopwatch.elapsed(MILLISECONDS);
    assertTrue(elapsedMillis < timelimitMillis);
  }

  /**
   * Await an interrupt, then clear the interrupt status. Similar to
   * {@code assertTrue(Thread.interrupted())} except that this version tolerates
   * late interrupts.
   */
  private static void assertInterrupted() {
    try {
      /*
       * The sleep() will end immediately if we've already been interrupted or
       * wait patiently for the interrupt if not.
       */
      Thread.sleep(LONG_DELAY_MS);
      fail("Dude, where's my interrupt?");
    } catch (InterruptedException expected) {
    }
  }

  private static void assertNotInterrupted() {
    assertFalse(Thread.interrupted());
  }

  private static void requestInterruptIn(long millis) {
    InterruptionUtil.requestInterruptIn(millis, MILLISECONDS);
  }
}
