/*
 * Copyright 2019 Google Inc.
 *
 * 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.firebase.internal;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.testing.http.FixedClock;
import com.google.api.client.testing.util.MockSleeper;
import com.google.api.client.util.Clock;
import com.google.api.client.util.Sleeper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Longs;
import com.google.firebase.testing.TestUtils;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;

import org.junit.Test;

public class RetryUnsuccessfulResponseHandlerTest {

  private static final int MAX_RETRIES = 4;
  private static final RetryConfig.Builder TEST_RETRY_CONFIG = RetryConfig.builder()
      .setRetryStatusCodes(ImmutableList.of(429, 503))
      .setMaxIntervalMillis(120 * 1000);

  @Test
  public void testDoesNotRetryOnUnspecifiedHttpStatus() throws IOException {
    MultipleCallSleeper sleeper = new MultipleCallSleeper();
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper));
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(404);
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(MAX_RETRIES);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(404, e.getStatusCode());
    }

    assertEquals(0, sleeper.getCount());
    assertEquals(1, failingRequest.getCount());
  }

  @Test
  public void testRetryOnHttpClientErrorWhenSpecified() throws IOException {
    MultipleCallSleeper sleeper = new MultipleCallSleeper();
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper));
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(429);
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(MAX_RETRIES);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(429, e.getStatusCode());
    }

    assertEquals(MAX_RETRIES, sleeper.getCount());
    assertArrayEquals(new long[]{500, 1000, 2000, 4000}, sleeper.getDelays());
    assertEquals(MAX_RETRIES + 1, failingRequest.getCount());
  }

  @Test
  public void testExponentialBackOffDoesNotExceedMaxInterval() throws IOException {
    MultipleCallSleeper sleeper = new MultipleCallSleeper();
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper));
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(503);
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(10);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(503, e.getStatusCode());
    }

    assertEquals(10, sleeper.getCount());
    assertArrayEquals(
        new long[]{500, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 120000, 120000},
        sleeper.getDelays());
    assertEquals(11, failingRequest.getCount());
  }

  @Test
  public void testRetryAfterGivenAsSeconds() throws IOException {
    MultipleCallSleeper sleeper = new MultipleCallSleeper();
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper));
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(
        503, ImmutableMap.of("retry-after", "2"));
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(MAX_RETRIES);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(503, e.getStatusCode());
    }

    assertEquals(MAX_RETRIES, sleeper.getCount());
    assertArrayEquals(new long[]{2000, 2000, 2000, 2000}, sleeper.getDelays());
    assertEquals(MAX_RETRIES + 1, failingRequest.getCount());
  }

  @Test
  public void testRetryAfterGivenAsDate() throws IOException {
    SimpleDateFormat dateFormat =
            new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
    dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
    Date date = new Date(1000);
    Clock clock = new FixedClock(date.getTime());
    String retryAfter = dateFormat.format(new Date(date.getTime() + 30000));

    MultipleCallSleeper sleeper = new MultipleCallSleeper();
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper), clock);
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(
        503, ImmutableMap.of("retry-after", retryAfter));
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(4);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(503, e.getStatusCode());
    }

    assertEquals(4, sleeper.getCount());
    assertArrayEquals(new long[]{30000, 30000, 30000, 30000}, sleeper.getDelays());
    assertEquals(5, failingRequest.getCount());
  }

  @Test
  public void testInvalidRetryAfterFailsOverToExpBackOff() throws IOException {
    MultipleCallSleeper sleeper = new MultipleCallSleeper();
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper));
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(
        503, ImmutableMap.of("retry-after", "not valid"));
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(4);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(503, e.getStatusCode());
    }

    assertEquals(4, sleeper.getCount());
    assertArrayEquals(new long[]{500, 1000, 2000, 4000}, sleeper.getDelays());
    assertEquals(5, failingRequest.getCount());
  }

  @Test
  public void testDoesNotRetryWhenRetryAfterIsTooLong() throws IOException {
    MultipleCallSleeper sleeper = new MultipleCallSleeper();
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper));
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(
        503, ImmutableMap.of("retry-after", "121"));
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(MAX_RETRIES);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(503, e.getStatusCode());
    }

    assertEquals(0, sleeper.getCount());
    assertEquals(1, failingRequest.getCount());
  }

  @Test
  public void testDoesNotRetryAfterInterruption() throws IOException {
    MockSleeper sleeper = new MockSleeper() {
      @Override
      public void sleep(long millis) throws InterruptedException {
        super.sleep(millis);
        throw new InterruptedException();
      }
    };
    RetryUnsuccessfulResponseHandler handler = new RetryUnsuccessfulResponseHandler(
        testRetryConfig(sleeper));
    CountingLowLevelHttpRequest failingRequest = CountingLowLevelHttpRequest.fromStatus(503);
    HttpRequest request = TestUtils.createRequest(failingRequest);
    request.setUnsuccessfulResponseHandler(handler);
    request.setNumberOfRetries(MAX_RETRIES);

    try {
      request.execute();
      fail("No exception thrown for HTTP error");
    } catch (HttpResponseException e) {
      assertEquals(503, e.getStatusCode());
    }

    assertEquals(1, sleeper.getCount());
    assertEquals(1, failingRequest.getCount());
  }

  private RetryConfig testRetryConfig(Sleeper sleeper) {
    return TEST_RETRY_CONFIG.setSleeper(sleeper).build();
  }


  private static class MultipleCallSleeper extends MockSleeper {

    private final List<Long> delays = new ArrayList<>();

    @Override
    public void sleep(long millis) throws InterruptedException {
      super.sleep(millis);
      delays.add(millis);
    }

    long[] getDelays() {
      return Longs.toArray(delays);
    }
  }
}