/*
 * Copyright (C) 2015 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 static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.text.TextUtils;

import androidx.annotation.IdRes;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.documentsui.tests.R;

import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.compressors.CompressorException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@RunWith(AndroidJUnit4.class)
@MediumTest
public class ReadableArchiveTest {

    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;

    @Before
    public void setUp() throws Exception {
        mExecutor = Executors.newSingleThreadExecutor();
        mTestUtils = new TestUtils(InstrumentationRegistry.getTargetContext(),
                InstrumentationRegistry.getContext(), mExecutor);
    }

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

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

    private void loadArchive(ParcelFileDescriptor descriptor, String mimeType)
            throws IOException, CompressorException, ArchiveException {
        mArchive = ReadableArchive.createForParcelFileDescriptor(
                InstrumentationRegistry.getTargetContext(),
                descriptor,
                ARCHIVE_URI,
                mimeType,
                ParcelFileDescriptor.MODE_READ_ONLY,
                Uri.parse(NOTIFICATION_URI));
    }

    private void loadArchive(ParcelFileDescriptor descriptor)
            throws IOException, CompressorException, ArchiveException {
        loadArchive(descriptor, "application/zip");
    }

    private static void assertRowExist(Cursor cursor, String targetDocId) {
        assertTrue(cursor.moveToFirst());

        boolean found = false;
        final int count = cursor.getCount();
        for (int i = 0; i < count; i++) {
            cursor.moveToPosition(i);
            if (TextUtils.equals(targetDocId, cursor.getString(
                    cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)))) {
                found = true;
                break;
            }
        }

        assertTrue(targetDocId + " should be exists", found);
    }

    @Test
    public void testQueryChildDocument()
            throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
        final Cursor cursor = mArchive.queryChildDocuments(
                createArchiveId("/").toDocumentId(), null, null);

        assertRowExist(cursor, createArchiveId("/file1.txt").toDocumentId());
        assertEquals("file1.txt",
                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
        assertEquals("text/plain",
                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
        assertEquals(13,
                cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));

        assertRowExist(cursor, createArchiveId("/dir1/").toDocumentId());
        assertEquals("dir1",
                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)));

        assertRowExist(cursor, createArchiveId("/dir2/").toDocumentId());
        assertEquals("dir2",
                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)));

        // Check if querying children works too.
        final Cursor childCursor = mArchive.queryChildDocuments(
                createArchiveId("/dir1/").toDocumentId(), null, null);

        assertTrue(childCursor.moveToFirst());
        assertEquals(
                createArchiveId("/dir1/cherries.txt").toDocumentId(),
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DOCUMENT_ID)));
        assertEquals("cherries.txt",
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DISPLAY_NAME)));
        assertEquals("text/plain",
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_MIME_TYPE)));
        assertEquals(17,
                childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
    }

    @Test
    public void testQueryChildDocument_NoDirs()
            throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.no_dirs));
        final Cursor cursor = mArchive.queryChildDocuments(
                createArchiveId("/").toDocumentId(), null, null);

        assertTrue(cursor.moveToFirst());
        assertEquals(
                createArchiveId("/dir1/").toDocumentId(),
                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
        assertEquals("dir1",
                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)));
        assertFalse(cursor.moveToNext());

        final Cursor childCursor = mArchive.queryChildDocuments(
                createArchiveId("/dir1/").toDocumentId(), null, null);

        assertTrue(childCursor.moveToFirst());
        assertEquals(
                createArchiveId("/dir1/dir2/").toDocumentId(),
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DOCUMENT_ID)));
        assertEquals("dir2",
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DISPLAY_NAME)));
        assertEquals(Document.MIME_TYPE_DIR,
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_MIME_TYPE)));
        assertEquals(0,
                childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
        assertFalse(childCursor.moveToNext());

        final Cursor childCursor2 = mArchive.queryChildDocuments(
                createArchiveId("/dir1/dir2/").toDocumentId(),
                null, null);

        assertTrue(childCursor2.moveToFirst());
        assertEquals(
                createArchiveId("/dir1/dir2/cherries.txt").toDocumentId(),
                childCursor2.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DOCUMENT_ID)));
        assertFalse(childCursor2.moveToNext());
    }

    @Test
    public void testQueryChildDocument_EmptyDirs()
            throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.empty_dirs));
        final Cursor cursor = mArchive.queryChildDocuments(
                createArchiveId("/").toDocumentId(), null, null);

        assertTrue(cursor.moveToFirst());
        assertEquals(
                createArchiveId("/dir1/").toDocumentId(),
                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
        assertEquals("dir1",
                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)));
        assertFalse(cursor.moveToNext());

        final Cursor childCursor = mArchive.queryChildDocuments(
                createArchiveId("/dir1/").toDocumentId(), null, null);

        assertRowExist(childCursor, createArchiveId("/dir1/dir2/").toDocumentId());
        assertEquals("dir2",
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DISPLAY_NAME)));
        assertEquals(Document.MIME_TYPE_DIR,
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_MIME_TYPE)));
        assertEquals(0,
                childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));

        assertRowExist(childCursor, createArchiveId("/dir1/dir3/").toDocumentId());
        assertEquals(
                createArchiveId("/dir1/dir3/").toDocumentId(),
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DOCUMENT_ID)));
        assertEquals("dir3",
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_DISPLAY_NAME)));
        assertEquals(Document.MIME_TYPE_DIR,
                childCursor.getString(childCursor.getColumnIndexOrThrow(
                        Document.COLUMN_MIME_TYPE)));
        assertEquals(0,
                childCursor.getInt(childCursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));

        final Cursor childCursor2 = mArchive.queryChildDocuments(
                createArchiveId("/dir1/dir2/").toDocumentId(),
                null, null);
        assertFalse(childCursor2.moveToFirst());

        final Cursor childCursor3 = mArchive.queryChildDocuments(
                createArchiveId("/dir1/dir3/").toDocumentId(),
                null, null);
        assertFalse(childCursor3.moveToFirst());
    }

    @Test
    public void testGetDocumentType() throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
        assertEquals(Document.MIME_TYPE_DIR, mArchive.getDocumentType(
                createArchiveId("/dir1/").toDocumentId()));
        assertEquals("text/plain", mArchive.getDocumentType(
                createArchiveId("/file1.txt").toDocumentId()));
    }

    @Test
    public void testIsChildDocument() throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
        final String documentId = createArchiveId("/").toDocumentId();
        assertTrue(mArchive.isChildDocument(documentId,
                createArchiveId("/dir1/").toDocumentId()));
        assertFalse(mArchive.isChildDocument(documentId,
                createArchiveId("/this-does-not-exist").toDocumentId()));
        assertTrue(mArchive.isChildDocument(
                createArchiveId("/dir1/").toDocumentId(),
                createArchiveId("/dir1/cherries.txt").toDocumentId()));
        assertTrue(mArchive.isChildDocument(documentId,
                createArchiveId("/dir1/cherries.txt").toDocumentId()));
    }

    @Test
    public void testQueryDocument() throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
        final Cursor cursor = mArchive.queryDocument(
                createArchiveId("/dir2/strawberries.txt").toDocumentId(),
                null);

        assertTrue(cursor.moveToFirst());
        assertEquals(
                createArchiveId("/dir2/strawberries.txt").toDocumentId(),
                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)));
        assertEquals("strawberries.txt",
                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)));
        assertEquals("text/plain",
                cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)));
        assertEquals(21,
                cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)));
    }

    private void queryDocumentByResIdWithMimeTypeAndVerify(@IdRes int resId, String mimeType)
            throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getSeekableDescriptor(resId),
                mimeType);
        final String documentId = createArchiveId("/hello/hello.txt").toDocumentId();

        final Cursor cursor = mArchive.queryDocument(documentId, null);
        cursor.moveToNext();

        assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DOCUMENT_ID)))
                .isEqualTo(documentId);
        assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_DISPLAY_NAME)))
                .isEqualTo("hello.txt");
        assertThat(cursor.getString(cursor.getColumnIndexOrThrow(Document.COLUMN_MIME_TYPE)))
                .isEqualTo("text/plain");
        assertThat(cursor.getInt(cursor.getColumnIndexOrThrow(Document.COLUMN_SIZE)))
                .isEqualTo(48);
    }

    @Test
    public void archive_sevenZFile_containsList()
            throws IOException, CompressorException, ArchiveException {
        queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_7z,
                "application/x-7z-compressed");
    }

    @Test
    public void archive_tar_containsList()
            throws IOException, CompressorException, ArchiveException {
        queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar, "application/x-tar");
    }

    @Test
    public void archive_tgz_containsList()
            throws IOException, CompressorException, ArchiveException {
        queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tgz,
                "application/x-compressed-tar");
    }

    @Test
    public void archive_tarXz_containsList()
            throws IOException, CompressorException, ArchiveException {
        queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar_xz,
                "application/x-xz-compressed-tar");
    }

    @Test
    public void archive_tarBz_containsList()
            throws IOException, CompressorException, ArchiveException {
        queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar_bz2,
                "application/x-bzip-compressed-tar");
    }

    @Test
    public void archive_tarBrotli_containsList()
            throws IOException, CompressorException, ArchiveException {
        queryDocumentByResIdWithMimeTypeAndVerify(R.raw.hello_tar_br,
                "application/x-brotli-compressed-tar");
    }

    @Test
    public void testOpenDocument()
            throws IOException, CompressorException, ArchiveException, ErrnoException {
        loadArchive(mTestUtils.getSeekableDescriptor(R.raw.archive));
        commonTestOpenDocument();
    }

    @Test
    public void testOpenDocument_NonSeekable()
            throws IOException, CompressorException, ArchiveException, ErrnoException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
        commonTestOpenDocument();
    }

    // Common part of testOpenDocument and testOpenDocument_NonSeekable.
    void commonTestOpenDocument() throws IOException, ErrnoException {
        final ParcelFileDescriptor descriptor = mArchive.openDocument(
                createArchiveId("/dir2/strawberries.txt").toDocumentId(),
                "r", null /* signal */);
        assertTrue(Archive.canSeek(descriptor));
        try (ParcelFileDescriptor.AutoCloseInputStream inputStream =
                new ParcelFileDescriptor.AutoCloseInputStream(descriptor)) {
            Os.lseek(descriptor.getFileDescriptor(), "I love ".length(), OsConstants.SEEK_SET);
            assertEquals("strawberries!", new Scanner(inputStream).nextLine());
            Os.lseek(descriptor.getFileDescriptor(), 0, OsConstants.SEEK_SET);
            assertEquals("I love strawberries!", new Scanner(inputStream).nextLine());
        }
    }

    @Test
    public void testCanSeek() throws IOException {
        assertTrue(Archive.canSeek(mTestUtils.getSeekableDescriptor(R.raw.archive)));
        assertFalse(Archive.canSeek(mTestUtils.getNonSeekableDescriptor(R.raw.archive)));
    }

    @Test
    public void testBrokenArchive() throws IOException, CompressorException, ArchiveException {
        loadArchive(mTestUtils.getNonSeekableDescriptor(R.raw.archive));
        final Cursor cursor = mArchive.queryChildDocuments(
                createArchiveId("/").toDocumentId(), null, null);
    }
}
