/*
 * Copyright (C) 2024 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.launcher3.ui;

import static android.os.Process.myUserHandle;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;

import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.os.Debug;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.rule.LimitDevicesRule;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.Until;

import com.android.launcher3.celllayout.FavoriteItemsTransaction;
import com.android.launcher3.tapl.HomeAllApps;
import com.android.launcher3.tapl.HomeAppIcon;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.tapl.TestHelpers;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.rule.ExtendedLongPressTimeoutRule;
import com.android.launcher3.util.rule.FailureWatcher;
import com.android.launcher3.util.rule.SamplerRule;
import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.ShellCommandRule;
import com.android.launcher3.util.rule.TestIsolationRule;
import com.android.launcher3.util.rule.TestStabilityRule;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * Base class for all TAPL tests in Launcher providing various utility methods.
 */
public abstract class BaseLauncherTaplTest {

    public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
    public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 10;

    public static final long DEFAULT_UI_TIMEOUT = TestUtil.DEFAULT_UI_TIMEOUT;
    private static final String TAG = "BaseLauncherTaplTest";

    private static final long BYTES_PER_MEGABYTE = 1 << 20;

    private static boolean sDumpWasGenerated = false;
    private static boolean sActivityLeakReported = false;
    private static boolean sSeenKeyguard = false;
    private static boolean sFirstTimeWaitingForWizard = true;

    private static final String SYSTEMUI_PACKAGE = "com.android.systemui";

    protected final UiDevice mDevice = getUiDevice();
    protected final LauncherInstrumentation mLauncher = createLauncherInstrumentation();

    @NonNull
    public static LauncherInstrumentation createLauncherInstrumentation() {
        waitForSetupWizardDismissal(); // precondition for creating LauncherInstrumentation
        return new LauncherInstrumentation(true);
    }

    protected Context mTargetContext;
    protected String mTargetPackage;
    private int mLauncherPid;

    private final ActivityManager.MemoryInfo mMemoryInfo = new ActivityManager.MemoryInfo();
    private final ActivityManager mActivityManager;
    private long mMemoryBefore;

    /** Detects activity leaks and throws an exception if a leak is found. */
    public static void checkDetectedLeaks(LauncherInstrumentation launcher) {
        checkDetectedLeaks(launcher, false);
    }

    /** Detects activity leaks and throws an exception if a leak is found. */
    public static void checkDetectedLeaks(LauncherInstrumentation launcher,
            boolean requireOneActiveActivityUnused) {
        if (TestStabilityRule.isPresubmit()) return; // b/313501215

        final boolean requireOneActiveActivity =
                false; // workaround for leaks when there is an unexpected Recents activity

        if (sActivityLeakReported) return;

        // Check whether activity leak detector has found leaked activities.
        Wait.atMost(() -> getActivityLeakErrorMessage(launcher, requireOneActiveActivity),
                () -> {
                    launcher.forceGc();
                    return MAIN_EXECUTOR.submit(
                            () -> launcher.noLeakedActivities(requireOneActiveActivity)).get();
                }, launcher, DEFAULT_UI_TIMEOUT);
    }

    public static String getAppPackageName() {
        return getInstrumentation().getContext().getPackageName();
    }

    private static String getActivityLeakErrorMessage(LauncherInstrumentation launcher,
            boolean requireOneActiveActivity) {
        sActivityLeakReported = true;
        return "Activity leak detector has found leaked activities, requirining 1 activity: "
                + requireOneActiveActivity + "; "
                + dumpHprofData(launcher, false, requireOneActiveActivity) + ".";
    }

    private static String dumpHprofData(LauncherInstrumentation launcher, boolean intentionalLeak,
            boolean requireOneActiveActivity) {
        if (intentionalLeak) return "intentional leak; not generating dump";

        String result;
        if (sDumpWasGenerated) {
            result = "dump has already been generated by another test";
        } else {
            try {
                final String fileName =
                        getInstrumentation().getTargetContext().getFilesDir().getPath()
                                + "/ActivityLeakHeapDump.hprof";
                if (TestHelpers.isInLauncherProcess()) {
                    Debug.dumpHprofData(fileName);
                } else {
                    final UiDevice device = getUiDevice();
                    device.executeShellCommand(
                            "am dumpheap " + device.getLauncherPackageName() + " " + fileName);
                }
                Log.d(TAG, "Saved leak dump, the leak is still present: "
                        + !launcher.noLeakedActivities(requireOneActiveActivity));
                sDumpWasGenerated = true;
                result = "saved memory dump as an artifact";
            } catch (Throwable e) {
                Log.e(TAG, "dumpHprofData failed", e);
                result = "failed to save memory dump";
            }
        }
        return result + ". Full list of activities: " + launcher.getRootedActivitiesList();
    }

    protected BaseLauncherTaplTest() {
        mActivityManager = InstrumentationRegistry.getContext()
                .getSystemService(ActivityManager.class);
        mLauncher.enableCheckEventsForSuccessfulGestures();
        mLauncher.setAnomalyChecker(BaseLauncherTaplTest::verifyKeyguardInvisible);
        try {
            mDevice.setOrientationNatural();
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
        mLauncher.enableDebugTracing();
        // Avoid double-reporting of Launcher crashes.
        mLauncher.setOnLauncherCrashed(() -> mLauncherPid = 0);
    }

    @Rule
    public ShellCommandRule mDisableHeadsUpNotification =
            ShellCommandRule.disableHeadsUpNotification();

    @Rule
    public ScreenRecordRule mScreenRecordRule = new ScreenRecordRule();

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

    @Rule
    public ExtendedLongPressTimeoutRule mLongPressTimeoutRule = new ExtendedLongPressTimeoutRule();

    @Rule
    public LimitDevicesRule mlimitDevicesRule = new LimitDevicesRule();

    protected void performInitialization() {
        reinitializeLauncherData();
        mDevice.pressHome();
        // Check that we switched to home.
        mLauncher.getWorkspace();
        checkDetectedLeaks(mLauncher, true);
    }

    protected void clearPackageData(String pkg) throws IOException, InterruptedException {
        assertTrue("pm clear command failed",
                mDevice.executeShellCommand(
                        String.format("pm clear --user %d %s", myUserHandle().getIdentifier(), pkg))
                        .contains("Success"));
        assertTrue("pm wait-for-handler command failed",
                mDevice.executeShellCommand("pm wait-for-handler")
                        .contains("Success"));
    }

    protected TestRule getRulesInsideActivityMonitor() {
        final RuleChain inner = RuleChain
                .outerRule(new FailureWatcher(mLauncher, null))
                .around(new TestIsolationRule(mLauncher, true));
        return TestHelpers.isInLauncherProcess()
                ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner)
                : inner;
    }

    @Rule
    public TestRule mOrderSensitiveRules = RuleChain
            .outerRule(new SamplerRule())
            .around(new TestStabilityRule())
            .around(getRulesInsideActivityMonitor());

    public UiDevice getDevice() {
        return mDevice;
    }

    @Before
    public void setUp() throws Exception {
        mLauncher.onTestStart();

        final String launcherPackageName = mDevice.getLauncherPackageName();
        try {
            final Context context = InstrumentationRegistry.getContext();
            final PackageManager pm = context.getPackageManager();
            final PackageInfo launcherPackage = pm.getPackageInfo(launcherPackageName, 0);

            if (!launcherPackage.versionName.equals("BuildFromAndroidStudio")) {
                Assert.assertEquals("Launcher version doesn't match tests version",
                        pm.getPackageInfo(context.getPackageName(), 0).getLongVersionCode(),
                        launcherPackage.getLongVersionCode());
            }
        } catch (PackageManager.NameNotFoundException e) {
            throw new RuntimeException(e);
        }

        mLauncherPid = 0;

        mTargetContext = InstrumentationRegistry.getTargetContext();
        mTargetPackage = mTargetContext.getPackageName();
        mLauncherPid = mLauncher.getPid();

        UserManager userManager = mTargetContext.getSystemService(UserManager.class);
        if (userManager != null) {
            for (UserHandle userHandle : userManager.getUserProfiles()) {
                if (!userHandle.isSystem()) {
                    mDevice.executeShellCommand(
                            "pm remove-user --wait " + userHandle.getIdentifier());
                }
            }
        }

        onTestStart();
        performInitialization();
    }

    private long getAvailableMemory() {
        mActivityManager.getMemoryInfo(mMemoryInfo);

        return Math.divideExact(mMemoryInfo.availMem,  BYTES_PER_MEGABYTE);
    }

    @Before
    public void saveMemoryBefore() {
        mMemoryBefore = getAvailableMemory();
    }

    @After
    public void logMemoryAfter() {
        long memoryAfter = getAvailableMemory();

        Log.d(TAG, "Available memory: before=" + mMemoryBefore
                + "MB, after=" + memoryAfter
                + "MB, delta=" + (memoryAfter - mMemoryBefore) + "MB");
    }

    /** Method that should be called when a test starts. */
    public static void onTestStart() {
        waitForSetupWizardDismissal();

        if (TestStabilityRule.isPresubmit()) {
            aggressivelyUnlockSysUi();
        } else {
            verifyKeyguardInvisible();
        }
    }

    private static boolean hasSystemUiObject(String resId) {
        return getUiDevice().hasObject(
                By.res(SYSTEMUI_PACKAGE, resId));
    }

    @NonNull
    private static UiDevice getUiDevice() {
        return UiDevice.getInstance(getInstrumentation());
    }

    private static void aggressivelyUnlockSysUi() {
        final UiDevice device = getUiDevice();
        for (int i = 0; i < 10 && hasSystemUiObject("keyguard_status_view"); ++i) {
            Log.d(TAG, "Before attempting to unlock the phone");
            try {
                device.executeShellCommand("input keyevent 82");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            device.waitForIdle();
        }
        Assert.assertTrue("Keyguard still visible",
                TestHelpers.wait(
                        Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000));
        Log.d(TAG, "Keyguard is not visible");
    }

    /** Waits for setup wizard to go away. */
    private static void waitForSetupWizardDismissal() {
        if (sFirstTimeWaitingForWizard) {
            try {
                getUiDevice().executeShellCommand(
                        "am force-stop com.google.android.setupwizard");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        final boolean wizardDismissed = TestHelpers.wait(
                Until.gone(By.pkg("com.google.android.setupwizard").depth(0)),
                sFirstTimeWaitingForWizard ? 120000 : 0);
        sFirstTimeWaitingForWizard = false;
        Assert.assertTrue("Setup wizard is still visible", wizardDismissed);
    }

    /** Asserts that keyguard is not visible */
    public static void verifyKeyguardInvisible() {
        final boolean keyguardAlreadyVisible = sSeenKeyguard;

        sSeenKeyguard = sSeenKeyguard
                || !TestHelpers.wait(
                Until.gone(By.res(SYSTEMUI_PACKAGE, "keyguard_status_view")), 60000);

        Assert.assertFalse(
                "Keyguard is visible, which is likely caused by a crash in SysUI, seeing keyguard"
                        + " for the first time = "
                        + !keyguardAlreadyVisible,
                sSeenKeyguard);
    }

    @After
    public void resetFreezeRecentTaskList() {
        try {
            mDevice.executeShellCommand("wm reset-freeze-recent-tasks");
        } catch (IOException e) {
            Log.e(TAG, "Failed to reset fozen recent tasks list", e);
        }
    }

    @After
    public void verifyLauncherState() {
        try {
            // Limits UI tests affecting tests running after them.
            mDevice.pressHome();
            mLauncher.waitForLauncherInitialized();
            if (mLauncherPid != 0) {
                assertEquals("Launcher crashed, pid mismatch:",
                        mLauncherPid, mLauncher.getPid().intValue());
            }
        } finally {
            mLauncher.onTestFinish();
        }
    }

    protected void reinitializeLauncherData() {
        reinitializeLauncherData(false);
    }

    protected void reinitializeLauncherData(boolean clearWorkspace) {
        if (clearWorkspace) {
            mLauncher.clearLauncherData();
        } else {
            mLauncher.reinitializeLauncherData();
        }
        mLauncher.waitForLauncherInitialized();
    }

    public static void startAppFast(String packageName) {
        startIntent(
                getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
                        packageName),
                By.pkg(packageName).depth(0),
                true /* newTask */);
    }

    public static void startTestActivity(String activityName, String activityLabel) {
        final String packageName = getAppPackageName();
        final Intent intent = getInstrumentation().getContext().getPackageManager()
                        .getLaunchIntentForPackage(packageName);
        intent.setComponent(new ComponentName(packageName,
                "com.android.launcher3.tests." + activityName));
        startIntent(intent, By.pkg(packageName).text(activityLabel),
                false /* newTask */);
    }

    public static void startTestActivity(int activityNumber) {
        startTestActivity("Activity" + activityNumber, "TestActivity" + activityNumber);
    }

    public static void startImeTestActivity() {
        final String packageName = getAppPackageName();
        final Intent intent = getInstrumentation().getContext().getPackageManager()
                        .getLaunchIntentForPackage(packageName);
        intent.setComponent(new ComponentName(packageName,
                "com.android.launcher3.testcomponent.ImeTestActivity"));
        startIntent(intent, By.pkg(packageName).text("ImeTestActivity"),
                false /* newTask */);
    }

    /** Starts ExcludeFromRecentsTestActivity, which has excludeFromRecents="true". */
    public static void startExcludeFromRecentsTestActivity() {
        final String packageName = getAppPackageName();
        final Intent intent = getInstrumentation().getContext().getPackageManager()
                .getLaunchIntentForPackage(packageName);
        intent.setComponent(new ComponentName(packageName,
                "com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity"));
        startIntent(intent, By.pkg(packageName).text("ExcludeFromRecentsTestActivity"),
                false /* newTask */);
    }

    private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
        intent.addCategory(Intent.CATEGORY_LAUNCHER);
        if (newTask) {
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        } else {
            intent.addFlags(
                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
        }
        getInstrumentation().getTargetContext().startActivity(intent);
        assertTrue("App didn't start: " + selector,
                TestHelpers.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));

        // Wait for the Launcher to stop.
        final LauncherInstrumentation launcherInstrumentation = new LauncherInstrumentation();
        Wait.atMost("Launcher activity didn't stop",
                () -> !launcherInstrumentation.isLauncherActivityStarted(),
                launcherInstrumentation, DEFAULT_ACTIVITY_TIMEOUT);
    }

    public static ActivityInfo resolveSystemAppInfo(String category) {
        return getInstrumentation().getContext().getPackageManager().resolveActivity(
                new Intent(Intent.ACTION_MAIN).addCategory(category),
                PackageManager.MATCH_SYSTEM_ONLY)
                .activityInfo;
    }


    public static String resolveSystemApp(String category) {
        return resolveSystemAppInfo(category).packageName;
    }

    protected HomeAppIcon createShortcutInCenterIfNotExist(String name) {
        Point dimension = mLauncher.getWorkspace().getIconGridDimensions();
        return createShortcutIfNotExist(name, dimension.x / 2, dimension.y / 2);
    }

    protected HomeAppIcon createShortcutIfNotExist(String name, Point cellPosition) {
        return createShortcutIfNotExist(name, cellPosition.x, cellPosition.y);
    }

    protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
        HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
        Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name
                + " cell: " + cellX + ", " + cellY);
        if (homeAppIcon == null) {
            HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
            allApps.freeze();
            try {
                allApps.getAppIcon(name).dragToWorkspace(cellX, cellY);
            } finally {
                allApps.unfreeze();
            }
            homeAppIcon = mLauncher.getWorkspace().getWorkspaceAppIcon(name);
        }
        return homeAppIcon;
    }

    protected void commitTransactionAndLoadHome(FavoriteItemsTransaction transaction) {
        transaction.commit();

        // Launch the home activity
        UiDevice.getInstance(getInstrumentation()).pressHome();
        mLauncher.waitForLauncherInitialized();
    }

    /** Clears all recent tasks */
    protected void clearAllRecentTasks() {
        if (!mLauncher.getRecentTasks().isEmpty()) {
            mLauncher.goHome().switchToOverview().dismissAllTasks();
        }
    }
}
