/*
 * Copyright (c) 2023 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.statusbar.notification.row

import android.app.ActivityManager
import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import android.provider.Settings.Secure
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.statusbar.notification.row.NotificationSettingsController.Listener
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.SecureSettings
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class NotificationSettingsControllerTest : SysuiTestCase() {

    val setting1: String = Secure.NOTIFICATION_BUBBLES
    val setting2: String = Secure.ACCESSIBILITY_ENABLED
    val settingUri1: Uri = Secure.getUriFor(setting1)
    val settingUri2: Uri = Secure.getUriFor(setting2)

    @Mock private lateinit var userTracker: UserTracker
    private lateinit var mainHandler: Handler
    private lateinit var backgroundHandler: Handler
    private lateinit var testableLooper: TestableLooper
    @Mock private lateinit var secureSettings: SecureSettings
    @Mock private lateinit var dumpManager: DumpManager

    @Captor private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback>
    @Captor private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver>

    private lateinit var controller: NotificationSettingsController

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        testableLooper = TestableLooper.get(this)
        mainHandler = Handler(testableLooper.looper)
        backgroundHandler = Handler(testableLooper.looper)
        allowTestableLooperAsMainThread()
        controller =
            NotificationSettingsController(
                userTracker,
                mainHandler,
                backgroundHandler,
                secureSettings,
                dumpManager
            )
    }

    @After
    fun tearDown() {
        disallowTestableLooperAsMainThread()
    }

    @Test
    fun creationRegistersCallbacks() {
        verify(userTracker).addCallback(any(), any())
        verify(dumpManager).registerNormalDumpable(anyString(), eq(controller))
    }
    @Test
    fun updateContentObserverRegistration_onUserChange_noSettingsListeners() {
        verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
        val userCallback = userTrackerCallbackCaptor.value
        val userId = 9

        // When: User is changed
        userCallback.onUserChanged(userId, context)

        // Validate: Nothing to do, since we aren't monitoring settings
        verify(secureSettings, never()).unregisterContentObserver(any())
        verify(secureSettings, never())
            .registerContentObserverForUser(any(Uri::class.java), anyBoolean(), any(), anyInt())
    }
    @Test
    fun updateContentObserverRegistration_onUserChange_withSettingsListeners() {
        // When: someone is listening to a setting
        controller.addCallback(settingUri1, Mockito.mock(Listener::class.java))

        verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any())
        val userCallback = userTrackerCallbackCaptor.value
        val userId = 9

        // Then: User is changed
        userCallback.onUserChanged(userId, context)

        // Validate: The tracker is unregistered and re-registered with the new user
        verify(secureSettings).unregisterContentObserver(any())
        verify(secureSettings)
            .registerContentObserverForUser(eq(settingUri1), eq(false), any(), eq(userId))
    }

    @Test
    fun addCallback_onlyFirstForUriRegistersObserver() {
        controller.addCallback(settingUri1, Mockito.mock(Listener::class.java))
        verifyZeroInteractions(secureSettings)
        testableLooper.processAllMessages()
        verify(secureSettings)
            .registerContentObserverForUser(
                eq(settingUri1),
                eq(false),
                any(),
                eq(ActivityManager.getCurrentUser())
            )

        controller.addCallback(settingUri1, Mockito.mock(Listener::class.java))
        verify(secureSettings)
            .registerContentObserverForUser(any(Uri::class.java), anyBoolean(), any(), anyInt())
    }

    @Test
    fun addCallback_secondUriRegistersObserver() {
        controller.addCallback(settingUri1, Mockito.mock(Listener::class.java))
        verifyZeroInteractions(secureSettings)
        testableLooper.processAllMessages()
        verify(secureSettings)
            .registerContentObserverForUser(
                eq(settingUri1),
                eq(false),
                any(),
                eq(ActivityManager.getCurrentUser())
            )
        clearInvocations(secureSettings)

        controller.addCallback(settingUri2, Mockito.mock(Listener::class.java))
        verifyNoMoreInteractions(secureSettings)
        testableLooper.processAllMessages()
        verify(secureSettings)
            .registerContentObserverForUser(
                eq(settingUri2),
                eq(false),
                any(),
                eq(ActivityManager.getCurrentUser())
            )
    }

    @Test
    fun removeCallback_lastUnregistersObserver() {
        val listenerSetting1: Listener = mock()
        val listenerSetting2: Listener = mock()
        controller.addCallback(settingUri1, listenerSetting1)
        verifyZeroInteractions(secureSettings)
        testableLooper.processAllMessages()
        verify(secureSettings)
            .registerContentObserverForUser(
                eq(settingUri1),
                eq(false),
                any(),
                eq(ActivityManager.getCurrentUser())
            )
        clearInvocations(secureSettings)

        controller.addCallback(settingUri2, listenerSetting2)
        verifyNoMoreInteractions(secureSettings)
        testableLooper.processAllMessages()
        verify(secureSettings)
            .registerContentObserverForUser(eq(settingUri2), anyBoolean(), any(), anyInt())
        clearInvocations(secureSettings)

        controller.removeCallback(settingUri2, listenerSetting2)
        testableLooper.processAllMessages()
        verify(secureSettings, never()).unregisterContentObserver(any())
        clearInvocations(secureSettings)

        controller.removeCallback(settingUri1, listenerSetting1)
        verifyNoMoreInteractions(secureSettings)
        testableLooper.processAllMessages()
        verify(secureSettings).unregisterContentObserver(any())
    }

    @Test
    fun addCallback_updatesCurrentValue() {
        whenever(secureSettings.getStringForUser(setting1, ActivityManager.getCurrentUser()))
            .thenReturn("9")
        whenever(secureSettings.getStringForUser(setting2, ActivityManager.getCurrentUser()))
            .thenReturn("5")

        val listenerSetting1a: Listener = mock()
        val listenerSetting1b: Listener = mock()
        val listenerSetting2: Listener = mock()

        controller.addCallback(settingUri1, listenerSetting1a)
        controller.addCallback(settingUri1, listenerSetting1b)
        controller.addCallback(settingUri2, listenerSetting2)

        verifyZeroInteractions(secureSettings)
        testableLooper.processAllMessages()

        verify(listenerSetting1a)
            .onSettingChanged(settingUri1, ActivityManager.getCurrentUser(), "9")
        verify(listenerSetting1b)
            .onSettingChanged(settingUri1, ActivityManager.getCurrentUser(), "9")
        verify(listenerSetting2)
            .onSettingChanged(settingUri2, ActivityManager.getCurrentUser(), "5")
    }

    @Test
    fun removeCallback_noMoreUpdates() {
        whenever(secureSettings.getStringForUser(setting1, ActivityManager.getCurrentUser()))
            .thenReturn("9")

        val listenerSetting1a: Listener = mock()
        val listenerSetting1b: Listener = mock()

        // First, register
        controller.addCallback(settingUri1, listenerSetting1a)
        controller.addCallback(settingUri1, listenerSetting1b)
        verifyZeroInteractions(secureSettings)
        testableLooper.processAllMessages()

        verify(secureSettings)
            .registerContentObserverForUser(
                any(Uri::class.java),
                anyBoolean(),
                capture(settingsObserverCaptor),
                anyInt()
            )
        verify(listenerSetting1a)
            .onSettingChanged(settingUri1, ActivityManager.getCurrentUser(), "9")
        verify(listenerSetting1b)
            .onSettingChanged(settingUri1, ActivityManager.getCurrentUser(), "9")
        clearInvocations(listenerSetting1b)
        clearInvocations(listenerSetting1a)

        // Remove one of them
        controller.removeCallback(settingUri1, listenerSetting1a)

        // On update, only remaining listener should get the callback
        settingsObserverCaptor.value.onChange(false, settingUri1)
        testableLooper.processAllMessages()

        verify(listenerSetting1a, never())
            .onSettingChanged(settingUri1, ActivityManager.getCurrentUser(), "9")
        verify(listenerSetting1b)
            .onSettingChanged(settingUri1, ActivityManager.getCurrentUser(), "9")
    }
}
