// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.bazel.repository.downloader;

import static com.google.common.io.ByteStreams.toByteArray;
import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.bazel.repository.downloader.DownloaderTestUtils.makeUrl;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.common.hash.Hashing;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.bazel.repository.downloader.RetryingInputStream.Reconnector;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Random;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.Timeout;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

/** Integration tests for {@link HttpStream.Factory} and friends. */
@RunWith(JUnit4.class)
public class HttpStreamTest {

  private static final Random randoCalrissian = new Random();
  private static final byte[] data = "hello".getBytes(UTF_8);
  private static final String GOOD_CHECKSUM =
      "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
  private static final String BAD_CHECKSUM =
      "0000000000000000000000000000000000000000000000000000000000000000";
  private static final URL AURL = makeUrl("http://doodle.example");

  @Rule
  public final ExpectedException thrown = ExpectedException.none();

  @Rule
  public final Timeout globalTimeout = new Timeout(10000);

  private final HttpURLConnection connection = mock(HttpURLConnection.class);
  private final Reconnector reconnector = mock(Reconnector.class);
  private final ProgressInputStream.Factory progress = mock(ProgressInputStream.Factory.class);
  private final HttpStream.Factory streamFactory = new HttpStream.Factory(progress);

  @Before
  public void before() throws Exception {
    when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(data));
    when(progress.create(any(InputStream.class), any(URL.class), any(URL.class))).thenAnswer(
        new Answer<InputStream>() {
          @Override
          public InputStream answer(InvocationOnMock invocation) throws Throwable {
            return (InputStream) invocation.getArguments()[0];
          }
        });
  }

  @Test
  public void noChecksum_readsOk() throws Exception {
    try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
      assertThat(toByteArray(stream)).isEqualTo(data);
    }
  }

  @Test
  public void smallDataWithValidChecksum_readsOk() throws Exception {
    try (HttpStream stream = streamFactory.create(connection, AURL, GOOD_CHECKSUM, reconnector)) {
      assertThat(toByteArray(stream)).isEqualTo(data);
    }
  }

  @Test
  public void smallDataWithInvalidChecksum_throwsIOExceptionInCreatePhase() throws Exception {
    thrown.expect(IOException.class);
    thrown.expectMessage("Checksum");
    streamFactory.create(connection, AURL, BAD_CHECKSUM, reconnector);
  }

  @Test
  public void bigDataWithValidChecksum_readsOk() throws Exception {
    // at google, we know big data
    byte[] bigData = new byte[HttpStream.PRECHECK_BYTES + 70001];
    randoCalrissian.nextBytes(bigData);
    when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(bigData));
    try (HttpStream stream =
            streamFactory.create(
                connection, AURL, Hashing.sha256().hashBytes(bigData).toString(), reconnector)) {
      assertThat(toByteArray(stream)).isEqualTo(bigData);
    }
  }

  @Test
  public void bigDataWithInvalidChecksum_throwsIOExceptionAfterCreateOnEof() throws Exception {
    // the probability of this test flaking is 8.6361686e-78
    byte[] bigData = new byte[HttpStream.PRECHECK_BYTES + 70001];
    randoCalrissian.nextBytes(bigData);
    when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(bigData));
    try (HttpStream stream = streamFactory.create(connection, AURL, BAD_CHECKSUM, reconnector)) {
      thrown.expect(IOException.class);
      thrown.expectMessage("Checksum");
      ByteStreams.exhaust(stream);
      fail("Should have thrown error before close()");
    }
  }

  @Test
  public void httpServerSaidGzippedButNotGzipped_throwsZipExceptionInCreate() throws Exception {
    when(connection.getURL()).thenReturn(AURL);
    when(connection.getContentEncoding()).thenReturn("gzip");
    thrown.expect(ZipException.class);
    streamFactory.create(connection, AURL, "", reconnector);
  }

  @Test
  public void javascriptGzippedInTransit_automaticallyGunzips() throws Exception {
    when(connection.getURL()).thenReturn(AURL);
    when(connection.getContentEncoding()).thenReturn("x-gzip");
    when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(gzipData(data)));
    try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
      assertThat(toByteArray(stream)).isEqualTo(data);
    }
  }

  @Test
  public void serverSaysTarballPathIsGzipped_doesntAutomaticallyGunzip() throws Exception {
    byte[] gzData = gzipData(data);
    when(connection.getURL()).thenReturn(new URL("http://doodle.example/foo.tar.gz"));
    when(connection.getContentEncoding()).thenReturn("gzip");
    when(connection.getInputStream()).thenReturn(new ByteArrayInputStream(gzData));
    try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
      assertThat(toByteArray(stream)).isEqualTo(gzData);
    }
  }

  @Test
  public void threadInterrupted_haltsReadingAndThrowsInterrupt() throws Exception {
    final AtomicBoolean wasInterrupted = new AtomicBoolean();
    Thread thread = new Thread(
        new Runnable() {
          @Override
          public void run() {
            try (HttpStream stream = streamFactory.create(connection, AURL, "", reconnector)) {
              stream.read();
              Thread.currentThread().interrupt();
              stream.read();
              fail();
            } catch (InterruptedIOException expected) {
              wasInterrupted.set(true);
            } catch (IOException ignored) {
              // ignored
            }
          }
        });
    thread.start();
    thread.join();
    assertThat(wasInterrupted.get()).isTrue();
  }

  private static byte[] gzipData(byte[] bytes) throws IOException {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try (InputStream input = new ByteArrayInputStream(bytes);
        OutputStream output = new GZIPOutputStream(baos)) {
      ByteStreams.copy(input, output);
    }
    return baos.toByteArray();
  }
}
