/*
 * 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.systemui.charging

import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.view.Surface
import android.view.View
import android.view.WindowManager
import android.view.WindowMetrics
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.commandline.CommandRegistry
import com.android.systemui.statusbar.policy.BatteryController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.surfaceeffects.ripple.RippleView
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.eq
import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidTestingRunner::class)
class WiredChargingRippleControllerTest : SysuiTestCase() {
    private lateinit var controller: WiredChargingRippleController
    @Mock private lateinit var commandRegistry: CommandRegistry
    @Mock private lateinit var batteryController: BatteryController
    @Mock private lateinit var featureFlags: FeatureFlags
    @Mock private lateinit var configurationController: ConfigurationController
    @Mock private lateinit var rippleView: RippleView
    @Mock private lateinit var windowManager: WindowManager
    @Mock private lateinit var uiEventLogger: UiEventLogger
    @Mock private lateinit var windowMetrics: WindowMetrics
    private val systemClock = FakeSystemClock()

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        `when`(featureFlags.isEnabled(Flags.CHARGING_RIPPLE)).thenReturn(true)
        controller = WiredChargingRippleController(
                commandRegistry, batteryController, configurationController,
                featureFlags, context, windowManager, systemClock, uiEventLogger)
        rippleView.setupShader()
        controller.rippleView = rippleView // Replace the real ripple view with a mock instance
        controller.registerCallbacks()

        `when`(windowMetrics.bounds).thenReturn(Rect(0, 0, 100, 100))
        `when`(windowManager.currentWindowMetrics).thenReturn(windowMetrics)
    }

    @Test
    fun testTriggerRipple_UnlockedState() {
        val captor = ArgumentCaptor
                .forClass(BatteryController.BatteryStateChangeCallback::class.java)
        verify(batteryController).addCallback(captor.capture())

        // Verify ripple added to window manager.
        captor.value.onBatteryLevelChanged(
                /* unusedBatteryLevel= */ 0,
                /* plugged in= */ true,
                /* charging= */ false)
        val attachListenerCaptor =
                ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
        verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
        verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>())

        // Verify ripple started
        val runnableCaptor =
                ArgumentCaptor.forClass(Runnable::class.java)
        attachListenerCaptor.value.onViewAttachedToWindow(rippleView)
        verify(rippleView).startRipple(runnableCaptor.capture())

        // Verify ripple removed
        runnableCaptor.value.run()
        verify(windowManager).removeView(rippleView)

        // Verify event logged
        verify(uiEventLogger).log(
                WiredChargingRippleController.WiredChargingRippleEvent.CHARGING_RIPPLE_PLAYED)
    }

    @Test
    fun testUpdateRippleColor() {
        val captor = ArgumentCaptor
                .forClass(ConfigurationController.ConfigurationListener::class.java)
        verify(configurationController).addCallback(captor.capture())

        reset(rippleView)
        captor.value.onThemeChanged()
        verify(rippleView).setColor(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())

        reset(rippleView)
        captor.value.onUiModeChanged()
        verify(rippleView).setColor(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
    }

    @Test
    fun testDebounceRipple() {
        var time: Long = 0
        systemClock.setElapsedRealtime(time)

        controller.startRippleWithDebounce()
        verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any())

        reset(rippleView)
        // Wait a short while and trigger.
        time += 100
        systemClock.setElapsedRealtime(time)
        controller.startRippleWithDebounce()

        // Verify the ripple is debounced.
        verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any())

        // Trigger many times.
        for (i in 0..100) {
            time += 100
            systemClock.setElapsedRealtime(time)
            controller.startRippleWithDebounce()
        }
        // Verify all attempts are debounced.
        verify(rippleView, never()).addOnAttachStateChangeListener(ArgumentMatchers.any())

        // Wait a long while and trigger.
        systemClock.setElapsedRealtime(time + 500000)
        controller.startRippleWithDebounce()
        // Verify that ripple is triggered.
        verify(rippleView).addOnAttachStateChangeListener(ArgumentMatchers.any())
    }

    @Test
    fun testRipple_whenDocked_doesNotPlayRipple() {
        `when`(batteryController.isChargingSourceDock).thenReturn(true)
        val captor = ArgumentCaptor
                .forClass(BatteryController.BatteryStateChangeCallback::class.java)
        verify(batteryController).addCallback(captor.capture())

        captor.value.onBatteryLevelChanged(
                /* unusedBatteryLevel= */ 0,
                /* plugged in= */ true,
                /* charging= */ false)

        val attachListenerCaptor =
                ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
        verify(rippleView, never()).addOnAttachStateChangeListener(attachListenerCaptor.capture())
        verify(windowManager, never()).addView(eq(rippleView), any<WindowManager.LayoutParams>())
    }

    @Test
    fun testRipple_layoutsCorrectly() {
        // Sets the correct ripple size.
        val width = 100
        val height = 200
        whenever(windowMetrics.bounds).thenReturn(Rect(0, 0, width, height))

        // Trigger ripple.
        val captor = ArgumentCaptor
                .forClass(BatteryController.BatteryStateChangeCallback::class.java)
        verify(batteryController).addCallback(captor.capture())

        captor.value.onBatteryLevelChanged(
                /* unusedBatteryLevel= */ 0,
                /* plugged in= */ true,
                /* charging= */ false)

        val attachListenerCaptor =
                ArgumentCaptor.forClass(View.OnAttachStateChangeListener::class.java)
        verify(rippleView).addOnAttachStateChangeListener(attachListenerCaptor.capture())
        verify(windowManager).addView(eq(rippleView), any<WindowManager.LayoutParams>())

        val runnableCaptor =
                ArgumentCaptor.forClass(Runnable::class.java)
        attachListenerCaptor.value.onViewAttachedToWindow(rippleView)
        verify(rippleView).startRipple(runnableCaptor.capture())

        // Verify size and center position.
        val maxSize = 400f // Double the max value between width and height.
        verify(rippleView).setMaxSize(maxWidth = maxSize, maxHeight = maxSize)

        val normalizedPortPosX =
                context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_x)
        val normalizedPortPosY =
                context.resources.getFloat(R.dimen.physical_charger_port_location_normalized_y)
        val expectedCenterX: Float
        val expectedCenterY: Float
        when (checkNotNull(context.display).rotation) {
            Surface.ROTATION_90 -> {
                expectedCenterX = width * normalizedPortPosY
                expectedCenterY = height * (1 - normalizedPortPosX)
            }
            Surface.ROTATION_180 -> {
                expectedCenterX = width * (1 - normalizedPortPosX)
                expectedCenterY = height * (1 - normalizedPortPosY)
            }
            Surface.ROTATION_270 -> {
                expectedCenterX = width * (1 - normalizedPortPosY)
                expectedCenterY = height * normalizedPortPosX
            }
            else -> { // Surface.ROTATION_0
                expectedCenterX = width * normalizedPortPosX
                expectedCenterY = height * normalizedPortPosY
            }
        }

        verify(rippleView).setCenter(expectedCenterX, expectedCenterY)
    }
}
