/*
 * Copyright (C) 2025 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 android.content.pm.cts_root;

import static android.content.Intent.ACTION_INSTALL_PACKAGE;
import static android.content.pm.Flags.FLAG_VERIFICATION_SERVICE;
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.content.pm.cts_root.utils.Constants.EMPTY_TEST_APK;
import static android.content.pm.cts_root.utils.Constants.EMPTY_TEST_PACKAGE_NAME;
import static android.content.pm.cts_root.utils.Constants.MODE_PASS;
import static android.content.pm.cts_root.utils.Constants.VERIFIER_PACKAGE_NAME;
import static android.content.pm.cts_root.utils.Utils.configureAndLaunchInstaller;
import static android.content.pm.cts_root.utils.Utils.configureVerifier;
import static android.content.pm.cts_root.utils.Utils.setDefaultVerificationPolicy;

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

import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.cts_root.utils.Constants;
import android.content.pm.cts_root.utils.Utils;
import android.net.Uri;
import android.os.PersistableBundle;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.util.Log;
import android.util.Pair;

import androidx.test.platform.app.InstrumentationRegistry;

import com.android.compatibility.common.util.DeviceConfigStateManager;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

import java.util.Arrays;
import java.util.Objects;

@RunWith(Parameterized.class)
@RequiresFlagsEnabled(FLAG_VERIFICATION_SERVICE)
public abstract class DeveloperVerificationTestBase {
    @Rule
    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();

    private static final String TAG = DeveloperVerificationTestBase.class.getSimpleName();
    private static final String NAMESPACE_PACKAGE_MANAGER_SERVICE = "package_manager_service";
    private static final String PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS =
            "verification_request_timeout_millis";
    private static final String PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS =
            "verifier_connection_timeout_millis";
    protected static final String REJECT_MESSAGE = "You shall not pass!";
    protected static final String EXTENSION_RESPONSE_KEY = "extension-response-key";
    protected static final String EXTENSION_RESPONSE_VALUE = "extension-response-value";

    @Parameter
    public static @PackageInstaller.DeveloperVerificationPolicy int sDefaultPolicy;

    /**
     * This test loops through all possible verification policies.
     */
    @Parameters
    public static Iterable<Object> initParameters() {
        return Arrays.asList(
                PackageInstaller.DEVELOPER_VERIFICATION_POLICY_NONE,
                PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_OPEN,
                PackageInstaller.DEVELOPER_VERIFICATION_POLICY_BLOCK_FAIL_CLOSED
        );
    }

    protected static final Context sContext =
            InstrumentationRegistry.getInstrumentation().getContext();
    private static @PackageInstaller.DeveloperVerificationPolicy int sPreviousDefaultPolicy;
    private static String sPreviousRequestTimeoutMillis;
    private static String sPreviousConnectionTimeoutMillis;

    @BeforeClass
    public static void setUpBeforeClass() {
        // Remember the previous default policy and restore it later
        sPreviousDefaultPolicy = Utils.getDefaultVerificationPolicy(sContext);
        // Remember the timeout configs and restore them later.
        sPreviousRequestTimeoutMillis =
                getTimeoutValueFromDeviceConfig(PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS);
        sPreviousConnectionTimeoutMillis =
                getTimeoutValueFromDeviceConfig(PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS);
        // Change the system timeouts to shorter durations to reduce the test runtime.
        setTimeoutValueInDeviceConfig(
                PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS,
                String.valueOf(3000)); // Wait for 3 seconds before a request times out
        setTimeoutValueInDeviceConfig(
                PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS,
                String.valueOf(1000)); // Wait for 1 second for the connection to establish

        // Make sure that the test verifier is installed
        PackageManager pm = sContext.getPackageManager();
        PackageInfo pi = null;
        try {
            pi = pm.getPackageInfo(VERIFIER_PACKAGE_NAME, /* flags= */ 0);
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Verifier app " + VERIFIER_PACKAGE_NAME + " is not installed");
        }
        assertThat(pi).isNotNull();
        assertThat(pi.applicationInfo).isNotNull();
        // Make sure that the test verifier is now the default verifier
        PackageInstaller packageInstaller = pm.getPackageInstaller();
        assertThat(packageInstaller.getDeveloperVerificationServiceProvider()).isNotNull();
        assertThat(packageInstaller.getDeveloperVerificationServiceProvider().getPackageName())
                .isEqualTo(VERIFIER_PACKAGE_NAME);
    }

    @Before
    public void setUp() throws Exception {
        // Change the default verification policy
        final int currentUserId = ActivityManager.getCurrentUser();
        setDefaultVerificationPolicy(
                sContext, sDefaultPolicy, currentUserId, currentUserId);
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        setTimeoutValueInDeviceConfig(
                PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS, sPreviousRequestTimeoutMillis);
        setTimeoutValueInDeviceConfig(
                PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS, sPreviousConnectionTimeoutMillis);
        final int currentUserId = ActivityManager.getCurrentUser();
        setDefaultVerificationPolicy(
                sContext, sPreviousDefaultPolicy, currentUserId, currentUserId);
    }

    protected void installAndVerifyResults(Context context, TestParams testParams)
            throws Exception {
        final Pair<String, String> extensionResponseKeyValuePairExpected =
                testParams.getExtensionResponseKeyValuePairExpected();
        final String extensionResponseKeyExpected = extensionResponseKeyValuePairExpected.first;
        final String extensionResponseValueExpected = extensionResponseKeyValuePairExpected.second;
        // Then change the verifier mode and set its extension response values
        configureVerifier(context, testParams.getVerifierMode(),
                testParams.getVerifierRejectMessage(),
                extensionResponseValueExpected, testParams.getSessionPolicyOverride(),
                testParams.getUserId());
        // Launch the installer to install a package via the Session APIs and wait for th result
        final Intent installerResultReceived = configureAndLaunchInstaller(context,
                testParams.getInstallerPackageName(),
                extensionResponseKeyExpected, testParams.getApkPath(), testParams.getPackageName(),
                testParams.isSplitInstall(), testParams.isInstallConfirmationExpected(),
                testParams.getUserId());

        // Parse the installation result
        final int statusCodeReceived = installerResultReceived.getIntExtra(
                PackageInstaller.EXTRA_STATUS, Integer.MIN_VALUE);
        final int sessionIdReceived = installerResultReceived.getIntExtra(
                PackageInstaller.EXTRA_SESSION_ID, Integer.MIN_VALUE);
        final int verificationFailureReasonReceived = installerResultReceived.getIntExtra(
                PackageInstaller.EXTRA_DEVELOPER_VERIFICATION_FAILURE_REASON, Integer.MIN_VALUE);
        final String errorMessageReceived = installerResultReceived.getStringExtra(
                PackageInstaller.EXTRA_STATUS_MESSAGE);
        final PersistableBundle extensionResponseReceived =
                installerResultReceived.getParcelableExtra(
                        PackageInstaller.EXTRA_DEVELOPER_VERIFICATION_EXTENSION_RESPONSE,
                        PersistableBundle.class);
        final Intent userActionIntentReceived = installerResultReceived.getParcelableExtra(
                Intent.EXTRA_INTENT, Intent.class);
        // Verify the bundle received by the installer for the installation result
        assertWithMessage(
                "Install result mismatch. Received error message: " + errorMessageReceived)
                .that(statusCodeReceived).isEqualTo(testParams.getStatusCodeExpected());
        assertThat(verificationFailureReasonReceived).isEqualTo(
                testParams.getVerificationFailureReasonExpected());
        if (testParams.getErrorMessageExpected() != null) {
            assertThat(errorMessageReceived).isEqualTo(testParams.getErrorMessageExpected());
        }
        if (testParams.hasUserActionIntentExpected()) {
            verifyUserActionIntentReceived(userActionIntentReceived, sessionIdReceived);
        }
        if (extensionResponseValueExpected != null) {
            assertThat(extensionResponseReceived).isNotNull();
            assertThat(extensionResponseReceived.getString(extensionResponseKeyExpected)).isEqualTo(
                    extensionResponseValueExpected);
        }
    }

    private static void verifyUserActionIntentReceived(
            Intent userActionIntentReceived, int sessionIdExpected) {
        assertThat(userActionIntentReceived).isNotNull();
        assertThat(userActionIntentReceived.getAction()).isEqualTo(
                PackageInstaller.ACTION_CONFIRM_DEVELOPER_VERIFICATION);
        // Verify the content of the intent
        assertThat(userActionIntentReceived.getPackage()).isEqualTo(
                getPackageInstallerPackageName());
        assertThat(userActionIntentReceived.getIntExtra(
                PackageInstaller.EXTRA_SESSION_ID, PackageInstaller.SessionInfo.INVALID_ID))
                .isEqualTo(sessionIdExpected);
    }

    private static String getPackageInstallerPackageName() {
        PackageManager pm = sContext.getPackageManager();
        Intent installerIntent = new Intent(ACTION_INSTALL_PACKAGE);
        installerIntent.setDataAndType(
                Uri.parse("content://com.example/"), "application/vnd.android.package-archive");
        return installerIntent.resolveActivityInfo(pm, MATCH_DEFAULT_ONLY).packageName;
    }

    private static String getTimeoutValueFromDeviceConfig(String propertyName) {
        final DeviceConfigStateManager stateManager =
                new DeviceConfigStateManager(
                        sContext, NAMESPACE_PACKAGE_MANAGER_SERVICE, propertyName);
        return stateManager.get();
    }

    private static void setTimeoutValueInDeviceConfig(String propertyName, String value) {
        final DeviceConfigStateManager stateManager =
                new DeviceConfigStateManager(
                        sContext, NAMESPACE_PACKAGE_MANAGER_SERVICE, propertyName);
        String currentValue = stateManager.get();
        if (!Objects.equals(currentValue, value)) {
            // Only change the value if the current value is different
            stateManager.set(value);
        }
    }

    protected static class TestParams {
        private final String mInstallerPackageName;
        @Constants.Mode
        private final int mVerifierMode;
        private final String mVerifierRejectMessage;
        private final int mStatusCodeExpected;
        private final int mVerificationFailureReasonExpected;
        private final String mErrorMessageExpected;
        private final Pair<String, String> mExtensionResponseKeyValuePairExpected;
        private final boolean mHasUserActionIntentExpected;
        private final @PackageInstaller.DeveloperVerificationPolicy int mSessionPolicyOverride;
        private final boolean mIsInstallConfirmationExpected;
        private final String mApkPath;
        private final String mPackageName;
        private final boolean mIsSplitInstall;
        private final int mUserId;

        protected TestParams(String installerPackageName, int verifierMode,
                String verifierRejectMessage, int statusCodeExpected,
                int verificationFailureReasonExpected, String errorMessageExpected,
                Pair<String, String> extensionResponseKeyValuePairExpected,
                boolean hasUserActionIntentExpected, int sessionPolicyOverride,
                boolean isInstallConfirmationExpected, String apkPath, String packageName,
                boolean isSplitInstall, int userId) {
            mInstallerPackageName = installerPackageName;
            mVerifierMode = verifierMode;
            mVerifierRejectMessage = verifierRejectMessage;
            mStatusCodeExpected = statusCodeExpected;
            mVerificationFailureReasonExpected = verificationFailureReasonExpected;
            mErrorMessageExpected = errorMessageExpected;
            mExtensionResponseKeyValuePairExpected = extensionResponseKeyValuePairExpected;
            mHasUserActionIntentExpected = hasUserActionIntentExpected;
            mSessionPolicyOverride = sessionPolicyOverride;
            mIsInstallConfirmationExpected = isInstallConfirmationExpected;
            mApkPath = apkPath;
            mPackageName = packageName;
            mIsSplitInstall = isSplitInstall;
            mUserId = userId;
        }

        public String getInstallerPackageName() {
            return mInstallerPackageName;
        }

        public int getVerifierMode() {
            return mVerifierMode;
        }
        public String getVerifierRejectMessage() {
            return mVerifierRejectMessage;
        }

        public int getStatusCodeExpected() {
            return mStatusCodeExpected;
        }

        public int getVerificationFailureReasonExpected() {
            return mVerificationFailureReasonExpected;
        }

        public String getErrorMessageExpected() {
            return mErrorMessageExpected;
        }

        public Pair<String, String> getExtensionResponseKeyValuePairExpected() {
            return mExtensionResponseKeyValuePairExpected;
        }

        public boolean hasUserActionIntentExpected() {
            return mHasUserActionIntentExpected;
        }

        public int getSessionPolicyOverride() {
            return mSessionPolicyOverride;
        }

        public boolean isInstallConfirmationExpected() {
            return mIsInstallConfirmationExpected;
        }

        public String getApkPath() {
            return mApkPath;
        }

        public String getPackageName() {
            return mPackageName;
        }

        public boolean isSplitInstall() {
            return mIsSplitInstall;
        }

        public int getUserId() {
            return mUserId;
        }

        static class TestParamsBuilder {
            private String mInstallerPackageName = null;
            @Constants.Mode
            private int mVerifierMode = MODE_PASS;
            private String mVerifierRejectMessage = null;
            private int mStatusCodeExpected = Integer.MIN_VALUE;
            private int mVerificationFailureReasonExpected = Integer.MIN_VALUE;
            private String mErrorMessageExpected = null;
            private Pair<String, String> mExtensionResponseKeyValuePairExpected =
                    new Pair<>(null, null);
            private boolean mHasUserActionIntentExpected = false;
            private @PackageInstaller.DeveloperVerificationPolicy int mSessionPolicyOverride =
                    sDefaultPolicy;

            private boolean mIsInstallConfirmationExpected = false;

            private String mApkPath = EMPTY_TEST_APK;
            private String mPackageName = EMPTY_TEST_PACKAGE_NAME;
            private boolean mIsSplitInstall = false;
            private int mUserId = ActivityManager.getCurrentUser();

            public TestParamsBuilder setInstallerPackageName(String installerPackageName) {
                this.mInstallerPackageName = installerPackageName;
                return this;
            }

            public TestParamsBuilder setVerifierMode(@Constants.Mode int verifierMode) {
                this.mVerifierMode = verifierMode;
                return this;
            }

            public TestParamsBuilder setVerifierRejectMessage(String errorMessage) {
                this.mVerifierRejectMessage = errorMessage;
                return this;
            }


            public TestParamsBuilder setStatusCodeExpected(int statusCodeExpected) {
                this.mStatusCodeExpected = statusCodeExpected;
                return this;
            }

            public TestParamsBuilder setVerificationFailureReasonExpected(
                    int verificationFailureReasonExpected) {
                this.mVerificationFailureReasonExpected = verificationFailureReasonExpected;
                return this;
            }

            public TestParamsBuilder setErrorMessageExpected(String errorMessageExpected) {
                this.mErrorMessageExpected = errorMessageExpected;
                return this;
            }

            public TestParamsBuilder setExtensionResponseKeyValuePairExpected(
                    Pair<String, String> extensionResponseKeyValuePairExpected) {
                this.mExtensionResponseKeyValuePairExpected = extensionResponseKeyValuePairExpected;
                return this;
            }

            public TestParamsBuilder setHasUserActionIntentExpected(
                    boolean hasUserActionIntentExpected) {
                this.mHasUserActionIntentExpected = hasUserActionIntentExpected;
                return this;
            }

            public TestParamsBuilder setSessionVerificationPolicyOverride(
                    @PackageInstaller.DeveloperVerificationPolicy int policyOverride) {
                this.mSessionPolicyOverride = policyOverride;
                return this;
            }

            public TestParamsBuilder setIsInstallConfirmationExpected(
                    boolean isInstallConfirmationExpected) {
                this.mIsInstallConfirmationExpected = isInstallConfirmationExpected;
                return this;
            }

            public TestParamsBuilder setApkPath(String apkPath) {
                this.mApkPath = apkPath;
                return this;
            }

            public TestParamsBuilder setPackageName(String packageName) {
                this.mPackageName = packageName;
                return this;
            }

            public TestParamsBuilder setSplitInstall(boolean isSplitInstall) {
                this.mIsSplitInstall = isSplitInstall;
                return this;
            }

            public TestParamsBuilder setUserId(int userId) {
                this.mUserId = userId;
                return this;
            }

            public TestParams build() {
                return new TestParams(this.mInstallerPackageName,
                        this.mVerifierMode, this.mVerifierRejectMessage, this.mStatusCodeExpected,
                        this.mVerificationFailureReasonExpected, this.mErrorMessageExpected,
                        this.mExtensionResponseKeyValuePairExpected,
                        this.mHasUserActionIntentExpected,
                        this.mSessionPolicyOverride, this.mIsInstallConfirmationExpected,
                        this.mApkPath, this.mPackageName, this.mIsSplitInstall, this.mUserId);
            }
        }
    }
}
