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

import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.nullable;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageDeleteObserver;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.view.inputmethod.InputMethodInfo;

import androidx.test.filters.SmallTest;

import com.android.settingslib.BaseTest;

import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

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

@SmallTest
public class AppRestrictionsHelperTest extends BaseTest {
    private @Mock Context mContext;
    private @Mock PackageManager mPm;
    private @Mock IPackageManager mIpm;
    private @Mock UserManager mUm;

    private TestInjector mInjector;
    private UserHandle mTestUser = UserHandle.of(1111);
    private AppRestrictionsHelper mHelper;

    private ArrayList<String> mInstalledApps;
    private ArrayList<ApplicationInfo> mInstalledAppInfos;

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        MockitoAnnotations.initMocks(this);

        mInjector = new TestInjector();
        final UserInfo user = new UserInfo(
                mTestUser.getIdentifier(), "test_user", UserInfo.FLAG_RESTRICTED);
        when(mUm.getUserInfo(mTestUser.getIdentifier())).thenReturn(user);
        mHelper = new AppRestrictionsHelper(mInjector);
        mInstalledApps = new ArrayList<>();
        mInstalledAppInfos = new ArrayList<>();
    }

    public void testFetchAndMergeApps() throws Exception {
        addSystemAppsWithRequiredAccounts("sys.app0");
        addsystemImes(new String[] {"sys.app1", "sys.app2"},
                new String[] {"sys.app3", "sys.app4", "sys.app5"});
        addSystemAppsForIntent(new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER),
                "sys.app1", "sys.app4", "sys.app6");
        addSystemAppsForIntent(new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE),
                "sys.app2", "sys.app5", "sys.app7");
        addDownloadedApps("app1", "app2");
        when(mPm.getInstalledApplications(anyInt())).thenReturn(mInstalledAppInfos);

        mHelper.fetchAndMergeApps();

        final ArrayList<String> notExpectedInVisibleApps = new ArrayList<>();
        // System apps that require an account and doesn't see restricted account are
        // not part of visibleApps.
        notExpectedInVisibleApps.add("sys.app0");
        // Default system IMEs are not part of visibleApps.
        notExpectedInVisibleApps.add("sys.app1");
        notExpectedInVisibleApps.add("sys.app2");

        final ArrayList<String> expectedInVisibleApps = new ArrayList<>();
        expectedInVisibleApps.add("sys.app4");
        expectedInVisibleApps.add("sys.app5");
        expectedInVisibleApps.add("sys.app6");
        expectedInVisibleApps.add("sys.app7");
        expectedInVisibleApps.add("app1");
        expectedInVisibleApps.add("app2");

        for (AppRestrictionsHelper.SelectableAppInfo info : mHelper.getVisibleApps()) {
            if (expectedInVisibleApps.contains(info.packageName)) {
                expectedInVisibleApps.remove(info.packageName);
            } else if (notExpectedInVisibleApps.contains(info.packageName)) {
                fail("Package: " + info.packageName + " should not be included in visibleApps");
            } else {
                fail("Unknown package: " + info.packageName);
            }
        }
        assertEquals("Some expected apps are not inclued in visibleApps: " + expectedInVisibleApps,
                0, expectedInVisibleApps.size());

        assertFalse("System apps that require an account and doesn't see restricted account "
                + "should be marked for removal", mHelper.isPackageSelected("sys.app0"));
    }

    public void testApplyUserAppsStates() throws Exception {
        final int testUserId = mTestUser.getIdentifier();
        mHelper.setPackageSelected("app1", true);

        mHelper.setPackageSelected("app2", true);
        ApplicationInfo info = new ApplicationInfo();
        info.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HIDDEN;
        info.flags |= ApplicationInfo.FLAG_INSTALLED;
        when(mIpm.getApplicationInfo(eq("app2"), anyLong(), eq(testUserId)))
                .thenReturn(info);

        mHelper.setPackageSelected("app3", false);
        info = new ApplicationInfo();
        when(mIpm.getApplicationInfo(eq("app3"), anyLong(), eq(testUserId)))
                .thenReturn(info);

        AppRestrictionsHelper.OnDisableUiForPackageListener mockListener =
                mock(AppRestrictionsHelper.OnDisableUiForPackageListener.class);
        mHelper.applyUserAppsStates(mockListener);

        verify(mIpm, times(1)).installExistingPackageAsUser("app1", testUserId,
                PackageManager.INSTALL_ALL_WHITELIST_RESTRICTED_PERMISSIONS,
                PackageManager.INSTALL_REASON_UNKNOWN, null);
        verify(mIpm, times(1)).setApplicationHiddenSettingAsUser("app2", false, testUserId);
        verify(mockListener).onDisableUiForPackage("app2");
        verify(mPm, times(1)).deletePackageAsUser(eq("app3"),
                nullable(IPackageDeleteObserver.class), anyInt(), eq(mTestUser.getIdentifier()));
    }

    private void addsystemImes(String[] defaultImes, String[] otherImes) throws
            PackageManager.NameNotFoundException, RemoteException {
        final ArrayList<InputMethodInfo> inputMethods = new ArrayList<>();
        for (String pkg : defaultImes) {
            final ResolveInfo ri = createResolveInfoForSystemApp(pkg);
            final InputMethodInfo inputMethodInfo = new InputMethodInfo(
                    ri, false, null, null, 0, true, true, false);
            inputMethods.add(inputMethodInfo);
            addInstalledApp(ri);
        }
        for (String pkg : otherImes) {
            final ResolveInfo ri = createResolveInfoForSystemApp(pkg);
            final InputMethodInfo inputMethodInfo = new InputMethodInfo(
                    ri, false, null, null, 0, false, true, false);
            inputMethods.add(inputMethodInfo);
            addInstalledApp(ri);
        }

        mInjector.setInputMethodList(inputMethods);
    }

    private void addSystemAppsForIntent(Intent intent, String... packages) throws Exception {
        List<ResolveInfo> resolveInfos = new ArrayList<>();
        for (String pkg : packages) {
            final ResolveInfo ri = createResolveInfoForSystemApp(pkg);
            resolveInfos.add(ri);
            addInstalledApp(ri);
        }
        when(mPm.queryIntentActivities(argThat(new IntentMatcher(intent)), anyInt()))
                .thenReturn(resolveInfos);
    }

    private void addSystemAppsWithRequiredAccounts(String... packages) throws Exception {
        for (String pkg : packages) {
            final ResolveInfo ri = createResolveInfoForSystemApp(pkg);
            final PackageInfo packageInfo = new PackageInfo();
            packageInfo.applicationInfo = ri.activityInfo.applicationInfo;
            packageInfo.applicationInfo.flags |= ApplicationInfo.FLAG_INSTALLED;
            packageInfo.requiredAccountType = "account";
            packageInfo.restrictedAccountType = null;
            mInstalledAppInfos.add(packageInfo.applicationInfo);
            when(mPm.getPackageInfo(eq(pkg), anyInt())).thenReturn(packageInfo);
        }
    }

    private void addDownloadedApps(String... packages) throws Exception {
        for (String pkg : packages) {
            final ResolveInfo ri = createResolveInfo(pkg);
            addInstalledApp(ri);
        }
    }

    private void addInstalledApp(ResolveInfo ri) throws PackageManager.NameNotFoundException {
        final String pkgName = ri.activityInfo.packageName;
        if (mInstalledApps.contains(pkgName)) {
            return;
        }
        final PackageInfo packageInfo = new PackageInfo();
        packageInfo.applicationInfo = ri.activityInfo.applicationInfo;
        packageInfo.applicationInfo.flags |= ApplicationInfo.FLAG_INSTALLED;
        mInstalledAppInfos.add(packageInfo.applicationInfo);
        when(mPm.getPackageInfo(eq(pkgName), anyInt())).thenReturn(packageInfo);
    }

    private ResolveInfo createResolveInfoForSystemApp(String packageName) {
        final ResolveInfo ri = createResolveInfo(packageName);
        ri.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
        ri.serviceInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM;
        return ri;
    }

    private ResolveInfo createResolveInfo(String packageName) {
        final ResolveInfo ri = new ResolveInfo();
        final ApplicationInfo applicationInfo = new ApplicationInfo();
        applicationInfo.packageName = packageName;
        final ActivityInfo activityInfo = new ActivityInfo();
        activityInfo.applicationInfo = applicationInfo;
        activityInfo.packageName = packageName;
        activityInfo.name = "";
        ri.activityInfo = activityInfo;
        final ServiceInfo serviceInfo = new ServiceInfo();
        serviceInfo.applicationInfo = applicationInfo;
        serviceInfo.packageName = packageName;
        serviceInfo.name = "";
        ri.serviceInfo = serviceInfo;
        return ri;
    }

    private class IntentMatcher implements ArgumentMatcher<Intent> {
        private final Intent mIntent;

        IntentMatcher(Intent intent) {
            mIntent = intent;
        }

        @Override
        public boolean matches(Intent argument) {
            return argument != null && argument.filterEquals(mIntent);
        }

        @Override
        public String toString() {
            return "Expected: " + mIntent;
        }
    }

    private class TestInjector extends AppRestrictionsHelper.Injector {
        List<InputMethodInfo> mImis;

        TestInjector() {
            super(mContext, mTestUser);
        }

        @Override
        Context getContext() {
            return mContext;
        }

        @Override
        UserHandle getUser() {
            return mTestUser;
        }

        @Override
        PackageManager getPackageManager() {
            return mPm;
        }

        @Override
        IPackageManager getIPackageManager() {
            return mIpm;
        }

        @Override
        UserManager getUserManager() {
            return mUm;
        }

        @Override
        List<InputMethodInfo> getInputMethodList() {
            return mImis;
        }

        void setInputMethodList(List<InputMethodInfo> imis) {
            mImis = imis;
        }
    }
}
