/*
 * Copyright (C) 2022 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.server.devicelock;

import static android.app.AppOpsManager.OPSTR_SYSTEM_EXEMPT_FROM_HIBERNATION;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.devicelock.DeviceId.DEVICE_ID_TYPE_IMEI;
import static android.devicelock.DeviceId.DEVICE_ID_TYPE_MEID;
import static android.devicelock.IDeviceLockService.KEY_REMOTE_CALLBACK_RESULT;
import static android.os.UserHandle.USER_SYSTEM;

import static com.android.server.devicelock.DeviceLockControllerPackageUtils.SERVICE_ACTION;
import static com.android.server.devicelock.DeviceLockServiceImpl.MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER;
import static com.android.server.devicelock.DeviceLockServiceImpl.OPSTR_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION;
import static com.android.server.devicelock.DeviceLockServiceImpl.OPSTR_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS;
import static com.android.server.devicelock.DeviceLockServiceImpl.OPSTR_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS;
import static com.android.server.devicelock.TestUtils.eventually;

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

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;

import android.app.AppOpsManager;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.devicelock.IGetDeviceIdCallback;
import android.os.Binder;
import android.os.Bundle;
import android.os.Looper;
import android.os.PowerExemptionManager;
import android.os.Process;
import android.os.RemoteCallback;
import android.os.UserHandle;
import android.os.UserManager;
import android.telephony.TelephonyManager;

import androidx.test.core.app.ApplicationProvider;

import com.android.devicelockcontroller.IDeviceLockControllerService;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowAppOpsManager;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowBinder;
import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.shadows.ShadowTelephonyManager;
import org.robolectric.shadows.ShadowUserManager;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Tests for {@link com.android.server.devicelock.DeviceLockServiceImpl}.
 *
 * TODO(b/329330992): Add tests for multi-user scenarios where users have different finalization
 * states. Robolectric does not support creating contexts as other users, so the package manager
 * infos are the same for all users. This makes it infeasible to unit test scenarios where the
 * package states are different for different users.
 *
 */
@RunWith(RobolectricTestRunner.class)
public final class DeviceLockServiceImplTest {
    private static final String DLC_PACKAGE_NAME = "test.package";

    private static final String DLC_SERVICE_NAME = "test.service";

    private static final String SYSTEM_USER_NAME = "system";
    private static final int USER_SECONDARY = 10;
    private static final String SECONDARY_USER_NAME = "secondary";

    private static final long ONE_SEC_MILLIS = 1000;

    @Rule
    public MockitoRule mMockitoRule = MockitoJUnit.rule();

    private Context mContext;
    private ShadowTelephonyManager mShadowTelephonyManager;
    private ShadowAppOpsManager mShadowAppOpsManager;
    private ShadowPackageManager mShadowPackageManager;
    private PackageManager mPackageManager;
    private ShadowUserManager mShadowUserManager;
    private UserHandle mSystemUser;
    private UserHandle mSecondaryUser;

    @Mock
    private IDeviceLockControllerService mDeviceLockControllerService;
    @Mock
    private PowerExemptionManager mPowerExemptionManager;

    private ShadowApplication mShadowApplication;

    private DeviceLockServiceImpl mService;
    private final ExecutorService mExecutorService = Executors.newSingleThreadExecutor();

    @Before
    public void setup() throws Exception {
        mContext = ApplicationProvider.getApplicationContext();
        mShadowApplication = shadowOf((Application) mContext);
        mShadowApplication.grantPermissions(MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER);
        mShadowApplication.setSystemService(
                mContext.getSystemServiceName(PowerExemptionManager.class),
                mPowerExemptionManager);

        mPackageManager = mContext.getPackageManager();
        mShadowPackageManager = shadowOf(mPackageManager);
        mShadowPackageManager.setPackagesForUid(Process.myUid(),
                new String[]{mContext.getPackageName()});

        PackageInfo dlcPackageInfo = new PackageInfo();
        dlcPackageInfo.packageName = DLC_PACKAGE_NAME;
        mShadowPackageManager.installPackage(dlcPackageInfo);

        Intent intent = new Intent(SERVICE_ACTION);
        ResolveInfo resolveInfo = makeDlcResolveInfo();
        mShadowPackageManager.addResolveInfoForIntent(intent, resolveInfo);

        TelephonyManager telephonyManager = mContext.getSystemService(TelephonyManager.class);
        mShadowTelephonyManager = shadowOf(telephonyManager);

        mShadowAppOpsManager = shadowOf(mContext.getSystemService(AppOpsManager.class));

        mShadowUserManager = shadowOf(mContext.getSystemService(UserManager.class));
        mSystemUser = mShadowUserManager.addUser(USER_SYSTEM, SYSTEM_USER_NAME, /* flags= */ 0);
        mSecondaryUser = mShadowUserManager.addUser(USER_SECONDARY, SECONDARY_USER_NAME,
                /* flags= */ 0);

        mService = new DeviceLockServiceImpl(mContext, telephonyManager, mExecutorService,
                mContext.getFilesDir());
        waitUntilBgExecutorIdle();
        shadowOf(Looper.getMainLooper()).idle();
    }

    @Test
    public void getDeviceId_withIMEIType_shouldReturnIMEI() throws Exception {
        // GIVEN an IMEI registered in telephony manager
        final String testImei = "983402979622353";
        mShadowTelephonyManager.setActiveModemCount(1);
        mShadowTelephonyManager.setImei(/* slotIndex= */ 0, testImei);

        // GIVEN a successful service call to DLC app
        doAnswer((Answer<Void>) invocation -> {
            RemoteCallback callback = invocation.getArgument(0);
            Bundle bundle = new Bundle();
            bundle.putString(IDeviceLockControllerService.KEY_RESULT, testImei);
            callback.sendResult(bundle);
            return null;
        }).when(mDeviceLockControllerService).getDeviceIdentifier(any(RemoteCallback.class));

        IGetDeviceIdCallback mockCallback = mock(IGetDeviceIdCallback.class);

        // WHEN the device id is requested with the IMEI device type
        mService.getDeviceId(mockCallback, 1 << DEVICE_ID_TYPE_IMEI);
        waitUntilConnected();

        // THEN the IMEI id is received
        verify(mockCallback, timeout(ONE_SEC_MILLIS)).onDeviceIdReceived(
                eq(DEVICE_ID_TYPE_IMEI), eq(testImei));
    }

    @Test
    public void getDeviceId_withMEIDType_shouldReturnMEID() throws Exception {
        // GIVEN an MEID registered in telephony manager
        final String testMeid = "354403064522046";
        mShadowTelephonyManager.setActiveModemCount(1);
        mShadowTelephonyManager.setMeid(/* slotIndex= */ 0, testMeid);
        mShadowPackageManager.setSystemFeature(PackageManager.FEATURE_TELEPHONY_CDMA,
                /* supported= */ true);

        // GIVEN a successful service call to DLC app
        doAnswer((Answer<Void>) invocation -> {
            RemoteCallback callback = invocation.getArgument(0);
            Bundle bundle = new Bundle();
            bundle.putString(IDeviceLockControllerService.KEY_RESULT, testMeid);
            callback.sendResult(bundle);
            return null;
        }).when(mDeviceLockControllerService).getDeviceIdentifier(any(RemoteCallback.class));

        IGetDeviceIdCallback mockCallback = mock(IGetDeviceIdCallback.class);

        // WHEN the device id is requested with the MEID device type
        mService.getDeviceId(mockCallback, 1 << DEVICE_ID_TYPE_MEID);
        waitUntilConnected();

        // THEN the MEID id is received
        verify(mockCallback, timeout(ONE_SEC_MILLIS)).onDeviceIdReceived(
                eq(DEVICE_ID_TYPE_MEID), eq(testMeid));
    }

    @Test
    public void setCallerAllowedToSendUndismissibleNotifications_trueAllowsAppOp() {
        final AtomicBoolean succeeded = new AtomicBoolean(false);
        RemoteCallback callback = new RemoteCallback(result -> {
            succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT));
        });
        mService.setCallerAllowedToSendUndismissibleNotifications(true, callback);

        assertThat(succeeded.get()).isTrue();
        final int opMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS,
                Process.myUid(),
                DLC_PACKAGE_NAME);
        assertThat(opMode).isEqualTo(AppOpsManager.MODE_ALLOWED);
    }

    @Test
    public void setCallerAllowedToSendUndismissibleNotifications_falseDisallowsAppOp() {
        final AtomicBoolean succeeded = new AtomicBoolean(false);
        RemoteCallback callback = new RemoteCallback(result -> {
            succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT));
        });
        mService.setCallerAllowedToSendUndismissibleNotifications(false, callback);

        assertThat(succeeded.get()).isTrue();
        final int opMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_DISMISSIBLE_NOTIFICATIONS,
                Process.myUid(),
                DLC_PACKAGE_NAME);
        assertThat(opMode).isEqualTo(AppOpsManager.MODE_DEFAULT);
    }

    @Test
    public void setCallerExemptFromActivityBgStartRestrictionState_trueAllowsAppOp() {
        final AtomicBoolean succeeded = new AtomicBoolean(false);
        RemoteCallback callback = new RemoteCallback(result -> {
            succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT));
        });
        mService.setCallerExemptFromActivityBgStartRestrictionState(true, callback);

        assertThat(succeeded.get()).isTrue();
        final int opMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
                Process.myUid(),
                DLC_PACKAGE_NAME);
        assertThat(opMode).isEqualTo(AppOpsManager.MODE_ALLOWED);
    }

    @Test
    public void setCallerExemptFromActivityBgStartRestrictionState_falseDisallowsAppOp() {
        final AtomicBoolean succeeded = new AtomicBoolean(false);
        RemoteCallback callback = new RemoteCallback(result -> {
            succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT));
        });
        mService.setCallerExemptFromActivityBgStartRestrictionState(false, callback);

        assertThat(succeeded.get()).isTrue();
        final int opMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_ACTIVITY_BG_START_RESTRICTION,
                Process.myUid(),
                DLC_PACKAGE_NAME);
        assertThat(opMode).isEqualTo(AppOpsManager.MODE_DEFAULT);
    }

    @Test
    public void setUidExemptFromRestrictionsState_trueAllowsAppOps() {
        final AtomicBoolean succeeded = new AtomicBoolean(false);
        RemoteCallback callback = new RemoteCallback(result -> {
            succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT));
        });
        mService.setUidExemptFromRestrictionsState(Process.myUid(), true, callback);

        assertThat(succeeded.get()).isTrue();
        final int hibernationOpMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_HIBERNATION,
                Process.myUid(),
                mContext.getPackageName());
        assertThat(hibernationOpMode).isEqualTo(AppOpsManager.MODE_ALLOWED);
        final int powerOpMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS,
                Process.myUid(),
                mContext.getPackageName());
        assertThat(powerOpMode).isEqualTo(AppOpsManager.MODE_ALLOWED);
    }

    @Test
    public void setUidExemptFromRestrictionsState_falseDisallowsAppOps() {
        final AtomicBoolean succeeded = new AtomicBoolean(false);
        RemoteCallback callback = new RemoteCallback(result -> {
            succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT));
        });
        mService.setUidExemptFromRestrictionsState(Process.myUid(), false, callback);

        assertThat(succeeded.get()).isTrue();
        final int hibernationOpMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_HIBERNATION,
                Process.myUid(),
                mContext.getPackageName());
        assertThat(hibernationOpMode).isEqualTo(AppOpsManager.MODE_DEFAULT);
        final int powerOpMode = mShadowAppOpsManager.unsafeCheckOpNoThrow(
                OPSTR_SYSTEM_EXEMPT_FROM_POWER_RESTRICTIONS,
                Process.myUid(),
                mContext.getPackageName());
        assertThat(powerOpMode).isEqualTo(AppOpsManager.MODE_DEFAULT);
    }

    @Test
    public void setDeviceFinalized_nonSystemUser_disablesPackage() throws Exception {
        ShadowBinder.setCallingUserHandle(mSecondaryUser);

        AtomicBoolean succeeded = new AtomicBoolean(false);
        mService.setDeviceFinalized(true, new RemoteCallback(result -> succeeded.set(true)));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isTrue();
        assertThat(mPackageManager.getApplicationEnabledSetting(DLC_PACKAGE_NAME))
                .isEqualTo(COMPONENT_ENABLED_STATE_DISABLED);
    }

    @Test
    public void setDeviceFinalized_systemUser_butOtherUserUnfinalized_doesNotDisablePackage()
            throws Exception {
        ShadowBinder.setCallingUserHandle(mSystemUser);

        AtomicBoolean succeeded = new AtomicBoolean(false);
        mService.setDeviceFinalized(true, new RemoteCallback(result -> succeeded.set(true)));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isTrue();
        assertThat(mPackageManager.getApplicationEnabledSetting(DLC_PACKAGE_NAME))
                .isEqualTo(COMPONENT_ENABLED_STATE_DEFAULT);
    }

    @Test
    public void onUserSwitching_ifNotFinalizedAndDlcDisabled_enables() throws Exception {
        // GIVEN device is not finalized and DLC is disabled
        ShadowBinder.setCallingUserHandle(mSecondaryUser);
        mPackageManager.setApplicationEnabledSetting(
                DLC_PACKAGE_NAME, COMPONENT_ENABLED_STATE_DISABLED, /* flags= */ 0);

        // WHEN the service checks finalization
        mService.onUserSwitching(mSecondaryUser);

        waitUntilBgExecutorIdle();
        shadowOf(Looper.getMainLooper()).idle();

        // THEN DLC is enabled
        assertThat(mPackageManager.getApplicationEnabledSetting(DLC_PACKAGE_NAME))
                .isEqualTo(COMPONENT_ENABLED_STATE_DEFAULT);
    }

    @Test
    public void onUserSwitching_ifFinalizedAndDisabledOnSecondary_doesNothing() throws Exception {
        // GIVEN device is finalized and DLC is disabled on a secondary user
        ShadowBinder.setCallingUserHandle(mSecondaryUser);
        mService.setDeviceFinalized(true, new RemoteCallback(result -> {}));
        waitUntilBgExecutorIdle();
        assertThat(mPackageManager.getApplicationEnabledSetting(DLC_PACKAGE_NAME))
                .isEqualTo(COMPONENT_ENABLED_STATE_DISABLED);

        // WHEN there is a user switch to a secondary user
        mService.onUserSwitching(mSecondaryUser);

        waitUntilBgExecutorIdle();
        shadowOf(Looper.getMainLooper()).idle();

        // THEN DLC stays disabled
        assertThat(mPackageManager.getApplicationEnabledSetting(DLC_PACKAGE_NAME))
                .isEqualTo(COMPONENT_ENABLED_STATE_DISABLED);
    }

    @Test
    public void enableKioskKeepalive_withoutPermission_shouldFail() throws Exception {
        mShadowApplication.denyPermissions(MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER);

        AtomicBoolean succeeded = new AtomicBoolean(true);
        mService.enableKioskKeepalive(mContext.getPackageName(), new RemoteCallback(result ->
                succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT))));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isFalse();
    }

    @Test
    public void disableKioskKeepalive_withoutPermission_shouldFail() throws Exception {
        mShadowApplication.denyPermissions(MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER);

        AtomicBoolean succeeded = new AtomicBoolean(true);
        mService.disableKioskKeepalive(new RemoteCallback(result ->
                succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT))));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isFalse();
    }

    @Test
    public void enableControllerKeepalive_withoutPermission_shouldFail() throws Exception {
        mShadowApplication.denyPermissions(MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER);

        AtomicBoolean succeeded = new AtomicBoolean(true);
        mService.enableControllerKeepalive(new RemoteCallback(result ->
                succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT))));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isFalse();
    }

    @Test
    public void disableControllerKeepalive_withoutPermission_shouldFail() throws Exception {
        mShadowApplication.denyPermissions(MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER);

        AtomicBoolean succeeded = new AtomicBoolean(true);
        mService.disableControllerKeepalive(new RemoteCallback(result ->
                succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT))));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isFalse();
    }

    @Test
    public void setDeviceFinalized_withoutPermission_shouldFail() throws Exception {
        mShadowApplication.denyPermissions(MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER);

        AtomicBoolean succeeded = new AtomicBoolean(true);
        mService.setDeviceFinalized(true, new RemoteCallback(result ->
                succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT))));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isFalse();
    }

    @Test
    public void setPostNotificationsSystemFixed_withoutPermission_shouldFail() throws Exception {
        mShadowApplication.denyPermissions(MANAGE_DEVICE_LOCK_SERVICE_FROM_CONTROLLER);

        AtomicBoolean succeeded = new AtomicBoolean(true);
        mService.setPostNotificationsSystemFixed(true, new RemoteCallback(result ->
                succeeded.set(result.getBoolean(KEY_REMOTE_CALLBACK_RESULT))));
        waitUntilBgExecutorIdle();

        assertThat(succeeded.get()).isFalse();
    }

    /**
     * Make the resolve info for the DLC package.
     */
    private ResolveInfo makeDlcResolveInfo() {
        ApplicationInfo appInfo = new ApplicationInfo();
        appInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
        appInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
        ServiceInfo serviceInfo = new ServiceInfo();
        serviceInfo.name = DLC_SERVICE_NAME;
        serviceInfo.packageName = DLC_PACKAGE_NAME;
        serviceInfo.applicationInfo = appInfo;
        ResolveInfo resolveInfo = new ResolveInfo();
        resolveInfo.serviceInfo = serviceInfo;

        return resolveInfo;
    }

    /**
     * Set-up calls to mock the service being connected.
     */
    private void waitUntilConnected() {
        eventually(() -> {
            shadowOf(Looper.getMainLooper()).idle();
            ServiceConnection connection = mShadowApplication.getBoundServiceConnections().get(0);
            Binder binder = new Binder();
            binder.attachInterface(mDeviceLockControllerService,
                    IDeviceLockControllerService.class.getName());
            connection.onServiceConnected(new ComponentName(DLC_PACKAGE_NAME, DLC_SERVICE_NAME),
                    binder);
        }, ONE_SEC_MILLIS);
    }

    private void waitUntilBgExecutorIdle() throws InterruptedException, ExecutionException {
        mExecutorService.submit(() -> {}).get();
    }
}
