/*
 * Copyright 2016 the original author or 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 net.jodah.failsafe;

import org.testng.annotations.Test;

import java.net.ConnectException;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static net.jodah.failsafe.Testing.failures;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.testng.Assert.*;

/**
 * @author Jonathan Halterman
 */
@Test
public class ExecutionTest {
  ConnectException e = new ConnectException();

  public void testCanRetryForResult() {
    // Given rpRetry for null
    Execution exec = new Execution(new RetryPolicy<>().handleResult(null));

    // When / Then
    assertFalse(exec.complete(null));
    assertTrue(exec.canRetryFor(null));
    assertFalse(exec.canRetryFor(1));

    // Then
    assertEquals(exec.getAttemptCount(), 3);
    assertTrue(exec.isComplete());
    assertEquals(exec.getLastResult(), Integer.valueOf(1));
    assertNull(exec.getLastFailure());

    // Given 2 max retries
    exec = new Execution(new RetryPolicy<>().handleResult(null).withMaxRetries(2));

    // When / Then
    assertFalse(exec.complete(null));
    assertTrue(exec.canRetryFor(null));
    assertFalse(exec.canRetryFor(null));

    // Then
    assertEquals(exec.getAttemptCount(), 3);
    assertTrue(exec.isComplete());
    assertNull(exec.getLastResult());
    assertNull(exec.getLastFailure());
  }

  public void testCanRetryForResultAndThrowable() {
    // Given rpRetry for null
    Execution exec = new Execution(new RetryPolicy<>().withMaxAttempts(10).handleResult(null));

    // When / Then
    assertFalse(exec.complete(null));
    assertTrue(exec.canRetryFor(null, null));
    assertTrue(exec.canRetryFor(1, new IllegalArgumentException()));
    assertFalse(exec.canRetryFor(1, null));

    // Then
    assertEquals(exec.getAttemptCount(), 4);
    assertTrue(exec.isComplete());

    // Given 2 max retries
    exec = new Execution(new RetryPolicy<>().handleResult(null).withMaxRetries(2));

    // When / Then
    assertFalse(exec.complete(null));
    assertTrue(exec.canRetryFor(null, e));
    assertFalse(exec.canRetryFor(null, e));

    // Then
    assertEquals(exec.getAttemptCount(), 3);
    assertTrue(exec.isComplete());
  }

  public void testCanRetryOn() {
    // Given rpRetry on IllegalArgumentException
    Execution exec = new Execution(new RetryPolicy<>().handle(IllegalArgumentException.class));

    // When / Then
    assertTrue(exec.canRetryOn(new IllegalArgumentException()));
    assertFalse(exec.canRetryOn(e));

    // Then
    assertEquals(exec.getAttemptCount(), 2);
    assertTrue(exec.isComplete());
    assertNull(exec.getLastResult());
    assertEquals(exec.getLastFailure(), e);

    // Given 2 max retries
    exec = new Execution(new RetryPolicy<>().withMaxRetries(2));

    // When / Then
    assertTrue(exec.canRetryOn(e));
    assertTrue(exec.canRetryOn(e));
    assertFalse(exec.canRetryOn(e));

    // Then
    assertEquals(exec.getAttemptCount(), 3);
    assertTrue(exec.isComplete());
    assertNull(exec.getLastResult());
    assertEquals(exec.getLastFailure(), e);
  }

  public void testComplete() {
    // Given
    Execution exec = new Execution(new RetryPolicy<>());

    // When
    exec.complete();

    // Then
    assertEquals(exec.getAttemptCount(), 1);
    assertTrue(exec.isComplete());
    assertNull(exec.getLastResult());
    assertNull(exec.getLastFailure());
  }

  public void testCompleteForResult() {
    // Given
    Execution exec = new Execution(new RetryPolicy<>().handleResult(null));

    // When / Then
    assertFalse(exec.complete(null));
    assertTrue(exec.complete(true));

    // Then
    assertEquals(exec.getAttemptCount(), 2);
    assertTrue(exec.isComplete());
    assertEquals(exec.getLastResult(), Boolean.TRUE);
    assertNull(exec.getLastFailure());
  }

  public void testGetAttemptCount() {
    Execution exec = new Execution(new RetryPolicy<>());
    exec.recordFailure(e);
    exec.recordFailure(e);
    assertEquals(exec.getAttemptCount(), 2);
  }

  public void testGetElapsedMillis() throws Throwable {
    Execution exec = new Execution(new RetryPolicy<>());
    assertTrue(exec.getElapsedTime().toMillis() < 100);
    Thread.sleep(150);
    assertTrue(exec.getElapsedTime().toMillis() > 100);
  }

  @SuppressWarnings("unchecked")
  public void testIsComplete() {
    List<Object> list = mock(List.class);
    when(list.size()).thenThrow(failures(2, new IllegalStateException())).thenReturn(5);

    RetryPolicy retryPolicy = new RetryPolicy<>().handle(IllegalStateException.class);
    Execution exec = new Execution(retryPolicy);

    while (!exec.isComplete()) {
      try {
        exec.complete(list.size());
      } catch (IllegalStateException e) {
        exec.recordFailure(e);
      }
    }

    assertEquals(exec.getLastResult(), Integer.valueOf(5));
    assertEquals(exec.getAttemptCount(), 3);
  }

  public void shouldAdjustWaitTimeForBackoff() {
    Execution exec = new Execution(new RetryPolicy<>().withMaxAttempts(10).withBackoff(1, 10, ChronoUnit.NANOS));
    assertEquals(exec.getWaitTime().toNanos(), 0);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 1);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 2);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 4);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 8);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 10);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 10);
  }

  public void shouldAdjustWaitTimeForComputedDelay() {
    Execution exec = new Execution(
        new RetryPolicy<>().withMaxAttempts(10).withDelay((r, f, ctx) -> Duration.ofNanos(ctx.getAttemptCount() * 2)));
    assertEquals(exec.getWaitTime().toNanos(), 0);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 2);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 4);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 6);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 8);
  }

  public void shouldFallbackWaitTimeFromComputedToFixedDelay() {
    Execution exec = new Execution(new RetryPolicy<>().withMaxAttempts(10)
        .withDelay(Duration.ofNanos(5))
        .withDelay((r, f, ctx) -> Duration.ofNanos(ctx.getAttemptCount() % 2 == 0 ? ctx.getAttemptCount() * 2 : -1)));
    assertEquals(exec.getWaitTime().toNanos(), 0);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 5);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 4);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 5);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 8);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 5);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 12);
  }

  public void shouldFallbackWaitTimeFromComputedToBackoffDelay() {
    Execution exec = new Execution(new RetryPolicy<>().withMaxAttempts(10)
        .withBackoff(1, 10, ChronoUnit.NANOS)
        .withDelay((r, f, ctx) -> Duration.ofNanos(ctx.getAttemptCount() % 2 == 0 ? ctx.getAttemptCount() * 2 : -1)));
    assertEquals(exec.getWaitTime().toNanos(), 0);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 1);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 4);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 2);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 8);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 4);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 12);
    exec.recordFailure(e);
    assertEquals(exec.getWaitTime().toNanos(), 8);
  }

  public void shouldAdjustWaitTimeForMaxDuration() throws Throwable {
    Execution exec = new Execution(
        new RetryPolicy<>().withDelay(Duration.ofMillis(49)).withMaxDuration(Duration.ofMillis(50)));
    Thread.sleep(10);
    assertTrue(exec.canRetryOn(e));
    assertTrue(exec.getWaitTime().toNanos() < TimeUnit.MILLISECONDS.toNanos(50) && exec.getWaitTime().toNanos() > 0);
  }

  public void shouldSupportMaxDuration() throws Exception {
    Execution exec = new Execution(new RetryPolicy<>().withMaxDuration(Duration.ofMillis(100)));
    assertTrue(exec.canRetryOn(e));
    assertTrue(exec.canRetryOn(e));
    Thread.sleep(105);
    assertFalse(exec.canRetryOn(e));
    assertTrue(exec.isComplete());
  }

  public void shouldSupportMaxRetries() {
    Execution exec = new Execution(new RetryPolicy<>().withMaxRetries(3));
    assertTrue(exec.canRetryOn(e));
    assertTrue(exec.canRetryOn(e));
    assertTrue(exec.canRetryOn(e));
    assertFalse(exec.canRetryOn(e));
    assertTrue(exec.isComplete());
  }

  public void shouldGetWaitMillis() throws Throwable {
    Execution exec = new Execution(new RetryPolicy<>().withDelay(Duration.ofMillis(100))
        .withMaxDuration(Duration.ofMillis(101))
        .handleResult(null));
    assertEquals(exec.getWaitTime().toMillis(), 0);
    exec.canRetryFor(null);
    assertTrue(exec.getWaitTime().toMillis() <= 100);
    Thread.sleep(150);
    assertFalse(exec.canRetryFor(null));
    assertEquals(exec.getWaitTime().toMillis(), 0);
  }

  @Test(expectedExceptions = IllegalStateException.class)
  public void shouldThrowOnMultipleCompletes() {
    Execution exec = new Execution(new RetryPolicy<>());
    exec.complete();
    exec.complete();
  }

  @Test(expectedExceptions = IllegalStateException.class)
  public void shouldThrowOnCanRetryWhenAlreadyComplete() {
    Execution exec = new Execution(new RetryPolicy<>());
    exec.complete();
    exec.canRetryOn(e);
  }
}