/*
 * Copyright (C) 2019 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.device.contentprovider;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;

import java.io.File;
import java.io.OutputStream;
import java.util.HashMap;

/** Run unit tests for {@link ContentProviderHandler}. */
@RunWith(JUnit4.class)
public class ContentProviderHandlerTest {

    private static final int TEST_USER_ID = 99;

    private ContentProviderHandler mProvider;
    private ITestDevice mMockDevice;

    @Before
    public void setUp() {
        mMockDevice = Mockito.mock(ITestDevice.class);
        mProvider = new ContentProviderHandler(mMockDevice, TEST_USER_ID);
    }

    @After
    public void tearDown() throws Exception {
        mProvider.tearDown();
    }

    @Test
    public void testGetUserId() {
        assertTrue(mProvider.getUserId() == TEST_USER_ID);
    }

    @Test
    public void testGetEffectiveUserId() throws Exception {
        assertEquals(mProvider.getEffectiveUserId(), TEST_USER_ID);
    }

    @Test
    public void testGetEffectiveUserId_returnsCurrentUserIdIfNotSpecified() throws Exception {
        int currentUserId = 100;
        when(mMockDevice.getCurrentUser()).thenReturn(currentUserId);
        ContentProviderHandler cph = new ContentProviderHandler(mMockDevice);
        // doReturn(currentUserId).when(mMockDevice).getCurrentUser();

        assertNull(cph.getUserId());
        assertEquals(cph.getEffectiveUserId(), currentUserId);
    }

    /** Test the install flow. */
    @Test
    public void testSetUp_install() throws Exception {
        doReturn(null).when(mMockDevice).installPackage(any(), eq(true), eq(true));
        CommandResult resSet = new CommandResult(CommandStatus.SUCCESS);
        doReturn(resSet)
                .when(mMockDevice)
                .executeShellV2Command(
                        String.format(
                                "cmd appops set %s android:legacy_storage allow",
                                ContentProviderHandler.PACKAGE_NAME));
        CommandResult res = new CommandResult(CommandStatus.SUCCESS);
        res.setStdout("LEGACY_STORAGE: allow");
        doReturn(res)
                .when(mMockDevice)
                .executeShellV2Command(
                        String.format("cmd appops get %s", ContentProviderHandler.PACKAGE_NAME));

        assertTrue(mProvider.setUp());
    }

    @Test
    public void testSetUp_alreadyInstalled() throws Exception {
        doReturn(true)
                .when(mMockDevice)
                .isPackageInstalled(
                        ContentProviderHandler.PACKAGE_NAME, String.valueOf(TEST_USER_ID));

        assertTrue(mProvider.setUp());
    }

    @Test
    public void testSetUp_installFail() throws Exception {
        doReturn("fail").when(mMockDevice).installPackage(any(), eq(true), eq(true));

        assertFalse(mProvider.setUp());
    }

    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
    @Test
    public void testDeleteFile() throws Exception {
        String devicePath = "path/somewhere/file.txt";
        doReturn(mockSuccess())
                .when(mMockDevice)
                .executeShellV2Command(
                        eq(
                                "content delete --user "
                                        + TEST_USER_ID
                                        + " --uri "
                                        + ContentProviderHandler.createEscapedContentUri(
                                                devicePath)));
        assertTrue(mProvider.deleteFile(devicePath));
    }

    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
    @Test
    public void testDeleteFile_fail() throws Exception {
        String devicePath = "path/somewhere/file.txt";
        CommandResult result = new CommandResult(CommandStatus.FAILED);
        result.setStdout("");
        result.setStderr("couldn't find the file");
        doReturn(result)
                .when(mMockDevice)
                .executeShellV2Command(
                        eq(
                                "content delete --user "
                                        + TEST_USER_ID
                                        + " --uri "
                                        + ContentProviderHandler.createEscapedContentUri(
                                                devicePath)));
        assertFalse(mProvider.deleteFile(devicePath));
    }

    /** Test {@link ContentProviderHandler#deleteFile(String)}. */
    @Test
    public void testError() throws Exception {
        String devicePath = "path/somewhere/file.txt";
        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
        result.setStdout("[ERROR] Unsupported operation: delete");
        doReturn(result)
                .when(mMockDevice)
                .executeShellV2Command(
                        eq(
                                "content delete --user "
                                        + TEST_USER_ID
                                        + " --uri "
                                        + ContentProviderHandler.createEscapedContentUri(
                                                devicePath)));
        assertFalse(mProvider.deleteFile(devicePath));
    }

    /** Test {@link ContentProviderHandler#pushFile(File, String)}. */
    @Test
    public void testPushFile() throws Exception {
        File toPush = FileUtil.createTempFile("content-provider-test", ".txt");
        try {
            String devicePath = "path/somewhere/file.txt";
            doReturn(mockSuccess())
                    .when(mMockDevice)
                    .executeShellV2Command(
                            eq(
                                    "content write --user "
                                            + TEST_USER_ID
                                            + " --uri "
                                            + ContentProviderHandler.createEscapedContentUri(
                                                    devicePath)),
                            eq(toPush));
            assertTrue(mProvider.pushFile(toPush, devicePath));
        } finally {
            FileUtil.deleteFile(toPush);
        }
    }

    /** Test {@link ContentProviderHandler#pushFile(File, String)} when the file doesn't exists */
    @Test
    public void testPushFile_notExists() throws Exception {
        File toPush = new File("content-provider-test.txt");
        try {
            String devicePath = "path/somewhere/file.txt";
            assertFalse(mProvider.pushFile(toPush, devicePath));
        } finally {
            FileUtil.deleteFile(toPush);
        }
    }

    /**
     * Test {@link ContentProviderHandler#pushFile(File, String)} when the file exists but is a
     * directory
     */
    @Test
    public void testPushFile_directory() throws Exception {
        File toPush = FileUtil.createTempDir("content-provider-test");
        try {
            String devicePath = "path/somewhere/file.txt";
            assertFalse(mProvider.pushFile(toPush, devicePath));
        } finally {
            FileUtil.recursiveDelete(toPush);
        }
    }

    /** Test {@link ContentProviderHandler#pullFile(String, File)}. */
    @Test
    public void testPullFile_verifyShellCommand() throws Exception {
        File pullTo = FileUtil.createTempFile("content-provider-test", ".txt");
        String devicePath = "path/somewhere/file.txt";
        mockPullFileSuccess();

        try {
            mProvider.pullFile(devicePath, pullTo);

            // Capture the shell command used by pullFile.
            ArgumentCaptor<String> shellCommandCaptor = ArgumentCaptor.forClass(String.class);
            verify(mMockDevice)
                    .executeShellV2Command(shellCommandCaptor.capture(), any(OutputStream.class));

            // Verify the command.
            assertEquals(
                    shellCommandCaptor.getValue(),
                    "content read --user "
                            + TEST_USER_ID
                            + " --uri "
                            + ContentProviderHandler.createEscapedContentUri(devicePath));
        } finally {
            FileUtil.deleteFile(pullTo);
        }
    }

    /** Test {@link ContentProviderHandler#pullFile(String, File)}. */
    @Test
    public void testPullFile_createLocalFileIfNotExist() throws Exception {
        File pullTo = FileUtil.createTempFile("content-provider-test", ".txt");
        // Delete to test unexisting file.
        pullTo.delete();
        String devicePath = "path/somewhere/file.txt";
        mockPullFileSuccess();

        try {
            assertFalse(pullTo.exists());
            assertTrue(mProvider.pullFile(devicePath, pullTo));
            assertTrue(pullTo.exists());
        } finally {
            FileUtil.deleteFile(pullTo);
        }
    }

    /** Test {@link ContentProviderHandler#pullDir(String, File)}. */
    @Test
    public void testPullDir_EmptyDirectory() throws Exception {
        File pullTo = FileUtil.createTempDir("content-provider-test");

        doReturn("No result found.\n").when(mMockDevice).executeShellCommand(anyString());

        try {
            assertTrue(mProvider.pullDir("path/somewhere", pullTo));
        } finally {
            FileUtil.recursiveDelete(pullTo);
        }
    }

    @Test
    public void testPullDir_failedDevice() throws Exception {
        File pullTo = FileUtil.createTempDir("content-provider-test");

        doReturn("Something crashed").when(mMockDevice).executeShellCommand(anyString());

        try {
            assertFalse(mProvider.pullDir("path/somewhere", pullTo));
        } finally {
            FileUtil.recursiveDelete(pullTo);
        }
    }

    /**
     * Test {@link ContentProviderHandler#pullDir(String, File)} to pull a directory that contains
     * one text file.
     */
    @Test
    public void testPullDir_OneFile() throws Exception {
        File pullTo = FileUtil.createTempDir("content-provider-test");

        String devicePath = "path/somewhere";
        String fileName = "content-provider-file.txt";

        doReturn(createMockFileRow(fileName, devicePath + "/" + fileName, "text/plain"))
                .when(mMockDevice)
                .executeShellCommand(anyString());
        mockPullFileSuccess();

        try {
            // Assert that local directory is empty.
            assertEquals(pullTo.listFiles().length, 0);
            mProvider.pullDir(devicePath, pullTo);

            // Assert that a file has been pulled inside the directory.
            assertEquals(pullTo.listFiles().length, 1);
            assertEquals(pullTo.listFiles()[0].getName(), fileName);
        } finally {
            FileUtil.recursiveDelete(pullTo);
        }
    }

    /**
     * Test {@link ContentProviderHandler#pullDir(String, File)} to pull a directory that contains
     * another directory.
     */
    @Test
    public void testPullDir_RecursiveSubDir() throws Exception {
        File pullTo = FileUtil.createTempDir("content-provider-test");

        String devicePath = "path/somewhere";
        String subDirName = "test-subdir";
        String subDirPath = devicePath + "/" + subDirName;
        String fileName = "test-file.txt";

        // Mock the result for the directory.
        doReturn(createMockDirRow(subDirName, subDirPath))
                .when(mMockDevice)
                .executeShellCommand(
                        "content query --user "
                                + TEST_USER_ID
                                + " --uri "
                                + ContentProviderHandler.createEscapedContentUri(devicePath));

        // Mock the result for the subdir.
        doReturn(createMockFileRow(fileName, subDirPath + "/" + fileName, "text/plain"))
                .when(mMockDevice)
                .executeShellCommand(
                        "content query --user "
                                + TEST_USER_ID
                                + " --uri "
                                + ContentProviderHandler.createEscapedContentUri(
                                        devicePath + "/" + subDirName));

        mockPullFileSuccess();

        try {
            // Assert that local directory is empty.
            assertEquals(pullTo.listFiles().length, 0);
            mProvider.pullDir(devicePath, pullTo);

            // Assert that a subdirectory has been created.
            assertEquals(pullTo.listFiles().length, 1);
            assertEquals(pullTo.listFiles()[0].getName(), subDirName);
            assertTrue(pullTo.listFiles()[0].isDirectory());

            // Assert that a file has been pulled inside the subdirectory.
            assertEquals(pullTo.listFiles()[0].listFiles().length, 1);
            assertEquals(pullTo.listFiles()[0].listFiles()[0].getName(), fileName);
        } finally {
            FileUtil.recursiveDelete(pullTo);
        }
    }

    @Test
    public void testCreateUri() {
        String espacedUrl =
                ContentProviderHandler.createEscapedContentUri("filepath/file name spaced (data)");
        // We expect the full url to be quoted to avoid space issues and the URL to be encoded.
        assertEquals(
                "\"content://android.tradefed.contentprovider/filepath%252Ffile+name+spaced+"
                        + "%2528data%2529\"",
                espacedUrl);
    }

    /** Test {@link ContentProviderHandler#doesFileExist(String)}. */
    @Test
    public void testDoesFileExist() throws Exception {
        String devicePath = "path/somewhere/file.txt";

        when(mMockDevice.executeShellCommand(
                        "content query --user "
                                + TEST_USER_ID
                                + " --uri "
                                + ContentProviderHandler.createEscapedContentUri(devicePath)))
                .thenReturn("");

        assertTrue(mProvider.doesFileExist(devicePath));
    }

    /**
     * Test {@link ContentProviderHandler#doesFileExist(String)} returns false when 'adb shell
     * content query' returns no results.
     */
    @Test
    public void testDoesFileExist_NotExists() throws Exception {
        String devicePath = "path/somewhere/";

        when(mMockDevice.executeShellCommand(
                        "content query --user "
                                + TEST_USER_ID
                                + " --uri "
                                + ContentProviderHandler.createEscapedContentUri(devicePath)))
                .thenReturn("No result found.\n");
        assertFalse(mProvider.doesFileExist(devicePath));
    }

    @Test
    public void testParseQueryResultRow() {
        String row =
                "Row: 1 name=name spaced with , ,comma, "
                        + "absolute_path=/storage/emulated/0/Alarms/name spaced with , ,comma, "
                        + "is_directory=true, mime_type=NULL, metadata=NULL";

        HashMap<String, String> columnValues = mProvider.parseQueryResultRow(row);

        assertEquals(
                columnValues.get(ContentProviderHandler.COLUMN_NAME), "name spaced with , ,comma");
        assertEquals(
                columnValues.get(ContentProviderHandler.COLUMN_ABSOLUTE_PATH),
                "/storage/emulated/0/Alarms/name spaced with , ,comma");
        assertEquals(columnValues.get(ContentProviderHandler.COLUMN_DIRECTORY), "true");
        assertEquals(columnValues.get(ContentProviderHandler.COLUMN_MIME_TYPE), "NULL");
        assertEquals(columnValues.get(ContentProviderHandler.COLUMN_METADATA), "NULL");
    }

    private CommandResult mockSuccess() {
        CommandResult result = new CommandResult(CommandStatus.SUCCESS);
        result.setStderr("");
        result.setStdout("");
        return result;
    }

    private void mockPullFileSuccess() throws Exception {
        doReturn(mockSuccess())
                .when(mMockDevice)
                .executeShellV2Command(anyString(), any(OutputStream.class));
    }

    private String createMockDirRow(String name, String path) {
        return String.format(
                "Row: 1 name=%s, absolute_path=%s, is_directory=%b, mime_type=NULL, metadata=NULL",
                name, path, true);
    }

    private String createMockFileRow(String name, String path, String mimeType) {
        return String.format(
                "Row: 1 name=%s, absolute_path=%s, is_directory=%b, mime_type=%s, metadata=NULL",
                name, path, false, mimeType);
    }
}
