/*
 * Copyright 2012 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.api.client.googleapis.media;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.LowLevelHttpRequest;
import com.google.api.client.http.LowLevelHttpResponse;
import com.google.api.client.testing.http.MockHttpTransport;
import com.google.api.client.testing.http.MockLowLevelHttpRequest;
import com.google.api.client.testing.http.MockLowLevelHttpResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import junit.framework.TestCase;

/**
 * Tests {@link MediaHttpDownloader}.
 *
 * @author [email protected] (Ravi Mistry)
 */
public class MediaHttpDownloaderTest extends TestCase {

  private static final String TEST_REQUEST_URL = "http://www.test.com/request/url?alt=media";
  private static final int TEST_CHUNK_SIZE = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;

  private static class MediaTransport extends MockHttpTransport {

    int lowLevelExecCalls;
    int contentLength;
    int bytesDownloaded;
    int lastBytePos = -1;
    boolean testServerError;
    boolean testClientError;
    boolean directDownloadEnabled;
    boolean contentLengthIncluded;

    protected MediaTransport(int contentLength) {
      this.contentLength = contentLength;
      this.contentLengthIncluded = true;
    }

    protected MediaTransport(int contentLength, boolean contentLengthIncluded) {
      this.contentLength = contentLength;
      this.contentLengthIncluded = contentLengthIncluded;
    }

    @Override
    public LowLevelHttpRequest buildRequest(String name, String url) {
      assertEquals(TEST_REQUEST_URL, url);

      return new MockLowLevelHttpRequest() {
          @Override
        public LowLevelHttpResponse execute() {
          lowLevelExecCalls++;
          MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();

          if (directDownloadEnabled) {
            if (bytesDownloaded != 0) {
              if (lastBytePos == -1) {
                assertEquals("bytes=" + bytesDownloaded + "-", getFirstHeaderValue("Range"));
              } else {
                assertEquals(
                    "bytes=" + bytesDownloaded + "-" + lastBytePos, getFirstHeaderValue("Range"));
              }
            }
            if (testServerError && lowLevelExecCalls == 1) {
              // send a server error in the 1st request
              response.setStatusCode(500);
              return response;
            }
            response.setStatusCode(200);
            if (contentLengthIncluded) {
              response.addHeader("Content-Length", String.valueOf(contentLength));
            }
            response.setContent(
                new ByteArrayInputStream(new byte[contentLength - bytesDownloaded]));
            return response;
          }

          // Assert that the required headers are set.
          long currentRequestLastBytePos = bytesDownloaded + TEST_CHUNK_SIZE - 1;
          if (lastBytePos != -1) {
            currentRequestLastBytePos = Math.min(lastBytePos, currentRequestLastBytePos);
          }
          assertEquals("bytes=" + bytesDownloaded + "-" + currentRequestLastBytePos,
              getFirstHeaderValue("Range"));

          if (testServerError && lowLevelExecCalls == 2) {
            // Send a server error in the 2nd request.
            response.setStatusCode(500);
            return response;
          }
          if (testClientError) {
            // Return a 404.
            response.setStatusCode(404);
            return response;
          }

          response.setStatusCode(206);
          int upper;
          if (lastBytePos != -1) {
            upper = Math.min(lastBytePos, contentLength) - 1;
          } else {
            upper = Math.min(bytesDownloaded + TEST_CHUNK_SIZE, contentLength) - 1;
          }
          response.addHeader(
              "Content-Range", "bytes " + bytesDownloaded + "-" + upper + "/" + contentLength);
          int bytesDownloadedCur = upper - bytesDownloaded + 1;
          response.setContent(new ByteArrayInputStream(new byte[bytesDownloadedCur]));
          bytesDownloaded += bytesDownloadedCur;
          return response;
        }
      };
    }
  }

  private static class ProgressListenerWithTwoDownloadCalls
      implements
        MediaHttpDownloaderProgressListener {

    int progressListenerCalls;

    public ProgressListenerWithTwoDownloadCalls() {
    }

    public void progressChanged(MediaHttpDownloader downloader) {
      progressListenerCalls++;

      switch (downloader.getDownloadState()) {
        case MEDIA_IN_PROGRESS:
          // Assert that the 1st call is media in progress and check the progress percent.
          assertTrue(progressListenerCalls == 1);
          assertEquals(0.5, downloader.getProgress());
          break;
        case MEDIA_COMPLETE:
          // Assert that the 2nd call is media complete.
          assertEquals(2, progressListenerCalls);
          assertEquals(1.0, downloader.getProgress());
          break;
        default:
          // TODO(b/18683919): go/enum-switch-lsc
      }
    }
  }

  public void testDownloadOneCallHalfChunkSize() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE / 2;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(contentLength, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testDownloadOneCallMaxChunkSize() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    try {
      downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
      assertEquals(TEST_CHUNK_SIZE, outputStream.size());
    } finally {
      outputStream.close();
    }

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testSetBytesDownloaded() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.bytesDownloaded = contentLength - 10000;
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setBytesDownloaded(contentLength - 10000);

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(10000, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testDownloadMultipleCallsMaxChunkSize() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE * 3;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(contentLength, outputStream.size());

    // There should be 3 download calls made.
    assertEquals(3, fakeTransport.lowLevelExecCalls);
  }

  public void testDownloadProgressListener() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE * 2;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setProgressListener(new ProgressListenerWithTwoDownloadCalls());
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
  }

  public void testDownloadServerErrorWithBackOffEnabled() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE * 2;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.testServerError = true;
    MediaHttpDownloader downloader = new MediaHttpDownloader(
        fakeTransport, new MediaHttpUploaderTest.ZeroBackOffRequestInitializer());
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);

    // There should be 3 calls made: 1 download request with server error and 2 successful download
    // requests.
    assertEquals(3, fakeTransport.lowLevelExecCalls);
  }

  public void testDownloadServerErrorWithBackOffDisabled() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE * 2;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.testServerError = true;
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    try {
      downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
      fail("Expected " + HttpResponseException.class);
    } catch (HttpResponseException e) {
      // Expected
    }

    // There should be 2 calls made: 1 successful download request and 1 download request with
    // server error.
    assertEquals(2, fakeTransport.lowLevelExecCalls);
  }

  public void testDownloadClientError() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE * 2;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.testClientError = true;
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    try {
      downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
      fail("Expected " + HttpResponseException.class);
    } catch (HttpResponseException e) {
      // Expected
    }

    // There should be only 1 call made: 1 download request that returned a 404.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testDirectMediaDownload() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.directDownloadEnabled = true;
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setDirectDownloadEnabled(true);
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(TEST_CHUNK_SIZE, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testSetBytesDownloadedWithIllegalArguments() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    try {
      downloader.setBytesDownloaded(-1);
      fail("Expected " + IllegalArgumentException.class);
    } catch (IllegalArgumentException e) {
      // Expected.
    }

    // Anything >= 0 should be accepted.
    downloader.setBytesDownloaded(0);
    downloader.setBytesDownloaded(1);
  }

  public void testSetContentRangeWithIllegalArguments() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);

    try {
      downloader.setContentRange(-1, 0);
      fail("Expected " + IllegalArgumentException.class);
    } catch (IllegalArgumentException e) {
      // Expected.
    }

    try {
      downloader.setContentRange(1, 0);
      fail("Expected " + IllegalArgumentException.class);
    } catch (IllegalArgumentException e) {
      // Expected.
    }

    try {
      downloader.setContentRange(200, 199);
      fail("Expected " + IllegalArgumentException.class);
    } catch (IllegalArgumentException e) {
      // Expected.
    }

    // The following should be accepted
    downloader.setContentRange(0, 0);
    downloader.setContentRange(0, 1);
    downloader.setContentRange(1, 1);
    downloader.setContentRange(199, 200);
    downloader.setContentRange(200, 200);
  }

  public void testSetBytesDownloadedWithDirectDownload() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.directDownloadEnabled = true;
    fakeTransport.bytesDownloaded = contentLength - 10000;
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setDirectDownloadEnabled(true);
    downloader.setBytesDownloaded(contentLength - 10000);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(10000, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testSetBytesDownloadedWithDirectDownloadAndContentLengthNull() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength,false);
    fakeTransport.directDownloadEnabled = true;
    fakeTransport.bytesDownloaded = contentLength - 10000;
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setDirectDownloadEnabled(true);
    downloader.setBytesDownloaded(contentLength - 10000);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(10000, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testSetContentRangeWithResumableDownload() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.bytesDownloaded = contentLength - 10000;
    fakeTransport.lastBytePos = contentLength;

    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setContentRange(contentLength - 10000, contentLength);

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(10000, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testSetContentRangeFromStartWithResumableDownload() throws Exception {
    MediaTransport fakeTransport = new MediaTransport(MediaHttpDownloader.MAXIMUM_CHUNK_SIZE);
    fakeTransport.bytesDownloaded = 0;
    fakeTransport.lastBytePos = 1024;

    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setContentRange(0, 1024);

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(1024, downloader.getLastBytePosition());
    assertEquals(1024, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testSetContentRangeFromMiddletWithResumableDownload() throws Exception {
    MediaTransport fakeTransport = new MediaTransport(MediaHttpDownloader.MAXIMUM_CHUNK_SIZE);
    fakeTransport.bytesDownloaded = 512;
    fakeTransport.lastBytePos = 1024;

    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setContentRange(512, 1024);

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(1024, downloader.getLastBytePosition());
    assertEquals(512, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testSetContentRangeWithDirectDownload() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE;
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.directDownloadEnabled = true;
    fakeTransport.bytesDownloaded = contentLength - 10000;
    fakeTransport.lastBytePos = contentLength;

    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    downloader.setDirectDownloadEnabled(true);
    downloader.setContentRange(contentLength - 10000, contentLength);

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
    assertEquals(10000, outputStream.size());

    // There should be 1 download call made.
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }

  public void testDirectDownloadServerErrorWithBackOffEnabled() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE * 2;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.directDownloadEnabled = true;
    fakeTransport.testServerError = true;
    MediaHttpDownloader downloader = new MediaHttpDownloader(
        fakeTransport, new MediaHttpUploaderTest.ZeroBackOffRequestInitializer());
    downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);

    // should be 2 calls made: 1 download request w/server error and 1 successful download request
    assertEquals(2, fakeTransport.lowLevelExecCalls);
  }

  public void testDirectDownloadServerErrorWithBackOffDisabled() throws Exception {
    int contentLength = MediaHttpDownloader.MAXIMUM_CHUNK_SIZE * 2;
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    MediaTransport fakeTransport = new MediaTransport(contentLength);
    fakeTransport.directDownloadEnabled = true;
    fakeTransport.testServerError = true;
    MediaHttpDownloader downloader = new MediaHttpDownloader(fakeTransport, null);
    try {
      downloader.download(new GenericUrl(TEST_REQUEST_URL), outputStream);
      fail("Expected " + HttpResponseException.class);
    } catch (HttpResponseException e) {
      // Expected
    }

    // should be 1 call made: 1 download request with server error
    assertEquals(1, fakeTransport.lowLevelExecCalls);
  }
}