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

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

import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;

import android.annotation.RawRes;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;

import androidx.test.InstrumentationRegistry;

import org.junit.Before;

import java.io.InputStream;
import java.util.List;

/**
 * Temporary copy of DpmTestBase using JUnit 4 - once all tests extend it, it should be renamed
 * back to DpmTestBase (with the temporary methods removed.
 *
 */
public abstract class DpmTestBase {

    public static final String TAG = "DpmTest";

    protected final Context mRealTestContext = InstrumentationRegistry.getTargetContext();
    protected DpmMockContext mMockContext;
    private MockSystemServices mServices;

    // Attributes below are public so they don't need to be prefixed with m
    public ComponentName admin1;
    public ComponentName admin2;
    public ComponentName admin3;
    public ComponentName adminAnotherPackage;
    public ComponentName adminNoPerm;
    public ComponentName delegateCertInstaller;

    @Before
    public void setFixtures() throws Exception {
        var mockBinder = new DpmMockContext.MockBinder();
        mServices = new MockSystemServices(mRealTestContext, "test-data", mockBinder);
        mMockContext = new DpmMockContext(mServices, mRealTestContext, mockBinder);

        admin1 = new ComponentName(mRealTestContext, DummyDeviceAdmins.Admin1.class);
        admin2 = new ComponentName(mRealTestContext, DummyDeviceAdmins.Admin2.class);
        admin3 = new ComponentName(mRealTestContext, DummyDeviceAdmins.Admin3.class);
        adminAnotherPackage = new ComponentName(DpmMockContext.ANOTHER_PACKAGE_NAME,
                "whatever.random.class");
        adminNoPerm = new ComponentName(mRealTestContext, DummyDeviceAdmins.AdminNoPerm.class);
        delegateCertInstaller = new ComponentName(DpmMockContext.DELEGATE_PACKAGE_NAME,
                "some.random.class");
        mockSystemPropertiesToReturnDefault();
    }

    protected DpmMockContext getContext() {
        return mMockContext;
    }

    public MockSystemServices getServices() {
        return mServices;
    }

    protected void sendBroadcastWithUser(DevicePolicyManagerServiceTestable dpms, String action,
            int userHandle) throws Exception {
        final Intent intent = new Intent(action);
        intent.putExtra(Intent.EXTRA_USER_HANDLE, userHandle);
        getServices().injectBroadcast(getContext(), intent, userHandle);
        flushTasks(dpms);
    }

    protected void flushTasks(DevicePolicyManagerServiceTestable dpms) throws Exception {
        dpms.mHandler.runWithScissors(() -> { }, 0 /*now*/);
        dpms.mBackgroundHandler.runWithScissors(() -> { }, 0 /*now*/);

        // We can't let exceptions happen on the background thread. Throw them here if they happen
        // so they still cause the test to fail despite being suppressed.
        getServices().rethrowBackgroundBroadcastExceptions();
    }

    protected interface DpmRunnable {
        void run(DevicePolicyManager dpm) throws Exception;
    }

    /**
     * Simulate an RPC from {@param caller} to the service context ({@link #mMockContext}).
     *
     * The caller sees its own context. The server also sees its own separate context, with the
     * appropriate calling UID and calling permissions fields already set up.
     */
    protected void runAsCaller(DpmMockContext caller, DevicePolicyManagerServiceTestable dpms,
            DpmRunnable action) {
        final DpmMockContext serviceContext = mMockContext;

        // Save calling UID and PID before clearing identity so we don't run into aliasing issues.
        final int callingUid = caller.binder.callingUid;
        final int callingPid = caller.binder.callingPid;

        final long origId = serviceContext.binder.clearCallingIdentity();
        try {
            serviceContext.binder.callingUid = callingUid;
            serviceContext.binder.callingPid = callingPid;
            serviceContext.binder.callingPermissions.put(callingUid, caller.permissions);
            action.run(new DevicePolicyManagerTestable(caller, dpms));
        } catch (Exception e) {
            throw new AssertionError(e);
        } finally {
            serviceContext.binder.restoreCallingIdentity(origId);
        }
    }

    private void markPackageAsInstalled(String packageName, ApplicationInfo ai, int userId)
            throws Exception {
        final PackageInfo pi = DpmTestUtils.cloneParcelable(
                mRealTestContext.getPackageManager().getPackageInfo(
                        mRealTestContext.getPackageName(), 0));
        assertThat(pi.applicationInfo.flags).isNotEqualTo(0);

        if (ai != null) {
            pi.applicationInfo = ai;
        }

        doReturn(pi).when(mServices.ipackageManager).getPackageInfo(packageName, 0, userId);

        mServices.addTestPackageUid(packageName, ai.uid);
    }

    protected void markDelegatedCertInstallerAsInstalled() throws Exception {
        final ApplicationInfo ai = new ApplicationInfo();
        ai.enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
        ai.flags = ApplicationInfo.FLAG_HAS_CODE;
        // Mark the package as installed on the work profile.
        ai.uid = UserHandle.getUid(DpmMockContext.CALLER_USER_HANDLE,
                DpmMockContext.DELEGATE_CERT_INSTALLER_UID);
        ai.packageName = delegateCertInstaller.getPackageName();
        ai.name = delegateCertInstaller.getClassName();

        markPackageAsInstalled(delegateCertInstaller.getPackageName(), ai,
                DpmMockContext.CALLER_USER_HANDLE);
    }

    protected void setUpPackageManagerForAdmin(ComponentName admin, int packageUid)
            throws Exception {
        setUpPackageManagerForAdmin(admin, packageUid,
                /* enabledSetting =*/ null, /* appTargetSdk = */ null);
    }

    protected void setUpPackageManagerForAdmin(ComponentName admin, int packageUid,
            int enabledSetting) throws Exception {
        setUpPackageManagerForAdmin(admin, packageUid, enabledSetting, /* appTargetSdk = */ null);
    }

    protected void setUpPackageManagerForAdmin(ComponentName admin, int packageUid,
            Integer enabledSetting, Integer appTargetSdk) throws Exception {
        setUpPackageManagerForFakeAdmin(admin, packageUid, enabledSetting, appTargetSdk,
                admin);
    }

    protected void setUpPackageManagerForFakeAdmin(ComponentName admin, int packageUid,
            ComponentName copyFromAdmin)
            throws Exception {
        setUpPackageManagerForFakeAdmin(admin, packageUid,
                /* enabledSetting =*/ null, /* appTargetSdk = */ null, copyFromAdmin);
    }

    /**
     * Set up a component in the mock package manager to be an active admin.
     *
     * @param admin ComponentName that's visible to the test code, which doesn't have to exist.
     * @param copyFromAdmin package information for {@code admin} will be built based on this
     *    component's information.
     */
    protected void setUpPackageManagerForFakeAdmin(ComponentName admin, int packageUid,
            Integer enabledSetting, Integer appTargetSdk, ComponentName copyFromAdmin)
            throws Exception {

        // Set up getApplicationInfo().

        final ApplicationInfo ai = DpmTestUtils.cloneParcelable(
                mRealTestContext.getPackageManager().getApplicationInfo(
                        copyFromAdmin.getPackageName(),
                        PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS));
        ai.enabledSetting = enabledSetting == null
                ? PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
                : enabledSetting;
        if (appTargetSdk != null) {
            ai.targetSdkVersion = appTargetSdk;
        }
        ai.uid = packageUid;
        ai.packageName = admin.getPackageName();
        ai.name = admin.getClassName();

        doReturn(ai).when(mServices.ipackageManager).getApplicationInfo(
                eq(admin.getPackageName()),
                anyLong(),
                eq(UserHandle.getUserId(packageUid)));

        // Set up queryBroadcastReceivers().

        final Intent resolveIntent = new Intent();
        resolveIntent.setComponent(copyFromAdmin);
        final List<ResolveInfo> realResolveInfo =
                mRealTestContext.getPackageManager().queryBroadcastReceivers(
                        resolveIntent,
                        PackageManager.GET_META_DATA);
        assertThat(realResolveInfo).isNotNull();
        assertThat(realResolveInfo).hasSize(1);

        // We need to change AI, so set a clone.
        realResolveInfo.set(0, DpmTestUtils.cloneParcelable(realResolveInfo.get(0)));

        // We need to rewrite the UID in the activity info.
        final ActivityInfo aci = realResolveInfo.get(0).activityInfo;
        aci.applicationInfo = ai;
        aci.packageName = admin.getPackageName();
        aci.name = admin.getClassName();

        // Note we don't set up queryBroadcastReceivers.  We don't use it in DPMS.

        doReturn(aci).when(mServices.ipackageManager).getReceiverInfo(
                eq(admin),
                anyLong(),
                eq(UserHandle.getUserId(packageUid)));

        doReturn(new String[] {admin.getPackageName()}).when(mServices.ipackageManager)
            .getPackagesForUid(eq(packageUid));
        doReturn(new String[] {admin.getPackageName()}).when(mServices.packageManager)
                .getPackagesForUid(eq(packageUid));
        // Set up getPackageInfo().
        markPackageAsInstalled(admin.getPackageName(), ai, UserHandle.getUserId(packageUid));
    }

    /**
     * By default, system properties are mocked to return default value. Override the mock if you
     * want a specific value.
     */
    private void mockSystemPropertiesToReturnDefault() {
        when(getServices().systemProperties.get(
                anyString(), anyString())).thenAnswer(
                invocation -> invocation.getArguments()[1]
        );

        when(getServices().systemProperties.getBoolean(
                anyString(), anyBoolean())).thenAnswer(
                invocation -> invocation.getArguments()[1]
        );

        when(getServices().systemProperties.getLong(
                anyString(), anyLong())).thenAnswer(
                invocation -> invocation.getArguments()[1]
        );
    }

    protected InputStream getRawStream(@RawRes int id) {
        return mRealTestContext.getResources().openRawResource(id);
    }
}
