/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.ratis.util;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.slf4j.event.Level;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static org.apache.ratis.util.TimeDuration.Abbreviation;
import static org.apache.ratis.util.TimeDuration.parse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class TestTimeDuration {
  {
    Slf4jUtils.setLogLevel(TimeDuration.LOG, Level.DEBUG);
  }

  @Test
  @Timeout(value = 1)
  public void testAbbreviation() {
    Arrays.asList(TimeUnit.values())
        .forEach(a -> assertNotNull(Abbreviation.valueOf(a.name())));
    assertEquals(TimeUnit.values().length, Abbreviation.values().length);

    final List<String> allSymbols = Arrays.stream(Abbreviation.values())
        .map(Abbreviation::getSymbols)
        .flatMap(List::stream)
        .collect(Collectors.toList());
    Arrays.asList(TimeUnit.values()).forEach(unit ->
        allSymbols.stream()
            .map(s -> "0" + s)
            .forEach(s -> assertEquals(0L, parse(s, unit), s)));
  }

  @Test
  @Timeout(value = 1)
  public void testParse() {
    assertEquals(1L, parse("1_000_000 ns", TimeUnit.MILLISECONDS));
    assertEquals(10L, parse("10_000_000 nanos", TimeUnit.MILLISECONDS));
    assertEquals(100L, parse("100_000_000 nanosecond", TimeUnit.MILLISECONDS));
    assertEquals(1000L, parse("1_000_000_000 nanoseconds", TimeUnit.MILLISECONDS));

    assertEquals(1L, parse("1000 us", TimeUnit.MILLISECONDS));
    assertEquals(10L, parse("10000 μs", TimeUnit.MILLISECONDS));
    assertEquals(100L, parse("100000 micros", TimeUnit.MILLISECONDS));
    assertEquals(1000L, parse("1000000 microsecond", TimeUnit.MILLISECONDS));
    assertEquals(10000L, parse("10000000 microseconds", TimeUnit.MILLISECONDS));

    assertEquals(1L, parse("1 ms", TimeUnit.MILLISECONDS));
    assertEquals(10L, parse("10 msec", TimeUnit.MILLISECONDS));
    assertEquals(100L, parse("100 millis", TimeUnit.MILLISECONDS));
    assertEquals(1000L, parse("1000 millisecond", TimeUnit.MILLISECONDS));
    assertEquals(10000L, parse("10000 milliseconds", TimeUnit.MILLISECONDS));

    assertEquals(1000L, parse("1 s", TimeUnit.MILLISECONDS));
    assertEquals(10000L, parse("10 sec", TimeUnit.MILLISECONDS));
    assertEquals(100000L, parse("100 second", TimeUnit.MILLISECONDS));
    assertEquals(1000000L, parse("1000 seconds", TimeUnit.MILLISECONDS));

    assertEquals(60, parse("1 m", TimeUnit.SECONDS));
    assertEquals(600, parse("10 min", TimeUnit.SECONDS));
    assertEquals(6000, parse("100 minutes", TimeUnit.SECONDS));
    assertEquals(60000, parse("1000 minutes", TimeUnit.SECONDS));

    assertEquals(60, parse("1 h", TimeUnit.MINUTES));
    assertEquals(600, parse("10 hr", TimeUnit.MINUTES));
    assertEquals(6000, parse("100 hour", TimeUnit.MINUTES));
    assertEquals(60000, parse("1000 hours", TimeUnit.MINUTES));

    assertEquals(24, parse("1 d", TimeUnit.HOURS));
    assertEquals(240, parse("10 day", TimeUnit.HOURS));
    assertEquals(2400, parse("100 days", TimeUnit.HOURS));
  }

  @Test
  @Timeout(value = 1)
  public void testRoundUp() {
    final long nanosPerSecond = 1_000_000_000L;
    final TimeDuration oneSecond = TimeDuration.valueOf(1, TimeUnit.SECONDS);
    assertEquals(-nanosPerSecond, oneSecond.roundUpNanos(-nanosPerSecond - 1));
    assertEquals(-nanosPerSecond, oneSecond.roundUpNanos(-nanosPerSecond));
    assertEquals(0, oneSecond.roundUpNanos(-nanosPerSecond + 1));
    assertEquals(0, oneSecond.roundUpNanos(-1));
    assertEquals(0, oneSecond.roundUpNanos(0));
    assertEquals(nanosPerSecond, oneSecond.roundUpNanos(1));
    assertEquals(nanosPerSecond, oneSecond.roundUpNanos(nanosPerSecond - 1));
    assertEquals(nanosPerSecond, oneSecond.roundUpNanos(nanosPerSecond));
    assertEquals(2*nanosPerSecond, oneSecond.roundUpNanos(nanosPerSecond + 1));
  }

  @Test
  @Timeout(value = 1)
  public void testTo() {
    final TimeDuration oneSecond = TimeDuration.valueOf(1, TimeUnit.SECONDS);
    assertTo(1000, "1000ms", oneSecond, TimeUnit.MILLISECONDS);
    final TimeDuration nanos = assertTo(1_000_000_000, "1000000000ns", oneSecond, TimeUnit.NANOSECONDS);
    assertTo(1000, "1000ms", nanos, TimeUnit.MILLISECONDS);

    assertTo(0, "0.0167min", oneSecond, TimeUnit.MINUTES);
    assertTo(0, "0.0167min", nanos, TimeUnit.MINUTES);

    final TimeDuration millis = TimeDuration.valueOf(1_999, TimeUnit.MILLISECONDS);
    assertTo(1, "1.9990s", millis, TimeUnit.SECONDS);
    assertTo(0, "0.0333min", millis, TimeUnit.MINUTES);
  }

  static TimeDuration assertTo(long expected, String expectedString, TimeDuration timeDuration, TimeUnit toUnit) {
    final TimeDuration computed = timeDuration.to(toUnit);
    assertEquals(expected, computed.getDuration());
    assertEquals(toUnit, computed.getUnit());
    assertEquals(expectedString, timeDuration.toString(toUnit, 4));
    return computed;
  }

  @Test
  @Timeout(value = 1)
  public void testAddAndSubtract() {
    final TimeDuration oneSecond = TimeDuration.valueOf(1, TimeUnit.SECONDS);
    final TimeDuration tenSecond = TimeDuration.valueOf(10, TimeUnit.SECONDS);
    {
      final TimeDuration d = oneSecond.subtract(oneSecond);
      assertEquals(0, d.getDuration());
      assertEquals(TimeUnit.SECONDS, d.getUnit());

      final TimeDuration sum = d.add(oneSecond);
      assertEquals(1, sum.getDuration());
      assertEquals(TimeUnit.SECONDS, sum.getUnit());
    }
    {
      final TimeDuration d = tenSecond.subtract(oneSecond);
      assertEquals(9, d.getDuration());
      assertEquals(TimeUnit.SECONDS, d.getUnit());

      final TimeDuration sum = d.add(oneSecond);
      assertEquals(10, sum.getDuration());
      assertEquals(TimeUnit.SECONDS, sum.getUnit());
    }
    {
      final TimeDuration d = oneSecond.subtract(tenSecond);
      assertEquals(-9, d.getDuration());
      assertEquals(TimeUnit.SECONDS, d.getUnit());

      final TimeDuration sum = d.add(tenSecond);
      assertEquals(1, sum.getDuration());
      assertEquals(TimeUnit.SECONDS, sum.getUnit());
    }

    final TimeDuration oneMS = TimeDuration.valueOf(1, TimeUnit.MILLISECONDS);
    {
      final TimeDuration d = oneSecond.subtract(oneMS);
      assertEquals(999, d.getDuration());
      assertEquals(TimeUnit.MILLISECONDS, d.getUnit());

      final TimeDuration sum = d.add(oneSecond);
      assertEquals(1999, sum.getDuration());
      assertEquals(TimeUnit.MILLISECONDS, sum.getUnit());
    }
    {
      final TimeDuration d = oneMS.subtract(oneSecond);
      assertEquals(-999, d.getDuration());
      assertEquals(TimeUnit.MILLISECONDS, d.getUnit());

      final TimeDuration sum = d.add(oneSecond);
      assertEquals(1, sum.getDuration());
      assertEquals(TimeUnit.MILLISECONDS, sum.getUnit());
    }
  }

  @Test
  @Timeout(value = 1)
  public void testNegate() {
    assertNegate(0);
    assertNegate(1);
    assertNegate(-1);
    assertNegate(Long.MAX_VALUE);

    Assertions.assertEquals(
        TimeDuration.valueOf(Long.MAX_VALUE, TimeUnit.SECONDS),
        TimeDuration.valueOf(Long.MIN_VALUE, TimeUnit.SECONDS).negate());
  }

  private static void assertNegate(long n) {
    Assertions.assertEquals(
        TimeDuration.valueOf(-n, TimeUnit.SECONDS),
        TimeDuration.valueOf(n, TimeUnit.SECONDS).negate());
    Assertions.assertEquals(
        TimeDuration.valueOf(n, TimeUnit.SECONDS),
        TimeDuration.valueOf(-n, TimeUnit.SECONDS).negate());
  }

  @Test
  @Timeout(value = 1)
  public void testMultiply() {
    assertMultiply(0, TimeDuration.ONE_SECOND, TimeDuration.valueOf(0, TimeUnit.SECONDS));
    assertMultiply(0.001, TimeDuration.ONE_SECOND, TimeDuration.ONE_MILLISECOND);
    assertMultiply(0.001/60, TimeDuration.ONE_MINUTE, TimeDuration.ONE_MILLISECOND);
    assertMultiply(100,
        TimeDuration.valueOf(Long.MAX_VALUE / 10, TimeUnit.NANOSECONDS),
        TimeDuration.valueOf(Long.MAX_VALUE / 100, TimeUnit.MICROSECONDS)
    );
    assertMultiply(1E-30,
        TimeDuration.valueOf(1_000_000_000_000_000_000L, TimeUnit.DAYS),
        TimeDuration.valueOf(86, TimeUnit.NANOSECONDS)
    );
  }

  private static void assertMultiply(double multiplier, TimeDuration t, TimeDuration expected) {
    assertMultiply(t         ,  multiplier, expected);
    assertMultiply(t.negate(), -multiplier, expected);
    assertMultiply(t.negate(),  multiplier, expected.negate());
    assertMultiply(t         , -multiplier, expected.negate());
  }

  private static void assertMultiply(TimeDuration t, double multiplier, TimeDuration expected) {
    final TimeDuration computed = t.multiply(multiplier);
    TimeDuration.LOG.info("assertMultiply: {} x {} = {} ?= {}\n\n", t, multiplier, computed, expected);
    Assertions.assertEquals(expected.getUnit(), computed.getUnit());
    final long d = Math.abs(computed.getDuration() - expected.getDuration());
    Assertions.assertTrue(d <= Math.abs(expected.getDuration()) * TimeDuration.ERROR_THRESHOLD);
  }

  @Test
  @Timeout(value = 1)
  public void testHigherLower() {
    final TimeUnit[] units = {TimeUnit.NANOSECONDS, TimeUnit.MICROSECONDS, TimeUnit.MILLISECONDS,
        TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS, TimeUnit.DAYS};
    for(int i = 1; i < units.length; i++) {
      assertHigherLower(units[i-1], units[i]);
    }

    Assertions.assertSame(TimeUnit.NANOSECONDS, TimeDuration.lowerUnit(TimeUnit.NANOSECONDS));
    Assertions.assertSame(TimeUnit.DAYS, TimeDuration.higherUnit(TimeUnit.DAYS));
  }

  @Test
  @Timeout(value = 1)
  public void testCompareTo() {
    assertTimeDurationCompareTo(TimeDuration.ONE_MINUTE, TimeDuration.ONE_SECOND);

    final TimeDuration fifteenSecond = TimeDuration.valueOf(15, TimeUnit.SECONDS);
    assertTimeDurationCompareTo(TimeDuration.ONE_DAY, fifteenSecond);

    assertTimeDurationEquals(TimeDuration.ONE_MINUTE, fifteenSecond.multiply(4));
    assertTimeDurationEquals(TimeDuration.ONE_DAY, TimeDuration.ONE_MINUTE.multiply(60).multiply(24));
  }

  static void assertTimeDurationEquals(TimeDuration left, TimeDuration right) {
    assertEquals(0, left.compareTo(right));
    assertEquals(0, right.compareTo(left));
    assertEquals(left, right);
    assertEquals(right, left);
  }

  static void assertTimeDurationCompareTo(TimeDuration larger, TimeDuration smaller) {
    assertTrue(smaller.compareTo(larger) < 0);
    assertTrue(larger.compareTo(smaller) > 0);
    assertEquals(smaller, TimeDuration.min(smaller, larger));
    assertEquals(smaller, TimeDuration.min(larger, smaller));
    assertEquals(larger, TimeDuration.max(smaller, larger));
    assertEquals(larger, TimeDuration.max(larger, smaller));

    final TimeDuration diff = larger.add(smaller.negate());
    assertTrue(diff.isPositive());
    assertTrue(diff.isNonNegative());
    assertFalse(diff.isNegative());
    assertFalse(diff.isNonPositive());
  }

  private static void assertHigherLower(TimeUnit lower, TimeUnit higher) {
    Assertions.assertSame(lower, TimeDuration.lowerUnit(higher));
    Assertions.assertSame(higher, TimeDuration.higherUnit(lower));
  }
}
