/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.android.exoplayer2.ext.cronet;

import static com.google.common.truth.Truth.assertThat;
import static java.lang.Math.min;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.chromium.net.NetworkException.ERROR_HOSTNAME_NOT_RESOLVED;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.net.Uri;
import android.os.ConditionVariable;
import android.os.SystemClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource.HttpDataSourceException;
import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Util;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import okhttp3.Headers;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.chromium.net.CronetEngine;
import org.chromium.net.NetworkException;
import org.chromium.net.UrlRequest;
import org.chromium.net.UrlResponseInfo;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.shadows.ShadowLooper;

/** Tests for {@link CronetDataSource}. */
@RunWith(AndroidJUnit4.class)
public final class CronetDataSourceTest {

  private static final int TEST_CONNECT_TIMEOUT_MS = 100;
  private static final int TEST_READ_TIMEOUT_MS = 100;
  private static final String TEST_URL = "http://google.com";
  private static final String TEST_CONTENT_TYPE = "test/test";
  private static final byte[] TEST_POST_BODY = Util.getUtf8Bytes("test post body");
  private static final long TEST_CONTENT_LENGTH = 16000L;
  private static final int TEST_CONNECTION_STATUS = 5;
  private static final int TEST_INVALID_CONNECTION_STATUS = -1;

  private DataSpec testDataSpec;
  private DataSpec testPostDataSpec;
  private DataSpec testHeadDataSpec;
  private Map<String, String> testResponseHeader;
  private UrlResponseInfo testUrlResponseInfo;

  @Mock private UrlRequest.Builder mockUrlRequestBuilder;
  @Mock private UrlRequest mockUrlRequest;
  @Mock private TransferListener mockTransferListener;
  @Mock private CronetEngine mockCronetEngine;

  private ExecutorService executorService;
  private CronetDataSource dataSourceUnderTest;
  private boolean redirectCalled;

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);

    Map<String, String> defaultRequestProperties = new HashMap<>();
    defaultRequestProperties.put("defaultHeader1", "defaultValue1");
    defaultRequestProperties.put("defaultHeader2", "defaultValue2");

    executorService = Executors.newSingleThreadExecutor();
    dataSourceUnderTest =
        (CronetDataSource)
            new CronetDataSource.Factory(mockCronetEngine, executorService)
                .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
                .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
                .setResetTimeoutOnRedirects(true)
                .setDefaultRequestProperties(defaultRequestProperties)
                .createDataSource();
    dataSourceUnderTest.addTransferListener(mockTransferListener);
    when(mockCronetEngine.newUrlRequestBuilder(
            anyString(), any(UrlRequest.Callback.class), any(Executor.class)))
        .thenReturn(mockUrlRequestBuilder);
    when(mockUrlRequestBuilder.setPriority(anyInt())).thenReturn(mockUrlRequestBuilder);
    when(mockUrlRequestBuilder.allowDirectExecutor()).thenReturn(mockUrlRequestBuilder);
    when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest);
    mockStatusResponse();

    testDataSpec = new DataSpec(Uri.parse(TEST_URL));
    testPostDataSpec =
        new DataSpec.Builder()
            .setUri(TEST_URL)
            .setHttpMethod(DataSpec.HTTP_METHOD_POST)
            .setHttpBody(TEST_POST_BODY)
            .build();
    testHeadDataSpec =
        new DataSpec.Builder().setUri(TEST_URL).setHttpMethod(DataSpec.HTTP_METHOD_HEAD).build();
    testResponseHeader = new HashMap<>();
    testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
    // This value can be anything since the DataSpec is unset.
    testResponseHeader.put("Content-Length", Long.toString(TEST_CONTENT_LENGTH));
    testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 200);
  }

  @After
  public void tearDown() {
    executorService.shutdown();
  }

  private UrlResponseInfo createUrlResponseInfo(int statusCode) {
    return createUrlResponseInfoWithUrl(TEST_URL, statusCode);
  }

  private UrlResponseInfo createUrlResponseInfoWithUrl(String url, int statusCode) {
    ArrayList<Map.Entry<String, String>> responseHeaderList = new ArrayList<>();
    Map<String, List<String>> responseHeaderMap = new HashMap<>();
    for (Map.Entry<String, String> entry : testResponseHeader.entrySet()) {
      responseHeaderList.add(entry);
      responseHeaderMap.put(entry.getKey(), Collections.singletonList(entry.getValue()));
    }
    return new UrlResponseInfo() {
      @Override
      public String getUrl() {
        return url;
      }

      @Override
      public List<String> getUrlChain() {
        return Collections.singletonList(url);
      }

      @Override
      public int getHttpStatusCode() {
        return statusCode;
      }

      @Override
      public String getHttpStatusText() {
        return null;
      }

      @Override
      public List<Map.Entry<String, String>> getAllHeadersAsList() {
        return responseHeaderList;
      }

      @Override
      public Map<String, List<String>> getAllHeaders() {
        return responseHeaderMap;
      }

      @Override
      public boolean wasCached() {
        return false;
      }

      @Override
      public String getNegotiatedProtocol() {
        return null;
      }

      @Override
      public String getProxyServer() {
        return null;
      }

      @Override
      public long getReceivedByteCount() {
        return 0;
      }
    };
  }

  @Test
  public void openingTwiceThrows() throws HttpDataSourceException {
    mockResponseStartSuccess();
    dataSourceUnderTest.open(testDataSpec);
    try {
      dataSourceUnderTest.open(testDataSpec);
      fail("Expected IllegalStateException.");
    } catch (IllegalStateException e) {
      // Expected.
    }
  }

  @Test
  public void callbackFromPreviousRequest() throws HttpDataSourceException {
    mockResponseStartSuccess();

    dataSourceUnderTest.open(testDataSpec);
    dataSourceUnderTest.close();
    // Prepare a mock UrlRequest to be used in the second open() call.
    final UrlRequest mockUrlRequest2 = mock(UrlRequest.class);
    when(mockUrlRequestBuilder.build()).thenReturn(mockUrlRequest2);
    doAnswer(
            invocation -> {
              // Invoke the callback for the previous request.
              dataSourceUnderTest.urlRequestCallback.onFailed(
                  mockUrlRequest,
                  testUrlResponseInfo,
                  createNetworkException(
                      /* errorCode= */ Integer.MAX_VALUE,
                      /* cause= */ new IllegalArgumentException()));
              dataSourceUnderTest.urlRequestCallback.onResponseStarted(
                  mockUrlRequest2, testUrlResponseInfo);
              return null;
            })
        .when(mockUrlRequest2)
        .start();
    dataSourceUnderTest.open(testDataSpec);
  }

  @Test
  public void requestStartCalled() throws HttpDataSourceException {
    mockResponseStartSuccess();

    dataSourceUnderTest.open(testDataSpec);
    verify(mockCronetEngine)
        .newUrlRequestBuilder(eq(TEST_URL), any(UrlRequest.Callback.class), any(Executor.class));
    verify(mockUrlRequest).start();
  }

  @Test
  public void requestSetsRangeHeader() throws HttpDataSourceException {
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
    mockResponseStartSuccess();
    mockReadSuccess(0, 1000);

    dataSourceUnderTest.open(testDataSpec);
    // The header value to add is current position to current position + length - 1.
    verify(mockUrlRequestBuilder).addHeader("Range", "bytes=1000-5999");
  }

  @Test
  public void requestHeadersSet() throws HttpDataSourceException {
    Map<String, String> headersSet = new HashMap<>();
    doAnswer(
            (invocation) -> {
              String key = invocation.getArgument(0);
              String value = invocation.getArgument(1);
              headersSet.put(key, value);
              return null;
            })
        .when(mockUrlRequestBuilder)
        .addHeader(ArgumentMatchers.anyString(), ArgumentMatchers.anyString());

    dataSourceUnderTest.setRequestProperty("defaultHeader2", "dataSourceOverridesDefault");
    dataSourceUnderTest.setRequestProperty("dataSourceHeader1", "dataSourceValue1");
    dataSourceUnderTest.setRequestProperty("dataSourceHeader2", "dataSourceValue2");

    Map<String, String> dataSpecRequestProperties = new HashMap<>();
    dataSpecRequestProperties.put("defaultHeader3", "dataSpecOverridesAll");
    dataSpecRequestProperties.put("dataSourceHeader2", "dataSpecOverridesDataSource");
    dataSpecRequestProperties.put("dataSpecHeader1", "dataSpecValue1");

    testDataSpec =
        new DataSpec.Builder()
            .setUri(TEST_URL)
            .setHttpRequestHeaders(dataSpecRequestProperties)
            .build();
    mockResponseStartSuccess();

    dataSourceUnderTest.open(testDataSpec);

    assertThat(headersSet.get("defaultHeader1")).isEqualTo("defaultValue1");
    assertThat(headersSet.get("defaultHeader2")).isEqualTo("dataSourceOverridesDefault");
    assertThat(headersSet.get("defaultHeader3")).isEqualTo("dataSpecOverridesAll");
    assertThat(headersSet.get("dataSourceHeader1")).isEqualTo("dataSourceValue1");
    assertThat(headersSet.get("dataSourceHeader2")).isEqualTo("dataSpecOverridesDataSource");
    assertThat(headersSet.get("dataSpecHeader1")).isEqualTo("dataSpecValue1");

    verify(mockUrlRequest).start();
  }

  @Test
  public void requestOpen() throws HttpDataSourceException {
    mockResponseStartSuccess();
    assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
    verify(mockTransferListener)
        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
  }

  @Test
  public void requestOpenGzippedCompressedReturnsDataSpecLength() throws HttpDataSourceException {
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 5000);
    testResponseHeader.put("Content-Encoding", "gzip");
    testResponseHeader.put("Content-Length", Long.toString(50L));
    mockResponseStartSuccess();

    assertThat(dataSourceUnderTest.open(testDataSpec)).isEqualTo(5000 /* contentLength */);
    verify(mockTransferListener)
        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
  }

  @Test
  public void requestOpenFail() {
    mockResponseStartFailure(
        /* errorCode= */ Integer.MAX_VALUE, /* cause= */ new IllegalArgumentException());

    try {
      dataSourceUnderTest.open(testDataSpec);
      fail("HttpDataSource.HttpDataSourceException expected");
    } catch (HttpDataSourceException e) {
      // Check for connection not automatically closed.
      assertThat(e).hasCauseThat().isNotInstanceOf(UnknownHostException.class);
      verify(mockUrlRequest, never()).cancel();
      verify(mockTransferListener, never())
          .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
    }
  }

  @Test
  public void open_ifBodyIsSetWithoutContentTypeHeader_fails() {
    testDataSpec =
        new DataSpec.Builder()
            .setUri(TEST_URL)
            .setHttpMethod(DataSpec.HTTP_METHOD_POST)
            .setHttpBody(new byte[1024])
            .setPosition(200)
            .setLength(1024)
            .setKey("key")
            .build();

    try {
      dataSourceUnderTest.open(testDataSpec);
      fail();
    } catch (IOException expected) {
      // Expected
    }
  }

  @Test
  public void requestOpenFailDueToDnsFailure() {
    mockResponseStartFailure(
        /* errorCode= */ ERROR_HOSTNAME_NOT_RESOLVED, /* cause= */ new UnknownHostException());

    try {
      dataSourceUnderTest.open(testDataSpec);
      fail("HttpDataSource.HttpDataSourceException expected");
    } catch (HttpDataSourceException e) {
      // Check for connection not automatically closed.
      assertThat(e).hasCauseThat().isInstanceOf(UnknownHostException.class);
      verify(mockUrlRequest, never()).cancel();
      verify(mockTransferListener, never())
          .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
    }
  }

  @Test
  public void requestOpen_withNon2xxResponseCode_throwsInvalidResponseCodeExceptionWithBody()
      throws Exception {
    mockResponseStartSuccess();
    // Use a size larger than CronetDataSource.READ_BUFFER_SIZE_BYTES
    int responseLength = 40 * 1024;
    mockReadSuccess(/* position= */ 0, /* length= */ responseLength);
    testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 500);

    try {
      dataSourceUnderTest.open(testDataSpec);
      fail("InvalidResponseCodeException expected");
    } catch (InvalidResponseCodeException e) {
      assertThat(e.responseBody).isEqualTo(buildTestDataArray(0, responseLength));
      // Check for connection not automatically closed.
      verify(mockUrlRequest, never()).cancel();
      verify(mockTransferListener, never())
          .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
    }
  }

  @Test
  public void
      requestOpen_withNon2xxResponseCode_andRequestBodyReadFailure_throwsInvalidResponseCodeExceptionWithoutBody()
          throws Exception {
    mockResponseStartSuccess();
    mockReadFailure();
    testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 500);

    try {
      dataSourceUnderTest.open(testDataSpec);
      fail("InvalidResponseCodeException expected");
    } catch (InvalidResponseCodeException e) {
      assertThat(e.responseBody).isEmpty();
      // Check for connection not automatically closed.
      verify(mockUrlRequest, never()).cancel();
      verify(mockTransferListener, never())
          .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
    }
  }

  @Test
  public void requestOpenValidatesContentTypePredicate() {
    mockResponseStartSuccess();

    ArrayList<String> testedContentTypes = new ArrayList<>();
    dataSourceUnderTest.setContentTypePredicate(
        (String input) -> {
          testedContentTypes.add(input);
          return false;
        });

    try {
      dataSourceUnderTest.open(testDataSpec);
      fail("HttpDataSource.HttpDataSourceException expected");
    } catch (HttpDataSourceException e) {
      assertThat(e).isInstanceOf(HttpDataSource.InvalidContentTypeException.class);
      // Check for connection not automatically closed.
      verify(mockUrlRequest, never()).cancel();
      assertThat(testedContentTypes).hasSize(1);
      assertThat(testedContentTypes.get(0)).isEqualTo(TEST_CONTENT_TYPE);
    }
  }

  @Test
  public void postRequestOpen() throws HttpDataSourceException {
    mockResponseStartSuccess();

    dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
    assertThat(dataSourceUnderTest.open(testPostDataSpec)).isEqualTo(TEST_CONTENT_LENGTH);
    verify(mockTransferListener)
        .onTransferStart(dataSourceUnderTest, testPostDataSpec, /* isNetwork= */ true);
  }

  @Test
  public void postRequestOpenValidatesContentType() {
    mockResponseStartSuccess();

    try {
      dataSourceUnderTest.open(testPostDataSpec);
      fail("HttpDataSource.HttpDataSourceException expected");
    } catch (HttpDataSourceException e) {
      verify(mockUrlRequest, never()).start();
    }
  }

  @Test
  public void postRequestOpenRejects307Redirects() {
    mockResponseStartSuccess();
    mockResponseStartRedirect();

    try {
      dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
      dataSourceUnderTest.open(testPostDataSpec);
      fail("HttpDataSource.HttpDataSourceException expected");
    } catch (HttpDataSourceException e) {
      verify(mockUrlRequest, never()).followRedirect();
    }
  }

  @Test
  public void headRequestOpen() throws HttpDataSourceException {
    mockResponseStartSuccess();
    dataSourceUnderTest.open(testHeadDataSpec);
    verify(mockTransferListener)
        .onTransferStart(dataSourceUnderTest, testHeadDataSpec, /* isNetwork= */ true);
    dataSourceUnderTest.close();
  }

  @Test
  public void requestReadTwice() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    byte[] returnedBuffer = new byte[8];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
    assertThat(bytesRead).isEqualTo(8);

    returnedBuffer = new byte[8];
    bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(8, 8));
    assertThat(bytesRead).isEqualTo(8);

    // Should have only called read on cronet once.
    verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
    verify(mockTransferListener, times(2))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
  }

  @Test
  public void secondRequestNoContentLength() throws HttpDataSourceException {
    mockResponseStartSuccess();
    testResponseHeader.put("Content-Length", Long.toString(1L));
    mockReadSuccess(0, 16);

    // First request.
    dataSourceUnderTest.open(testDataSpec);
    byte[] returnedBuffer = new byte[8];
    dataSourceUnderTest.read(returnedBuffer, 0, 1);
    dataSourceUnderTest.close();

    testResponseHeader.remove("Content-Length");
    mockReadSuccess(0, 16);

    // Second request.
    dataSourceUnderTest.open(testDataSpec);
    returnedBuffer = new byte[16];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
    assertThat(bytesRead).isEqualTo(10);
    bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
    assertThat(bytesRead).isEqualTo(6);
    bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 10);
    assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
  }

  @Test
  public void readWithOffset() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    byte[] returnedBuffer = new byte[16];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
    assertThat(bytesRead).isEqualTo(8);
    assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
  }

  @Test
  public void rangeRequestWith206Response() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(1000, 5000);
    testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);

    long length = dataSourceUnderTest.open(testDataSpec);
    assertThat(length).isEqualTo(5000);

    byte[] returnedBuffer = new byte[16];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
    assertThat(bytesRead).isEqualTo(16);
    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
  }

  @Test
  public void rangeRequestWith200Response() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 7000);
    testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);

    long length = dataSourceUnderTest.open(testDataSpec);
    assertThat(length).isEqualTo(5000);

    byte[] returnedBuffer = new byte[16];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
    assertThat(bytesRead).isEqualTo(16);
    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
  }

  @Test
  public void unboundedRangeRequestWith200Response() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, (int) TEST_CONTENT_LENGTH);
    testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, C.LENGTH_UNSET);

    long length = dataSourceUnderTest.open(testDataSpec);
    assertThat(length).isEqualTo(TEST_CONTENT_LENGTH - 1000);

    byte[] returnedBuffer = new byte[16];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
    assertThat(bytesRead).isEqualTo(16);
    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(1000, 16));
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
  }

  @Test
  public void readWithUnsetLength() throws HttpDataSourceException {
    testResponseHeader.remove("Content-Length");
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    byte[] returnedBuffer = new byte[16];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 8, 8);
    assertThat(returnedBuffer).isEqualTo(prefixZeros(buildTestDataArray(0, 8), 16));
    assertThat(bytesRead).isEqualTo(8);
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
  }

  @Test
  public void readReturnsWhatItCan() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    byte[] returnedBuffer = new byte[24];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 24);
    assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(0, 16), 24));
    assertThat(bytesRead).isEqualTo(16);
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
  }

  @Test
  public void closedMeansClosed() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    int bytesRead = 0;
    dataSourceUnderTest.open(testDataSpec);

    byte[] returnedBuffer = new byte[8];
    bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));
    assertThat(bytesRead).isEqualTo(8);

    dataSourceUnderTest.close();
    verify(mockTransferListener)
        .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);

    try {
      bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
      fail();
    } catch (IllegalStateException e) {
      // Expected.
    }

    // 16 bytes were attempted but only 8 should have been successfully read.
    assertThat(bytesRead).isEqualTo(8);
  }

  @Test
  public void overread() throws HttpDataSourceException {
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
    testResponseHeader.put("Content-Length", Long.toString(16L));
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    byte[] returnedBuffer = new byte[8];
    int bytesRead = dataSourceUnderTest.read(returnedBuffer, 0, 8);
    assertThat(bytesRead).isEqualTo(8);
    assertThat(returnedBuffer).isEqualTo(buildTestDataArray(0, 8));

    // The current buffer is kept if not completely consumed by DataSource reader.
    returnedBuffer = new byte[8];
    bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 6);
    assertThat(bytesRead).isEqualTo(14);
    assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(8, 6), 8));

    // 2 bytes left at this point.
    returnedBuffer = new byte[8];
    bytesRead += dataSourceUnderTest.read(returnedBuffer, 0, 8);
    assertThat(bytesRead).isEqualTo(16);
    assertThat(returnedBuffer).isEqualTo(suffixZeros(buildTestDataArray(14, 2), 8));

    // Should have only called read on cronet once.
    verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);

    // Now we already returned the 16 bytes initially asked.
    // Try to read again even though all requested 16 bytes are already returned.
    // Return C.RESULT_END_OF_INPUT
    returnedBuffer = new byte[16];
    int bytesOverRead = dataSourceUnderTest.read(returnedBuffer, 0, 16);
    assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
    assertThat(returnedBuffer).isEqualTo(new byte[16]);
    // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
    verify(mockTransferListener, never())
        .onBytesTransferred(
            dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
    // There should still be only one call to read on cronet.
    verify(mockUrlRequest, times(1)).read(any(ByteBuffer.class));
    // Check for connection not automatically closed.
    verify(mockUrlRequest, never()).cancel();
    assertThat(bytesRead).isEqualTo(16);
  }

  @Test
  public void requestReadByteBufferTwice() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(8);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));

    // Use a wrapped ByteBuffer instead of direct for coverage.
    returnedBuffer.rewind();
    bytesRead = dataSourceUnderTest.read(returnedBuffer);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 8));
    assertThat(bytesRead).isEqualTo(8);

    // Separate cronet calls for each read.
    verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
    verify(mockTransferListener, times(2))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
  }

  @Test
  public void requestIntermixRead() throws HttpDataSourceException {
    mockResponseStartSuccess();
    // Chunking reads into parts 6, 7, 8, 9.
    mockReadSuccess(0, 30);

    dataSourceUnderTest.open(testDataSpec);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(6);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 6));
    assertThat(bytesRead).isEqualTo(6);

    byte[] returnedBytes = new byte[7];
    bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 7);
    assertThat(returnedBytes).isEqualTo(buildTestDataArray(6, 7));
    assertThat(bytesRead).isEqualTo(6 + 7);

    returnedBuffer = ByteBuffer.allocateDirect(8);
    bytesRead += dataSourceUnderTest.read(returnedBuffer);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(13, 8));
    assertThat(bytesRead).isEqualTo(6 + 7 + 8);

    returnedBytes = new byte[9];
    bytesRead += dataSourceUnderTest.read(returnedBytes, 0, 9);
    assertThat(returnedBytes).isEqualTo(buildTestDataArray(21, 9));
    assertThat(bytesRead).isEqualTo(6 + 7 + 8 + 9);

    // First ByteBuffer call. The first byte[] call populates enough bytes for the rest.
    verify(mockUrlRequest, times(2)).read(any(ByteBuffer.class));
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 7);
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 9);
  }

  @Test
  public void secondRequestNoContentLengthReadByteBuffer() throws HttpDataSourceException {
    mockResponseStartSuccess();
    testResponseHeader.put("Content-Length", Long.toString(1L));
    mockReadSuccess(0, 16);

    // First request.
    dataSourceUnderTest.open(testDataSpec);
    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
    dataSourceUnderTest.read(returnedBuffer);
    dataSourceUnderTest.close();

    testResponseHeader.remove("Content-Length");
    mockReadSuccess(0, 16);

    // Second request.
    dataSourceUnderTest.open(testDataSpec);
    returnedBuffer = ByteBuffer.allocateDirect(16);
    returnedBuffer.limit(10);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(10);
    returnedBuffer.limit(returnedBuffer.capacity());
    bytesRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(6);
    returnedBuffer.rewind();
    bytesRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(C.RESULT_END_OF_INPUT);
  }

  @Test
  public void rangeRequestWith206ResponseReadByteBuffer() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(1000, 5000);
    testUrlResponseInfo = createUrlResponseInfo(206); // Server supports range requests.
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);

    long length = dataSourceUnderTest.open(testDataSpec);
    assertThat(length).isEqualTo(5000);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(16);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
  }

  @Test
  public void rangeRequestWith200ResponseReadByteBuffer() throws HttpDataSourceException {
    // Tests for skipping bytes.
    mockResponseStartSuccess();
    mockReadSuccess(0, 7000);
    testUrlResponseInfo = createUrlResponseInfo(200); // Server does not support range requests.
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);

    long length = dataSourceUnderTest.open(testDataSpec);
    assertThat(length).isEqualTo(5000);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(16);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(1000, 16));
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
  }

  @Test
  public void readByteBufferWithUnsetLength() throws HttpDataSourceException {
    testResponseHeader.remove("Content-Length");
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
    returnedBuffer.limit(8);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
    assertThat(bytesRead).isEqualTo(8);
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
  }

  @Test
  public void readByteBufferReturnsWhatItCan() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(24);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 16));
    assertThat(bytesRead).isEqualTo(16);
    verify(mockTransferListener)
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 16);
  }

  @Test
  public void overreadByteBuffer() throws HttpDataSourceException {
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, 16);
    testResponseHeader.put("Content-Length", Long.toString(16L));
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    dataSourceUnderTest.open(testDataSpec);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
    int bytesRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(8);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));

    // The current buffer is kept if not completely consumed by DataSource reader.
    returnedBuffer = ByteBuffer.allocateDirect(6);
    bytesRead += dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(14);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(8, 6));

    // 2 bytes left at this point.
    returnedBuffer = ByteBuffer.allocateDirect(8);
    bytesRead += dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesRead).isEqualTo(16);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(14, 2));

    // Called on each.
    verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 8);
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 6);
    verify(mockTransferListener, times(1))
        .onBytesTransferred(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, 2);

    // Now we already returned the 16 bytes initially asked.
    // Try to read again even though all requested 16 bytes are already returned.
    // Return C.RESULT_END_OF_INPUT
    returnedBuffer = ByteBuffer.allocateDirect(16);
    int bytesOverRead = dataSourceUnderTest.read(returnedBuffer);
    assertThat(bytesOverRead).isEqualTo(C.RESULT_END_OF_INPUT);
    assertThat(returnedBuffer.position()).isEqualTo(0);
    // C.RESULT_END_OF_INPUT should not be reported though the TransferListener.
    verify(mockTransferListener, never())
        .onBytesTransferred(
            dataSourceUnderTest, testDataSpec, /* isNetwork= */ true, C.RESULT_END_OF_INPUT);
    // Number of calls to cronet should not have increased.
    verify(mockUrlRequest, times(3)).read(any(ByteBuffer.class));
    // Check for connection not automatically closed.
    verify(mockUrlRequest, never()).cancel();
    assertThat(bytesRead).isEqualTo(16);
  }

  @Test
  public void closedMeansClosedReadByteBuffer() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadSuccess(0, 16);

    int bytesRead = 0;
    dataSourceUnderTest.open(testDataSpec);

    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(16);
    returnedBuffer.limit(8);
    bytesRead += dataSourceUnderTest.read(returnedBuffer);
    returnedBuffer.flip();
    assertThat(copyByteBufferToArray(returnedBuffer)).isEqualTo(buildTestDataArray(0, 8));
    assertThat(bytesRead).isEqualTo(8);

    dataSourceUnderTest.close();
    verify(mockTransferListener)
        .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);

    try {
      bytesRead += dataSourceUnderTest.read(returnedBuffer);
      fail();
    } catch (IllegalStateException e) {
      // Expected.
    }

    // 16 bytes were attempted but only 8 should have been successfully read.
    assertThat(bytesRead).isEqualTo(8);
  }

  @Test
  public void connectTimeout() throws InterruptedException {
    long startTimeMs = SystemClock.elapsedRealtime();
    final ConditionVariable startCondition = buildUrlRequestStartedCondition();
    final CountDownLatch timedOutLatch = new CountDownLatch(1);

    new Thread() {
      @Override
      public void run() {
        try {
          dataSourceUnderTest.open(testDataSpec);
          fail();
        } catch (HttpDataSourceException e) {
          // Expected.
          assertThat(e).isInstanceOf(CronetDataSource.OpenException.class);
          assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class);
          assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
              .isEqualTo(TEST_CONNECTION_STATUS);
          timedOutLatch.countDown();
        }
      }
    }.start();
    startCondition.block();

    // We should still be trying to open.
    assertNotCountedDown(timedOutLatch);
    // We should still be trying to open as we approach the timeout.
    setSystemClockInMsAndTriggerPendingMessages(
        /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
    assertNotCountedDown(timedOutLatch);
    // Now we timeout.
    setSystemClockInMsAndTriggerPendingMessages(
        /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS + 10);
    timedOutLatch.await();

    verify(mockTransferListener, never())
        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
  }

  @Test
  public void connectInterrupted() throws InterruptedException {
    long startTimeMs = SystemClock.elapsedRealtime();
    final ConditionVariable startCondition = buildUrlRequestStartedCondition();
    final CountDownLatch timedOutLatch = new CountDownLatch(1);

    Thread thread =
        new Thread() {
          @Override
          public void run() {
            try {
              dataSourceUnderTest.open(testDataSpec);
              fail();
            } catch (HttpDataSourceException e) {
              // Expected.
              assertThat(e).isInstanceOf(CronetDataSource.OpenException.class);
              assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class);
              assertThat(((CronetDataSource.OpenException) e).cronetConnectionStatus)
                  .isEqualTo(TEST_INVALID_CONNECTION_STATUS);
              timedOutLatch.countDown();
            }
          }
        };
    thread.start();
    startCondition.block();

    // We should still be trying to open.
    assertNotCountedDown(timedOutLatch);
    // We should still be trying to open as we approach the timeout.
    setSystemClockInMsAndTriggerPendingMessages(
        /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
    assertNotCountedDown(timedOutLatch);
    // Now we interrupt.
    thread.interrupt();
    timedOutLatch.await();

    verify(mockTransferListener, never())
        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
  }

  @Test
  public void connectResponseBeforeTimeout() throws Exception {
    long startTimeMs = SystemClock.elapsedRealtime();
    final ConditionVariable startCondition = buildUrlRequestStartedCondition();
    final CountDownLatch openLatch = new CountDownLatch(1);

    AtomicReference<Exception> exceptionOnTestThread = new AtomicReference<>();
    new Thread() {
      @Override
      public void run() {
        try {
          dataSourceUnderTest.open(testDataSpec);
        } catch (HttpDataSourceException e) {
          exceptionOnTestThread.set(e);
        } finally {
          openLatch.countDown();
        }
      }
    }.start();
    startCondition.block();

    // We should still be trying to open.
    assertNotCountedDown(openLatch);
    // We should still be trying to open as we approach the timeout.
    setSystemClockInMsAndTriggerPendingMessages(
        /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
    assertNotCountedDown(openLatch);
    // The response arrives just in time.
    dataSourceUnderTest.urlRequestCallback.onResponseStarted(mockUrlRequest, testUrlResponseInfo);
    openLatch.await();
    assertThat(exceptionOnTestThread.get()).isNull();
  }

  @Test
  public void redirectIncreasesConnectionTimeout() throws Exception {
    long startTimeMs = SystemClock.elapsedRealtime();
    final ConditionVariable startCondition = buildUrlRequestStartedCondition();
    final CountDownLatch timedOutLatch = new CountDownLatch(1);
    final AtomicInteger openExceptions = new AtomicInteger(0);

    new Thread() {
      @Override
      public void run() {
        try {
          dataSourceUnderTest.open(testDataSpec);
          fail();
        } catch (HttpDataSourceException e) {
          // Expected.
          assertThat(e).isInstanceOf(CronetDataSource.OpenException.class);
          assertThat(e).hasCauseThat().isInstanceOf(SocketTimeoutException.class);
          openExceptions.getAndIncrement();
          timedOutLatch.countDown();
        }
      }
    }.start();
    startCondition.block();

    // We should still be trying to open.
    assertNotCountedDown(timedOutLatch);
    // We should still be trying to open as we approach the timeout.
    setSystemClockInMsAndTriggerPendingMessages(
        /* nowMs= */ startTimeMs + TEST_CONNECT_TIMEOUT_MS - 1);
    assertNotCountedDown(timedOutLatch);
    // A redirect arrives just in time.
    dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
        mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl1");

    long newTimeoutMs = 2 * TEST_CONNECT_TIMEOUT_MS - 1;
    setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1);
    // We should still be trying to open as we approach the new timeout.
    assertNotCountedDown(timedOutLatch);
    // A redirect arrives just in time.
    dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
        mockUrlRequest, testUrlResponseInfo, "RandomRedirectedUrl2");

    newTimeoutMs = 3 * TEST_CONNECT_TIMEOUT_MS - 2;
    setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs - 1);
    // We should still be trying to open as we approach the new timeout.
    assertNotCountedDown(timedOutLatch);
    // Now we timeout.
    setSystemClockInMsAndTriggerPendingMessages(/* nowMs= */ startTimeMs + newTimeoutMs + 10);
    timedOutLatch.await();

    verify(mockTransferListener, never())
        .onTransferStart(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
    assertThat(openExceptions.get()).isEqualTo(1);
  }

  @Test
  public void redirectParseAndAttachCookie_dataSourceDoesNotHandleSetCookie_followsRedirect()
      throws HttpDataSourceException {
    mockSingleRedirectSuccess(/* responseCode= */ 300);
    mockFollowRedirectSuccess();

    testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");

    dataSourceUnderTest.open(testDataSpec);
    verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
    verify(mockUrlRequest).followRedirect();
  }

  @Test
  public void
      testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeaders()
          throws HttpDataSourceException {
    dataSourceUnderTest =
        (CronetDataSource)
            new CronetDataSource.Factory(mockCronetEngine, executorService)
                .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
                .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
                .setResetTimeoutOnRedirects(true)
                .setHandleSetCookieRequests(true)
                .createDataSource();
    dataSourceUnderTest.addTransferListener(mockTransferListener);
    dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);

    mockSingleRedirectSuccess(/* responseCode= */ 300);

    testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");

    dataSourceUnderTest.open(testDataSpec);
    verify(mockUrlRequestBuilder).addHeader(eq("Cookie"), any(String.class));
    verify(mockUrlRequestBuilder, never()).addHeader(eq("Range"), any(String.class));
    verify(mockUrlRequestBuilder, times(2)).addHeader("Content-Type", TEST_CONTENT_TYPE);
    verify(mockUrlRequest, never()).followRedirect();
    verify(mockUrlRequest, times(2)).start();
  }

  @Test
  public void
      testRedirectParseAndAttachCookie_dataSourceHandlesSetCookie_andPreservesOriginalRequestHeadersIncludingByteRangeHeader()
          throws HttpDataSourceException {
    testDataSpec = new DataSpec(Uri.parse(TEST_URL), 1000, 5000);
    dataSourceUnderTest =
        (CronetDataSource)
            new CronetDataSource.Factory(mockCronetEngine, executorService)
                .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
                .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
                .setResetTimeoutOnRedirects(true)
                .setHandleSetCookieRequests(true)
                .createDataSource();
    dataSourceUnderTest.addTransferListener(mockTransferListener);
    dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);

    mockSingleRedirectSuccess(/* responseCode= */ 300);
    mockReadSuccess(0, 1000);

    testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");

    dataSourceUnderTest.open(testDataSpec);
    verify(mockUrlRequestBuilder).addHeader(eq("Cookie"), any(String.class));
    verify(mockUrlRequestBuilder, times(2)).addHeader("Range", "bytes=1000-5999");
    verify(mockUrlRequestBuilder, times(2)).addHeader("Content-Type", TEST_CONTENT_TYPE);
    verify(mockUrlRequest, never()).followRedirect();
    verify(mockUrlRequest, times(2)).start();
  }

  @Test
  public void redirectNoSetCookieFollowsRedirect() throws HttpDataSourceException {
    mockSingleRedirectSuccess(/* responseCode= */ 300);
    mockFollowRedirectSuccess();

    dataSourceUnderTest.open(testDataSpec);
    verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
    verify(mockUrlRequest).followRedirect();
  }

  @Test
  public void redirectNoSetCookieFollowsRedirect_dataSourceHandlesSetCookie()
      throws HttpDataSourceException {
    dataSourceUnderTest =
        (CronetDataSource)
            new CronetDataSource.Factory(mockCronetEngine, executorService)
                .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
                .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
                .setResetTimeoutOnRedirects(true)
                .setHandleSetCookieRequests(true)
                .createDataSource();
    dataSourceUnderTest.addTransferListener(mockTransferListener);
    mockSingleRedirectSuccess(/* responseCode= */ 300);
    mockFollowRedirectSuccess();

    dataSourceUnderTest.open(testDataSpec);
    verify(mockUrlRequestBuilder, never()).addHeader(eq("Cookie"), any(String.class));
    verify(mockUrlRequest).followRedirect();
  }

  @Test
  public void redirectPostFollowRedirect() throws HttpDataSourceException {
    mockSingleRedirectSuccess(/* responseCode= */ 302);
    mockFollowRedirectSuccess();
    dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);

    dataSourceUnderTest.open(testPostDataSpec);

    verify(mockUrlRequest).followRedirect();
  }

  @Test
  public void redirect302ChangesPostToGet() throws HttpDataSourceException {
    dataSourceUnderTest =
        (CronetDataSource)
            new CronetDataSource.Factory(mockCronetEngine, executorService)
                .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
                .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
                .setResetTimeoutOnRedirects(true)
                .setKeepPostFor302Redirects(false)
                .setHandleSetCookieRequests(true)
                .createDataSource();
    mockSingleRedirectSuccess(/* responseCode= */ 302);
    dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);
    testResponseHeader.put("Set-Cookie", "testcookie=testcookie; Path=/video");

    dataSourceUnderTest.open(testPostDataSpec);

    verify(mockUrlRequest, never()).followRedirect();
    ArgumentCaptor<String> methodCaptor = ArgumentCaptor.forClass(String.class);
    verify(mockUrlRequestBuilder, times(2)).setHttpMethod(methodCaptor.capture());
    assertThat(methodCaptor.getAllValues()).containsExactly("POST", "GET").inOrder();
  }

  @Test
  public void redirectKeeps302Post() throws HttpDataSourceException {
    dataSourceUnderTest =
        (CronetDataSource)
            new CronetDataSource.Factory(mockCronetEngine, executorService)
                .setConnectionTimeoutMs(TEST_CONNECT_TIMEOUT_MS)
                .setReadTimeoutMs(TEST_READ_TIMEOUT_MS)
                .setResetTimeoutOnRedirects(true)
                .setKeepPostFor302Redirects(true)
                .createDataSource();
    mockSingleRedirectSuccess(/* responseCode= */ 302);
    dataSourceUnderTest.setRequestProperty("Content-Type", TEST_CONTENT_TYPE);

    dataSourceUnderTest.open(testPostDataSpec);

    verify(mockUrlRequest, never()).followRedirect();
    ArgumentCaptor<String> methodCaptor = ArgumentCaptor.forClass(String.class);
    verify(mockUrlRequestBuilder, times(2)).setHttpMethod(methodCaptor.capture());
    assertThat(methodCaptor.getAllValues()).containsExactly("POST", "POST").inOrder();
    ArgumentCaptor<ByteArrayUploadDataProvider> postBodyCaptor =
        ArgumentCaptor.forClass(ByteArrayUploadDataProvider.class);
    verify(mockUrlRequestBuilder, times(2)).setUploadDataProvider(postBodyCaptor.capture(), any());
    assertThat(postBodyCaptor.getAllValues().get(0).getLength()).isEqualTo(TEST_POST_BODY.length);
    assertThat(postBodyCaptor.getAllValues().get(1).getLength()).isEqualTo(TEST_POST_BODY.length);
  }

  @Test
  public void exceptionFromTransferListener() throws HttpDataSourceException {
    mockResponseStartSuccess();

    // Make mockTransferListener throw an exception in CronetDataSource.close(). Ensure that
    // the subsequent open() call succeeds.
    doThrow(new NullPointerException())
        .when(mockTransferListener)
        .onTransferEnd(dataSourceUnderTest, testDataSpec, /* isNetwork= */ true);
    dataSourceUnderTest.open(testDataSpec);
    try {
      dataSourceUnderTest.close();
      fail("NullPointerException expected");
    } catch (NullPointerException e) {
      // Expected.
    }
    // Open should return successfully.
    dataSourceUnderTest.open(testDataSpec);
  }

  @Test
  public void readFailure() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadFailure();

    dataSourceUnderTest.open(testDataSpec);
    byte[] returnedBuffer = new byte[8];
    try {
      dataSourceUnderTest.read(returnedBuffer, 0, 8);
      fail("dataSourceUnderTest.read() returned, but IOException expected");
    } catch (IOException e) {
      // Expected.
    }
  }

  @Test
  public void readByteBufferFailure() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadFailure();

    dataSourceUnderTest.open(testDataSpec);
    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
    try {
      dataSourceUnderTest.read(returnedBuffer);
      fail("dataSourceUnderTest.read() returned, but IOException expected");
    } catch (IOException e) {
      // Expected.
    }
  }

  @Test
  public void readNonDirectedByteBufferFailure() throws HttpDataSourceException {
    mockResponseStartSuccess();
    mockReadFailure();

    dataSourceUnderTest.open(testDataSpec);
    byte[] returnedBuffer = new byte[8];
    try {
      dataSourceUnderTest.read(ByteBuffer.wrap(returnedBuffer));
      fail("dataSourceUnderTest.read() returned, but IllegalArgumentException expected");
    } catch (IllegalArgumentException e) {
      // Expected.
    }
  }

  @Test
  public void readInterrupted() throws HttpDataSourceException, InterruptedException {
    mockResponseStartSuccess();
    dataSourceUnderTest.open(testDataSpec);

    final ConditionVariable startCondition = buildReadStartedCondition();
    final CountDownLatch timedOutLatch = new CountDownLatch(1);
    byte[] returnedBuffer = new byte[8];
    Thread thread =
        new Thread() {
          @Override
          public void run() {
            try {
              dataSourceUnderTest.read(returnedBuffer, 0, 8);
              fail();
            } catch (HttpDataSourceException e) {
              // Expected.
              assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class);
              timedOutLatch.countDown();
            }
          }
        };
    thread.start();
    startCondition.block();

    assertNotCountedDown(timedOutLatch);
    // Now we interrupt.
    thread.interrupt();
    timedOutLatch.await();
  }

  @Test
  public void readByteBufferInterrupted() throws HttpDataSourceException, InterruptedException {
    mockResponseStartSuccess();
    dataSourceUnderTest.open(testDataSpec);

    final ConditionVariable startCondition = buildReadStartedCondition();
    final CountDownLatch timedOutLatch = new CountDownLatch(1);
    ByteBuffer returnedBuffer = ByteBuffer.allocateDirect(8);
    Thread thread =
        new Thread() {
          @Override
          public void run() {
            try {
              dataSourceUnderTest.read(returnedBuffer);
              fail();
            } catch (HttpDataSourceException e) {
              // Expected.
              assertThat(e).hasCauseThat().isInstanceOf(InterruptedIOException.class);
              timedOutLatch.countDown();
            }
          }
        };
    thread.start();
    startCondition.block();

    assertNotCountedDown(timedOutLatch);
    // Now we interrupt.
    thread.interrupt();
    timedOutLatch.await();
  }

  @Test
  public void allowDirectExecutor() throws HttpDataSourceException {
    testDataSpec = new DataSpec(Uri.parse(TEST_URL));
    mockResponseStartSuccess();

    dataSourceUnderTest.open(testDataSpec);
    verify(mockUrlRequestBuilder).allowDirectExecutor();
  }

  @SuppressWarnings("deprecation") // Tests deprecated fallback functionality.
  @Test
  public void factorySetFallbackHttpDataSourceFactory_cronetNotAvailable_usesFallbackFactory()
      throws HttpDataSourceException, InterruptedException {
    MockWebServer mockWebServer = new MockWebServer();
    mockWebServer.enqueue(new MockResponse());
    CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
    DefaultHttpDataSource.Factory fallbackFactory =
        new DefaultHttpDataSource.Factory().setUserAgent("customFallbackFactoryUserAgent");
    HttpDataSource dataSourceUnderTest =
        new CronetDataSource.Factory(cronetEngineWrapper, executorService)
            .setFallbackFactory(fallbackFactory)
            .createDataSource();

    dataSourceUnderTest.open(
        new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build());

    Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
    assertThat(headers.get("user-agent")).isEqualTo("customFallbackFactoryUserAgent");
  }

  @SuppressWarnings("deprecation") // Tests deprecated fallback functionality.
  @Test
  public void
      factory_noFallbackFactoryCronetNotAvailable_delegateTransferListenerToInternalFallbackFactory()
          throws HttpDataSourceException {
    MockWebServer mockWebServer = new MockWebServer();
    mockWebServer.enqueue(new MockResponse());
    CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
    HttpDataSource dataSourceUnderTest =
        new CronetDataSource.Factory(cronetEngineWrapper, executorService)
            .setTransferListener(mockTransferListener)
            .createDataSource();
    DataSpec dataSpec =
        new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build();

    dataSourceUnderTest.open(dataSpec);

    verify(mockTransferListener)
        .onTransferInitializing(eq(dataSourceUnderTest), eq(dataSpec), /* isNetwork= */ eq(true));
    verify(mockTransferListener)
        .onTransferStart(eq(dataSourceUnderTest), eq(dataSpec), /* isNetwork= */ eq(true));
  }

  @SuppressWarnings("deprecation") // Tests deprecated fallback functionality.
  @Test
  public void
      factory_noFallbackFactoryCronetNotAvailable_delegateDefaultRequestPropertiesToInternalFallbackFactory()
          throws HttpDataSourceException, InterruptedException {
    MockWebServer mockWebServer = new MockWebServer();
    mockWebServer.enqueue(new MockResponse());
    CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
    Map<String, String> defaultRequestProperties = new HashMap<>();
    defaultRequestProperties.put("0", "defaultRequestProperty0");
    HttpDataSource dataSourceUnderTest =
        new CronetDataSource.Factory(cronetEngineWrapper, executorService)
            .setDefaultRequestProperties(defaultRequestProperties)
            .createDataSource();

    dataSourceUnderTest.open(
        new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build());

    Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
    assertThat(headers.get("0")).isEqualTo("defaultRequestProperty0");
    assertThat(dataSourceUnderTest).isInstanceOf(DefaultHttpDataSource.class);
  }

  @SuppressWarnings("deprecation") // Tests deprecated fallback functionality.
  @Test
  public void
      factory_noFallbackFactoryCronetNotAvailable_delegateDefaultRequestPropertiesToInternalFallbackFactoryAfterCreation()
          throws HttpDataSourceException, InterruptedException {
    MockWebServer mockWebServer = new MockWebServer();
    mockWebServer.enqueue(new MockResponse());
    CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper((CronetEngine) null);
    Map<String, String> defaultRequestProperties = new HashMap<>();
    defaultRequestProperties.put("0", "defaultRequestProperty0");
    CronetDataSource.Factory factory =
        new CronetDataSource.Factory(cronetEngineWrapper, executorService);
    HttpDataSource dataSourceUnderTest =
        factory.setDefaultRequestProperties(defaultRequestProperties).createDataSource();
    defaultRequestProperties.clear();
    defaultRequestProperties.put("1", "defaultRequestPropertyAfterCreation");
    factory.setDefaultRequestProperties(defaultRequestProperties);

    dataSourceUnderTest.open(
        new DataSpec.Builder().setUri(mockWebServer.url("/test-path").toString()).build());

    Headers headers = mockWebServer.takeRequest(10, SECONDS).getHeaders();
    assertThat(headers.get("0")).isNull();
    assertThat(headers.get("1")).isEqualTo("defaultRequestPropertyAfterCreation");
    assertThat(dataSourceUnderTest).isInstanceOf(DefaultHttpDataSource.class);
  }

  // Helper methods.

  private void mockStatusResponse() {
    doAnswer(
            invocation -> {
              UrlRequest.StatusListener statusListener =
                  (UrlRequest.StatusListener) invocation.getArguments()[0];
              statusListener.onStatus(TEST_CONNECTION_STATUS);
              return null;
            })
        .when(mockUrlRequest)
        .getStatus(any(UrlRequest.StatusListener.class));
  }

  private void mockResponseStartSuccess() {
    doAnswer(
            invocation -> {
              dataSourceUnderTest.urlRequestCallback.onResponseStarted(
                  mockUrlRequest, testUrlResponseInfo);
              return null;
            })
        .when(mockUrlRequest)
        .start();
  }

  private void mockResponseStartRedirect() {
    doAnswer(
            invocation -> {
              dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
                  mockUrlRequest,
                  createUrlResponseInfo(307), // statusCode
                  "http://redirect.location.com");
              return null;
            })
        .when(mockUrlRequest)
        .start();
  }

  private void mockSingleRedirectSuccess(int responseCode) {
    doAnswer(
            invocation -> {
              if (!redirectCalled) {
                redirectCalled = true;
                dataSourceUnderTest.urlRequestCallback.onRedirectReceived(
                    mockUrlRequest,
                    createUrlResponseInfoWithUrl("http://example.com/video", responseCode),
                    "http://example.com/video/redirect");
              } else {
                dataSourceUnderTest.urlRequestCallback.onResponseStarted(
                    mockUrlRequest, testUrlResponseInfo);
              }
              return null;
            })
        .when(mockUrlRequest)
        .start();
  }

  private void mockFollowRedirectSuccess() {
    doAnswer(
            invocation -> {
              dataSourceUnderTest.urlRequestCallback.onResponseStarted(
                  mockUrlRequest, testUrlResponseInfo);
              return null;
            })
        .when(mockUrlRequest)
        .followRedirect();
  }

  private void mockResponseStartFailure(int errorCode, Throwable cause) {
    doAnswer(
            invocation -> {
              dataSourceUnderTest.urlRequestCallback.onFailed(
                  mockUrlRequest,
                  createUrlResponseInfo(500), // statusCode
                  createNetworkException(errorCode, cause));
              return null;
            })
        .when(mockUrlRequest)
        .start();
  }

  private void mockReadSuccess(int position, int length) {
    final int[] positionAndRemaining = new int[] {position, length};
    doAnswer(
            invocation -> {
              if (positionAndRemaining[1] == 0) {
                dataSourceUnderTest.urlRequestCallback.onSucceeded(
                    mockUrlRequest, testUrlResponseInfo);
              } else {
                ByteBuffer inputBuffer = (ByteBuffer) invocation.getArguments()[0];
                int readLength = min(positionAndRemaining[1], inputBuffer.remaining());
                inputBuffer.put(buildTestDataBuffer(positionAndRemaining[0], readLength));
                positionAndRemaining[0] += readLength;
                positionAndRemaining[1] -= readLength;
                dataSourceUnderTest.urlRequestCallback.onReadCompleted(
                    mockUrlRequest, testUrlResponseInfo, inputBuffer);
              }
              return null;
            })
        .when(mockUrlRequest)
        .read(any(ByteBuffer.class));
  }

  private void mockReadFailure() {
    doAnswer(
            invocation -> {
              dataSourceUnderTest.urlRequestCallback.onFailed(
                  mockUrlRequest,
                  createUrlResponseInfo(500), // statusCode
                  createNetworkException(
                      /* errorCode= */ Integer.MAX_VALUE,
                      /* cause= */ new IllegalArgumentException()));
              return null;
            })
        .when(mockUrlRequest)
        .read(any(ByteBuffer.class));
  }

  private ConditionVariable buildReadStartedCondition() {
    final ConditionVariable startedCondition = new ConditionVariable();
    doAnswer(
            invocation -> {
              startedCondition.open();
              return null;
            })
        .when(mockUrlRequest)
        .read(any(ByteBuffer.class));
    return startedCondition;
  }

  private ConditionVariable buildUrlRequestStartedCondition() {
    final ConditionVariable startedCondition = new ConditionVariable();
    doAnswer(
            invocation -> {
              startedCondition.open();
              return null;
            })
        .when(mockUrlRequest)
        .start();
    return startedCondition;
  }

  private void assertNotCountedDown(CountDownLatch countDownLatch) throws InterruptedException {
    // We are asserting that another thread does not count down the latch. We therefore sleep some
    // time to give the other thread the chance to fail this test.
    Thread.sleep(50);
    assertThat(countDownLatch.getCount()).isGreaterThan(0L);
  }

  private static byte[] buildTestDataArray(int position, int length) {
    return buildTestDataBuffer(position, length).array();
  }

  public static byte[] prefixZeros(byte[] data, int requiredLength) {
    byte[] prefixedData = new byte[requiredLength];
    System.arraycopy(data, 0, prefixedData, requiredLength - data.length, data.length);
    return prefixedData;
  }

  public static byte[] suffixZeros(byte[] data, int requiredLength) {
    return Arrays.copyOf(data, requiredLength);
  }

  private static ByteBuffer buildTestDataBuffer(int position, int length) {
    ByteBuffer testBuffer = ByteBuffer.allocate(length);
    for (int i = 0; i < length; i++) {
      testBuffer.put((byte) (position + i));
    }
    testBuffer.flip();
    return testBuffer;
  }

  // Returns a copy of what is remaining in the src buffer from the current position to capacity.
  private static byte[] copyByteBufferToArray(ByteBuffer src) {
    if (src == null) {
      return null;
    }
    byte[] copy = new byte[src.remaining()];
    int index = 0;
    while (src.hasRemaining()) {
      copy[index++] = src.get();
    }
    return copy;
  }

  private static void setSystemClockInMsAndTriggerPendingMessages(long nowMs) {
    SystemClock.setCurrentTimeMillis(nowMs);
    ShadowLooper.idleMainLooper();
  }

  private static NetworkException createNetworkException(int errorCode, Throwable cause) {
    return new NetworkException("", cause) {
      @Override
      public int getErrorCode() {
        return errorCode;
      }

      @Override
      public int getCronetInternalErrorCode() {
        return errorCode;
      }

      @Override
      public boolean immediatelyRetryable() {
        return false;
      }
    };
  }
}
