/*
 * 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.services;

import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
import static com.android.documentsui.services.FileOperations.createBaseIntent;
import static com.android.documentsui.services.FileOperations.createJobId;

import static org.junit.Assert.fail;

import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.test.ServiceTestCase;

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

import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.Features;
import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.testing.DocsProviders;
import com.android.documentsui.testing.TestFeatures;
import com.android.documentsui.testing.TestHandler;
import com.android.documentsui.testing.TestScheduledExecutorService;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@MediumTest
public class FileOperationServiceTest extends ServiceTestCase<FileOperationService> {

    private static final Uri SRC_PARENT =
            Uri.parse("content://com.android.documentsui.testing/parent");
    private static final DocumentInfo ALPHA_DOC = createDoc("alpha");
    private static final DocumentInfo BETA_DOC = createDoc("alpha");
    private static final DocumentInfo GAMMA_DOC = createDoc("gamma");
    private static final DocumentInfo DELTA_DOC = createDoc("delta");

    private final List<TestJob> mCopyJobs = new ArrayList<>();
    private final List<TestJob> mDeleteJobs = new ArrayList<>();

    private FileOperationService mService;
    private TestScheduledExecutorService mExecutor;
    private TestScheduledExecutorService mDeletionExecutor;
    private TestHandler mHandler;
    private TestForegroundManager mForegroundManager;
    private TestNotificationManager mTestNotificationManager;

    public FileOperationServiceTest() {
        super(FileOperationService.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        setupService();  // must be called first for our test setup to work correctly.

        mExecutor = new TestScheduledExecutorService();
        mDeletionExecutor = new TestScheduledExecutorService();
        mHandler = new TestHandler();
        mTestNotificationManager = new TestNotificationManager();
        mForegroundManager = new TestForegroundManager(mTestNotificationManager);
        TestFeatures features = new TestFeatures();
        features.notificationChannel = InstrumentationRegistry.getTargetContext()
                .getResources().getBoolean(R.bool.feature_notification_channel);

        mCopyJobs.clear();
        mDeleteJobs.clear();

        // Install test doubles.
        mService = getService();

        assertNull(mService.executor);
        mService.executor = mExecutor;

        assertNull(mService.deletionExecutor);
        mService.deletionExecutor = mDeletionExecutor;

        assertNull(mService.handler);
        mService.handler = mHandler;

        assertNull(mService.foregroundManager);
        mService.foregroundManager = mForegroundManager;

        assertNull(mService.notificationManager);
        mService.notificationManager = mTestNotificationManager.createNotificationManager();

        assertNull(mService.features);
        mService.features = features;

        mService.mVisualSignalsEnabled = false;
    }

    @Override
    protected void tearDown() {
        // Release all possibly held wake lock here
        mExecutor.runAll();
        mDeletionExecutor.runAll();

        // There are lots of progress notifications generated in this test case.
        // Dismiss all of them here.
        mHandler.dispatchAllMessages();
    }

    public void testRunsCopyJobs() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        mExecutor.runAll();
        assertAllCopyJobsStarted();
    }

    public void testRunsCopyJobs_AfterExceptionInJobCreation() throws Exception {
        try {
            startService(createCopyIntent(new ArrayList<>(), BETA_DOC));
            fail("Should have throw exception.");
        } catch (IllegalArgumentException expected) {
            // We're sending a naughty empty list that should result in an IllegalArgumentException.
        }
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        assertJobsCreated(1);

        mExecutor.runAll();
        assertAllCopyJobsStarted();
    }

    public void testRunsCopyJobs_AfterFailure() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        mCopyJobs.get(0).fail(ALPHA_DOC);

        mExecutor.runAll();
        assertAllCopyJobsStarted();
    }

    public void testRunsCopyJobs_notRunsDeleteJobs() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createDeleteIntent(Arrays.asList(GAMMA_DOC)));

        mExecutor.runAll();
        assertNoDeleteJobsStarted();
    }

    public void testRunsDeleteJobs() throws Exception {
        startService(createDeleteIntent(Arrays.asList(ALPHA_DOC)));

        mDeletionExecutor.runAll();
        assertAllDeleteJobsStarted();
    }

    public void testRunsDeleteJobs_NotRunsCopyJobs() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createDeleteIntent(Arrays.asList(GAMMA_DOC)));

        mDeletionExecutor.runAll();
        assertNoCopyJobsStarted();
    }

    public void testUpdatesNotification() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        mExecutor.runAll();

        // Assert monitoring continues until job is done
        assertTrue(mHandler.hasScheduledMessage());
        // Two notifications -- one for setup; one for progress
        assertEquals(2, mCopyJobs.get(0).getNumOfNotifications());
    }

    public void testStopsUpdatingNotificationAfterFinished() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        mExecutor.runAll();

        mHandler.dispatchNextMessage();
        // Assert monitoring stops once job is completed.
        assertFalse(mHandler.hasScheduledMessage());

        // Assert no more notification is generated after finish.
        assertEquals(2, mCopyJobs.get(0).getNumOfNotifications());

    }

    public void testHoldsWakeLockWhileWorking() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));

        assertTrue(mService.holdsWakeLock());
    }

    public void testReleasesWakeLock_AfterSuccess() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));

        assertTrue(mService.holdsWakeLock());
        mExecutor.runAll();
        assertFalse(mService.holdsWakeLock());
    }

    public void testReleasesWakeLock_AfterFailure() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));

        assertTrue(mService.holdsWakeLock());
        mExecutor.runAll();
        assertFalse(mService.holdsWakeLock());
    }

    public void testShutdownStopsExecutor_AfterSuccess() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));

        mExecutor.assertAlive();
        mDeletionExecutor.assertAlive();

        mExecutor.runAll();
        shutdownService();

        assertExecutorsShutdown();
    }

    public void testShutdownStopsExecutor_AfterMixedFailures() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        mCopyJobs.get(0).fail(ALPHA_DOC);

        mExecutor.runAll();
        shutdownService();

        assertExecutorsShutdown();
    }

    public void testShutdownStopsExecutor_AfterTotalFailure() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        mCopyJobs.get(0).fail(ALPHA_DOC);
        mCopyJobs.get(1).fail(GAMMA_DOC);

        mExecutor.runAll();
        shutdownService();

        assertExecutorsShutdown();
    }

    public void testRunsInForeground_MultipleJobs() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        mExecutor.run(0);
        mForegroundManager.assertInForeground();

        mHandler.dispatchAllMessages();
        mForegroundManager.assertInForeground();
    }

    public void testFinishesInBackground_MultipleJobs() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        mExecutor.run(0);
        mForegroundManager.assertInForeground();

        mHandler.dispatchAllMessages();
        mForegroundManager.assertInForeground();

        mExecutor.run(0);
        mHandler.dispatchAllMessages();
        mForegroundManager.assertInBackground();
    }

    public void testAllNotificationsDismissedAfterShutdown() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));

        mExecutor.runAll();

        mHandler.dispatchAllMessages();
        mTestNotificationManager.assertNumberOfNotifications(0);
    }

    public void testNotificationUpdateAfterForegroundJobSwitch() throws Exception {
        startService(createCopyIntent(Arrays.asList(ALPHA_DOC), BETA_DOC));
        startService(createCopyIntent(Arrays.asList(GAMMA_DOC), DELTA_DOC));
        Job job1 = mCopyJobs.get(0);
        Job job2 = mCopyJobs.get(1);

        mService.onStart(job1);
        mTestNotificationManager.assertHasNotification(
                FileOperationService.NOTIFICATION_ID_PROGRESS, null);

        mService.onStart(job2);
        mTestNotificationManager.assertHasNotification(
                FileOperationService.NOTIFICATION_ID_PROGRESS, job2.id);

        job1.cancel();
        mService.onFinished(job1);
        mTestNotificationManager.assertHasNotification(
                FileOperationService.NOTIFICATION_ID_PROGRESS, null);
        mTestNotificationManager.assertNoNotification(
                FileOperationService.NOTIFICATION_ID_PROGRESS, job2.id);

        job2.cancel();
        mService.onFinished(job2);
        mTestNotificationManager.assertNumberOfNotifications(0);
    }

    private Intent createCopyIntent(List<DocumentInfo> files, DocumentInfo dest)
            throws Exception {
        DocumentStack stack = new DocumentStack();
        stack.push(dest);

        List<Uri> uris = new ArrayList<>(files.size());
        for (DocumentInfo file : files) {
            uris.add(file.derivedUri);
        }

        UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris);
        TestFileOperation operation = new TestFileOperation(OPERATION_COPY, urisSupplier, stack);

        return createBaseIntent(getContext(), createJobId(), operation);
    }

    private Intent createDeleteIntent(List<DocumentInfo> files) {
        DocumentStack stack = new DocumentStack();

        List<Uri> uris = new ArrayList<>(files.size());
        for (DocumentInfo file : files) {
            uris.add(file.derivedUri);
        }

        UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris);
        TestFileOperation operation = new TestFileOperation(OPERATION_DELETE, urisSupplier, stack);

        return createBaseIntent(getContext(), createJobId(), operation);
    }

    private static DocumentInfo createDoc(String name) {
        // Doesn't need to be valid content Uri, just some urly looking thing.
        Uri uri = new Uri.Builder()
                .scheme("content")
                .authority("com.android.documentsui.testing")
                .path(name)
                .build();

        return createDoc(uri);
    }

    void assertAllCopyJobsStarted() {
        for (TestJob job : mCopyJobs) {
            job.assertStarted();
        }
    }

    void assertAllDeleteJobsStarted() {
        for (TestJob job : mDeleteJobs) {
            job.assertStarted();
        }
    }

    void assertNoCopyJobsStarted() {
        for (TestJob job : mCopyJobs) {
            job.assertNotStarted();
        }
    }

    void assertNoDeleteJobsStarted() {
        for (TestJob job : mDeleteJobs) {
            job.assertNotStarted();
        }
    }

    void assertJobsCreated(int expected) {
        assertEquals(expected, mCopyJobs.size() + mDeleteJobs.size());
    }

    private static DocumentInfo createDoc(Uri destination) {
        DocumentInfo destDoc = new DocumentInfo();
        destDoc.derivedUri = destination;
        return destDoc;
    }

    private void assertExecutorsShutdown() {
        mExecutor.assertShutdown();
        mDeletionExecutor.assertShutdown();
    }

    private final class TestFileOperation extends FileOperation {

        private final Runnable mJobRunnable = () -> {
            // The following statement is executed concurrently to Job.start() in real situation.
            // Call it in TestJob.start() to mimic this behavior.
            mHandler.dispatchNextMessage();
        };
        private final @OpType int mOpType;
        private final UrisSupplier mSrcs;
        private final DocumentStack mDestination;

        private TestFileOperation(
                @OpType int opType, UrisSupplier srcs, DocumentStack destination) {
            super(opType, srcs, destination);
            mOpType = opType;
            mSrcs = srcs;
            mDestination = destination;
        }

        @Override
        public Job createJob(Context service, Job.Listener listener, String id, Features features) {
            TestJob job = new TestJob(
                    service, listener, id, mOpType, mDestination, mSrcs, mJobRunnable, features);

            if (mOpType == OPERATION_COPY) {
                mCopyJobs.add(job);
            }

            if (mOpType == OPERATION_DELETE) {
                mDeleteJobs.add(job);
            }

            return job;
        }

        /**
         * CREATOR is required for Parcelables, but we never pass this class via parcel.
         */
        public Parcelable.Creator<TestFileOperation> CREATOR =
                new Parcelable.Creator<TestFileOperation>() {

                    @Override
                    public TestFileOperation createFromParcel(Parcel source) {
                        throw new UnsupportedOperationException("Can't create from a parcel.");
                    }

                    @Override
                    public TestFileOperation[] newArray(int size) {
                        throw new UnsupportedOperationException("Can't create a new array.");
                    }
                };
    }
}
