/*
 * Copyright (C) 2018 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.android.tradefed.util;

import com.android.tradefed.build.BuildRetrievalError;

import com.google.api.client.googleapis.batch.BatchCallback;
import com.google.api.client.googleapis.batch.BatchRequest;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.InputStreamContent;
import com.google.api.services.storage.Storage;
import com.google.api.services.storage.Storage.Objects.List;
import com.google.api.services.storage.model.Objects;
import com.google.api.services.storage.model.StorageObject;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Paths;
import java.util.Collections;

/** {@link GCSFileDownloader} functional test. */
@RunWith(JUnit4.class)
public class GCSFileDownloaderFuncTest {

    private static final String BUCKET_NAME = "tradefed_function_test";
    private static final String FILE_NAME1 = "a_host_config.xml";
    private static final String FILE_NAME2 = "file2.txt";
    private static final String FILE_NAME3 = "file3.txt";
    private static final String FILE_NAME4 = "file4.txt";
    private static final String FILE_NAME5 = "file5.txt";
    private static final String FILE_NAME6 = "file6.txt";
    private static final String EMPTY_FILE = "empty.txt";
    private static final String FOLDER_NAME1 = "folder1";
    private static final String FOLDER_NAME2 = "folder2";
    private static final String FOLDER_NAME3 = "folder3";
    private static final String FOLDER_NAME4 = "folder4";
    private static final String FILE_CONTENT = "Hello World!";

    private GCSFileDownloader mDownloader;
    private String mRemoteRoot;
    private File mLocalRoot;
    private Storage mStorage;

    private static void createFile(
            Storage storage, String content, String bucketName, String... pathSegs)
            throws IOException {
        String path = String.join("/", pathSegs);
        StorageObject object = new StorageObject();
        object.setName(path);
        storage.objects()
                .insert(
                        bucketName,
                        object,
                        new InputStreamContent(null, new ByteArrayInputStream(content.getBytes())))
                .execute();
    }

    @Before
    public void setUp() throws IOException {
        File tempFile =
                FileUtil.createTempFile(GCSFileDownloaderFuncTest.class.getSimpleName(), "");
        mRemoteRoot = tempFile.getName();
        FileUtil.deleteFile(tempFile);
        mDownloader =
                new GCSFileDownloader() {

                    @Override
                    protected File createTempFile(String remoteFilePath, File rootDir)
                            throws BuildRetrievalError {
                        try {
                            File tmpFile =
                                    FileUtil.createTempFileForRemote(remoteFilePath, mLocalRoot);
                            tmpFile.delete();
                            return tmpFile;
                        } catch (IOException e) {
                            throw new BuildRetrievalError(e.getMessage(), e);
                        }
                    }
                };
        mStorage =
                mDownloader.getStorage(
                        Collections.singleton(
                                "https://www.googleapis.com/auth/devstorage.read_write"));
        createFile(mStorage, FILE_CONTENT, BUCKET_NAME, mRemoteRoot, FILE_NAME1);
        createFile(mStorage, FILE_NAME2, BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FILE_NAME2);
        createFile(mStorage, FILE_NAME3, BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FILE_NAME3);
        // Create a special case condition where folder name is also a file name.
        createFile(mStorage, FILE_NAME3, BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FOLDER_NAME2);
        createFile(
                mStorage,
                FILE_NAME4,
                BUCKET_NAME,
                mRemoteRoot,
                FOLDER_NAME1,
                FOLDER_NAME2,
                FILE_NAME4);
        // folder3 is a special file, it's a text file with 0 size.
        // it is the same as a folder created from UI.
        createFile(mStorage, "", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FOLDER_NAME3 + '/');
        createFile(
                mStorage,
                FILE_NAME5,
                BUCKET_NAME,
                mRemoteRoot,
                FOLDER_NAME1,
                FOLDER_NAME3,
                FILE_NAME5);
        // folder4 is a special file, it's a text file with 0 size.
        // it is the same as a folder created from UI.
        createFile(
                mStorage,
                "",
                BUCKET_NAME,
                mRemoteRoot,
                FOLDER_NAME1,
                FOLDER_NAME3,
                FOLDER_NAME4 + "/");
        createFile(
                mStorage,
                FILE_NAME6,
                BUCKET_NAME,
                mRemoteRoot,
                FOLDER_NAME1,
                FOLDER_NAME3,
                FOLDER_NAME4,
                FILE_NAME6);

        createFile(mStorage, "", BUCKET_NAME, mRemoteRoot, EMPTY_FILE);
        mLocalRoot = FileUtil.createTempDir(GCSFileDownloaderFuncTest.class.getSimpleName());
    }

    @After
    public void tearDown() throws IOException {
        FileUtil.recursiveDelete(mLocalRoot);
        String pageToken = null;
        BatchRequest batchRequest = mStorage.batch();

        while (true) {
            List listOperation = mStorage.objects().list(BUCKET_NAME).setPrefix(mRemoteRoot);
            if (pageToken == null) {
                listOperation.setPageToken(pageToken);
            }
            Objects objects = listOperation.execute();
            for (StorageObject object : objects.getItems()) {
                batchRequest.queue(
                        mStorage.objects().delete(BUCKET_NAME, object.getName()).buildHttpRequest(),
                        Void.class,
                        IOException.class,
                        new BatchCallback<Void, IOException>() {
                            @Override
                            public void onSuccess(Void arg0, HttpHeaders arg1) throws IOException {}

                            @Override
                            public void onFailure(IOException e, HttpHeaders arg1)
                                    throws IOException {
                                throw e;
                            }
                        });
            }
            pageToken = objects.getNextPageToken();
            if (pageToken == null) {
                batchRequest.execute();
                return;
            }
        }
    }

    @Test
    public void testDownloadFile_streamOutput() throws Exception {
        InputStream inputStream =
                mDownloader.downloadFile(BUCKET_NAME, mRemoteRoot + "/" + FILE_NAME1);
        String content = StreamUtil.getStringFromStream(inputStream);
        Assert.assertEquals(FILE_CONTENT, content);
        inputStream.reset();
    }

    @Test
    public void testDownloadFile_streamOutput_notExist() throws Exception {
        try {
            mDownloader.downloadFile(BUCKET_NAME, mRemoteRoot + "/" + "non_exist_file");
            Assert.fail("Should throw IOException.");
        } catch (IOException e) {
            // Expect IOException
        }
    }

    @Test
    public void testGetRemoteFileMetaData() throws Exception {
        String filename = mRemoteRoot + "/" + FILE_NAME1;
        StorageObject object = mDownloader.getRemoteFileMetaData(BUCKET_NAME, filename);
        Assert.assertEquals(filename, object.getName());
    }

    @Test
    public void testGetRemoteFileMetaData_notExist() throws Exception {
        String filename = mRemoteRoot + "/" + "not_exist";
        StorageObject object = mDownloader.getRemoteFileMetaData(BUCKET_NAME, filename);
        Assert.assertNull(object);
    }

    @Test
    public void testIsRemoteFolder() throws Exception {
        Assert.assertFalse(
                mDownloader.isRemoteFolder(
                        BUCKET_NAME, Paths.get(mRemoteRoot, FILE_NAME1).toString()));
        Assert.assertTrue(
                mDownloader.isRemoteFolder(
                        BUCKET_NAME, Paths.get(mRemoteRoot, FOLDER_NAME1).toString()));
    }

    @Test
    public void testDownloadFile() throws Exception {
        File localFile =
                mDownloader.downloadFile(
                        String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FILE_NAME1));
        String content = FileUtil.readStringFromFile(localFile);
        Assert.assertEquals(FILE_CONTENT, content);
    }

    @Test
    public void testDownloadFile_nonExist() throws Exception {
        try {
            mDownloader.downloadFile(
                    String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, "non_exist_file"));
            Assert.fail("Should throw BuildRetrievalError.");
        } catch (BuildRetrievalError e) {
            // Expect BuildRetrievalError
        }
    }

    @Test
    public void testDownloadFile_folder() throws Exception {
        File localFile =
                mDownloader.downloadFile(
                        String.format("gs://%s/%s/%s/", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1));
        checkDownloadedFolder(localFile);
    }

    @Test
    public void testDownloadFile_folderNotsanitize() throws Exception {
        File localFile =
                mDownloader.downloadFile(
                        String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1));
        checkDownloadedFolder(localFile);
    }

    private void checkDownloadedFolder(File localFile) throws Exception {
        Assert.assertTrue(localFile.isDirectory());
        Assert.assertEquals(5, localFile.list().length);
        for (String filename : localFile.list()) {
            if (filename.equals(FILE_NAME2)) {
                Assert.assertEquals(
                        FILE_NAME2,
                        FileUtil.readStringFromFile(
                                new File(localFile.getAbsolutePath(), filename)));
            } else if (filename.equals(FILE_NAME3)) {
                Assert.assertEquals(
                        FILE_NAME3,
                        FileUtil.readStringFromFile(
                                new File(localFile.getAbsolutePath(), filename)));
            } else if (filename.equals(FOLDER_NAME2 + "_folder")) {
                File subFolder = new File(localFile.getAbsolutePath(), filename);
                Assert.assertTrue(subFolder.isDirectory());
                Assert.assertEquals(1, subFolder.list().length);
                Assert.assertEquals(
                        FILE_NAME4,
                        FileUtil.readStringFromFile(
                                new File(subFolder.getAbsolutePath(), subFolder.list()[0])));
            } else if (filename.equals(FOLDER_NAME2)) {
                File fileWithFolderName = new File(localFile.getAbsolutePath(), filename);
                Assert.assertTrue(fileWithFolderName.isFile());
            } else if (filename.equals(FOLDER_NAME3)) {
                File subFolder = new File(localFile.getAbsolutePath(), filename);
                Assert.assertTrue(subFolder.isDirectory());
                Assert.assertEquals(2, subFolder.list().length);
                Assert.assertEquals(
                        FILE_NAME5,
                        FileUtil.readStringFromFile(
                                new File(subFolder.getAbsolutePath(), FILE_NAME5)));
                File subSubFolder = new File(subFolder, FOLDER_NAME4);
                Assert.assertTrue(subSubFolder.isDirectory());
                Assert.assertEquals(1, subSubFolder.list().length);
                Assert.assertEquals(
                        FILE_NAME6,
                        FileUtil.readStringFromFile(
                                new File(subSubFolder.getAbsolutePath(), FILE_NAME6)));
            } else {
                Assert.assertTrue(String.format("Unknonwn file %s", filename), false);
            }
        }
    }

    @Test
    public void testDownloadFile_folder_nonExist() throws Exception {
        try {
            mDownloader.downloadFile(
                    String.format("gs://%s/%s/%s/", BUCKET_NAME, "mRemoteRoot", "nonExistFolder"));
            Assert.fail("Should throw BuildRetrievalError.");
        } catch (BuildRetrievalError e) {
            // Expect BuildRetrievalError
        }
    }

    @Test
    public void testDownloadFile_size0Folder() throws Exception {
        // Test the downloader work with size0 folder correctly.
        File localFile =
                mDownloader.downloadFile(
                        String.format(
                                "gs://%s/%s/%s/%s/",
                                BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FOLDER_NAME3));
        Assert.assertTrue(localFile.isDirectory());
        Assert.assertEquals(2, localFile.list().length);
        Assert.assertEquals(
                FILE_NAME5,
                FileUtil.readStringFromFile(new File(localFile.getAbsolutePath(), FILE_NAME5)));

        File subFolder = new File(localFile.getAbsolutePath(), FOLDER_NAME4);
        Assert.assertTrue(subFolder.isDirectory());
        Assert.assertEquals(
                FILE_NAME6,
                FileUtil.readStringFromFile(new File(subFolder.getAbsolutePath(), FILE_NAME6)));
    }

    @Test
    public void testDownloadFile_folderWithOnlyOneFile() throws Exception {
        File localFile =
                mDownloader.downloadFile(
                        String.format(
                                "gs://%s/%s/%s/%s/%s/",
                                BUCKET_NAME,
                                mRemoteRoot,
                                FOLDER_NAME1,
                                FOLDER_NAME3,
                                FOLDER_NAME4));
        Assert.assertTrue(localFile.isDirectory());
        Assert.assertEquals(1, localFile.list().length);
        Assert.assertEquals(
                FILE_NAME6,
                FileUtil.readStringFromFile(new File(localFile.getAbsolutePath(), FILE_NAME6)));
    }

    @Test
    public void testDownloadFile_emptyFile() throws Exception {
        try {
            mDownloader.downloadFile(
                    String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, EMPTY_FILE));
            Assert.fail("Should throw BuildRetrievalError.");
        } catch (BuildRetrievalError e) {
            // Expect BuildRetrievalError
        }
    }

    @Test
    public void testCheckFreshness() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FILE_NAME1);
        File localFile = mDownloader.downloadFile(remotePath);
        Assert.assertTrue(mDownloader.isFresh(localFile, remotePath));
    }

    @Test
    public void testCheckFreshness_notExist() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FILE_NAME1);
        Assert.assertFalse(mDownloader.isFresh(new File("/not/exist"), remotePath));
    }

    @Test
    public void testCheckFreshness_folderNotExist() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1);
        Assert.assertFalse(mDownloader.isFresh(new File("/not/exist"), remotePath));
    }

    @Test
    public void testCheckFreshness_remoteNotExist() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FILE_NAME1);
        String remoteNotExistPath = String.format("gs://%s/%s/no_exist", BUCKET_NAME, mRemoteRoot);
        File localFile = mDownloader.downloadFile(remotePath);
        Assert.assertFalse(mDownloader.isFresh(localFile, remoteNotExistPath));
    }

    @Test
    public void testCheckFreshness_remoteFolderNotExist() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1);
        String remoteNotExistPath = String.format("gs://%s/%s/no_exist/", BUCKET_NAME, mRemoteRoot);
        File localFolder = mDownloader.downloadFile(remotePath);
        Assert.assertFalse(mDownloader.isFresh(localFolder, remoteNotExistPath));
    }

    @Test
    public void testCheckFreshness_notFresh() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FILE_NAME1);
        File localFile = mDownloader.downloadFile(remotePath);
        mDownloader.clearCache();
        // Change the remote file.
        createFile(mStorage, "New content.", BUCKET_NAME, mRemoteRoot, FILE_NAME1);
        Assert.assertFalse(mDownloader.isFresh(localFile, remotePath));
    }

    @Test
    public void testCheckFreshness_folder() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1);
        File localFolder = mDownloader.downloadFile(remotePath);
        Assert.assertTrue(mDownloader.isFresh(localFolder, remotePath));
    }

    @Test
    public void testCheckFreshness_folder_addFile() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1);
        File localFolder = mDownloader.downloadFile(remotePath);
        mDownloader.clearCache();
        createFile(
                mStorage,
                "A new file",
                BUCKET_NAME,
                mRemoteRoot,
                FOLDER_NAME1,
                FOLDER_NAME2,
                "new_file.txt");
        Assert.assertFalse(mDownloader.isFresh(localFolder, remotePath));
    }

    @Test
    public void testCheckFreshness_folder_removeFile() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1);
        File localFolder = mDownloader.downloadFile(remotePath);
        mDownloader.clearCache();
        mStorage.objects()
                .delete(BUCKET_NAME, Paths.get(mRemoteRoot, FOLDER_NAME1, FILE_NAME3).toString())
                .execute();
        Assert.assertFalse(mDownloader.isFresh(localFolder, remotePath));
    }

    @Test
    public void testCheckFreshness_folder_changeFile() throws Exception {
        String remotePath = String.format("gs://%s/%s/%s", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1);
        File localFolder = mDownloader.downloadFile(remotePath);
        mDownloader.clearCache();
        createFile(mStorage, "New content", BUCKET_NAME, mRemoteRoot, FOLDER_NAME1, FILE_NAME3);
        Assert.assertFalse(mDownloader.isFresh(localFolder, remotePath));
    }
}
