// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.storage;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import com.google.android.gms.tasks.OnCanceledListener;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.RuntimeExecutionException;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.storage.UploadTask.TaskSnapshot;
import com.google.firebase.storage.internal.MockClockHelper;
import com.google.firebase.storage.internal.RobolectricThreadFix;
import com.google.firebase.storage.network.MockConnectionFactory;
import com.google.firebase.storage.network.NetworkLayerMock;
import com.google.firebase.storage.network.ResumableUploadCancelRequest;
import com.google.firebase.testing.FirebaseAppRule;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowNetworkInfo;

/** Tests for {@link FirebaseStorage}. */
@SuppressWarnings("ConstantConditions")
@RunWith(RobolectricTestRunner.class)
@Config(sdk = Build.VERSION_CODES.LOLLIPOP_MR1)
public class UploadTest {

  private static final String TEST_ASSET_ROOT = "assets/";

  @Rule public RetryRule retryRule = new RetryRule(3);
  @Rule public final FirebaseAppRule firebaseAppRule = new FirebaseAppRule();

  @Rule public TemporaryFolder folder = new TemporaryFolder();

  private FirebaseApp app;

  @Before
  public void setUp() throws Exception {
    RobolectricThreadFix.install();
    MockClockHelper.install();
    app = TestUtil.createApp();
  }

  @After
  public void tearDown() {
    FirebaseStorageComponent component = app.get(FirebaseStorageComponent.class);
    component.clearInstancesForTesting();
  }

  @Test
  public void smallTextUpload() throws Exception {
    System.out.println("Starting test smallTextUpload.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("smallTextUpload", true);
    Task<StringBuilder> task = TestUploadHelper.smallTextUpload();

    TestUtil.await(task);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("smallTextUpload", task.getResult().toString());
  }

  @Test
  public void cantUploadToRoot() throws Exception {
    System.out.println("Starting test cantUploadToRoot.");

    StorageReference storage =
        FirebaseStorage.getInstance().getReferenceFromUrl("gs://fooey.appspot.com");

    AtomicReference<Exception> taskException = new AtomicReference<>();

    final UploadTask task = storage.putBytes(new byte[] {});

    try {
      task.getResult();
      Assert.fail();
    } catch (IllegalStateException ignore) {
      // Task is not yet done.
    }

    Assert.assertNull(task.getException());

    task.addOnFailureListener(
        (exception) -> {
          Assert.assertEquals(
              "Cannot upload to getRoot. You should upload to a storage location such as "
                  + ".getReference('image.png').putFile...",
              exception.getCause().getMessage());
          taskException.set(exception);
        });

    // TODO(mrschmidt): Lower the timeout
    TestUtil.await(task, 300, TimeUnit.SECONDS);

    try {
      task.getResult();
      Assert.fail();
    } catch (RuntimeExecutionException e) {
      Assert.assertEquals(taskException.get().getCause(), e.getCause().getCause());
    }

    try {
      task.getResult(StorageException.class);
      Assert.fail();
    } catch (StorageException e) {
      Assert.assertEquals(taskException.get().getCause(), e.getCause());
    }

    Assert.assertEquals(taskException.get().getCause(), task.getException().getCause());
  }

  @Test
  public void addAndRemoveListeners() throws Exception {
    System.out.println("Starting test addAndRemoveListeners.");

    StorageReference storage =
        FirebaseStorage.getInstance().getReferenceFromUrl("gs://fooey.appspot.com/listeners.txt");

    ActivityController<Activity> activityController =
        Robolectric.buildActivity(Activity.class).create();
    Activity activity = activityController.get();
    Executor executor = Executors.newSingleThreadExecutor();
    Uri sourceFile = Uri.parse("file://dev/random");
    StorageMetadata metadata = new StorageMetadata.Builder().build();

    List<UploadTask> pendingTasks = new ArrayList<>();

    UploadTask task = storage.putBytes(new byte[] {});
    pendingTasks.add(task);
    task.cancel();

    OnPausedListener<TaskSnapshot> pausedListener = snapshot -> {};

    task.addOnPausedListener(pausedListener);
    task.removeOnPausedListener(pausedListener);
    task.addOnPausedListener(executor, pausedListener);
    task.removeOnPausedListener(pausedListener);
    task.addOnPausedListener(activity, pausedListener);
    task.removeOnPausedListener(pausedListener);

    OnProgressListener<TaskSnapshot> progessListener = snapshot -> {};

    task = storage.putBytes(new byte[] {}, metadata);
    pendingTasks.add(task);
    task.cancel();

    task.addOnProgressListener(progessListener);
    task.removeOnProgressListener(progessListener);
    task.addOnProgressListener(executor, progessListener);
    task.removeOnProgressListener(progessListener);
    task.addOnProgressListener(activity, progessListener);
    task.removeOnProgressListener(progessListener);

    task = storage.putFile(sourceFile);
    pendingTasks.add(task);
    task.cancel();

    OnSuccessListener<TaskSnapshot> successListener = snapshot -> {};

    task.addOnSuccessListener(successListener);
    task.removeOnSuccessListener(successListener);
    task.addOnSuccessListener(executor, successListener);
    task.removeOnSuccessListener(successListener);
    task.addOnSuccessListener(activity, successListener);
    task.removeOnSuccessListener(successListener);

    task = storage.putFile(sourceFile, metadata);
    pendingTasks.add(task);
    task.cancel();

    OnCanceledListener cancelListener = () -> {};

    task = storage.putFile(sourceFile, metadata, null);
    pendingTasks.add(task);
    task.cancel();

    task.addOnCanceledListener(cancelListener);
    task.removeOnCanceledListener(cancelListener);
    task.addOnCanceledListener(executor, cancelListener);
    task.removeOnCanceledListener(cancelListener);
    task.addOnCanceledListener(activity, cancelListener);
    task.removeOnCanceledListener(cancelListener);

    OnCompleteListener<TaskSnapshot> completeListener = snapshot -> {};

    task = storage.putFile(sourceFile, metadata, null);
    pendingTasks.add(task);
    task.cancel();

    task.addOnCompleteListener(completeListener);
    task.removeOnCompleteListener(completeListener);
    task.addOnCompleteListener(executor, completeListener);
    task.removeOnCompleteListener(completeListener);
    task.addOnCompleteListener(activity, completeListener);
    task.removeOnCompleteListener(completeListener);

    OnFailureListener failureListener = exception -> {};

    task = storage.putFile(sourceFile, metadata, null);
    pendingTasks.add(task);
    task.cancel();

    task.addOnFailureListener(failureListener);
    task.removeOnFailureListener(failureListener);
    task.addOnFailureListener(executor, failureListener);
    task.removeOnFailureListener(failureListener);
    task.addOnFailureListener(activity, failureListener);
    task.removeOnFailureListener(failureListener);

    activityController.stop();

    TestUtil.await(Tasks.whenAll(pendingTasks), 5, TimeUnit.SECONDS);

    Assert.assertTrue(
        StorageTaskManager.getInstance().getUploadTasksUnder(storage.getParent()).isEmpty());
  }

  @Test
  public void cancelledUpload() throws Exception {
    System.out.println("Starting test cancelledUpload.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("cancelledUpload", true);
    Task<StringBuilder> task = TestUploadHelper.byteUploadCancel();

    // TODO(mrschmidt): Lower the timeout
    TestUtil.await(task, 500, TimeUnit.SECONDS);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("cancelledUpload", task.getResult().toString());
  }

  @Test
  public void uploadWithSpace() throws Exception {
    System.out.println("Starting test uploadWithSpace.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("uploadWithSpace", true);
    StorageReference storage =
        FirebaseStorage.getInstance().getReference().child("hello world.txt");
    Task<StringBuilder> task = TestUploadHelper.byteUpload(storage);

    TestUtil.await(task);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("uploadWithSpace", task.getResult().toString());
  }

  @Test
  public void smallTextUpload2() throws Exception {
    System.out.println("Starting test smallTextUpload2.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("smallTextUpload2", true);

    Task<StringBuilder> task = TestUploadHelper.smallTextUpload2();

    TestUtil.await(task);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("smallTextUpload2", task.getResult().toString());
  }

  @Test
  public void fileUpload() throws Exception {
    System.out.println("Starting test fileUpload.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("fileUpload", true);

    String filename = TEST_ASSET_ROOT + "image.jpg";
    ClassLoader classLoader = UploadTest.class.getClassLoader();
    InputStream imageStream = classLoader.getResourceAsStream(filename);
    Uri sourceFile = Uri.parse("file://" + filename);

    ContentResolver resolver =
        RuntimeEnvironment.application.getApplicationContext().getContentResolver();
    Shadows.shadowOf(resolver).registerInputStream(sourceFile, imageStream);

    Task<StringBuilder> task = TestUploadHelper.fileUpload(sourceFile, "image.jpg");

    TestUtil.await(task, 5, TimeUnit.SECONDS);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("fileUpload", task.getResult().toString());
  }

  @Test
  public void emptyUpload() throws Exception {
    System.out.println("Starting test emptyUpload.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("emptyUpload", true);

    String filename = TEST_ASSET_ROOT + "empty.dat";
    ClassLoader classLoader = UploadTest.class.getClassLoader();
    InputStream imageStream = classLoader.getResourceAsStream(filename);
    Uri sourceFile = Uri.parse("file://" + filename);

    ContentResolver resolver =
        RuntimeEnvironment.application.getApplicationContext().getContentResolver();
    Shadows.shadowOf(resolver).registerInputStream(sourceFile, imageStream);

    Task<StringBuilder> task = TestUploadHelper.fileUpload(sourceFile, "empty.dat");

    TestUtil.await(task, 5, TimeUnit.SECONDS);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("emptyUpload", task.getResult().toString());
  }

  @Test
  public void unicodeUpload() throws Exception {
    System.out.println("Starting test unicodeUpload.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("uploadWithUnicode", true);
    StorageReference storage = FirebaseStorage.getInstance().getReference().child("\\%:😊");
    Task<StringBuilder> task = TestUploadHelper.byteUpload(storage);

    TestUtil.await(task);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("uploadWithUnicode", task.getResult().toString());
  }

  @Test
  public void fileUploadWithPauseCancel() throws Exception {
    System.out.println("Starting test fileUploadWithPauseCancel.");

    ResumableUploadCancelRequest.cancelCalled = false;

    MockConnectionFactory factory =
        NetworkLayerMock.ensureNetworkMock("fileUploadWithPauseCancel", true);

    factory.setPauseRecord(4);

    String filename = TEST_ASSET_ROOT + "image.jpg";
    ClassLoader classLoader = UploadTest.class.getClassLoader();
    InputStream imageStream = classLoader.getResourceAsStream(filename);
    Uri sourceFile = Uri.parse("file://" + filename);

    ContentResolver resolver =
        RuntimeEnvironment.application.getApplicationContext().getContentResolver();
    Shadows.shadowOf(resolver).registerInputStream(sourceFile, imageStream);

    Task<StringBuilder> task =
        TestUploadHelper.fileUploadWithPauseCancel(factory.getSemaphore(), sourceFile);

    // This is 20 seconds due to a fairness bug where resumed tasks can be put at the end.
    TestUtil.await(task, 20, TimeUnit.SECONDS);

    TestUtil.verifyTaskStateChanges("fileUploadWithPauseCancel", task.getResult().toString());
    Assert.assertTrue(ResumableUploadCancelRequest.cancelCalled);
  }

  @Test
  public void fileUploadWithPauseResume() throws Exception {
    System.out.println("Starting test fileUploadWithPauseResume.");

    MockConnectionFactory factory =
        NetworkLayerMock.ensureNetworkMock("fileUploadWithPauseResume", true);

    factory.setPauseRecord(4);

    String filename = TEST_ASSET_ROOT + "image.jpg";
    ClassLoader classLoader = UploadTest.class.getClassLoader();
    InputStream imageStream = classLoader.getResourceAsStream(filename);
    Uri sourceFile = Uri.parse("file://" + filename);

    ContentResolver resolver =
        RuntimeEnvironment.application.getApplicationContext().getContentResolver();
    Shadows.shadowOf(resolver).registerInputStream(sourceFile, imageStream);

    Task<StringBuilder> task =
        TestUploadHelper.fileUploadWithPauseResume(factory.getSemaphore(), sourceFile);

    // This is 20 seconds due to a fairness bug where resumed tasks can be put at the end.
    TestUtil.await(task, 20, TimeUnit.SECONDS);

    TestUtil.verifyTaskStateChanges("fileUploadWithPauseResume", task.getResult().toString());
  }

  @Test
  public void fileUploadWithQueueCancel() throws Exception {
    System.out.println("Starting test fileUploadWithQueueCancel.");

    ResumableUploadCancelRequest.cancelCalled = false;

    final StringBuilder taskOutput = new StringBuilder();
    NetworkLayerMock.ensureNetworkMock("fileUploadWithPauseCancel", true);

    String filename = TEST_ASSET_ROOT + "image.jpg";
    ClassLoader classLoader = UploadTest.class.getClassLoader();
    InputStream imageStream = classLoader.getResourceAsStream(filename);
    Uri sourceFile = Uri.parse("file://" + filename);

    ContentResolver resolver =
        RuntimeEnvironment.application.getApplicationContext().getContentResolver();
    Shadows.shadowOf(resolver).registerInputStream(sourceFile, imageStream);

    Task<Void> task = TestUploadHelper.fileUploadQueuedCancel(taskOutput, sourceFile);

    TestUtil.await(task, 2, TimeUnit.SECONDS);

    TestUtil.verifyTaskStateChanges("fileUploadWithQueueCancel", taskOutput.toString());
    Assert.assertFalse(ResumableUploadCancelRequest.cancelCalled);
  }

  @Test
  public void adaptiveChunking() throws Exception {
    System.out.println("Starting test adaptiveChunking.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("adaptiveChunking", false);

    Task<StringBuilder> task = TestUploadHelper.adaptiveChunking();

    TestUtil.await(task);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("adaptiveChunking", task.getResult().toString());
  }

  @Test
  public void fileUploadRecovery() throws Exception {
    System.out.println("Starting test fileUploadRecovery.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("fileUploadRecovery", false);

    String filename = TEST_ASSET_ROOT + "flubbertest.jpg";
    ClassLoader classLoader = UploadTest.class.getClassLoader();
    InputStream imageStream = classLoader.getResourceAsStream(filename);
    Uri sourceFile = Uri.parse("file://" + filename);

    ContentResolver resolver =
        RuntimeEnvironment.application.getApplicationContext().getContentResolver();
    Shadows.shadowOf(resolver).registerInputStream(sourceFile, imageStream);

    Task<StringBuilder> task = TestUploadHelper.fileUpload(sourceFile, "flubbertest.jpg");

    TestUtil.await(task, 5, TimeUnit.SECONDS);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("fileUploadRecovery", task.getResult().toString());
  }

  @Test
  public void fileUploadNoRecovery() throws Exception {
    System.out.println("Starting test fileUploadNoRecovery.");

    MockConnectionFactory factory =
        NetworkLayerMock.ensureNetworkMock("fileUploadNoRecovery", false);

    String filename = TEST_ASSET_ROOT + "flubbertest.jpg";
    ClassLoader classLoader = UploadTest.class.getClassLoader();
    InputStream imageStream = classLoader.getResourceAsStream(filename);
    Uri sourceFile = Uri.parse("file://" + filename);

    ContentResolver resolver =
        RuntimeEnvironment.application.getApplicationContext().getContentResolver();
    Shadows.shadowOf(resolver).registerInputStream(sourceFile, imageStream);

    Task<StringBuilder> task = TestUploadHelper.fileUpload(sourceFile, "flubbertest.jpg");

    TestUtil.await(task, 5, TimeUnit.SECONDS);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("fileUploadNoRecovery", task.getResult().toString());
  }

  @Test
  public void streamUploadWithInterruptions() throws InterruptedException {
    System.out.println("Starting test streamUploadWithInterruptions.");

    MockConnectionFactory factory =
        NetworkLayerMock.ensureNetworkMock("streamUploadWithInterruptions", false);

    Task<StringBuilder> task = TestUploadHelper.streamUploadWithInterruptions();

    TestUtil.await(task, 5, TimeUnit.SECONDS);

    factory.verifyOldMock();
    TestUtil.verifyTaskStateChanges("streamUploadWithInterruptions", task.getResult().toString());
  }

  @Test
  public void removeListeners() throws InterruptedException {
    System.out.println("Starting test removeListeners.");

    NetworkLayerMock.ensureNetworkMock("streamDownload", true);

    StorageReference storage =
        FirebaseStorage.getInstance().getReferenceFromUrl("gs://fooey.appspot.com/image.jpg");
    final Semaphore semaphore = new Semaphore(0);

    StreamDownloadTask task =
        storage.getStream(
            (state, stream) -> {
              try {
                semaphore.tryAcquire(3, TimeUnit.SECONDS);
              } catch (InterruptedException e) {
                e.printStackTrace();
                Assert.fail();
              }
            });

    OnCompleteListener<StreamDownloadTask.TaskSnapshot> completeListener = ignored -> {};
    OnSuccessListener<StreamDownloadTask.TaskSnapshot> successListener = ignored -> {};
    OnProgressListener<StreamDownloadTask.TaskSnapshot> progressListener = ignored -> {};
    OnPausedListener<StreamDownloadTask.TaskSnapshot> pausedListener = ignored -> {};
    OnFailureListener failureListener = ignored -> {};
    OnCanceledListener canceledListener = () -> {};

    task.addOnCompleteListener(completeListener)
        .addOnSuccessListener(successListener)
        .addOnProgressListener(progressListener)
        .addOnFailureListener(failureListener)
        .addOnPausedListener(pausedListener)
        .addOnCanceledListener(canceledListener);

    Assert.assertEquals(1, task.completeListener.getListenerCount());
    Assert.assertEquals(1, task.successManager.getListenerCount());
    Assert.assertEquals(1, task.failureManager.getListenerCount());
    Assert.assertEquals(1, task.progressManager.getListenerCount());
    Assert.assertEquals(1, task.cancelManager.getListenerCount());
    Assert.assertEquals(1, task.pausedManager.getListenerCount());

    task.removeOnCompleteListener(completeListener)
        .removeOnSuccessListener(successListener)
        .removeOnProgressListener(progressListener)
        .removeOnFailureListener(failureListener)
        .removeOnPausedListener(pausedListener)
        .removeOnCanceledListener(canceledListener);

    Assert.assertEquals(0, task.completeListener.getListenerCount());
    Assert.assertEquals(0, task.successManager.getListenerCount());
    Assert.assertEquals(0, task.failureManager.getListenerCount());
    Assert.assertEquals(0, task.progressManager.getListenerCount());
    Assert.assertEquals(0, task.cancelManager.getListenerCount());
    Assert.assertEquals(0, task.pausedManager.getListenerCount());
    semaphore.release();
  }

  @Test
  public void badConnectivitySmallUpload() throws Exception {
    System.out.println("Starting test badConnectivitySmallUpload.");

    MockConnectionFactory factory = NetworkLayerMock.ensureNetworkMock("smallTextUpload", true);

    ConnectivityManager connectivityManager =
        (ConnectivityManager)
            RuntimeEnvironment.application.getSystemService(Context.CONNECTIVITY_SERVICE);
    final NetworkInfo originalNetwork = connectivityManager.getActiveNetworkInfo();

    try {
      Shadows.shadowOf(connectivityManager)
          .setActiveNetworkInfo(
              ShadowNetworkInfo.newInstance(
                  NetworkInfo.DetailedState.DISCONNECTED,
                  ConnectivityManager.TYPE_MOBILE,
                  0,
                  false,
                  NetworkInfo.State.DISCONNECTED));
      // after 10 seconds of simulated time, turn the network back on.
      MockClockHelper.install(
          new MockClockHelper() {
            @Override
            public void advance(int millis) {
              super.advance(millis);
              if (this.currentTimeMillis() > 10000) {
                Shadows.shadowOf(connectivityManager).setActiveNetworkInfo(originalNetwork);
              }
            }
          });

      Task<StringBuilder> task = TestUploadHelper.smallTextUpload();

      // TODO(mrschmidt): Lower the timeout
      TestUtil.await(task, 300, TimeUnit.SECONDS);

      factory.verifyOldMock();
      TestUtil.verifyTaskStateChanges("smallTextUpload", task.getResult().toString());
    } finally {
      MockClockHelper.install(new MockClockHelper());
      Shadows.shadowOf(connectivityManager).setActiveNetworkInfo(originalNetwork);
    }
  }
}