/*
 * Copyright (C) 2021 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.settings.fuelgauge;

import static com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_OPTIMIZED;
import static com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_RESTRICTED;
import static com.android.settings.fuelgauge.BatteryOptimizeUtils.MODE_UNRESTRICTED;

import static com.google.common.truth.Truth.assertThat;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ParceledListSlice;
import android.content.pm.UserInfo;
import android.os.UserManager;
import android.util.ArraySet;

import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settingslib.datastore.DataChangeReason;
import com.android.settingslib.datastore.Observer;
import com.android.settingslib.fuelgauge.PowerAllowlistBackend;

import com.google.common.util.concurrent.MoreExecutors;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

@RunWith(RobolectricTestRunner.class)
public class BatteryOptimizeUtilsTest {

    private static final int UID = 12345;
    private static final String PACKAGE_NAME = "com.android.app";

    @Mock private BatteryUtils mMockBatteryUtils;
    @Mock private AppOpsManager mMockAppOpsManager;
    @Mock private PowerAllowlistBackend mMockBackend;
    @Mock private IPackageManager mMockIPackageManager;
    @Mock private UserManager mMockUserManager;
    @Mock private Observer mObserver;

    private Context mContext;
    private BatteryOptimizeUtils mBatteryOptimizeUtils;
    private BatterySettingsStorage mBatterySettingsStorage;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mContext = spy(RuntimeEnvironment.application);
        mBatterySettingsStorage = BatterySettingsStorage.get(mContext);
        mBatterySettingsStorage.addObserver(mObserver, MoreExecutors.directExecutor());
        mBatteryOptimizeUtils = spy(new BatteryOptimizeUtils(mContext, UID, PACKAGE_NAME));
        mBatteryOptimizeUtils.mAppOpsManager = mMockAppOpsManager;
        mBatteryOptimizeUtils.mBatteryUtils = mMockBatteryUtils;
        mBatteryOptimizeUtils.mPowerAllowListBackend = mMockBackend;
        // Sets the default mode as MODE_RESTRICTED.
        mBatteryOptimizeUtils.mMode = AppOpsManager.MODE_IGNORED;
        mBatteryOptimizeUtils.mAllowListed = false;
        doReturn(mMockUserManager).when(mContext).getSystemService(UserManager.class);
    }

    @Test
    public void testGetAppOptimizationMode_returnRestricted() {
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(false);
        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString()))
                .thenReturn(AppOpsManager.MODE_IGNORED);

        assertThat(mBatteryOptimizeUtils.getAppOptimizationMode()).isEqualTo(MODE_RESTRICTED);
    }

    @Test
    public void testGetAppOptimizationMode_returnUnrestricted() {
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(true);
        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString()))
                .thenReturn(AppOpsManager.MODE_ALLOWED);

        assertThat(mBatteryOptimizeUtils.getAppOptimizationMode()).isEqualTo(MODE_UNRESTRICTED);
    }

    @Test
    public void testGetAppOptimizationMode_returnOptimized() {
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(false);
        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString()))
                .thenReturn(AppOpsManager.MODE_ALLOWED);

        assertThat(mBatteryOptimizeUtils.getAppOptimizationMode()).isEqualTo(MODE_OPTIMIZED);
    }

    @Test
    public void testIsSystemOrDefaultApp_isSystemOrDefaultApp_returnTrue() {
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(true);
        when(mMockBackend.isDefaultActiveApp(anyString(), anyInt())).thenReturn(true);

        assertThat(mBatteryOptimizeUtils.isSystemOrDefaultApp()).isTrue();
    }

    @Test
    public void testIsSystemOrDefaultApp_notSystemOrDefaultApp_returnFalse() {
        assertThat(mBatteryOptimizeUtils.isSystemOrDefaultApp()).isFalse();
    }

    @Test
    public void isDisabledForOptimizeModeOnly_invalidPackageName_returnTrue() {
        final BatteryOptimizeUtils testBatteryOptimizeUtils =
                new BatteryOptimizeUtils(mContext, UID, null);

        assertThat(testBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).isTrue();
    }

    @Test
    public void isDisabledForOptimizeModeOnly_validPackageName_returnFalse() {
        assertThat(mBatteryOptimizeUtils.isDisabledForOptimizeModeOnly()).isFalse();
    }

    @Test
    public void testSetAppUsageState_Restricted_verifyAction() throws Exception {
        // Sets the current mode as MODE_UNRESTRICTED.
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(true);
        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString()))
                .thenReturn(AppOpsManager.MODE_ALLOWED);

        mBatteryOptimizeUtils.setAppUsageState(MODE_RESTRICTED, Action.UNKNOWN);
        TimeUnit.SECONDS.sleep(1);

        verifySetAppOptimizationMode(AppOpsManager.MODE_IGNORED, /* allowListed */ false);
        verify(mObserver).onChanged(DataChangeReason.UPDATE);
    }

    @Test
    public void testSetAppUsageState_Unrestricted_verifyAction() throws Exception {
        // Sets the current mode as MODE_RESTRICTED.
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(false);
        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString()))
                .thenReturn(AppOpsManager.MODE_IGNORED);

        mBatteryOptimizeUtils.setAppUsageState(MODE_UNRESTRICTED, Action.UNKNOWN);
        TimeUnit.SECONDS.sleep(1);

        verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ true);
        verify(mObserver).onChanged(DataChangeReason.UPDATE);
    }

    @Test
    public void testSetAppUsageState_Optimized_verifyAction() throws Exception {
        // Sets the current mode as MODE_UNRESTRICTED.
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(true);
        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString()))
                .thenReturn(AppOpsManager.MODE_ALLOWED);

        mBatteryOptimizeUtils.setAppUsageState(MODE_OPTIMIZED, Action.UNKNOWN);
        TimeUnit.SECONDS.sleep(1);

        verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false);
        verify(mObserver).onChanged(DataChangeReason.UPDATE);
    }

    @Test
    public void testSetAppUsageState_sameUnrestrictedMode_verifyNoAction() throws Exception {
        // Sets the current mode as MODE_UNRESTRICTED.
        when(mMockBackend.isAllowlisted(anyString(), anyInt())).thenReturn(true);
        when(mMockAppOpsManager.checkOpNoThrow(anyInt(), anyInt(), anyString()))
                .thenReturn(AppOpsManager.MODE_ALLOWED);

        mBatteryOptimizeUtils.setAppUsageState(MODE_UNRESTRICTED, Action.UNKNOWN);
        TimeUnit.SECONDS.sleep(1);

        verify(mMockBatteryUtils, never()).setForceAppStandby(anyInt(), anyString(), anyInt());
        verify(mMockBackend, never()).addApp(anyString(), anyInt());
        verify(mMockBackend, never()).removeApp(anyString(), anyInt());
        verifyNoInteractions(mObserver);
    }

    @Test
    public void testGetInstalledApplications_returnEmptyArray() {
        assertTrue(
                BatteryOptimizeUtils.getInstalledApplications(mContext, mMockIPackageManager)
                        .isEmpty());
    }

    @Test
    public void testGetInstalledApplications_returnNull() throws Exception {
        final UserInfo userInfo =
                new UserInfo(/* userId= */ 0, /* userName= */ "google", /* flag= */ 0);
        doReturn(Arrays.asList(userInfo)).when(mMockUserManager).getProfiles(anyInt());
        doThrow(new RuntimeException())
                .when(mMockIPackageManager)
                .getInstalledApplications(anyLong(), anyInt());

        assertNull(BatteryOptimizeUtils.getInstalledApplications(mContext, mMockIPackageManager));
    }

    @Test
    public void testGetInstalledApplications_returnInstalledApps() throws Exception {
        final UserInfo userInfo =
                new UserInfo(/* userId= */ 0, /* userName= */ "google", /* flag= */ 0);
        doReturn(Arrays.asList(userInfo)).when(mMockUserManager).getProfiles(anyInt());

        final ApplicationInfo applicationInfo1 = new ApplicationInfo();
        applicationInfo1.enabled = true;
        applicationInfo1.uid = 1;
        final ApplicationInfo applicationInfo2 = new ApplicationInfo();
        applicationInfo2.enabled = false;
        applicationInfo2.uid = 2;
        applicationInfo2.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
        final ApplicationInfo applicationInfo3 = new ApplicationInfo();
        applicationInfo3.enabled = false;
        applicationInfo3.uid = 3;
        applicationInfo3.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
        final ApplicationInfo applicationInfo4 = new ApplicationInfo();
        applicationInfo4.enabled = true;
        applicationInfo4.uid = 4;
        applicationInfo4.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER;
        doReturn(
                        new ParceledListSlice<ApplicationInfo>(
                                Arrays.asList(
                                        applicationInfo1,
                                        applicationInfo2,
                                        applicationInfo3,
                                        applicationInfo4)))
                .when(mMockIPackageManager)
                .getInstalledApplications(anyLong(), anyInt());

        final ArraySet<ApplicationInfo> applications =
                BatteryOptimizeUtils.getInstalledApplications(mContext, mMockIPackageManager);
        assertThat(applications.size()).isEqualTo(3);
        // applicationInfo3 should be filtered.
        assertTrue(applications.contains(applicationInfo1));
        assertTrue(applications.contains(applicationInfo2));
        assertFalse(applications.contains(applicationInfo3));
        assertTrue(applications.contains(applicationInfo4));
    }

    @Test
    public void testResetAppOptimizationModeInternal_Optimized_verifyAction() throws Exception {
        runTestForResetWithMode(
                AppOpsManager.MODE_ALLOWED, /* allowListed */
                false,
                /* isSystemOrDefaultApp */ false);

        verifyNoInteractions(mMockBatteryUtils);

        final InOrder inOrder = inOrder(mMockBackend);
        inOrder.verify(mMockBackend).refreshList();
        inOrder.verify(mMockBackend).isAllowlisted(PACKAGE_NAME, UID);
        verifyNoMoreInteractions(mMockBackend);
    }

    @Test
    public void testResetAppOptimizationModeInternal_SystemOrDefault_verifyAction()
            throws Exception {
        runTestForResetWithMode(
                AppOpsManager.MODE_ALLOWED, /* allowListed */
                true,
                /* isSystemOrDefaultApp */ true);

        verifyNoInteractions(mMockBatteryUtils);

        final InOrder inOrder = inOrder(mMockBackend);
        inOrder.verify(mMockBackend).refreshList();
        inOrder.verify(mMockBackend).isAllowlisted(PACKAGE_NAME, UID);
        inOrder.verify(mMockBackend).isSysAllowlisted(PACKAGE_NAME);
        verifyNoMoreInteractions(mMockBackend);
        verify(mObserver).onChanged(DataChangeReason.DELETE);
    }

    @Test
    public void testResetAppOptimizationModeInternal_Restricted_verifyAction() throws Exception {
        runTestForResetWithMode(
                AppOpsManager.MODE_IGNORED, /* allowListed */
                false,
                /* isSystemOrDefaultApp */ false);

        verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false);
        verify(mObserver).onChanged(DataChangeReason.DELETE);
    }

    @Test
    public void testResetAppOptimizationModeInternal_Unrestricted_verifyAction() throws Exception {
        runTestForResetWithMode(
                AppOpsManager.MODE_ALLOWED, /* allowListed */
                true,
                /* isSystemOrDefaultApp */ false);

        verifySetAppOptimizationMode(AppOpsManager.MODE_ALLOWED, /* allowListed */ false);
        verify(mObserver).onChanged(DataChangeReason.DELETE);
    }

    private void runTestForResetWithMode(
            int appStandbyMode, boolean allowListed, boolean isSystemOrDefaultApp)
            throws Exception {
        final UserInfo userInfo =
                new UserInfo(/* userId= */ 0, /* userName= */ "google", /* flag= */ 0);
        doReturn(Arrays.asList(userInfo)).when(mMockUserManager).getProfiles(anyInt());
        final ApplicationInfo applicationInfo = new ApplicationInfo();
        applicationInfo.uid = UID;
        applicationInfo.packageName = PACKAGE_NAME;
        applicationInfo.enabled = true;
        doReturn(new ParceledListSlice<ApplicationInfo>(Arrays.asList(applicationInfo)))
                .when(mMockIPackageManager)
                .getInstalledApplications(anyLong(), anyInt());

        doReturn(appStandbyMode)
                .when(mMockAppOpsManager)
                .checkOpNoThrow(anyInt(), anyInt(), anyString());
        doReturn(allowListed).when(mMockBackend).isAllowlisted(anyString(), anyInt());
        doReturn(isSystemOrDefaultApp).when(mMockBackend).isSysAllowlisted(anyString());
        doReturn(isSystemOrDefaultApp).when(mMockBackend).isDefaultActiveApp(anyString(), anyInt());

        BatteryOptimizeUtils.resetAppOptimizationModeInternal(
                mContext,
                mMockIPackageManager,
                mMockAppOpsManager,
                mMockBackend,
                mMockBatteryUtils);
        TimeUnit.SECONDS.sleep(1);
    }

    private void verifySetAppOptimizationMode(int appStandbyMode, boolean allowListed) {
        verify(mMockBatteryUtils).setForceAppStandby(UID, PACKAGE_NAME, appStandbyMode);
        if (allowListed) {
            verify(mMockBackend).addApp(PACKAGE_NAME, UID);
        } else {
            verify(mMockBackend).removeApp(PACKAGE_NAME, UID);
        }
    }
}
