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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.AppOpsManager;
import android.compat.testing.PlatformCompatChangeRule;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.permission.flags.Flags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.telephony.RadioAccessFamily;
import android.telephony.TelephonyManager;

import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import com.android.TelephonyTestBase;
import com.android.internal.telephony.IIntegerConsumer;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.flags.FeatureFlags;
import com.android.internal.telephony.subscription.SubscriptionManagerService;

import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.mockito.Mock;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Locale;

/**
 * Unit Test for PhoneInterfaceManager.
 */
@RunWith(AndroidJUnit4.class)
public class PhoneInterfaceManagerTest extends TelephonyTestBase {
    @Rule
    public TestRule compatChangeRule = new PlatformCompatChangeRule();

    private PhoneInterfaceManager mPhoneInterfaceManager;
    private SharedPreferences mSharedPreferences;
    private IIntegerConsumer mIIntegerConsumer;
    private static final String sDebugPackageName =
            PhoneInterfaceManagerTest.class.getPackageName();

    @Mock
    PhoneGlobals mPhoneGlobals;
    @Mock
    Phone mPhone;
    @Mock
    FeatureFlags mFeatureFlags;
    @Mock
    PackageManager mPackageManager;
    @Mock
    private SubscriptionManagerService mSubscriptionManagerService;

    @Mock
    private AppOpsManager mAppOps;

    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();

    @Before
    @UiThreadTest
    public void setUp() throws Exception {
        super.setUp();
        doReturn(sDebugPackageName).when(mPhoneGlobals).getOpPackageName();

        // Note that PhoneInterfaceManager is a singleton. Calling init gives us a handle to the
        // global singleton, but the context that is passed in is unused if the phone app is already
        // alive on a test devices. You must use the spy to mock behavior. Mocks stemming from the
        // passed context will remain unused.
        mPhoneInterfaceManager = spy(PhoneInterfaceManager.init(mPhoneGlobals, mFeatureFlags));
        doReturn(mSubscriptionManagerService).when(mPhoneInterfaceManager)
                .getSubscriptionManagerService();
        TelephonyManager.setupISubForTest(mSubscriptionManagerService);
        mSharedPreferences = mPhoneInterfaceManager.getSharedPreferences();
        mSharedPreferences.edit().remove(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED).commit();
        mSharedPreferences.edit().remove(Phone.PREF_NULL_CIPHER_NOTIFICATIONS_ENABLED).commit();
        mIIntegerConsumer = mock(IIntegerConsumer.class);

        // In order not to affect the existing implementation, define a telephony features
        // and disabled enforce_telephony_feature_mapping_for_public_apis feature flag
        mPhoneInterfaceManager.setFeatureFlags(mFeatureFlags);
        doReturn(false).when(mFeatureFlags).enforceTelephonyFeatureMappingForPublicApis();
        mPhoneInterfaceManager.setPackageManager(mPackageManager);
        doReturn(true).when(mPackageManager).hasSystemFeature(anyString());

        mPhoneInterfaceManager.setAppOpsManager(mAppOps);
    }

    @Test
    public void cleanUpAllowedNetworkTypes_validPhoneAndSubId_doSetAllowedNetwork() {
        long defaultNetworkType = RadioAccessFamily.getRafFromNetworkType(
                RILConstants.PREFERRED_NETWORK_MODE);

        mPhoneInterfaceManager.cleanUpAllowedNetworkTypes(mPhone, 1);

        verify(mPhone).loadAllowedNetworksFromSubscriptionDatabase();
        verify(mPhone).setAllowedNetworkTypes(TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER,
                defaultNetworkType, null);
    }

    @Test
    public void cleanUpAllowedNetworkTypes_validPhoneAndInvalidSubId_doNotSetAllowedNetwork() {
        long defaultNetworkType = RadioAccessFamily.getRafFromNetworkType(
                RILConstants.PREFERRED_NETWORK_MODE);

        mPhoneInterfaceManager.cleanUpAllowedNetworkTypes(mPhone, -1);

        verify(mPhone, never()).loadAllowedNetworksFromSubscriptionDatabase();
        verify(mPhone, never()).setAllowedNetworkTypes(
                TelephonyManager.ALLOWED_NETWORK_TYPES_REASON_USER, defaultNetworkType, null);
    }

    @Test
    public void matchLocaleFromSupportedLocaleList_inputLocaleChangeToSupportedLocale_notMatched() {
        Context context = mock(Context.class);
        when(mPhone.getContext()).thenReturn(context);
        Resources resources = mock(Resources.class);
        when(context.getResources()).thenReturn(resources);
        when(resources.getStringArray(anyInt()))
                .thenReturn(new String[]{"fi-FI", "ff-Adlm-BF", "en-US"});

        // Input empty string, then return default locale of ICU.
        String resultInputEmpty = mPhoneInterfaceManager.matchLocaleFromSupportedLocaleList(mPhone,
                Locale.forLanguageTag(""));

        assertEquals("und", resultInputEmpty);

        // Input en, then look up the matched supported locale. No matched, so return input locale.
        String resultOnlyLanguage = mPhoneInterfaceManager.matchLocaleFromSupportedLocaleList(
                mPhone,
                Locale.forLanguageTag("en"));

        assertEquals("en", resultOnlyLanguage);
    }

    @Test
    public void matchLocaleFromSupportedLocaleList_inputLocaleChangeToSupportedLocale() {
        Context context = mock(Context.class);
        when(mPhone.getContext()).thenReturn(context);
        Resources resources = mock(Resources.class);
        when(context.getResources()).thenReturn(resources);
        when(resources.getStringArray(anyInt())).thenReturn(new String[]{"zh-Hant-TW"});

        // Input zh-TW, then look up the matched supported locale, zh-Hant-TW, instead.
        String resultInputZhTw = mPhoneInterfaceManager.matchLocaleFromSupportedLocaleList(mPhone,
                Locale.forLanguageTag("zh-TW"));

        assertEquals("zh-Hant-TW", resultInputZhTw);

        when(resources.getStringArray(anyInt())).thenReturn(
                new String[]{"fi-FI", "ff-Adlm-BF", "ff-Latn-BF"});

        // Input ff-BF, then find the matched supported locale, ff-Latn-BF, instead.
        String resultFfBf = mPhoneInterfaceManager.matchLocaleFromSupportedLocaleList(mPhone,
                Locale.forLanguageTag("ff-BF"));

        assertEquals("ff-Latn-BF", resultFfBf);
    }

    @Test
    public void setNullCipherAndIntegrityEnabled_successfullyEnable() {
        whenModemSupportsNullCiphers();
        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
        assertFalse(mSharedPreferences.contains(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED));

        mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(true);

        assertTrue(
                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED, false));
    }

    @Test
    public void setNullCipherAndIntegrityEnabled_successfullyDisable() {
        whenModemSupportsNullCiphers();
        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
        assertFalse(mSharedPreferences.contains(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED));

        mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(false);

        assertFalse(
                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED, true));
    }

    @Test
    public void setNullCipherAndIntegrityEnabled_lackingNecessaryHal() {
        doReturn(101).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();

        assertThrows(UnsupportedOperationException.class, () -> {
            mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(true);
        });

    }

    @Test
    public void setNullCipherAndIntegrityEnabled_lackingPermissions() {
        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doThrow(SecurityException.class).when(mPhoneInterfaceManager).enforceModifyPermission();

        assertThrows(SecurityException.class, () -> {
            mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(true);
        });
    }

    @Test
    public void isNullCipherAndIntegrityPreferenceEnabled() {
        whenModemSupportsNullCiphers();
        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();

        mPhoneInterfaceManager.setNullCipherAndIntegrityEnabled(false);
        assertFalse(
                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_AND_INTEGRITY_ENABLED, true));
    }

    @Test
    public void isNullCipherAndIntegrityPreferenceEnabled_lackingNecessaryHal() {
        doReturn(101).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();

        assertThrows(UnsupportedOperationException.class, () -> {
            mPhoneInterfaceManager.isNullCipherAndIntegrityPreferenceEnabled();
        });

    }

    @Test
    public void isNullCipherAndIntegrityPreferenceEnabled_lackingModemSupport() {
        whenModemDoesNotSupportNullCiphers();
        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();

        assertThrows(UnsupportedOperationException.class, () -> {
            mPhoneInterfaceManager.isNullCipherAndIntegrityPreferenceEnabled();
        });

    }

    @Test
    public void isNullCipherAndIntegrityPreferenceEnabled_lackingPermissions() {
        doReturn(201).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doThrow(SecurityException.class).when(mPhoneInterfaceManager).enforceReadPermission();

        assertThrows(SecurityException.class, () -> {
            mPhoneInterfaceManager.isNullCipherAndIntegrityPreferenceEnabled();
        });
    }

    private void whenModemDoesNotSupportNullCiphers() {
        doReturn(false).when(mPhone).isNullCipherAndIntegritySupported();
        doReturn(mPhone).when(
                mPhoneInterfaceManager).getDefaultPhone();
    }

    private void whenModemSupportsNullCiphers() {
        doReturn(true).when(mPhone).isNullCipherAndIntegritySupported();
        doReturn(mPhone).when(
                mPhoneInterfaceManager).getDefaultPhone();
    }

    @Test
    public void setNullCipherNotificationsEnabled_allReqsMet_successfullyEnabled() {
        setModemSupportsNullCipherNotification(true);
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
        doReturn(202).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        assertFalse(mSharedPreferences.contains(Phone.PREF_NULL_CIPHER_NOTIFICATIONS_ENABLED));

        mPhoneInterfaceManager.setNullCipherNotificationsEnabled(true);

        assertTrue(
                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_NOTIFICATIONS_ENABLED, false));
    }

    @Test
    public void setNullCipherNotificationsEnabled_allReqsMet_successfullyDisabled() {
        setModemSupportsNullCipherNotification(true);
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
        doReturn(202).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        assertFalse(mSharedPreferences.contains(Phone.PREF_NULL_CIPHER_NOTIFICATIONS_ENABLED));

        mPhoneInterfaceManager.setNullCipherNotificationsEnabled(false);

        assertFalse(
                mSharedPreferences.getBoolean(Phone.PREF_NULL_CIPHER_NOTIFICATIONS_ENABLED, true));
    }

    @Test
    public void setNullCipherNotificationsEnabled_lackingNecessaryHal_throwsException() {
        setModemSupportsNullCipherNotification(true);
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
        doReturn(102).when(mPhoneInterfaceManager).getHalVersion(anyInt());

        assertThrows(UnsupportedOperationException.class,
                () -> mPhoneInterfaceManager.setNullCipherNotificationsEnabled(true));
    }

    @Test
    public void setNullCipherNotificationsEnabled_lackingModemSupport_throwsException() {
        setModemSupportsNullCipherNotification(false);
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();
        doReturn(202).when(mPhoneInterfaceManager).getHalVersion(anyInt());

        assertThrows(UnsupportedOperationException.class,
                () -> mPhoneInterfaceManager.setNullCipherNotificationsEnabled(true));
    }

    @Test
    public void setNullCipherNotificationsEnabled_lackingPermissions_throwsException() {
        setModemSupportsNullCipherNotification(true);
        doReturn(202).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doThrow(SecurityException.class).when(mPhoneInterfaceManager).enforceModifyPermission();

        assertThrows(SecurityException.class, () ->
                mPhoneInterfaceManager.setNullCipherNotificationsEnabled(true));
    }

    @Test
    public void isNullCipherNotificationsEnabled_allReqsMet_returnsTrue() {
        setModemSupportsNullCipherNotification(true);
        doReturn(202).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceReadPrivilegedPermission(anyString());
        doReturn(true).when(mPhone).getNullCipherNotificationsPreferenceEnabled();

        assertTrue(mPhoneInterfaceManager.isNullCipherNotificationsEnabled());
    }

    @Test
    public void isNullCipherNotificationsEnabled_lackingNecessaryHal_throwsException() {
        setModemSupportsNullCipherNotification(true);
        doReturn(102).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceReadPrivilegedPermission(anyString());

        assertThrows(UnsupportedOperationException.class, () ->
                mPhoneInterfaceManager.isNullCipherNotificationsEnabled());
    }

    @Test
    public void isNullCipherNotificationsEnabled_lackingModemSupport_throwsException() {
        setModemSupportsNullCipherNotification(false);
        doReturn(202).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doNothing().when(mPhoneInterfaceManager).enforceReadPrivilegedPermission(anyString());

        assertThrows(UnsupportedOperationException.class, () ->
                mPhoneInterfaceManager.isNullCipherNotificationsEnabled());
    }

    @Test
    public void isNullCipherNotificationsEnabled_lackingPermissions_throwsException() {
        setModemSupportsNullCipherNotification(true);
        doReturn(202).when(mPhoneInterfaceManager).getHalVersion(anyInt());
        doThrow(SecurityException.class).when(
                mPhoneInterfaceManager).enforceReadPrivilegedPermission(anyString());

        assertThrows(SecurityException.class, () ->
                mPhoneInterfaceManager.isNullCipherNotificationsEnabled());
    }

    private void setModemSupportsNullCipherNotification(boolean enable) {
        doReturn(enable).when(mPhone).isNullCipherNotificationSupported();
        doReturn(mPhone).when(mPhoneInterfaceManager).getDefaultPhone();
    }

    /**
     * Verify getCarrierRestrictionStatus throws exception for invalid caller package name.
     */
    @Test
    public void getCarrierRestrictionStatus_ReadPrivilegedException2() {
        doThrow(SecurityException.class).when(
                mPhoneInterfaceManager).enforceReadPrivilegedPermission(anyString());
        assertThrows(SecurityException.class, () -> {
            mPhoneInterfaceManager.getCarrierRestrictionStatus(mIIntegerConsumer, "");
        });
    }

    /**
     * Verify getCarrierRestrictionStatus doesn't throw any exception with valid package name
     * and with READ_PHONE_STATE permission granted.
     */
    @Test
    public void getCarrierRestrictionStatus() {
        when(mPhoneInterfaceManager.validateCallerAndGetCarrierIds(anyString())).thenReturn(
                Collections.singleton(1));
        mPhoneInterfaceManager.getCarrierRestrictionStatus(mIIntegerConsumer,
                "com.test.package");
    }

    @Test
    public void notifyEnableDataWithAppOps_enableByUser_doNoteOp() {
        mSetFlagsRule.enableFlags(Flags.FLAG_OP_ENABLE_MOBILE_DATA_BY_USER);
        String packageName = "INVALID_PACKAGE";
        mPhoneInterfaceManager.setDataEnabledForReason(1,
                TelephonyManager.DATA_ENABLED_REASON_USER, true, packageName);
        verify(mAppOps).noteOpNoThrow(eq(AppOpsManager.OPSTR_ENABLE_MOBILE_DATA_BY_USER), anyInt(),
                eq(packageName), isNull(), isNull());
    }

    @Test
    public void notifyEnableDataWithAppOps_enableByCarrier_doNotNoteOp() {
        mSetFlagsRule.enableFlags(Flags.FLAG_OP_ENABLE_MOBILE_DATA_BY_USER);
        String packageName = "INVALID_PACKAGE";
        verify(mAppOps, never()).noteOpNoThrow(eq(AppOpsManager.OPSTR_ENABLE_MOBILE_DATA_BY_USER),
                anyInt(), eq(packageName), isNull(), isNull());
    }

    @Test
    public void notifyEnableDataWithAppOps_disableByUser_doNotNoteOp() {
        mSetFlagsRule.enableFlags(Flags.FLAG_OP_ENABLE_MOBILE_DATA_BY_USER);
        String packageName = "INVALID_PACKAGE";
        String error = "";
        try {
            mPhoneInterfaceManager.setDataEnabledForReason(1,
                    TelephonyManager.DATA_ENABLED_REASON_USER, false, packageName);
        } catch (SecurityException expected) {
            // The test doesn't have access to note the op, but we're just interested that it makes
            // the attempt.
            error = expected.getMessage();
        }
        assertEquals("Expected error to be empty, was " + error, error, "");
    }

    @Test
    public void notifyEnableDataWithAppOps_noPackageNameAndEnableByUser_doNotnoteOp() {
        mSetFlagsRule.enableFlags(Flags.FLAG_OP_ENABLE_MOBILE_DATA_BY_USER);
        String error = "";
        try {
            mPhoneInterfaceManager.setDataEnabledForReason(1,
                    TelephonyManager.DATA_ENABLED_REASON_USER, false, null);
        } catch (SecurityException expected) {
            // The test doesn't have access to note the op, but we're just interested that it makes
            // the attempt.
            error = expected.getMessage();
        }
        assertEquals("Expected error to be empty, was " + error, error, "");
    }

    @Test
    @EnableCompatChanges({TelephonyManager.ENABLE_FEATURE_MAPPING})
    public void testWithTelephonyFeatureAndCompatChanges() throws Exception {
        doReturn(true).when(mFeatureFlags).enforceTelephonyFeatureMappingForPublicApis();
        mPhoneInterfaceManager.setFeatureFlags(mFeatureFlags);
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();

        try {
            // FEATURE_TELEPHONY_CALLING
            mPhoneInterfaceManager.handlePinMmiForSubscriber(1, "123456789");

            // FEATURE_TELEPHONY_RADIO_ACCESS
            mPhoneInterfaceManager.toggleRadioOnOffForSubscriber(1);
        } catch (Exception e) {
            fail("Not expect exception " + e.getMessage());
        }
    }

    @Test
    @EnableCompatChanges({TelephonyManager.ENABLE_FEATURE_MAPPING})
    public void testWithoutTelephonyFeatureAndCompatChanges() throws Exception {
        // Replace field to set SDK version of vendor partition to Android V
        int vendorApiLevel = Build.VERSION_CODES.VANILLA_ICE_CREAM;
        replaceInstance(PhoneInterfaceManager.class, "mVendorApiLevel", mPhoneInterfaceManager,
                vendorApiLevel);

        // telephony features is not defined, expect UnsupportedOperationException.
        doReturn(false).when(mPackageManager).hasSystemFeature(
                PackageManager.FEATURE_TELEPHONY_CALLING);
        doReturn(false).when(mPackageManager).hasSystemFeature(
                PackageManager.FEATURE_TELEPHONY_RADIO_ACCESS);
        mPhoneInterfaceManager.setPackageManager(mPackageManager);
        doReturn(true).when(mFeatureFlags).enforceTelephonyFeatureMappingForPublicApis();
        mPhoneInterfaceManager.setFeatureFlags(mFeatureFlags);
        doNothing().when(mPhoneInterfaceManager).enforceModifyPermission();

        assertThrows(UnsupportedOperationException.class,
                () -> mPhoneInterfaceManager.handlePinMmiForSubscriber(1, "123456789"));
        assertThrows(UnsupportedOperationException.class,
                () -> mPhoneInterfaceManager.toggleRadioOnOffForSubscriber(1));
    }

    @Test
    public void testGetCurrentPackageNameWithNoKnownPackage() throws Exception {
        Field field = PhoneInterfaceManager.class.getDeclaredField("mApp");
        field.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("accessFlags");
        modifiersField.setAccessible(true);
        modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
        field.set(mPhoneInterfaceManager, mPhoneGlobals);

        doReturn(mPackageManager).when(mPhoneGlobals).getPackageManager();
        doReturn(null).when(mPackageManager).getPackagesForUid(anyInt());

        String packageName = mPhoneInterfaceManager.getCurrentPackageName();
        assertEquals(null, packageName);
    }
}
