/*
 * Copyright (C) 2019 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.broadcast

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import android.os.Looper
import android.os.PatternMatcher
import android.os.UserHandle
import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.logging.BroadcastDispatcherLogger
import com.android.systemui.dump.DumpManager
import com.android.systemui.settings.UserTracker
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor
import junit.framework.Assert.assertSame
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations

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

    companion object {
        val user0 = UserHandle.of(0)
        val user1 = UserHandle.of(1)
        const val DEFAULT_FLAG = Context.RECEIVER_EXPORTED
        val DEFAULT_PERMISSION: String? = null

        fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
        const val TEST_ACTION = "TEST_ACTION"
        const val TEST_SCHEME = "TEST_SCHEME"
        const val TEST_PATH = "TEST_PATH"
        const val TEST_TYPE = "test/type"
    }

    @Mock
    private lateinit var mockContext: Context
    @Mock
    private lateinit var mockUBRUser0: UserBroadcastDispatcher
    @Mock
    private lateinit var mockUBRUser1: UserBroadcastDispatcher
    @Mock
    private lateinit var broadcastReceiver: BroadcastReceiver
    @Mock
    private lateinit var broadcastReceiverOther: BroadcastReceiver
    @Mock
    private lateinit var intentFilter: IntentFilter
    @Mock
    private lateinit var intentFilterOther: IntentFilter
    @Mock
    private lateinit var mockHandler: Handler
    @Mock
    private lateinit var logger: BroadcastDispatcherLogger
    @Mock
    private lateinit var userTracker: UserTracker
    @Mock
    private lateinit var removalPendingStore: PendingRemovalStore

    private lateinit var mainExecutor: Executor

    @Captor
    private lateinit var argumentCaptor: ArgumentCaptor<ReceiverData>

    private lateinit var testableLooper: TestableLooper
    private lateinit var broadcastDispatcher: BroadcastDispatcher

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        testableLooper = TestableLooper.get(this)
        mainExecutor = FakeExecutor(FakeSystemClock())
        `when`(mockContext.mainExecutor).thenReturn(mainExecutor)

        broadcastDispatcher = TestBroadcastDispatcher(
                mockContext,
                mainExecutor,
                testableLooper.looper,
                mock(Executor::class.java),
                mock(DumpManager::class.java),
                logger,
                userTracker,
                removalPendingStore,
                mapOf(0 to mockUBRUser0, 1 to mockUBRUser1))

        // These should be valid filters
        `when`(intentFilter.countActions()).thenReturn(1)
        `when`(intentFilterOther.countActions()).thenReturn(1)
        setUserMock(mockContext, user0)
    }

    @Test
    fun testAddingReceiverToCorrectUBR() {
        broadcastDispatcher.registerReceiverWithHandler(broadcastReceiver, intentFilter,
                mockHandler, user0)
        broadcastDispatcher.registerReceiverWithHandler(
                broadcastReceiverOther, intentFilterOther, mockHandler, user1)

        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)

        verify(mockUBRUser1).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))
        assertSame(broadcastReceiverOther, argumentCaptor.value.receiver)
        assertSame(intentFilterOther, argumentCaptor.value.filter)
    }

    @Test
    fun testAddingReceiverToCorrectUBR_executor() {
        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, mainExecutor, user0)
        broadcastDispatcher.registerReceiver(
                broadcastReceiverOther, intentFilterOther, mainExecutor, user1)

        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)

        verify(mockUBRUser1).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))
        assertSame(broadcastReceiverOther, argumentCaptor.value.receiver)
        assertSame(intentFilterOther, argumentCaptor.value.filter)
    }

    @Test
    fun testAddReceiverDefaultFlag_handler() {
        broadcastDispatcher.registerReceiverWithHandler(
                broadcastReceiver, intentFilter, mockHandler)
        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)
    }

    @Test
    fun testAddReceiverCorrectFlag_handler() {
        val flag = 3

        broadcastDispatcher.registerReceiverWithHandler(
                broadcastReceiver, intentFilter, mockHandler, flags = flag)
        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(flag))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)
    }

    @Test
    fun testAddReceiverDefaultFlag_executor() {
        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter)
        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)
    }

    @Test
    fun testAddReceiverCorrectPermission_executor() {
        val flag = 3
        val permission = "CUSTOM_PERMISSION"

        broadcastDispatcher.registerReceiver(
            broadcastReceiver,
            intentFilter,
            flags = flag,
            permission = permission
        )
        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(flag))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)
        assertSame(permission, argumentCaptor.value.permission)
    }

    @Test
    fun testAddReceiverDefaultPermission_executor() {
        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter)
        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)
        assertSame(DEFAULT_PERMISSION, argumentCaptor.value.permission)
    }

    @Test
    fun testAddReceiverCorrectFlag_executor() {
        val flag = 3

        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, flags = flag)
        testableLooper.processAllMessages()

        verify(mockUBRUser0).registerReceiver(capture(argumentCaptor), eq(flag))

        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
        assertSame(intentFilter, argumentCaptor.value.filter)
    }

    @Test
    fun testRemovingReceiversRemovesFromAllUBR() {
        broadcastDispatcher.registerReceiverWithHandler(broadcastReceiver, intentFilter,
                mockHandler, user0)
        broadcastDispatcher.registerReceiverWithHandler(broadcastReceiver, intentFilter,
                mockHandler, user1)

        broadcastDispatcher.unregisterReceiver(broadcastReceiver)

        testableLooper.processAllMessages()

        verify(mockUBRUser0).unregisterReceiver(broadcastReceiver)
        verify(mockUBRUser1).unregisterReceiver(broadcastReceiver)
    }

    @Test
    fun testRemoveReceiverFromUser() {
        broadcastDispatcher.registerReceiverWithHandler(broadcastReceiver, intentFilter,
                mockHandler, user0)
        broadcastDispatcher.registerReceiverWithHandler(broadcastReceiver, intentFilter,
                mockHandler, user1)

        broadcastDispatcher.unregisterReceiverForUser(broadcastReceiver, user0)

        testableLooper.processAllMessages()

        verify(mockUBRUser0).unregisterReceiver(broadcastReceiver)
        verify(mockUBRUser1, never()).unregisterReceiver(broadcastReceiver)
    }

    @Test
    fun testRegisterCurrentAsActualUser() {
        `when`(userTracker.userId).thenReturn(user1.identifier)

        broadcastDispatcher.registerReceiverWithHandler(broadcastReceiver, intentFilter,
                mockHandler, UserHandle.CURRENT)

        testableLooper.processAllMessages()

        verify(mockUBRUser1).registerReceiver(
                capture(argumentCaptor), eq(Context.RECEIVER_EXPORTED))
        assertSame(broadcastReceiver, argumentCaptor.value.receiver)
    }

    @Test(expected = IllegalArgumentException::class)
    fun testFilterMustContainActions() {
        val testFilter = IntentFilter()
        broadcastDispatcher.registerReceiver(broadcastReceiver, testFilter)
    }

    @Test(expected = IllegalArgumentException::class)
    fun testFilterMustNotContainDataScheme() {
        val testFilter = IntentFilter(TEST_ACTION).apply {
            addDataScheme(TEST_SCHEME)
        }
        broadcastDispatcher.registerReceiver(broadcastReceiver, testFilter)
    }

    @Test(expected = IllegalArgumentException::class)
    fun testFilterMustNotContainDataAuthority() {
        val testFilter = IntentFilter(TEST_ACTION).apply {
            addDataAuthority(mock(IntentFilter.AuthorityEntry::class.java))
        }
        broadcastDispatcher.registerReceiver(broadcastReceiver, testFilter)
    }

    @Test(expected = IllegalArgumentException::class)
    fun testFilterMustNotContainDataPath() {
        val testFilter = IntentFilter(TEST_ACTION).apply {
            addDataPath(TEST_PATH, PatternMatcher.PATTERN_LITERAL)
        }
        broadcastDispatcher.registerReceiver(broadcastReceiver, testFilter)
    }

    @Test(expected = IllegalArgumentException::class)
    fun testFilterMustNotContainDataType() {
        val testFilter = IntentFilter(TEST_ACTION).apply {
            addDataType(TEST_TYPE)
        }
        broadcastDispatcher.registerReceiver(broadcastReceiver, testFilter)
    }

    @Test(expected = IllegalArgumentException::class)
    fun testFilterMustNotSetPriority() {
        val testFilter = IntentFilter(TEST_ACTION).apply {
            priority = IntentFilter.SYSTEM_HIGH_PRIORITY
        }
        broadcastDispatcher.registerReceiver(broadcastReceiver, testFilter)
    }

    @Test
    fun testTaggedReceiverForRemovalImmediately_allUsers() {
        broadcastDispatcher.unregisterReceiver(broadcastReceiver)

        verify(removalPendingStore).tagForRemoval(broadcastReceiver, UserHandle.USER_ALL)
        verify(removalPendingStore, never()).clearPendingRemoval(eq(broadcastReceiver), anyInt())
    }

    @Test
    fun testTaggedReceiverForRemovalImmediately_singleUser() {
        val user = 0
        broadcastDispatcher.unregisterReceiverForUser(broadcastReceiver, UserHandle.of(user))

        verify(removalPendingStore).tagForRemoval(broadcastReceiver, user)
        verify(removalPendingStore, never()).clearPendingRemoval(eq(broadcastReceiver), anyInt())
    }

    @Test
    fun testUnregisterReceiverClearsPendingRemovalAfterRemoving_allUsers() {
        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, null, user0)
        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, null, user1)

        broadcastDispatcher.unregisterReceiver(broadcastReceiver)

        testableLooper.processAllMessages()

        val inOrderUser0 = inOrder(mockUBRUser0, removalPendingStore)
        inOrderUser0.verify(mockUBRUser0).unregisterReceiver(broadcastReceiver)
        inOrderUser0.verify(removalPendingStore)
            .clearPendingRemoval(broadcastReceiver, UserHandle.USER_ALL)

        val inOrderUser1 = inOrder(mockUBRUser1, removalPendingStore)
        inOrderUser1.verify(mockUBRUser1).unregisterReceiver(broadcastReceiver)
        inOrderUser1.verify(removalPendingStore)
            .clearPendingRemoval(broadcastReceiver, UserHandle.USER_ALL)
    }

    @Test
    fun testUnregisterReceiverclearPendingRemovalAfterRemoving_singleUser() {
        broadcastDispatcher.registerReceiver(broadcastReceiver, intentFilter, null, user1)

        broadcastDispatcher.unregisterReceiverForUser(broadcastReceiver, user1)

        testableLooper.processAllMessages()

        val inOrderUser1 = inOrder(mockUBRUser1, removalPendingStore)
        inOrderUser1.verify(mockUBRUser1).unregisterReceiver(broadcastReceiver)
        inOrderUser1.verify(removalPendingStore)
            .clearPendingRemoval(broadcastReceiver, user1.identifier)
    }

    @Test
    fun testBroadcastFlow() = runBlockingTest {
        val flow = broadcastDispatcher.broadcastFlow(intentFilter, user1) { intent, receiver ->
            intent to receiver
        }

        // Collect the values into collectedValues.
        val collectedValues = mutableListOf<Pair<Intent, BroadcastReceiver>>()
        val job = launch {
            flow.collect { collectedValues.add(it) }
        }

        testableLooper.processAllMessages()
        verify(mockUBRUser1).registerReceiver(capture(argumentCaptor), eq(DEFAULT_FLAG))
        val receiver = argumentCaptor.value.receiver

        // Simulate fake broadcasted intents.
        val fakeIntents = listOf<Intent>(mock(), mock(), mock())
        fakeIntents.forEach { receiver.onReceive(mockContext, it) }

        // The intents should have been collected.
        advanceUntilIdle()

        val expectedValues = fakeIntents.map { it to receiver }
        assertThat(collectedValues).containsExactlyElementsIn(expectedValues)

        // Stop the collection.
        job.cancel()

        testableLooper.processAllMessages()
        verify(mockUBRUser1).unregisterReceiver(receiver)
    }

    private fun setUserMock(mockContext: Context, user: UserHandle) {
        `when`(mockContext.user).thenReturn(user)
        `when`(mockContext.userId).thenReturn(user.identifier)
    }

    private class TestBroadcastDispatcher(
        context: Context,
        mainExecutor: Executor,
        backgroundRunningLooper: Looper,
        backgroundRunningExecutor: Executor,
        dumpManager: DumpManager,
        logger: BroadcastDispatcherLogger,
        userTracker: UserTracker,
        removalPendingStore: PendingRemovalStore,
        var mockUBRMap: Map<Int, UserBroadcastDispatcher>
    ) : BroadcastDispatcher(
        context,
        mainExecutor,
        backgroundRunningLooper,
        backgroundRunningExecutor,
        dumpManager,
        logger,
        userTracker,
        removalPendingStore
    ) {
        override fun createUBRForUser(userId: Int): UserBroadcastDispatcher {
            return mockUBRMap.getOrDefault(userId, mock(UserBroadcastDispatcher::class.java))
        }
    }
}
