package com.android.systemui.keyguard

import android.app.ActivityManager
import android.app.WallpaperManager
import android.app.WindowConfiguration
import android.graphics.Point
import android.graphics.Rect
import android.os.PowerManager
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.view.RemoteAnimationTarget
import android.view.SurfaceControl
import android.view.SyncRtSurfaceTransactionApplier
import android.view.View
import android.view.ViewRootImpl
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardViewController
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.SysuiStatusBarStateController
import com.android.systemui.statusbar.phone.BiometricUnlockController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argThat
import com.android.systemui.util.mockito.whenever
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import java.util.function.Predicate

@RunWith(AndroidTestingRunner::class)
@RunWithLooper
@SmallTest
class KeyguardUnlockAnimationControllerTest : SysuiTestCase() {
    private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController

    @Mock
    private lateinit var keyguardViewMediator: KeyguardViewMediator
    @Mock
    private lateinit var keyguardStateController: KeyguardStateController
    @Mock
    private lateinit var keyguardViewController: KeyguardViewController
    @Mock
    private lateinit var featureFlags: FeatureFlags
    @Mock
    private lateinit var biometricUnlockController: BiometricUnlockController
    @Mock
    private lateinit var surfaceTransactionApplier: SyncRtSurfaceTransactionApplier
    @Mock
    private lateinit var statusBarStateController: SysuiStatusBarStateController
    @Mock
    private lateinit var notificationShadeWindowController: NotificationShadeWindowController
    @Mock
    private lateinit var powerManager: PowerManager
    @Mock
    private lateinit var wallpaperManager: WallpaperManager

    @Mock
    private lateinit var launcherUnlockAnimationController: ILauncherUnlockAnimationController.Stub

    private var surfaceControl1 = mock(SurfaceControl::class.java)
    private var remoteTarget1 = RemoteAnimationTarget(
            0 /* taskId */, 0, surfaceControl1, false, Rect(), Rect(), 0, Point(), Rect(), Rect(),
            mock(WindowConfiguration::class.java), false, surfaceControl1, Rect(),
            mock(ActivityManager.RunningTaskInfo::class.java), false)

    private var surfaceControl2 = mock(SurfaceControl::class.java)
    private var remoteTarget2 = RemoteAnimationTarget(
            1 /* taskId */, 0, surfaceControl2, false, Rect(), Rect(), 0, Point(), Rect(), Rect(),
            mock(WindowConfiguration::class.java), false, surfaceControl2, Rect(),
            mock(ActivityManager.RunningTaskInfo::class.java), false)
    private lateinit var remoteAnimationTargets: Array<RemoteAnimationTarget>

    private var surfaceControlWp = mock(SurfaceControl::class.java)
    private var wallpaperTarget = RemoteAnimationTarget(
            2 /* taskId */, 0, surfaceControlWp, false, Rect(), Rect(), 0, Point(), Rect(), Rect(),
            mock(WindowConfiguration::class.java), false, surfaceControlWp, Rect(),
            mock(ActivityManager.RunningTaskInfo::class.java), false)
    private lateinit var wallpaperTargets: Array<RemoteAnimationTarget>

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        keyguardUnlockAnimationController = KeyguardUnlockAnimationController(
            context, keyguardStateController, { keyguardViewMediator }, keyguardViewController,
            featureFlags, { biometricUnlockController }, statusBarStateController,
            notificationShadeWindowController, powerManager, wallpaperManager
        )
        keyguardUnlockAnimationController.setLauncherUnlockController(
            "", launcherUnlockAnimationController)

        whenever(keyguardViewController.viewRootImpl).thenReturn(mock(ViewRootImpl::class.java))
        whenever(powerManager.isInteractive).thenReturn(true)

        // All of these fields are final, so we can't mock them, but are needed so that the surface
        // appear amount setter doesn't short circuit.
        remoteAnimationTargets = arrayOf(remoteTarget1)
        wallpaperTargets = arrayOf(wallpaperTarget)

        // Set the surface applier to our mock so that we can verify the arguments passed to it.
        // This applier does not have any side effects within the unlock animation controller, so
        // this is a reasonable way to test.
        keyguardUnlockAnimationController.surfaceTransactionApplier = surfaceTransactionApplier
    }

    @After
    fun tearDown() {
        keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(true)
    }

    /**
     * If we're wake and unlocking, we are animating from the black/AOD screen to the app/launcher
     * underneath. The LightRevealScrim will animate circularly from the fingerprint reader,
     * revealing the app/launcher below. In this case, we want to make sure we are not animating the
     * surface, or the user will see the wallpaper briefly as the app animates in.
     */
    @Test
    fun noSurfaceAnimation_ifWakeAndUnlocking() {
        whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            arrayOf(),
            0 /* startTime */,
            false /* requestedShowSurfaceBehindKeyguard */
        )

        val captorSb = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
        verify(surfaceTransactionApplier, times(1)).scheduleApply(
                captorSb.capture { sp -> sp.surface == surfaceControl1 })

        val params = captorSb.getLastValue()

        // We expect that we've instantly set the surface behind to alpha = 1f, and have no
        // transforms (translate, scale) on its matrix.
        assertEquals(1f, params.alpha)
        assertTrue(params.matrix.isIdentity)

        // Also expect we've immediately asked the keyguard view mediator to finish the remote
        // animation.
        verify(keyguardViewMediator, times(1)).exitKeyguardAndFinishSurfaceBehindRemoteAnimation(
            false /* cancelled */)

        verifyNoMoreInteractions(surfaceTransactionApplier)
    }

    /**
     * If we are not wake and unlocking, we expect the unlock animation to play normally.
     */
    @Test
    fun surfaceAnimation_ifNotWakeAndUnlocking() {
        whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(false)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            wallpaperTargets,
            0 /* startTime */,
            false /* requestedShowSurfaceBehindKeyguard */
        )

        // Since the animation is running, we should not have finished the remote animation.
        verify(keyguardViewMediator, times(0)).exitKeyguardAndFinishSurfaceBehindRemoteAnimation(
            false /* cancelled */)
    }

    @Test
    fun onWakeAndUnlock_notifiesListenerWithTrue() {
        whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
        whenever(biometricUnlockController.mode).thenReturn(
            BiometricUnlockController.MODE_WAKE_AND_UNLOCK)

        val listener = mock(
            KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener::class.java)
        keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(listener)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            wallpaperTargets,
            0 /* startTime */,
            false /* requestedShowSurfaceBehindKeyguard */
        )

        verify(listener).onUnlockAnimationStarted(any(), eq(true), any(), any())
    }

    @Test
    fun onWakeAndUnlockFromDream_notifiesListenerWithFalse() {
        whenever(biometricUnlockController.isWakeAndUnlock).thenReturn(true)
        whenever(biometricUnlockController.mode).thenReturn(
            BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM)

        val listener = mock(
            KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener::class.java)
        keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(listener)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            wallpaperTargets,
            0 /* startTime */,
            false /* requestedShowSurfaceBehindKeyguard */
        )

        verify(listener).onUnlockAnimationStarted(any(), eq(false), any(), any())
    }

    /**
     * If we requested that the surface behind be made visible, and we're not flinging away the
     * keyguard, it means that we're swiping to unlock and want the surface visible so it can follow
     * the user's touch event as they swipe to unlock.
     *
     * In this case, we should verify that the surface was made visible via the alpha fade in
     * animator, and verify that we did not start the canned animation to animate the surface in
     * (since it's supposed to be following the touch events).
     */
    @Test
    fun fadeInSurfaceBehind_ifRequestedShowSurface_butNotFlinging() {
        whenever(keyguardStateController.isFlingingToDismissKeyguard).thenReturn(false)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            wallpaperTargets,
            0 /* startTime */,
            true /* requestedShowSurfaceBehindKeyguard */
        )

        assertTrue(keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.isRunning)
        assertFalse(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
    }

    /**
     * We requested the surface behind to be made visible, but we're now flinging to dismiss the
     * keyguard. This means this was a swipe to dismiss gesture but the user flung the keyguard and
     * lifted their finger while we were requesting the surface be made visible.
     *
     * In this case, we should verify that we are playing the canned unlock animation and not
     * simply fading in the surface.
     */
    @Test
    fun playCannedUnlockAnimation_ifRequestedShowSurface_andFlinging() {
        whenever(keyguardStateController.isFlingingToDismissKeyguard).thenReturn(true)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            wallpaperTargets,
            0 /* startTime */,
            true /* requestedShowSurfaceBehindKeyguard */
        )

        assertTrue(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
        assertFalse(keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.isRunning)
    }

    /**
     * We never requested the surface behind to be made visible, which means no swiping to unlock
     * ever happened and we're just playing the simple canned animation (happens via UDFPS unlock,
     * long press on the lock icon, etc).
     *
     * In this case, we should verify that we are playing the canned unlock animation and not
     * simply fading in the surface.
     */
    @Test
    fun playCannedUnlockAnimation_ifDidNotRequestShowSurface() {
        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            wallpaperTargets,
            0 /* startTime */,
            false /* requestedShowSurfaceBehindKeyguard */
        )

        assertTrue(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
        assertFalse(keyguardUnlockAnimationController.surfaceBehindAlphaAnimator.isRunning)
    }

    @Test
    fun doNotPlayCannedUnlockAnimation_ifLaunchingApp() {
        whenever(notificationShadeWindowController.isLaunchingActivity).thenReturn(true)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
            remoteAnimationTargets,
            wallpaperTargets,
            0 /* startTime */,
            true /* requestedShowSurfaceBehindKeyguard */
        )

        assertFalse(keyguardUnlockAnimationController.canPerformInWindowLauncherAnimations())
        assertFalse(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
    }

    @Test
    fun playCannedUnlockAnimation_nullSmartspaceView_doesNotThrowExecption() {
        keyguardUnlockAnimationController.lockscreenSmartspace = null
        keyguardUnlockAnimationController.willUnlockWithInWindowLauncherAnimations = true

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                remoteAnimationTargets,
                wallpaperTargets,
                0 /* startTime */,
                false /* requestedShowSurfaceBehindKeyguard */
        )

        assertTrue(keyguardUnlockAnimationController.isPlayingCannedUnlockAnimation())
    }

    /**
     * If we are not wake and unlocking, we expect the unlock animation to play normally.
     */
    @Test
    fun surfaceAnimation_multipleTargets() {
        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                arrayOf(remoteTarget1, remoteTarget2),
                wallpaperTargets,
                0 /* startTime */,
                false /* requestedShowSurfaceBehindKeyguard */
        )

        // Set appear to 50%, we'll just verify that we're not applying the identity matrix which
        // means an animation is in progress.
        keyguardUnlockAnimationController.setSurfaceBehindAppearAmount(0.5f)

        val captorSb = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
        verify(surfaceTransactionApplier, times(2)).scheduleApply(captorSb
                .capture { sp -> sp.surface == surfaceControl1 || sp.surface == surfaceControl2 })
        val captorWp = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
        verify(surfaceTransactionApplier, times(1).description(
                "WallpaperSurface was expected to receive scheduleApply once"
        )).scheduleApply(captorWp.capture { sp -> sp.surface == surfaceControlWp})

        val allParams = captorSb.getAllValues()

        val remainingTargets = mutableListOf(surfaceControl1, surfaceControl2)
        allParams.forEach { params ->
            assertTrue(!params.matrix.isIdentity)
            remainingTargets.remove(params.surface)
        }

        // Make sure we called applyParams with each of the surface controls once. The order does
        // not matter, so don't explicitly check for that.
        assertTrue(remainingTargets.isEmpty())

        // Since the animation is running, we should not have finished the remote animation.
        verify(keyguardViewMediator, times(0)).exitKeyguardAndFinishSurfaceBehindRemoteAnimation(
                false /* cancelled */)
    }

    @Test
    fun surfaceBehindAlphaOverriddenTo0_ifNotInteractive() {
        whenever(powerManager.isInteractive).thenReturn(false)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                remoteAnimationTargets,
                wallpaperTargets,
                0 /* startTime */,
                false /* requestedShowSurfaceBehindKeyguard */
        )

        keyguardUnlockAnimationController.setSurfaceBehindAppearAmount(1f)
        keyguardUnlockAnimationController.setWallpaperAppearAmount(1f)

        val captorSb = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
        verify(surfaceTransactionApplier, times(1)).scheduleApply(
                captorSb.capture { sp -> sp.surface == surfaceControl1})
        val captorWp = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
        verify(surfaceTransactionApplier, atLeastOnce().description("Wallpaper surface has  not " +
                "received scheduleApply")).scheduleApply(
                captorWp.capture { sp -> sp.surface == surfaceControlWp })

        val params = captorSb.getLastValue()

        // We expect that we've set the surface behind to alpha = 0f since we're not interactive.
        assertEquals(0f, params.alpha)
        assertTrue(params.matrix.isIdentity)

        verifyNoMoreInteractions(surfaceTransactionApplier)
    }

    @Test
    fun surfaceBehindAlphaNotOverriddenTo0_ifInteractive() {
        whenever(powerManager.isInteractive).thenReturn(true)

        keyguardUnlockAnimationController.notifyStartSurfaceBehindRemoteAnimation(
                remoteAnimationTargets,
                wallpaperTargets,
                0 /* startTime */,
                false /* requestedShowSurfaceBehindKeyguard */
        )

        keyguardUnlockAnimationController.setSurfaceBehindAppearAmount(1f)
        keyguardUnlockAnimationController.setWallpaperAppearAmount(1f)

        val captorSb = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
        verify(surfaceTransactionApplier, times(1)).scheduleApply(
                captorSb.capture { sp -> sp.surface == surfaceControl1 })
        val captorWp = ArgThatCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>()
        verify(surfaceTransactionApplier, atLeastOnce().description("Wallpaper surface has  not " +
                "received scheduleApply")).scheduleApply(
                captorWp.capture { sp -> sp.surface == surfaceControlWp })

        val params = captorSb.getLastValue()
        assertEquals(1f, params.alpha)
        assertTrue(params.matrix.isIdentity)
        assertEquals("Wallpaper surface was expected to have opacity 1",
                1f, captorWp.getLastValue().alpha)

        verifyNoMoreInteractions(surfaceTransactionApplier)
    }

    @Test
    fun unlockToLauncherWithInWindowAnimations_ssViewIsVisible() {
        val mockLockscreenSmartspaceView = mock(View::class.java)
        whenever(mockLockscreenSmartspaceView.visibility).thenReturn(View.VISIBLE)
        keyguardUnlockAnimationController.lockscreenSmartspace = mockLockscreenSmartspaceView

        keyguardUnlockAnimationController.unlockToLauncherWithInWindowAnimations()

        verify(mockLockscreenSmartspaceView).visibility = View.INVISIBLE
    }

    @Test
    fun unlockToLauncherWithInWindowAnimations_ssViewIsInvisible() {
        val mockLockscreenSmartspaceView = mock(View::class.java)
        whenever(mockLockscreenSmartspaceView.visibility).thenReturn(View.INVISIBLE)
        keyguardUnlockAnimationController.lockscreenSmartspace = mockLockscreenSmartspaceView

        keyguardUnlockAnimationController.unlockToLauncherWithInWindowAnimations()

        verify(mockLockscreenSmartspaceView, never()).visibility = View.INVISIBLE
    }

    @Test
    fun unlockToLauncherWithInWindowAnimations_ssViewIsGone() {
        val mockLockscreenSmartspaceView = mock(View::class.java)
        whenever(mockLockscreenSmartspaceView.visibility).thenReturn(View.GONE)
        keyguardUnlockAnimationController.lockscreenSmartspace = mockLockscreenSmartspaceView

        keyguardUnlockAnimationController.unlockToLauncherWithInWindowAnimations()

        verify(mockLockscreenSmartspaceView, never()).visibility = View.INVISIBLE
    }

    @Test
    fun notifyFinishedKeyguardExitAnimation_ssViewIsInvisibleAndCancelledIsTrue() {
        val mockLockscreenSmartspaceView = mock(View::class.java)
        whenever(mockLockscreenSmartspaceView.visibility).thenReturn(View.INVISIBLE)
        keyguardUnlockAnimationController.lockscreenSmartspace = mockLockscreenSmartspaceView

        keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(true)

        verify(mockLockscreenSmartspaceView).visibility = View.VISIBLE
    }

    @Test
    fun notifyFinishedKeyguardExitAnimation_ssViewIsGoneAndCancelledIsTrue() {
        val mockLockscreenSmartspaceView = mock(View::class.java)
        whenever(mockLockscreenSmartspaceView.visibility).thenReturn(View.GONE)
        keyguardUnlockAnimationController.lockscreenSmartspace = mockLockscreenSmartspaceView

        keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(true)

        verify(mockLockscreenSmartspaceView, never()).visibility = View.VISIBLE
    }

    @Test
    fun notifyFinishedKeyguardExitAnimation_ssViewIsInvisibleAndCancelledIsFalse() {
        val mockLockscreenSmartspaceView = mock(View::class.java)
        whenever(mockLockscreenSmartspaceView.visibility).thenReturn(View.INVISIBLE)
        keyguardUnlockAnimationController.lockscreenSmartspace = mockLockscreenSmartspaceView

        keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(false)

        verify(mockLockscreenSmartspaceView).visibility = View.VISIBLE
    }

    @Test
    fun notifyFinishedKeyguardExitAnimation_ssViewIsGoneAndCancelledIsFalse() {
        val mockLockscreenSmartspaceView = mock(View::class.java)
        whenever(mockLockscreenSmartspaceView.visibility).thenReturn(View.GONE)
        keyguardUnlockAnimationController.lockscreenSmartspace = mockLockscreenSmartspaceView

        keyguardUnlockAnimationController.notifyFinishedKeyguardExitAnimation(false)

        verify(mockLockscreenSmartspaceView, never()).visibility = View.VISIBLE
    }

    private class ArgThatCaptor<T> {
        private var allArgs: MutableList<T> = mutableListOf()

        fun capture(predicate: Predicate<T>): T {
            return argThat{x: T ->
                if (predicate.test(x)) {
                    allArgs.add(x)
                    return@argThat true
                }
                return@argThat false
            }
        }

        fun getLastValue(): T {
            return allArgs.last()
        }

        fun getAllValues(): List<T> {
            return allArgs
        }
    }
}
