/*
 * Copyright 2016 The gRPC 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 io.grpc;

import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import static io.grpc.testing.DeadlineSubject.deadline;
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 static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import com.google.common.testing.EqualsTester;
import com.google.common.truth.Truth;
import java.util.Arrays;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.mockito.ArgumentCaptor;

/**
 * Tests for {@link Context}.
 */
@RunWith(Parameterized.class)
public class DeadlineTest {
  /** Ticker epochs to vary testing. */
  @Parameters
  public static Iterable<Object[]> data() {
    return Arrays.asList(new Object[][] {
      // MAX_VALUE / 2 is important because the signs are generally the same for past and future
      // deadlines.
      {Long.MAX_VALUE / 2}, {0}, {Long.MAX_VALUE}, {Long.MIN_VALUE}
    });
  }

  private FakeTicker ticker = new FakeTicker();

  public DeadlineTest(long epoch) {
    ticker.reset(epoch);
  }

  @Test
  public void defaultTickerIsSystemTicker() {
    Deadline d = Deadline.after(0, TimeUnit.SECONDS);
    ticker.reset(System.nanoTime());
    Deadline reference = Deadline.after(0, TimeUnit.SECONDS, ticker);
    // Allow inaccuracy to account for system time advancing during test.
    assertAbout(deadline()).that(d).isWithin(1, TimeUnit.SECONDS).of(reference);
  }

  @Test
  public void minimum() {
    Deadline d1 = Deadline.after(1, TimeUnit.MINUTES, ticker);
    Deadline d2 = Deadline.after(2, TimeUnit.MINUTES, ticker);
    Deadline d3 = Deadline.after(3, TimeUnit.MINUTES, ticker);

    assertThat(d1.minimum(d2)).isSameInstanceAs(d1);
    assertThat(d2.minimum(d1)).isSameInstanceAs(d1);
    assertThat(d3.minimum(d2)).isSameInstanceAs(d2);
    assertThat(d2.minimum(d3)).isSameInstanceAs(d2);
  }

  @Test
  public void timeCanOverflow() {
    ticker.reset(Long.MAX_VALUE);
    Deadline d = Deadline.after(10, TimeUnit.DAYS, ticker);
    assertEquals(10, d.timeRemaining(TimeUnit.DAYS));
    assertTrue(Deadline.after(0, TimeUnit.DAYS, ticker).isBefore(d));
    assertFalse(d.isExpired());

    ticker.increment(10, TimeUnit.DAYS);
    assertTrue(d.isExpired());
  }

  @Test
  public void timeCanUnderflow() {
    ticker.reset(Long.MIN_VALUE);
    Deadline d = Deadline.after(-10, TimeUnit.DAYS, ticker);
    assertEquals(-10, d.timeRemaining(TimeUnit.DAYS));
    assertTrue(d.isBefore(Deadline.after(0, TimeUnit.DAYS, ticker)));
    assertTrue(d.isExpired());
  }

  @Test
  public void deadlineClamps() {
    Deadline d = Deadline.after(-300 * 365, TimeUnit.DAYS, ticker);
    Deadline d2 = Deadline.after(300 * 365, TimeUnit.DAYS, ticker);
    assertTrue(d.isBefore(d2));

    Deadline d3 = Deadline.after(-200 * 365, TimeUnit.DAYS, ticker);
    // d and d3 are equal
    assertFalse(d.isBefore(d3));
    assertFalse(d3.isBefore(d));
  }

  @Test
  public void immediateDeadlineIsExpired() {
    Deadline deadline = Deadline.after(0, TimeUnit.SECONDS, ticker);
    assertTrue(deadline.isExpired());
  }

  @Test
  public void shortDeadlineEventuallyExpires() throws Exception {
    Deadline d = Deadline.after(100, TimeUnit.MILLISECONDS, ticker);
    assertTrue(d.timeRemaining(TimeUnit.NANOSECONDS) > 0);
    assertFalse(d.isExpired());
    ticker.increment(101, TimeUnit.MILLISECONDS);

    assertTrue(d.isExpired());
    assertEquals(-1, d.timeRemaining(TimeUnit.MILLISECONDS));
  }

  @Test
  public void deadlineMatchesLongValue() {
    assertEquals(10, Deadline.after(10, TimeUnit.MINUTES, ticker).timeRemaining(TimeUnit.MINUTES));
  }

  @Test
  public void pastDeadlineIsExpired() {
    Deadline d = Deadline.after(-1, TimeUnit.SECONDS, ticker);
    assertTrue(d.isExpired());
    assertEquals(-1000, d.timeRemaining(TimeUnit.MILLISECONDS));
  }

  @Test
  public void deadlineDoesNotOverflowOrUnderflow() {
    Deadline after = Deadline.after(Long.MAX_VALUE, TimeUnit.NANOSECONDS, ticker);
    assertFalse(after.isExpired());

    Deadline before = Deadline.after(-Long.MAX_VALUE, TimeUnit.NANOSECONDS, ticker);
    assertTrue(before.isExpired());

    assertTrue(before.isBefore(after));
  }

  @Test
  public void beforeExpiredDeadlineIsExpired() {
    Deadline base = Deadline.after(0, TimeUnit.SECONDS, ticker);
    assertTrue(base.isExpired());
    assertTrue(base.offset(-1, TimeUnit.SECONDS).isExpired());
  }

  @Test
  public void beforeNotExpiredDeadlineMayBeExpired() {
    Deadline base = Deadline.after(10, TimeUnit.SECONDS, ticker);
    assertFalse(base.isExpired());
    assertFalse(base.offset(-1, TimeUnit.SECONDS).isExpired());
    assertTrue(base.offset(-11, TimeUnit.SECONDS).isExpired());
  }

  @Test
  public void afterExpiredDeadlineMayBeExpired() {
    Deadline base = Deadline.after(-10, TimeUnit.SECONDS, ticker);
    assertTrue(base.isExpired());
    assertTrue(base.offset(1, TimeUnit.SECONDS).isExpired());
    assertFalse(base.offset(11, TimeUnit.SECONDS).isExpired());
  }

  @Test
  public void zeroOffsetIsSameDeadline() {
    Deadline base = Deadline.after(0, TimeUnit.SECONDS, ticker);
    assertSame(base, base.offset(0, TimeUnit.SECONDS));
  }

  @Test
  public void runOnEventualExpirationIsExecuted() throws Exception {
    Deadline base = Deadline.after(50, TimeUnit.MICROSECONDS, ticker);
    ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
    final AtomicBoolean executed = new AtomicBoolean();
    Future<?> unused = base.runOnExpiration(
        new Runnable() {
          @Override
          public void run() {
            executed.set(true);
          }
        }, mockScheduler);
    assertFalse(executed.get());
    ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
    verify(mockScheduler).schedule(runnableCaptor.capture(), eq(50000L), eq(TimeUnit.NANOSECONDS));
    runnableCaptor.getValue().run();
    assertTrue(executed.get());
  }

  @Test
  public void runOnAlreadyExpiredIsExecutedOnExecutor() throws Exception {
    Deadline base = Deadline.after(0, TimeUnit.MICROSECONDS, ticker);
    ScheduledExecutorService mockScheduler = mock(ScheduledExecutorService.class);
    final AtomicBoolean executed = new AtomicBoolean();
    Future<?> unused = base.runOnExpiration(
        new Runnable() {
          @Override
          public void run() {
            executed.set(true);
          }
        }, mockScheduler);
    assertFalse(executed.get());
    ArgumentCaptor<Runnable> runnableCaptor = ArgumentCaptor.forClass(Runnable.class);
    verify(mockScheduler).schedule(runnableCaptor.capture(), eq(0L), eq(TimeUnit.NANOSECONDS));
    runnableCaptor.getValue().run();
    assertTrue(executed.get());
  }

  @Test
  public void toString_systemTickerNotShown() {
    Deadline d = Deadline.after(0, TimeUnit.MILLISECONDS);
    assertThat(d.toString()).endsWith("s from now");
  }

  @Test
  public void toString_exact() {
    Deadline d = Deadline.after(0, TimeUnit.MILLISECONDS, ticker);
    assertEquals("0s from now (ticker=FAKE_TICKER)", d.toString());
  }

  @Test
  public void toString_after() {
    Deadline d;

    d = Deadline.after(-1, TimeUnit.MINUTES, ticker);
    assertEquals("-60s from now (ticker=FAKE_TICKER)", d.toString());
    d = Deadline.after(-1, TimeUnit.MILLISECONDS, ticker);
    assertEquals("-0.001000000s from now (ticker=FAKE_TICKER)", d.toString());
    d = Deadline.after(-500, TimeUnit.MILLISECONDS, ticker);
    assertEquals("-0.500000000s from now (ticker=FAKE_TICKER)", d.toString());
    d = Deadline.after(-1000, TimeUnit.MILLISECONDS, ticker);
    assertEquals("-1s from now (ticker=FAKE_TICKER)", d.toString());
    d = Deadline.after(-1500, TimeUnit.MILLISECONDS, ticker);
    assertEquals("-1.500000000s from now (ticker=FAKE_TICKER)", d.toString());
    d = Deadline.after(-1023456789, TimeUnit.NANOSECONDS, ticker);
    assertEquals("-1.023456789s from now (ticker=FAKE_TICKER)", d.toString());
  }

  @Test
  public void compareTo_greater() {
    Deadline d1 = Deadline.after(10, TimeUnit.SECONDS, ticker);
    ticker.increment(1, TimeUnit.NANOSECONDS);
    Deadline d2 = Deadline.after(10, TimeUnit.SECONDS, ticker);
    Truth.assertThat(d2).isGreaterThan(d1);
  }

  @Test
  public void compareTo_less() {
    Deadline d1 = Deadline.after(10, TimeUnit.SECONDS, ticker);
    ticker.increment(1, TimeUnit.NANOSECONDS);
    Deadline d2 = Deadline.after(10, TimeUnit.SECONDS, ticker);
    Truth.assertThat(d1).isLessThan(d2);
  }

  @Test
  public void compareTo_same() {
    Deadline d1 = Deadline.after(10, TimeUnit.SECONDS, ticker);
    Deadline d2 = Deadline.after(10, TimeUnit.SECONDS, ticker);
    Truth.assertThat(d1).isEquivalentAccordingToCompareTo(d2);
  }

  @Test
  public void tickersDontMatch() {
    Deadline d1 = Deadline.after(10, TimeUnit.SECONDS);
    Deadline d2 = Deadline.after(10, TimeUnit.SECONDS, ticker);
    boolean success = false;
    try {
      d1.compareTo(d2);
      success = true;
    } catch (AssertionError e) {
      // Expected
    }
    assertFalse(success);

    try {
      d1.minimum(d2);
      success = true;
    } catch (AssertionError e) {
      // Expected
    }
    assertFalse(success);

    try {
      d1.isBefore(d2);
      success = true;
    } catch (AssertionError e) {
      // Expected
    }
    assertFalse(success);
  }

  @Test
  public void toString_before() {
    Deadline d = Deadline.after(12, TimeUnit.MICROSECONDS, ticker);
    assertEquals("0.000012000s from now (ticker=FAKE_TICKER)", d.toString());
  }

  @Test
  public void equality() {
    final Deadline d1 = Deadline.after(12, TimeUnit.MICROSECONDS, ticker);
    final Deadline d2 = Deadline.after(12, TimeUnit.MICROSECONDS, ticker);
    final Deadline d3 = Deadline.after(12, TimeUnit.MICROSECONDS, new FakeTicker());
    final Deadline d4 = Deadline.after(10, TimeUnit.MICROSECONDS, ticker);

    new EqualsTester()
            .addEqualityGroup(d1, d2)
            .addEqualityGroup(d3)
            .addEqualityGroup(d4)
            .testEquals();
  }

  private static class FakeTicker extends Deadline.Ticker {
    private long time;

    @Override
    public long nanoTime() {
      return time;
    }

    public void reset(long time) {
      this.time = time;
    }

    public void increment(long period, TimeUnit unit) {
      if (period < 0) {
        throw new IllegalArgumentException();
      }
      this.time += unit.toNanos(period);
    }

    @Override
    public String toString() {
      return "FAKE_TICKER";
    }
  }
}
