/*
 * Copyright (C) 2016 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.storagemanager.automatic;

import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.nullable;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.annotation.LooperMode.Mode.LEGACY;

import android.app.NotificationManager;
import android.app.job.JobParameters;
import android.app.usage.StorageStatsManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.os.BatteryManager;
import android.os.SystemProperties;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.provider.Settings;

import com.android.settingslib.deviceinfo.StorageVolumeProvider;
import com.android.storagemanager.overlay.FeatureFactory;
import com.android.storagemanager.overlay.StorageManagementJobProvider;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.LooperMode;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.util.ReflectionHelpers;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
@LooperMode(LEGACY)
public class AutomaticStorageManagementJobServiceTest {
    @Mock private BatteryManager mBatteryManager;
    @Mock private NotificationManager mNotificationManager;
    @Mock private VolumeInfo mVolumeInfo;
    @Mock private File mFile;
    @Mock private JobParameters mJobParameters;
    @Mock private StorageManagementJobProvider mStorageManagementJobProvider;
    @Mock private FeatureFactory mFeatureFactory;
    @Mock private StorageVolumeProvider mStorageVolumeProvider;
    @Mock private AutomaticStorageManagementJobService.Clock mClock;
    private AutomaticStorageManagementJobService mJobService;
    private Context mContext;
    private ShadowApplication mApplication;
    private List<VolumeInfo> mVolumes;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        when(mJobParameters.getJobId()).thenReturn(0);

        // Let's set up our system services to act like a device that has the following conditions:
        // 1. We're plugged in and charging.
        // 2. We have a completely full device.
        // 3. ASM is disabled.
        when(mBatteryManager.isCharging()).thenReturn(true);

        when(mVolumeInfo.getPath()).thenReturn(mFile);
        when(mVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE);
        when(mVolumeInfo.getFsUuid()).thenReturn(StorageManager.UUID_PRIMARY_PHYSICAL);
        when(mVolumeInfo.isMountedReadable()).thenReturn(true);

        mVolumes = new ArrayList<>();
        mVolumes.add(mVolumeInfo);

        when(mStorageVolumeProvider.getPrimaryStorageSize()).thenReturn(100L);
        when(mStorageVolumeProvider.getVolumes()).thenReturn(mVolumes);
        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(mVolumeInfo)))
                .thenReturn(0L);
        when(mStorageVolumeProvider.getTotalBytes(
                        nullable(StorageStatsManager.class), eq(mVolumeInfo)))
                .thenReturn(100L);

        mContext = RuntimeEnvironment.application;
        mApplication = ShadowApplication.getInstance();
        mApplication.setSystemService(Context.BATTERY_SERVICE, mBatteryManager);
        mApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNotificationManager);

        // This is a hack-y injection of our own FeatureFactory.
        // By default, the Storage Manager has a FeatureFactory which returns null for all features.
        // Using reflection, we can inject our own FeatureFactory which returns a mock for the
        // StorageManagementJobProvider feature. This lets us observe when the ASMJobService
        // actually tries to run the job.
        when(mFeatureFactory.getStorageManagementJobProvider())
                .thenReturn(mStorageManagementJobProvider);
        when(mStorageManagementJobProvider.onStartJob(
                        nullable(Context.class), nullable(JobParameters.class), anyInt()))
                .thenReturn(false);
        ReflectionHelpers.setStaticField(FeatureFactory.class, "sFactory", mFeatureFactory);

        // And we can't forget to initialize the actual job service.
        mJobService = spy(Robolectric.setupService(AutomaticStorageManagementJobService.class));
        mJobService.onBind(null);
        mJobService.setStorageVolumeProvider(mStorageVolumeProvider);
        mJobService.setClock(mClock);

        Resources fakeResources = mock(Resources.class);
        when(fakeResources.getInteger(
                        com.android.internal.R.integer.config_storageManagerDaystoRetainDefault))
                .thenReturn(90);

        when(mJobService.getResources()).thenReturn(fakeResources);
    }

    @Test
    public void testJobRequiresCharging() {
        when(mBatteryManager.isCharging()).thenReturn(false);
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        // The job should report that it needs to be retried, if not charging.
        assertJobFinished(true);

        when(mBatteryManager.isCharging()).thenReturn(true);
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertJobFinished(false);
    }

    @Test
    public void testStartJobTriesUpsellWhenASMDisabled() {
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertJobFinished(false);
        mApplication.runBackgroundTasks();

        List<Intent> broadcastedIntents = mApplication.getBroadcastIntents();
        assertThat(broadcastedIntents.size()).isEqualTo(1);

        Intent lastIntent = broadcastedIntents.get(0);
        assertThat(lastIntent.getAction())
                .isEqualTo(NotificationController.INTENT_ACTION_SHOW_NOTIFICATION);
        assertThat(lastIntent.getComponent().getClassName())
                .isEqualTo(NotificationController.class.getCanonicalName());

        assertStorageManagerJobDidNotRun();
    }

    @Test
    public void testASMJobRunsWithValidConditions() {
        activateASM();
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobRan();
    }

    @Test
    public void testASMJobRunsWithValidConditionsIfEnabledByDefaultAndUnset() {
        SystemProperties.set("ro.storage_manager.enabled", "true");
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobRan();
    }

    @Test
    public void testJobDoesntRunIfStorageNotFull() throws Exception {
        activateASM();
        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(mVolumeInfo)))
                .thenReturn(100L);
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobDidNotRun();
    }

    @Test
    public void testJobOnlyRunsIfFreeStorageIsUnder15Percent() throws Exception {
        activateASM();
        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(mVolumeInfo)))
                .thenReturn(15L);
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobDidNotRun();

        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(mVolumeInfo)))
                .thenReturn(14L);
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobRan();
    }

    @Test
    public void testNonDefaultDaysToRetain() {
        ContentResolver resolver = mContext.getContentResolver();
        Settings.Secure.putInt(resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN,
                30);
        activateASM();
        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobRan(30);
    }

    @Test
    public void testNonPrivateDrivesIgnoredForFreeSpaceCalculation() throws Exception {
        File notPrivate = mock(File.class);
        VolumeInfo nonPrivateVolume = mock(VolumeInfo.class);
        when(nonPrivateVolume.getPath()).thenReturn(notPrivate);
        when(nonPrivateVolume.getType()).thenReturn(VolumeInfo.TYPE_PUBLIC);
        mVolumes.add(nonPrivateVolume);
        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(nonPrivateVolume)))
                .thenReturn(0L);
        when(mStorageVolumeProvider.getTotalBytes(
                        nullable(StorageStatsManager.class), eq(nonPrivateVolume)))
                .thenReturn(100L);
        activateASM();
        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(mVolumeInfo)))
                .thenReturn(15L);

        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobDidNotRun();
    }

    @Test
    public void testMultiplePrivateVolumesCountedForASMActivationThreshold() throws Exception {
        File privateVolume = mock(File.class);
        VolumeInfo privateVolumeInfo = mock(VolumeInfo.class);
        when(privateVolumeInfo.getPath()).thenReturn(privateVolume);
        when(privateVolumeInfo.getType()).thenReturn(VolumeInfo.TYPE_PRIVATE);
        when(privateVolumeInfo.isMountedReadable()).thenReturn(true);
        when(privateVolumeInfo.getFsUuid()).thenReturn(StorageManager.UUID_PRIVATE_INTERNAL);
        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(privateVolumeInfo)))
                .thenReturn(0L);
        when(mStorageVolumeProvider.getTotalBytes(
                        nullable(StorageStatsManager.class), eq(privateVolumeInfo)))
                .thenReturn(100L);
        mVolumes.add(privateVolumeInfo);
        activateASM();
        when(mStorageVolumeProvider.getFreeBytes(
                        nullable(StorageStatsManager.class), eq(mVolumeInfo)))
                .thenReturn(15L);

        assertThat(mJobService.onStartJob(mJobParameters)).isFalse();
        assertStorageManagerJobRan();
    }

    @Test
    public void disableSmartStorageIfPastThreshold() throws Exception {
        ContentResolver resolver = mContext.getContentResolver();
        activateASM();

        AutomaticStorageManagementJobService.Clock fakeClock =
                mock(AutomaticStorageManagementJobService.Clock.class);
        when(fakeClock.currentTimeMillis()).thenReturn(1001L);
        when(mStorageManagementJobProvider.getDisableThresholdMillis(any(Context.class)))
                .thenReturn(1000L);
        AutomaticStorageManagementJobService.maybeDisableDueToPolicy(
                mStorageManagementJobProvider, mContext, fakeClock);

        assertThat(
                        Settings.Secure.getInt(
                                resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED))
                .isEqualTo(0);
    }

    @Test
    public void dontDisableSmartStorageIfPastThresholdAndDisabledInThePast() throws Exception {
        ContentResolver resolver = mContext.getContentResolver();
        activateASM();
        Settings.Secure.putInt(
                resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_TURNED_OFF_BY_POLICY, 1);

        AutomaticStorageManagementJobService.Clock fakeClock =
                mock(AutomaticStorageManagementJobService.Clock.class);
        when(fakeClock.currentTimeMillis()).thenReturn(1001L);
        when(mStorageManagementJobProvider.getDisableThresholdMillis(any(Context.class)))
                .thenReturn(1000L);
        AutomaticStorageManagementJobService.maybeDisableDueToPolicy(
                mStorageManagementJobProvider, mContext, fakeClock);

        assertThat(
                        Settings.Secure.getInt(
                                resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED))
                .isNotEqualTo(0);
    }

    @Test
    public void logDisabledByPolicyIfPastThreshold() throws Exception {
        ContentResolver resolver = mContext.getContentResolver();
        activateASM();

        AutomaticStorageManagementJobService.Clock fakeClock =
                mock(AutomaticStorageManagementJobService.Clock.class);
        when(fakeClock.currentTimeMillis()).thenReturn(1001L);
        when(mStorageManagementJobProvider.getDisableThresholdMillis(any(Context.class)))
                .thenReturn(1000L);
        AutomaticStorageManagementJobService.maybeDisableDueToPolicy(
                mStorageManagementJobProvider, mContext, fakeClock);

        assertThat(
                        Settings.Secure.getInt(
                                resolver,
                                Settings.Secure.AUTOMATIC_STORAGE_MANAGER_TURNED_OFF_BY_POLICY))
                .isGreaterThan(0);
    }

    @Test
    public void dontDisableSmartStorageIfNotPastThreshold() throws Exception {
        ContentResolver resolver = mContext.getContentResolver();
        activateASM();

        AutomaticStorageManagementJobService.Clock fakeClock =
                mock(AutomaticStorageManagementJobService.Clock.class);
        when(fakeClock.currentTimeMillis()).thenReturn(999L);
        when(mStorageManagementJobProvider.getDisableThresholdMillis(any(Context.class)))
                .thenReturn(1000L);
        AutomaticStorageManagementJobService.maybeDisableDueToPolicy(
                mStorageManagementJobProvider, mContext, fakeClock);

        assertThat(
                        Settings.Secure.getInt(
                                resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED))
                .isNotEqualTo(0);
    }

    private void assertJobFinished(boolean retryNeeded) {
        verify(mJobService).jobFinished(nullable(JobParameters.class), eq(retryNeeded));
    }

    private void assertStorageManagerJobRan() {
        assertStorageManagerJobRan(
                Settings.Secure.AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_DEFAULT);
    }

    private void assertStorageManagerJobRan(int daysToRetain) {
        verify(mStorageManagementJobProvider).onStartJob(eq(mJobService), eq(mJobParameters),
                eq(daysToRetain));
    }

    private void assertStorageManagerJobDidNotRun() {
        verify(mStorageManagementJobProvider, never())
                .onStartJob(any(Context.class), any(JobParameters.class), anyInt());
    }

    private void activateASM() {
        ContentResolver resolver = mContext.getContentResolver();
        Settings.Secure.putInt(resolver, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 1);
    }
}
