/*
 * Copyright 2016, gRPC Authors All rights reserved.
 *
 * 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 io.grpc.internal;

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

import com.google.common.base.Stopwatch;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Unit tests for {@link FakeClock}. */
@RunWith(JUnit4.class)
public class FakeClockTest {

  @Test
  public void testScheduledExecutorService_sameInstance() {
    FakeClock fakeClock = new FakeClock();
    ScheduledExecutorService scheduledExecutorService1 = fakeClock.getScheduledExecutorService();
    ScheduledExecutorService scheduledExecutorService2 = fakeClock.getScheduledExecutorService();
    assertTrue(scheduledExecutorService1 == scheduledExecutorService2);
  }

  @Test
  public void testScheduledExecutorService_isDone() {
    FakeClock fakeClock = new FakeClock();
    ScheduledFuture<?> future = fakeClock.getScheduledExecutorService()
        .schedule(newRunnable(), 100L, TimeUnit.NANOSECONDS);

    fakeClock.forwardNanos(99L);
    assertFalse(future.isDone());

    fakeClock.forwardNanos(2L);
    assertTrue(future.isDone());
  }

  @Test
  public void testScheduledExecutorService_cancel() {
    FakeClock fakeClock = new FakeClock();
    ScheduledFuture<?> future = fakeClock.getScheduledExecutorService()
        .schedule(newRunnable(), 100L, TimeUnit.NANOSECONDS);

    fakeClock.forwardNanos(99L);
    future.cancel(false);

    fakeClock.forwardNanos(2);
    assertTrue(future.isCancelled());
  }

  @Test
  public void testScheduledExecutorService_getDelay() {
    FakeClock fakeClock = new FakeClock();
    ScheduledFuture<?> future = fakeClock.getScheduledExecutorService()
        .schedule(newRunnable(), 100L, TimeUnit.NANOSECONDS);

    fakeClock.forwardNanos(90L);
    assertEquals(10L, future.getDelay(TimeUnit.NANOSECONDS));
  }

  @Test
  public void testScheduledExecutorService_result() {
    FakeClock fakeClock = new FakeClock();
    final boolean[] result = new boolean[]{false};
    ScheduledFuture<?> unused = fakeClock.getScheduledExecutorService().schedule(
        new Runnable() {
          @Override
          public void run() {
            result[0] = true;
          }
        },
        100L,
        TimeUnit.NANOSECONDS);

    fakeClock.forwardNanos(100L);
    assertTrue(result[0]);
  }

  @Test
  public void testStopWatch() {
    FakeClock fakeClock = new FakeClock();
    Stopwatch stopwatch = fakeClock.getStopwatchSupplier().get();
    long expectedElapsedNanos = 0L;

    stopwatch.start();

    fakeClock.forwardNanos(100L);
    expectedElapsedNanos += 100L;
    assertEquals(expectedElapsedNanos, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    fakeClock.forwardTime(10L, TimeUnit.MINUTES);
    expectedElapsedNanos += TimeUnit.MINUTES.toNanos(10L);
    assertEquals(expectedElapsedNanos, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.stop();

    fakeClock.forwardNanos(1000L);
    assertEquals(expectedElapsedNanos, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.reset();

    expectedElapsedNanos = 0L;
    assertEquals(expectedElapsedNanos, stopwatch.elapsed(TimeUnit.NANOSECONDS));
  }

  @Test
  @SuppressWarnings("FutureReturnValueIgnored")
  public void testPendingAndDueTasks() {
    FakeClock fakeClock = new FakeClock();
    ScheduledExecutorService scheduledExecutorService = fakeClock.getScheduledExecutorService();

    scheduledExecutorService.schedule(newRunnable(), 200L, TimeUnit.NANOSECONDS);
    scheduledExecutorService.execute(newRunnable());
    scheduledExecutorService.schedule(newRunnable(), 0L, TimeUnit.NANOSECONDS);
    scheduledExecutorService.schedule(newRunnable(), 80L, TimeUnit.NANOSECONDS);
    scheduledExecutorService.schedule(newRunnable(), 90L, TimeUnit.NANOSECONDS);
    scheduledExecutorService.schedule(newRunnable(), 100L, TimeUnit.NANOSECONDS);
    scheduledExecutorService.schedule(newRunnable(), 110L, TimeUnit.NANOSECONDS);
    scheduledExecutorService.schedule(newRunnable(), 120L, TimeUnit.NANOSECONDS);


    assertEquals(8, fakeClock.numPendingTasks());
    assertEquals(2, fakeClock.getDueTasks().size());

    fakeClock.runDueTasks();

    assertEquals(6, fakeClock.numPendingTasks());
    assertEquals(0, fakeClock.getDueTasks().size());

    fakeClock.forwardNanos(90L);

    assertEquals(4, fakeClock.numPendingTasks());
    assertEquals(0, fakeClock.getDueTasks().size());

    fakeClock.forwardNanos(20L);

    assertEquals(2, fakeClock.numPendingTasks());
    assertEquals(0, fakeClock.getDueTasks().size());
  }

  @Test
  public void testTaskFilter() {
    FakeClock fakeClock = new FakeClock();
    ScheduledExecutorService scheduledExecutorService = fakeClock.getScheduledExecutorService();
    final AtomicBoolean selectedDone = new AtomicBoolean();
    final AtomicBoolean ignoredDone = new AtomicBoolean();
    final Runnable selectedRunnable = new Runnable() {
      @Override
      public void run() {
        selectedDone.set(true);
      }
    };
    Runnable ignoredRunnable = new Runnable() {
      @Override
      public void run() {
        ignoredDone.set(true);
      }
    };
    FakeClock.TaskFilter filter = new FakeClock.TaskFilter() {
        @Override
        public boolean shouldAccept(Runnable runnable) {
          return runnable == selectedRunnable;
        }
      };
    scheduledExecutorService.execute(selectedRunnable);
    scheduledExecutorService.execute(ignoredRunnable);
    assertEquals(2, fakeClock.numPendingTasks());
    assertEquals(1, fakeClock.numPendingTasks(filter));
    assertEquals(2, fakeClock.getPendingTasks().size());
    assertEquals(1, fakeClock.getPendingTasks(filter).size());
    assertSame(selectedRunnable, fakeClock.getPendingTasks(filter).iterator().next().command);
    assertEquals(1, fakeClock.runDueTasks(filter));
    assertTrue(selectedDone.get());
    assertFalse(ignoredDone.get());
  }

  private Runnable newRunnable() {
    return new Runnable() {
      @Override
      public void run() {
      }
    };
  }
}
