/*
 * Copyright (C) 2015 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.truth.Truth.assertThat;
import static com.google.common.util.concurrent.Futures.getDone;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static com.google.common.util.concurrent.ReflectionFreeAssertThrows.assertThrows;
import static com.google.common.util.concurrent.Runnables.doNothing;
import static com.google.common.util.concurrent.TestPlatform.getDoneFromTimeoutOverload;
import static com.google.common.util.concurrent.TestPlatform.verifyGetOnPendingFuture;
import static com.google.common.util.concurrent.TestPlatform.verifyTimedGetOnPendingFuture;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;

import com.google.common.annotations.GwtCompatible;
import com.google.common.annotations.GwtIncompatible;
import com.google.common.annotations.J2ktIncompatible;
import com.google.common.util.concurrent.AbstractFutureTest.TimedWaiterThread;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import junit.framework.TestCase;
import org.jspecify.annotations.NullUnmarked;
import org.jspecify.annotations.Nullable;

/**
 * Base class for tests for emulated {@link AbstractFuture} that allow subclasses to swap in a
 * different "source Future" for {@link AbstractFuture#setFuture} calls.
 */
@GwtCompatible
@NullUnmarked
abstract class AbstractAbstractFutureTest extends TestCase {
  private TestedFuture<Integer> future;
  private AbstractFuture<Integer> delegate;

  abstract AbstractFuture<Integer> newDelegate();

  @Override
  protected void setUp() {
    future = TestedFuture.create();
    delegate = newDelegate();
  }

  public void testPending() {
    assertPending(future);
  }

  public void testSuccessful() throws Exception {
    assertThat(future.set(1)).isTrue();
    assertSuccessful(future, 1);
  }

  public void testFailed() throws Exception {
    Exception cause = new Exception();
    assertThat(future.setException(cause)).isTrue();
    assertFailed(future, cause);
  }

  public void testCanceled() throws Exception {
    assertThat(future.cancel(false /* mayInterruptIfRunning */)).isTrue();
    assertCancelled(future, false);
  }

  public void testInterrupted() throws Exception {
    assertThat(future.cancel(true /* mayInterruptIfRunning */)).isTrue();
    assertCancelled(future, true);
  }

  public void testSetFuturePending() throws Exception {
    assertThat(future.setFuture(delegate)).isTrue();
    assertSetAsynchronously(future);
  }

  public void testSetFutureThenCancel() throws Exception {
    assertThat(future.setFuture(delegate)).isTrue();
    assertThat(future.cancel(false /* mayInterruptIfRunning */)).isTrue();
    assertCancelled(future, false);
    assertCancelled(delegate, false);
  }

  public void testSetFutureThenInterrupt() throws Exception {
    assertThat(future.setFuture(delegate)).isTrue();
    assertThat(future.cancel(true /* mayInterruptIfRunning */)).isTrue();
    assertCancelled(future, true);
    assertCancelled(delegate, true);
  }

  public void testSetFutureDelegateAlreadySuccessful() throws Exception {
    delegate.set(5);
    assertThat(future.setFuture(delegate)).isTrue();
    assertSuccessful(future, 5);
  }

  public void testSetFutureDelegateLaterSuccessful() throws Exception {
    assertThat(future.setFuture(delegate)).isTrue();
    delegate.set(6);
    assertSuccessful(future, 6);
  }

  public void testSetFutureDelegateAlreadyCancelled() throws Exception {
    delegate.cancel(/* mayInterruptIfRunning= */ false);
    assertThat(future.setFuture(delegate)).isTrue();
    assertCancelled(future, false);
  }

  public void testSetFutureDelegateLaterCancelled() throws Exception {
    assertThat(future.setFuture(delegate)).isTrue();
    delegate.cancel(/* mayInterruptIfRunning= */ false);
    assertCancelled(future, false);
  }

  public void testSetFutureDelegateAlreadyInterrupted() throws Exception {
    delegate.cancel(/* mayInterruptIfRunning= */ true);
    assertThat(future.setFuture(delegate)).isTrue();
    assertCancelled(future, /* expectWasInterrupted= */ false);
  }

  public void testSetFutureDelegateLaterInterrupted() throws Exception {
    assertThat(future.setFuture(delegate)).isTrue();
    delegate.cancel(/* mayInterruptIfRunning= */ true);
    assertCancelled(future, /* expectWasInterrupted= */ false);
  }

  public void testListenLaterSuccessful() {
    CountingRunnable listener = new CountingRunnable();

    future.addListener(listener, directExecutor());
    listener.assertNotRun();

    future.set(1);
    listener.assertRun();
  }

  public void testListenLaterFailed() {
    CountingRunnable listener = new CountingRunnable();

    future.addListener(listener, directExecutor());
    listener.assertNotRun();

    future.setException(new Exception());
    listener.assertRun();
  }

  public void testListenLaterCancelled() {
    CountingRunnable listener = new CountingRunnable();

    future.addListener(listener, directExecutor());
    listener.assertNotRun();

    future.cancel(false);
    listener.assertRun();
  }

  public void testListenLaterInterrupted() {
    CountingRunnable listener = new CountingRunnable();

    future.addListener(listener, directExecutor());
    listener.assertNotRun();

    future.cancel(true);
    listener.assertRun();
  }

  public void testListenLaterSetAsynchronously() {
    CountingRunnable listener = new CountingRunnable();

    future.addListener(listener, directExecutor());
    listener.assertNotRun();

    future.setFuture(delegate);
    listener.assertNotRun();
  }

  public void testListenLaterSetAsynchronouslyLaterDelegateSuccessful() {
    CountingRunnable before = new CountingRunnable();
    CountingRunnable inBetween = new CountingRunnable();
    CountingRunnable after = new CountingRunnable();

    future.addListener(before, directExecutor());
    future.setFuture(delegate);
    future.addListener(inBetween, directExecutor());
    delegate.set(1);
    future.addListener(after, directExecutor());

    before.assertRun();
    inBetween.assertRun();
    after.assertRun();
  }

  public void testListenLaterSetAsynchronouslyLaterDelegateFailed() {
    CountingRunnable before = new CountingRunnable();
    CountingRunnable inBetween = new CountingRunnable();
    CountingRunnable after = new CountingRunnable();

    future.addListener(before, directExecutor());
    future.setFuture(delegate);
    future.addListener(inBetween, directExecutor());
    delegate.setException(new Exception());
    future.addListener(after, directExecutor());

    before.assertRun();
    inBetween.assertRun();
    after.assertRun();
  }

  public void testListenLaterSetAsynchronouslyLaterDelegateCancelled() {
    CountingRunnable before = new CountingRunnable();
    CountingRunnable inBetween = new CountingRunnable();
    CountingRunnable after = new CountingRunnable();

    future.addListener(before, directExecutor());
    future.setFuture(delegate);
    future.addListener(inBetween, directExecutor());
    delegate.cancel(false);
    future.addListener(after, directExecutor());

    before.assertRun();
    inBetween.assertRun();
    after.assertRun();
  }

  public void testListenLaterSetAsynchronouslyLaterDelegateInterrupted() {
    CountingRunnable before = new CountingRunnable();
    CountingRunnable inBetween = new CountingRunnable();
    CountingRunnable after = new CountingRunnable();

    future.addListener(before, directExecutor());
    future.setFuture(delegate);
    future.addListener(inBetween, directExecutor());
    delegate.cancel(true);
    future.addListener(after, directExecutor());

    before.assertRun();
    inBetween.assertRun();
    after.assertRun();
  }

  public void testListenLaterSetAsynchronouslyLaterSelfCancelled() {
    CountingRunnable before = new CountingRunnable();
    CountingRunnable inBetween = new CountingRunnable();
    CountingRunnable after = new CountingRunnable();

    future.addListener(before, directExecutor());
    future.setFuture(delegate);
    future.addListener(inBetween, directExecutor());
    future.cancel(false);
    future.addListener(after, directExecutor());

    before.assertRun();
    inBetween.assertRun();
    after.assertRun();
  }

  public void testListenLaterSetAsynchronouslyLaterSelfInterrupted() {
    CountingRunnable before = new CountingRunnable();
    CountingRunnable inBetween = new CountingRunnable();
    CountingRunnable after = new CountingRunnable();

    future.addListener(before, directExecutor());
    future.setFuture(delegate);
    future.addListener(inBetween, directExecutor());
    future.cancel(true);
    future.addListener(after, directExecutor());

    before.assertRun();
    inBetween.assertRun();
    after.assertRun();
  }

  public void testMisbehavingListenerAlreadyDone() {
    class BadRunnableException extends RuntimeException {}

    Runnable bad =
        new Runnable() {
          @Override
          public void run() {
            throw new BadRunnableException();
          }
        };

    future.set(1);
    future.addListener(bad, directExecutor()); // BadRunnableException must not propagate.
  }

  public void testMisbehavingListenerLaterDone() {
    class BadRunnableException extends RuntimeException {}

    CountingRunnable before = new CountingRunnable();
    Runnable bad =
        new Runnable() {
          @Override
          public void run() {
            throw new BadRunnableException();
          }
        };
    CountingRunnable after = new CountingRunnable();

    future.addListener(before, directExecutor());
    future.addListener(bad, directExecutor());
    future.addListener(after, directExecutor());

    future.set(1); // BadRunnableException must not propagate.

    before.assertRun();
    after.assertRun();
  }

  public void testNullListener() {
    assertThrows(NullPointerException.class, () -> future.addListener(null, directExecutor()));
  }

  public void testNullExecutor() {
    assertThrows(NullPointerException.class, () -> future.addListener(doNothing(), null));
  }

  public void testNullTimeUnit() throws Exception {
    future.set(1);
    assertThrows(NullPointerException.class, () -> future.get(0, null));
  }

  public void testNegativeTimeout() throws Exception {
    future.set(1);
    assertEquals(1, future.get(-1, SECONDS).intValue());
  }

  @J2ktIncompatible
  @GwtIncompatible // threads
  public void testOverflowTimeout() throws Exception {
    // First, sanity check that naive multiplication would really overflow to a negative number:
    long nanosPerSecond = NANOSECONDS.convert(1, SECONDS);
    assertThat(nanosPerSecond * Long.MAX_VALUE).isLessThan(0L);

    // Check that we wait long enough anyway (presumably as long as MAX_VALUE nanos):
    TimedWaiterThread waiter = new TimedWaiterThread(future, Long.MAX_VALUE, SECONDS);
    waiter.start();
    waiter.awaitWaiting();

    future.set(1);
    waiter.join();
  }

  @J2ktIncompatible // TODO(b/324550390): Enable
  public void testSetNull() throws Exception {
    future.set(null);
    assertSuccessful(future, null);
  }

  public void testSetExceptionNull() throws Exception {
    assertThrows(NullPointerException.class, () -> future.setException(null));

    assertThat(future.isDone()).isFalse();
    assertThat(future.set(1)).isTrue();
    assertSuccessful(future, 1);
  }

  public void testSetFutureNull() throws Exception {
    assertThrows(NullPointerException.class, () -> future.setFuture(null));

    assertThat(future.isDone()).isFalse();
    assertThat(future.set(1)).isTrue();
    assertSuccessful(future, 1);
  }

  /** Concrete subclass for testing. */
  private static class TestedFuture<V> extends AbstractFuture<V> {
    private static <V> TestedFuture<V> create() {
      return new TestedFuture<V>();
    }
  }

  private static final class CountingRunnable implements Runnable {
    int count;

    @Override
    public void run() {
      count++;
    }

    void assertNotRun() {
      assertEquals(0, count);
    }

    void assertRun() {
      assertEquals(1, count);
    }
  }

  private static void assertSetAsynchronously(AbstractFuture<Integer> future) {
    assertCannotSet(future);
    assertPending(future);
  }

  private static void assertPending(AbstractFuture<Integer> future) {
    assertThat(future.isDone()).isFalse();
    assertThat(future.isCancelled()).isFalse();

    CountingRunnable listener = new CountingRunnable();
    future.addListener(listener, directExecutor());
    listener.assertNotRun();

    verifyGetOnPendingFuture(future);
    verifyTimedGetOnPendingFuture(future);
  }

  private static void assertSuccessful(
      AbstractFuture<Integer> future, @Nullable Integer expectedResult)
      throws InterruptedException, TimeoutException, ExecutionException {
    assertDone(future);
    assertThat(future.isCancelled()).isFalse();

    assertThat(getDone(future)).isEqualTo(expectedResult);
    assertThat(getDoneFromTimeoutOverload(future)).isEqualTo(expectedResult);
  }

  private static void assertFailed(AbstractFuture<Integer> future, Throwable expectedException)
      throws InterruptedException, TimeoutException {
    assertDone(future);
    assertThat(future.isCancelled()).isFalse();

    try {
      getDone(future);
      fail();
    } catch (ExecutionException e) {
      assertThat(e).hasCauseThat().isSameInstanceAs(expectedException);
    }

    try {
      getDoneFromTimeoutOverload(future);
      fail();
    } catch (ExecutionException e) {
      assertThat(e).hasCauseThat().isSameInstanceAs(expectedException);
    }
  }

  private static void assertCancelled(AbstractFuture<Integer> future, boolean expectWasInterrupted)
      throws InterruptedException, TimeoutException, ExecutionException {
    assertDone(future);
    assertThat(future.isCancelled()).isTrue();
    assertThat(future.wasInterrupted()).isEqualTo(expectWasInterrupted);

    try {
      getDone(future);
      fail();
    } catch (CancellationException expected) {
    }

    try {
      getDoneFromTimeoutOverload(future);
      fail();
    } catch (CancellationException expected) {
    }
  }

  private static void assertDone(AbstractFuture<Integer> future) {
    CountingRunnable listener = new CountingRunnable();
    future.addListener(listener, directExecutor());
    listener.assertRun();

    assertThat(future.isDone()).isTrue();
    assertCannotSet(future);
    assertCannotCancel(future);
  }

  private static void assertCannotSet(AbstractFuture<Integer> future) {
    assertThat(future.set(99)).isFalse();
    assertThat(future.setException(new IndexOutOfBoundsException())).isFalse();
    assertThat(future.setFuture(new AbstractFuture<Integer>() {})).isFalse();
    assertThat(future.setFuture(immediateFuture(99))).isFalse();
  }

  private static void assertCannotCancel(AbstractFuture<Integer> future) {
    assertThat(future.cancel(true)).isFalse();
    assertThat(future.cancel(false)).isFalse();
  }
}
