/*
 * Copyright (C) 2017 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.documentsui.archives;

import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.test.AndroidTestCase;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

@MediumTest
public class WriteableArchiveTest extends AndroidTestCase {

    private static final Uri ARCHIVE_URI = Uri.parse("content://i/love/strawberries");
    private static final String NOTIFICATION_URI =
            "content://com.android.documentsui.archives/notification-uri";
    private ExecutorService mExecutor = null;
    private Archive mArchive = null;
    private TestUtils mTestUtils = null;
    private File mFile = null;

    @Override
    public void setUp() throws Exception {
        super.setUp();
        mExecutor = Executors.newSingleThreadExecutor();
        mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
                InstrumentationRegistry.getContext(), mExecutor);
        mFile = mTestUtils.createTemporaryFile();

        mArchive = WriteableArchive.createForParcelFileDescriptor(
                InstrumentationRegistry.getTargetContext(),
                ParcelFileDescriptor.open(mFile, ParcelFileDescriptor.MODE_WRITE_ONLY),
                ARCHIVE_URI,
                ParcelFileDescriptor.MODE_WRITE_ONLY,
                Uri.parse(NOTIFICATION_URI));
    }

    @Override
    public void tearDown() throws Exception {
        mExecutor.shutdown();
        assertTrue(mExecutor.awaitTermination(3 /* timeout */, TimeUnit.SECONDS));
        if (mFile != null) {
            mFile.delete();
        }
        if (mArchive != null) {
            mArchive.close();
        }
        super.tearDown();
    }

    public static ArchiveId createArchiveId(String path) {
        return new ArchiveId(ARCHIVE_URI, ParcelFileDescriptor.MODE_WRITE_ONLY, path);
    }

    public void testCreateDocument() throws IOException {
        final String dirDocumentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
                Document.MIME_TYPE_DIR, "dir");
        assertEquals(createArchiveId("/dir/").toDocumentId(), dirDocumentId);

        final String documentId = mArchive.createDocument(dirDocumentId, "image/jpeg", "test.jpeg");
        assertEquals(createArchiveId("/dir/test.jpeg").toDocumentId(), documentId);

        try {
            mArchive.createDocument(dirDocumentId,
                    "image/jpeg", "test.jpeg");
            fail("Creating should fail, as the document already exists.");
        } catch (IllegalStateException e) {
            // Expected.
        }

        try {
            mArchive.createDocument(createArchiveId("/").toDocumentId(),
                    "image/jpeg", "test.jpeg/");
            fail("Creating should fail, as the document name is invalid.");
        } catch (IllegalStateException e) {
            // Expected.
        }

        try {
            mArchive.createDocument(createArchiveId("/").toDocumentId(),
                    Document.MIME_TYPE_DIR, "test/");
            fail("Creating should fail, as the document name is invalid.");
        } catch (IllegalStateException e) {
            // Expected.
        }

        try {
            mArchive.createDocument(createArchiveId("/").toDocumentId(),
                    Document.MIME_TYPE_DIR, "..");
            fail("Creating should fail, as the document name is invalid.");
        } catch (IllegalStateException e) {
            // Expected.
        }

        try {
            mArchive.createDocument(createArchiveId("/").toDocumentId(),
                    Document.MIME_TYPE_DIR, ".");
            fail("Creating should fail, as the document name is invalid.");
        } catch (IllegalStateException e) {
            // Expected.
        }

        try {
            mArchive.createDocument(createArchiveId("/").toDocumentId(),
                    Document.MIME_TYPE_DIR, "");
            fail("Creating should fail, as the document name is invalid.");
        } catch (IllegalStateException e) {
            // Expected.
        }

        try {
            mArchive.createDocument(createArchiveId("/").toDocumentId(),
                    "image/jpeg", "a/b.jpeg");
            fail("Creating should fail, as the document name is invalid.");
        } catch (IllegalStateException e) {
            // Expected.
        }
    }

    public void testAddDirectory() throws IOException {
        final String documentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
                Document.MIME_TYPE_DIR, "dir");

        {
            final Cursor cursor = mArchive.queryDocument(documentId, null);
            assertTrue(cursor.moveToFirst());
            assertEquals(documentId,
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
            assertEquals("dir",
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
            assertEquals(Document.MIME_TYPE_DIR,
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
            assertEquals(0,
                    cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
        }

        {
            final Cursor cursor = mArchive.queryChildDocuments(
                    createArchiveId("/").toDocumentId(), null, null);

            assertTrue(cursor.moveToFirst());
            assertEquals(documentId,
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
            assertEquals("dir",
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
            assertEquals(Document.MIME_TYPE_DIR,
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
            assertEquals(0,
                    cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
        }

        mArchive.close();

        // Verify archive.
        ZipFile zip = null;
        try {
            zip = new ZipFile(mFile);
            final Enumeration<? extends ZipEntry> entries = zip.entries();
            assertTrue(entries.hasMoreElements());
            final ZipEntry entry = entries.nextElement();
            assertEquals("dir/", entry.getName());
            assertFalse(entries.hasMoreElements());
        } finally {
            if (zip != null) {
                zip.close();
            }
        }
    }

    public void testAddFile() throws IOException, InterruptedException {
        final String documentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
                "text/plain", "hoge.txt");

        {
            final Cursor cursor = mArchive.queryDocument(documentId, null);
            assertTrue(cursor.moveToFirst());
            assertEquals(documentId,
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
            assertEquals("hoge.txt",
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
            assertEquals("text/plain",
                    cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
            assertEquals(0,
                    cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
        }

        try {
            mArchive.openDocument(documentId, "r", null);
            fail("Should fail when opened for reading!");
        } catch (IllegalArgumentException e) {
            // Expected.
        }

        final ParcelFileDescriptor fd = mArchive.openDocument(documentId, "w", null);
        try (ParcelFileDescriptor.AutoCloseOutputStream outputStream =
                new ParcelFileDescriptor.AutoCloseOutputStream(fd)) {
            outputStream.write("Hello world!".getBytes());
        }

        try {
            mArchive.openDocument(documentId, "w", null);
            fail("Should fail when opened for the second time!");
        } catch (IllegalStateException e) {
            // Expected.
        }

        // Wait until the pipe thread fully writes all the data from the pipe.
        // TODO: Maybe add some method in WriteableArchive to wait until the executor
        // completes the job?
        Thread.sleep(500);

        {
            final Cursor cursor = mArchive.queryDocument(documentId, null);
            assertTrue(cursor.moveToFirst());
            assertEquals(12,
                    cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
        }

        mArchive.close();

        // Verify archive.
        ZipFile zip = null;
        try {
            try {
                zip = new ZipFile(mFile);
            } catch (Exception e) {
                throw new IOException(mFile.getAbsolutePath());
            }
            final Enumeration<? extends ZipEntry> entries = zip.entries();
            assertTrue(entries.hasMoreElements());
            final ZipEntry entry = entries.nextElement();
            assertEquals("hoge.txt", entry.getName());
            assertFalse(entries.hasMoreElements());
            final InputStream inputStream = zip.getInputStream(entry);
            final Scanner scanner = new Scanner(inputStream);
            assertEquals("Hello world!", scanner.nextLine());
            assertFalse(scanner.hasNext());
        } finally {
            if (zip != null) {
                zip.close();
            }
        }
    }

    public void testAddFile_empty() throws IOException, Exception {
        final String documentId = mArchive.createDocument(createArchiveId("/").toDocumentId(),
                "text/plain", "hoge.txt");
        mArchive.close();

        // Verify archive.
        ZipFile zip = null;
        try {
            try {
                zip = new ZipFile(mFile);
            } catch (Exception e) {
                throw new IOException(mFile.getAbsolutePath());
            }
            final Enumeration<? extends ZipEntry> entries = zip.entries();
            assertTrue(entries.hasMoreElements());
            final ZipEntry entry = entries.nextElement();
            assertEquals("hoge.txt", entry.getName());
            assertFalse(entries.hasMoreElements());
            final InputStream inputStream = zip.getInputStream(entry);
            final Scanner scanner = new Scanner(inputStream);
            assertFalse(scanner.hasNext());
        } finally {
            if (zip != null) {
                zip.close();
            }
        }
    }
}