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

import static android.server.biometrics.Components.CLASS_2_BIOMETRIC_ACTIVITY;
import static android.server.biometrics.Components.CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY;

import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_PAUSED;
import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING;
import static com.android.server.biometrics.nano.BiometricServiceStateProto.STATE_SHOWING_DEVICE_CREDENTIAL;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;

import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.BiometricTestSession;
import android.hardware.biometrics.SensorProperties;
import android.platform.test.annotations.Presubmit;
import android.server.wm.TestJournalProvider;
import android.server.wm.WindowManagerState;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.junit.Ignore;
import org.junit.Test;

/**
 * Tests that require the use of a test activity.
 */
@Presubmit
public class BiometricActivityTests extends BiometricTestBase {
    private static final String TAG = "BiometricTests/Activity";

    @Test
    public void testBiometricOnly_authenticateFromForegroundActivity() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        for (SensorProperties prop : mSensorProperties) {
            if (prop.getSensorStrength() == SensorProperties.STRENGTH_CONVENIENCE) {
                continue;
            }

            try (BiometricTestSession session =
                         mBiometricManager.createTestSession(prop.getSensorId());
                 ActivitySession activitySession =
                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
                testBiometricOnly_authenticateFromForegroundActivity_forSensor(
                        session, prop.getSensorId(), activitySession);
            }
        }
    }

    private void testBiometricOnly_authenticateFromForegroundActivity_forSensor(
            @NonNull BiometricTestSession session, int sensorId,
            @NonNull ActivitySession activitySession) throws Exception {
        Log.d(TAG, "testBiometricOnly_authenticateFromForegroundActivity_forSensor: " + sensorId);
        final int userId = Utils.getUserId();
        waitForAllUnenrolled();
        enrollForSensor(session, sensorId);
        final TestJournalProvider.TestJournal journal = TestJournalProvider.TestJournalContainer
                .get(activitySession.getComponentName());

        // Launch test activity
        launchActivityAndWaitForResumed(activitySession);

        // The sensor being tested should not be idle
        BiometricServiceState state = getCurrentState();
        assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());

        // Nothing happened yet
        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());

        // Auth and check again now
        successfullyAuthenticate(session, userId);

        mInstrumentation.waitForIdleSync();
        callbackState = getCallbackState(journal);
        assertTrue(callbackState.toString(), callbackState.mErrorsReceived.isEmpty());
        assertTrue(callbackState.toString(), callbackState.mAcquiredReceived.isEmpty());
        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
    }

    @Test
    public void testBiometricOnly_rejectThenErrorFromForegroundActivity() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        for (SensorProperties prop : mSensorProperties) {
            if (prop.getSensorStrength() == SensorProperties.STRENGTH_CONVENIENCE) {
                continue;
            }

            try (BiometricTestSession session =
                         mBiometricManager.createTestSession(prop.getSensorId());
                 ActivitySession activitySession =
                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
                testBiometricOnly_rejectThenErrorFromForegroundActivity_forSensor(
                        session, prop.getSensorId(), activitySession);
            }
        }
    }

    private void testBiometricOnly_rejectThenErrorFromForegroundActivity_forSensor(
            @NonNull BiometricTestSession session, int sensorId,
            @NonNull ActivitySession activitySession) throws Exception {
        Log.d(TAG, "testBiometricOnly_rejectThenErrorFromForegroundActivity_forSensor: "
                + sensorId);
        final int userId = Utils.getUserId();
        waitForAllUnenrolled();
        enrollForSensor(session, sensorId);

        final TestJournalProvider.TestJournal journal =
                TestJournalProvider.TestJournalContainer.get(activitySession.getComponentName());

        // Launch test activity
        launchActivityAndWaitForResumed(activitySession);
        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
        assertNotNull(callbackState);

        BiometricServiceState state = getCurrentState();
        assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());

        // Biometric rejected
        session.rejectAuthentication(userId);
        mInstrumentation.waitForIdleSync();
        callbackState = getCallbackState(journal);
        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());

        state = getCurrentState();
        Log.d(TAG, "State after rejectAuthentication: " + state);
        if (state.mState == STATE_AUTH_PAUSED) {
            findAndPressButton(BUTTON_ID_TRY_AGAIN);
            mInstrumentation.waitForIdleSync();
            waitForState(STATE_AUTH_STARTED_UI_SHOWING);
        }

        // Send an error
        session.notifyError(userId, BiometricPrompt.BIOMETRIC_ERROR_CANCELED);
        mInstrumentation.waitForIdleSync();
        callbackState = getCallbackState(journal);
        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
        assertEquals(callbackState.toString(), 1, callbackState.mErrorsReceived.size());
        assertEquals(callbackState.toString(), BiometricPrompt.BIOMETRIC_ERROR_CANCELED,
                (int) callbackState.mErrorsReceived.get(0));
    }

    @Test
    public void testBiometricOnly_rejectThenAuthenticate() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        for (SensorProperties prop : mSensorProperties) {
            if (prop.getSensorStrength() == SensorProperties.STRENGTH_CONVENIENCE) {
                continue;
            }

            try (BiometricTestSession session =
                         mBiometricManager.createTestSession(prop.getSensorId());
                 ActivitySession activitySession =
                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
                testBiometricOnly_rejectThenAuthenticate_forSensor(
                        session, prop.getSensorId(), activitySession);
            }
        }
    }

    private void testBiometricOnly_rejectThenAuthenticate_forSensor(
            @NonNull BiometricTestSession session, int sensorId,
            @NonNull ActivitySession activitySession) throws Exception {
        Log.d(TAG, "testBiometricOnly_rejectThenAuthenticate_forSensor: " + sensorId);

        final int userId = Utils.getUserId();
        waitForAllUnenrolled();
        enrollForSensor(session, sensorId);

        final TestJournalProvider.TestJournal journal =
                TestJournalProvider.TestJournalContainer.get(activitySession.getComponentName());

        // Launch test activity
        activitySession.start();
        mWmState.waitForActivityState(activitySession.getComponentName(),
                WindowManagerState.STATE_RESUMED);
        mInstrumentation.waitForIdleSync();
        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
        assertNotNull(callbackState);

        BiometricServiceState state = getCurrentState();
        assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());

        session.rejectAuthentication(userId);
        mInstrumentation.waitForIdleSync();
        callbackState = getCallbackState(journal);
        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());

        state = getCurrentState();
        Log.d(TAG, "State after rejectAuthentication: " + state);
        if (state.mState == STATE_AUTH_PAUSED) {
            findAndPressButton(BUTTON_ID_TRY_AGAIN);
            mInstrumentation.waitForIdleSync();
            waitForState(STATE_AUTH_STARTED_UI_SHOWING);
        }

        // Accept authentication and end
        successfullyAuthenticate(session, userId);

        mInstrumentation.waitForIdleSync();
        callbackState = getCallbackState(journal);
        assertTrue(callbackState.toString(), callbackState.mErrorsReceived.isEmpty());
        assertTrue(callbackState.toString(), callbackState.mAcquiredReceived.isEmpty());
        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthRejected);
    }

    // TODO(b/236763921): fix this test and unignore.
    @Ignore
    @Test
    public void testBiometricOnly_negativeButtonInvoked() throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        for (SensorProperties prop : mSensorProperties) {
            if (prop.getSensorStrength() == SensorProperties.STRENGTH_CONVENIENCE) {
                continue;
            }

            try (BiometricTestSession session =
                         mBiometricManager.createTestSession(prop.getSensorId());
                 ActivitySession activitySession =
                         new ActivitySession(this, CLASS_2_BIOMETRIC_ACTIVITY)) {
                testBiometricOnly_negativeButtonInvoked_forSensor(
                        session, prop.getSensorId(), activitySession);
            }
        }
    }

    private void testBiometricOnly_negativeButtonInvoked_forSensor(
            @NonNull BiometricTestSession session, int sensorId,
            @NonNull ActivitySession activitySession) throws Exception {
        Log.d(TAG, "testBiometricOnly_negativeButtonInvoked_forSensor: " + sensorId);
        waitForAllUnenrolled();
        enrollForSensor(session, sensorId);
        final TestJournalProvider.TestJournal journal = TestJournalProvider.TestJournalContainer
                .get(activitySession.getComponentName());

        // Launch test activity
        launchActivityAndWaitForResumed(activitySession);
        BiometricCallbackHelper.State callbackState = getCallbackState(journal);
        assertNotNull(callbackState);

        BiometricServiceState state = getCurrentState();
        assertFalse(state.toString(), state.mSensorStates.areAllSensorsIdle());
        assertFalse(state.toString(), callbackState.mNegativeButtonPressed);

        // Press the negative button
        findAndPressButton(BUTTON_ID_NEGATIVE);

        callbackState = getCallbackState(journal);
        assertTrue(callbackState.toString(), callbackState.mNegativeButtonPressed);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
    }


    // TODO(b/236763921): fix this test and unignore.
    @Ignore
    @Test
    public void testBiometricOrCredential_credentialButtonInvoked_biometricEnrolled()
            throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        // Test behavior for each sensor when biometrics are enrolled
        try (CredentialSession credentialSession = new CredentialSession()) {
            credentialSession.setCredential();
            for (SensorProperties prop : mSensorProperties) {
                if (prop.getSensorStrength() == SensorProperties.STRENGTH_CONVENIENCE) {
                    continue;
                }

                try (BiometricTestSession session =
                             mBiometricManager.createTestSession(prop.getSensorId());
                     ActivitySession activitySession =
                             new ActivitySession(this, CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY)) {
                    testBiometricOrCredential_credentialButtonInvoked_forConfiguration(
                            session, prop.getSensorId(), true /* shouldEnrollBiometric */,
                            activitySession);
                }
            }
        }
    }

    @Test
    public void testBiometricOrCredential_credentialButtonInvoked_biometricNotEnrolled()
            throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        // Test behavior for each sensor when biometrics are not enrolled
        try (CredentialSession credentialSession = new CredentialSession()) {
            credentialSession.setCredential();
            for (SensorProperties prop : mSensorProperties) {
                try (BiometricTestSession session =
                             mBiometricManager.createTestSession(prop.getSensorId());
                     ActivitySession activitySession =
                             new ActivitySession(this, CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY)) {
                    testBiometricOrCredential_credentialButtonInvoked_forConfiguration(
                            session, prop.getSensorId(), false /* shouldEnrollBiometric */,
                            activitySession);
                }
            }
        }
    }

    @Test
    public void testBiometricOrCredential_credentialButtonInvoked_noBiometricSensor()
            throws Exception {
        assumeTrue(Utils.isFirstApiLevel29orGreater());
        assumeTrue(mSensorProperties.isEmpty());
        try (CredentialSession credentialSession = new CredentialSession()) {
            credentialSession.setCredential();
            try (ActivitySession activitySession =
                         new ActivitySession(this, CLASS_2_BIOMETRIC_OR_CREDENTIAL_ACTIVITY)){
                testBiometricOrCredential_credentialButtonInvoked_forConfiguration(null,
                        0 /* sensorId */, false /* shouldEnrollBiometric */, activitySession);
            }
        }
    }

    private void testBiometricOrCredential_credentialButtonInvoked_forConfiguration(
            @Nullable BiometricTestSession session, int sensorId, boolean shouldEnrollBiometric,
            @NonNull ActivitySession activitySession)
            throws Exception {
        Log.d(TAG, "testBiometricOrCredential_credentialButtonInvoked_forConfiguration: "
                + "sensorId=" + sensorId
                + ", shouldEnrollBiometric=" + shouldEnrollBiometric);
        if (shouldEnrollBiometric) {
            assertNotNull(session);
            waitForAllUnenrolled();
            enrollForSensor(session, sensorId);
        }

        final TestJournalProvider.TestJournal journal = TestJournalProvider.TestJournalContainer
                .get(activitySession.getComponentName());

        // Launch test activity
        launchActivityAndWaitForResumed(activitySession);
        BiometricCallbackHelper.State callbackState;

        BiometricServiceState state = getCurrentState();
        Log.d(TAG, "State after launching activity: " + state);
        if (shouldEnrollBiometric) {
            waitForState(STATE_AUTH_STARTED_UI_SHOWING);
            assertTrue(state.toString(), state.mSensorStates.sensorStates.get(sensorId).isBusy());
            // Press the credential button
            findAndPressButton(BUTTON_ID_USE_CREDENTIAL);
            callbackState = getCallbackState(journal);
            assertFalse(callbackState.toString(), callbackState.mNegativeButtonPressed);
            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
            assertEquals(callbackState.toString(), 0, callbackState.mNumAuthAccepted);
            assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
            assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
            waitForState(STATE_SHOWING_DEVICE_CREDENTIAL);
        }

        successfullyEnterCredential();

        callbackState = getCallbackState(journal);
        assertEquals(callbackState.toString(), 0, callbackState.mNumAuthRejected);
        assertEquals(callbackState.toString(), 1, callbackState.mNumAuthAccepted);
        assertEquals(callbackState.toString(), 0, callbackState.mAcquiredReceived.size());
        assertEquals(callbackState.toString(), 0, callbackState.mErrorsReceived.size());
    }

}
