/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package mozilla.components.feature.downloads

import android.app.DownloadManager
import android.app.DownloadManager.EXTRA_DOWNLOAD_ID
import android.app.Notification
import android.app.NotificationManager
import android.app.Service
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Environment
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.state.content.DownloadState.Status.CANCELLED
import mozilla.components.browser.state.state.content.DownloadState.Status.COMPLETED
import mozilla.components.browser.state.state.content.DownloadState.Status.DOWNLOADING
import mozilla.components.browser.state.state.content.DownloadState.Status.FAILED
import mozilla.components.browser.state.state.content.DownloadState.Status.INITIATED
import mozilla.components.browser.state.state.content.DownloadState.Status.PAUSED
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.MutableHeaders
import mozilla.components.concept.fetch.Request
import mozilla.components.concept.fetch.Response
import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_CANCEL
import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_PAUSE
import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_REMOVE_PRIVATE_DOWNLOAD
import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_RESUME
import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.ACTION_TRY_AGAIN
import mozilla.components.feature.downloads.AbstractFetchDownloadService.Companion.PROGRESS_UPDATE_INTERVAL
import mozilla.components.feature.downloads.AbstractFetchDownloadService.CopyInChuckStatus.ERROR_IN_STREAM_CLOSED
import mozilla.components.feature.downloads.AbstractFetchDownloadService.DownloadJobState
import mozilla.components.feature.downloads.DownloadNotification.NOTIFICATION_DOWNLOAD_GROUP_ID
import mozilla.components.feature.downloads.facts.DownloadsFacts.Items.NOTIFICATION
import mozilla.components.feature.downloads.fake.FakeDateTimeProvider
import mozilla.components.feature.downloads.fake.FakeFileSizeFormatter
import mozilla.components.feature.downloads.fake.FakePackageNameProvider
import mozilla.components.support.base.android.NotificationsDelegate
import mozilla.components.support.base.facts.Action
import mozilla.components.support.base.facts.processor.CollectionProcessor
import mozilla.components.support.test.any
import mozilla.components.support.test.argumentCaptor
import mozilla.components.support.test.eq
import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import mozilla.components.support.utils.ext.stopForegroundCompat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mock
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doCallRealMethod
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doThrow
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.mockito.MockitoAnnotations.openMocks
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
import org.robolectric.shadows.ShadowNotificationManager
import java.io.File
import java.io.IOException
import java.io.InputStream
import kotlin.random.Random
import kotlin.time.Duration.Companion.milliseconds

@RunWith(AndroidJUnit4::class)
@Config(shadows = [ShadowFileProvider::class])
class AbstractFetchDownloadServiceTest {

    @Rule @JvmField
    val folder = TemporaryFolder()

    // We need different scopes and schedulers because:
    // - the service will continuously try to update the download notification using MainScope()
    // - if using the same scope in tests the test won't end
    // - need a way to advance main dispatcher used by the service.
    @get:Rule
    val coroutinesTestRule = MainCoroutineRule()
    private val mainDispatcher = coroutinesTestRule.testDispatcher
    private val testsDispatcher = StandardTestDispatcher(TestCoroutineScheduler())

    private val fakeFileSizeFormatter: FileSizeFormatter = FakeFileSizeFormatter()
    private val fakeDateTimeProvider: DateTimeProvider = FakeDateTimeProvider()
    private val fakeDownloadEstimator: DownloadEstimator = DownloadEstimator(fakeDateTimeProvider)
    private val fakePackageNameProvider: PackageNameProvider =
        FakePackageNameProvider("mozilla.components.feature.downloads.test")

    @Mock private lateinit var client: Client
    private lateinit var browserStore: BrowserStore
    private lateinit var notificationManagerCompat: NotificationManagerCompat

    private lateinit var notificationsDelegate: NotificationsDelegate

    private lateinit var service: AbstractFetchDownloadService

    private lateinit var shadowNotificationService: ShadowNotificationManager

    private val delayTime = PROGRESS_UPDATE_INTERVAL.milliseconds

    fun createService(
        browserStore: BrowserStore,
    ): AbstractFetchDownloadService = spy(
        object : AbstractFetchDownloadService() {
            override val httpClient = client
            override val store = browserStore
            override val notificationsDelegate = this@AbstractFetchDownloadServiceTest.notificationsDelegate
            override val fileSizeFormatter = fakeFileSizeFormatter
            override val downloadEstimator = fakeDownloadEstimator
            override val packageNameProvider = fakePackageNameProvider
            override val context: Context = testContext
        },
    )

    @Before
    fun setup() {
        openMocks(this)
        browserStore = BrowserStore()
        notificationManagerCompat = spy(NotificationManagerCompat.from(testContext))
        notificationsDelegate = NotificationsDelegate(notificationManagerCompat)
        service = createService(browserStore)

        doNothing().`when`(service).useFileStream(any(), anyBoolean(), any())
        doReturn(true).`when`(notificationManagerCompat).areNotificationsEnabled()

        shadowNotificationService =
            shadowOf(testContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager)
    }

    @Test
    fun `begins download when started`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        assertEquals(download.url, providedDownload.value.state.url)
        assertEquals(download.fileName, providedDownload.value.state.fileName)

        // Ensure the job is properly added to the map
        assertEquals(1, service.downloadJobs.count())
        assertNotNull(service.downloadJobs[providedDownload.value.state.id])
    }

    @Test
    fun `WHEN a download intent is received THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val downloadIntent = Intent("ACTION_DOWNLOAD")

        doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
        doNothing().`when`(service).handleDownloadIntent(any())

        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))

        service.onStartCommand(downloadIntent, 0, 0)

        verify(service).handleDownloadIntent(download)
        verify(service, never()).handleRemovePrivateDownloadIntent(download)
    }

    @Test
    fun `WHEN an intent does not provide an action THEN handleDownloadIntent must be called`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val downloadIntent = Intent()

        doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
        doNothing().`when`(service).handleDownloadIntent(any())

        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))

        service.onStartCommand(downloadIntent, 0, 0)

        verify(service).handleDownloadIntent(download)
        verify(service, never()).handleRemovePrivateDownloadIntent(download)
    }

    @Test
    fun `WHEN a try again intent is received THEN handleDownloadIntent must be called`() =
        runTest(testsDispatcher) {
            val download = DownloadState("https://example.com/file.txt", "file.txt")
            val downloadIntent = Intent(ACTION_TRY_AGAIN)

            doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
            doNothing().`when`(service).handleDownloadIntent(any())

            downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)
            val newDownloadState = download.copy(status = DOWNLOADING)
            browserStore.dispatch(DownloadAction.AddDownloadAction(newDownloadState))

            service.onStartCommand(downloadIntent, 0, 0)

            verify(service).handleDownloadIntent(newDownloadState)
            assertEquals(newDownloadState.status, DOWNLOADING)
            verify(service, never()).handleRemovePrivateDownloadIntent(newDownloadState)
        }

    @Test
    fun `WHEN a remove download intent is received THEN handleRemoveDownloadIntent must be called`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val downloadIntent = Intent(ACTION_REMOVE_PRIVATE_DOWNLOAD)

        doNothing().`when`(service).handleRemovePrivateDownloadIntent(any())
        doNothing().`when`(service).handleDownloadIntent(any())

        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))

        service.onStartCommand(downloadIntent, 0, 0)

        verify(service).handleRemovePrivateDownloadIntent(download)
        verify(service, never()).handleDownloadIntent(download)
    }

    @Test
    fun `WHEN handleRemovePrivateDownloadIntent with a private download is called THEN removeDownloadJob must be called`() {
        val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = true)
        val downloadJobState = DownloadJobState(state = downloadState, status = COMPLETED)
        val browserStore = mock<BrowserStore>()
        val service = createService(browserStore)

        doAnswer { }.`when`(service).removeDownloadJob(any())

        service.downloadJobs[downloadState.id] = downloadJobState

        service.handleRemovePrivateDownloadIntent(downloadState)

        verify(service, times(0)).cancelDownloadJob(downloadJobState)
        verify(service).removeDownloadJob(downloadJobState)
        verify(browserStore).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id))
    }

    @Test
    fun `WHEN handleRemovePrivateDownloadIntent is called with a private download AND not COMPLETED status THEN removeDownloadJob and cancelDownloadJob must be called`() {
        val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = true)
        val downloadJobState = DownloadJobState(state = downloadState, status = DOWNLOADING)
        val browserStore = mock<BrowserStore>()
        val service = createService(browserStore)

        doAnswer { }.`when`(service).removeDownloadJob(any())

        service.downloadJobs[downloadState.id] = downloadJobState

        service.handleRemovePrivateDownloadIntent(downloadState)

        verify(service).cancelDownloadJob(
            currentDownloadJobState = eq(downloadJobState),
            coroutineScope = any(),
        )
        verify(service).removeDownloadJob(downloadJobState)
        verify(browserStore).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id))
    }

    @Test
    fun `WHEN handleRemovePrivateDownloadIntent is called with with a non-private (or regular) download THEN removeDownloadJob must not be called`() {
        val downloadState = DownloadState(url = "mozilla.org/mozilla.txt", private = false)
        val downloadJobState = DownloadJobState(state = downloadState, status = COMPLETED)
        val browserStore = mock<BrowserStore>()

        doAnswer { }.`when`(service).removeDownloadJob(any())

        service.downloadJobs[downloadState.id] = downloadJobState

        service.handleRemovePrivateDownloadIntent(downloadState)

        verify(service, never()).removeDownloadJob(downloadJobState)
        verify(browserStore, never()).dispatch(DownloadAction.RemoveDownloadAction(downloadState.id))
    }

    @Test
    fun `service redelivers if no download extra is passed `() = runTest(testsDispatcher) {
        val downloadIntent = Intent("ACTION_DOWNLOAD")

        val intentCode = service.onStartCommand(downloadIntent, 0, 0)

        assertEquals(Service.START_REDELIVER_INTENT, intentCode)
    }

    @Test
    fun `verifyDownload sets the download to failed if it is not complete`() = runTest(testsDispatcher) {
        val downloadState = DownloadState(
            url = "mozilla.org/mozilla.txt",
            contentLength = 50L,
            currentBytesCopied = 5,
            status = DOWNLOADING,
        )

        val downloadJobState = DownloadJobState(
            job = null,
            state = downloadState,
            foregroundServiceId = 1,
            downloadDeleted = false,
            currentBytesCopied = 5,
            status = DOWNLOADING,
        )

        service.verifyDownload(downloadJobState)

        assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
        verify(service).setDownloadJobStatus(downloadJobState, FAILED)
        verify(service).updateDownloadState(downloadState.copy(status = FAILED))
    }

    @Test
    fun `verifyDownload does NOT set the download to failed if it is paused`() = runTest(testsDispatcher) {
        val downloadState = DownloadState(
            url = "mozilla.org/mozilla.txt",
            contentLength = 50L,
            currentBytesCopied = 5,
            status = DownloadState.Status.PAUSED,
        )

        val downloadJobState = DownloadJobState(
            job = null,
            state = downloadState,
            currentBytesCopied = 5,
            status = DownloadState.Status.PAUSED,
            foregroundServiceId = 1,
            downloadDeleted = false,
        )

        service.verifyDownload(downloadJobState)

        assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState))
        verify(service, times(0)).setDownloadJobStatus(downloadJobState, DownloadState.Status.FAILED)
        verify(service, times(0)).updateDownloadState(downloadState.copy(status = DownloadState.Status.FAILED))
    }

    @Test
    fun `verifyDownload does NOT set the download to failed if it is complete`() = runTest(testsDispatcher) {
        val downloadState = DownloadState(
            url = "mozilla.org/mozilla.txt",
            contentLength = 50L,
            currentBytesCopied = 50,
            status = DOWNLOADING,
        )

        val downloadJobState = DownloadJobState(
            job = null,
            state = downloadState,
            currentBytesCopied = 50,
            status = DOWNLOADING,
            foregroundServiceId = 1,
            downloadDeleted = false,
        )

        service.verifyDownload(downloadJobState)

        assertNotEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
        verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED)
        verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED))
    }

    @Test
    fun `verifyDownload does NOT set the download to failed if it is cancelled`() = runTest(testsDispatcher) {
        val downloadState = DownloadState(
            url = "mozilla.org/mozilla.txt",
            contentLength = 50L,
            currentBytesCopied = 50,
            status = DownloadState.Status.CANCELLED,
        )

        val downloadJobState = DownloadJobState(
            job = null,
            state = downloadState,
            currentBytesCopied = 50,
            status = DownloadState.Status.CANCELLED,
            foregroundServiceId = 1,
            downloadDeleted = false,
        )

        service.verifyDownload(downloadJobState)

        assertNotEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
        verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED)
        verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED))
    }

    @Test
    fun `verifyDownload does NOT set the download to failed if it is status COMPLETED`() = runTest(testsDispatcher) {
        val downloadState = DownloadState(
            url = "mozilla.org/mozilla.txt",
            contentLength = 50L,
            currentBytesCopied = 50,
            status = DownloadState.Status.COMPLETED,
        )

        val downloadJobState = DownloadJobState(
            job = null,
            state = downloadState,
            currentBytesCopied = 50,
            status = DownloadState.Status.COMPLETED,
            foregroundServiceId = 1,
            downloadDeleted = false,
        )

        service.verifyDownload(downloadJobState)

        verify(service, times(0)).setDownloadJobStatus(downloadJobState, FAILED)
        verify(service, times(0)).updateDownloadState(downloadState.copy(status = FAILED))
    }

    @Test
    fun `verify that a COMPLETED download contains a file size`() {
        val downloadState = DownloadState(
            url = "mozilla.org/mozilla.txt",
            contentLength = 0L,
            currentBytesCopied = 50,
            status = DOWNLOADING,
        )
        val downloadJobState = DownloadJobState(
            job = null,
            state = downloadState,
            currentBytesCopied = 50,
            status = DOWNLOADING,
            foregroundServiceId = 1,
            downloadDeleted = false,
        )

        browserStore.dispatch(DownloadAction.AddDownloadAction(downloadState))
        service.downloadJobs[downloadJobState.state.id] = downloadJobState
        service.verifyDownload(downloadJobState)

        assertEquals(downloadJobState.state.contentLength, service.downloadJobs[downloadJobState.state.id]!!.state.contentLength)
        assertEquals(downloadJobState.state.contentLength, browserStore.state.downloads.values.first().contentLength)
    }

    @Test
    fun `broadcastReceiver handles ACTION_PAUSE`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        val pauseIntent = Intent(ACTION_PAUSE).apply {
            setPackage(testContext.applicationContext.packageName)
            putExtra(INTENT_EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
        }

        CollectionProcessor.withFactCollection { facts ->
            service.broadcastReceiver.onReceive(testContext, pauseIntent)

            val pauseFact = facts[0]
            assertEquals(Action.PAUSE, pauseFact.action)
            assertEquals(NOTIFICATION, pauseFact.item)
        }

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()
        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState))
    }

    @Test
    fun `broadcastReceiver handles ACTION_CANCEL`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        val cancelIntent = Intent(ACTION_CANCEL).apply {
            setPackage(testContext.applicationContext.packageName)
            putExtra(INTENT_EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
        }

        assertFalse(service.downloadJobs[providedDownload.value.state.id]!!.downloadDeleted)

        CollectionProcessor.withFactCollection { facts ->
            service.broadcastReceiver.onReceive(testContext, cancelIntent)

            val cancelFact = facts[0]
            assertEquals(Action.CANCEL, cancelFact.action)
            assertEquals(NOTIFICATION, cancelFact.item)
        }
    }

    @Test
    fun `WHEN an intent is sent with an ACTION_RESUME action and the file exists THEN the broadcastReceiver resumes the download`() = runTest(testsDispatcher) {
        folder.newFile("file.txt")

        val download = DownloadState(
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            destinationDirectory = folder.root.path,
        )

        val downloadResponse = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        val resumeResponse = Response(
            "https://example.com/file.txt",
            206,
            MutableHeaders("Content-Range" to "1-67589/67589"),
            Response.Body(mock()),
        )
        doReturn(downloadResponse).`when`(client)
            .fetch(Request("https://example.com/file.txt"))
        doReturn(resumeResponse).`when`(client)
            .fetch(Request("https://example.com/file.txt", headers = MutableHeaders("Range" to "bytes=1-")))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        // Simulate a pause
        var downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        downloadJobState.currentBytesCopied = 1
        service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)

        service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
        service.downloadJobs[providedDownload.value.state.id]?.job?.cancel()

        val resumeIntent = Intent(ACTION_RESUME).apply {
            setPackage(testContext.applicationContext.packageName)
            putExtra(INTENT_EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
        }

        CollectionProcessor.withFactCollection { facts ->
            service.broadcastReceiver.onReceive(testContext, resumeIntent)

            val resumeFact = facts[0]
            assertEquals(Action.RESUME, resumeFact.action)
            assertEquals(NOTIFICATION, resumeFact.item)
        }

        downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState))

        // Make sure the download job is completed (break out of copyInChunks)
        service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()

        verify(service).startDownloadJob(providedDownload.value)

        File(downloadJobState.state.filePath).delete()
    }

    @Test
    fun `WHEN an intent is sent with an ACTION_RESUME action and the file doesn't exist THEN the broadcastReceiver sets the download status to FAILED`() =
        runTest(testsDispatcher) {
            folder.newFile("file.txt")

            val download = DownloadState(
                url = "https://example.com/file.txt",
                fileName = "file.txt",
                destinationDirectory = folder.root.path,
            )

            val downloadResponse = Response(
                "https://example.com/file.txt",
                200,
                MutableHeaders(),
                Response.Body(mock()),
            )
            val resumeResponse = Response(
                "https://example.com/file.txt",
                206,
                MutableHeaders("Content-Range" to "1-67589/67589"),
                Response.Body(mock()),
            )

            doReturn(downloadResponse).`when`(client)
                .fetch(Request("https://example.com/file.txt"))
            doReturn(resumeResponse).`when`(client)
                .fetch(
                    Request(
                        "https://example.com/file.txt",
                        headers = MutableHeaders("Range" to "bytes=1-"),
                    ),
                )

            val downloadIntent = Intent("ACTION_DOWNLOAD")
            downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

            browserStore.dispatch(DownloadAction.AddDownloadAction(download))
            service.onStartCommand(downloadIntent, 0, 0)
            service.downloadJobs.values.forEach { it.job?.join() }

            val providedDownload = argumentCaptor<DownloadJobState>()
            verify(service).performDownload(providedDownload.capture(), anyBoolean())

            // Simulate a pause
            var downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
            downloadJobState.currentBytesCopied = 1
            service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)

            File(downloadJobState.state.filePath).delete()

            val resumeIntent = Intent(ACTION_RESUME).apply {
                setPackage(testContext.applicationContext.packageName)
                putExtra(INTENT_EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
            }

            doNothing().`when`(service).updateDownloadNotification(any(), any(), any())

            CollectionProcessor.withFactCollection { facts ->
                service.broadcastReceiver.onReceive(testContext, resumeIntent)

                val resumeFact = facts[0]
                assertEquals(Action.RESUME, resumeFact.action)
                assertEquals(NOTIFICATION, resumeFact.item)
            }

            downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!

            assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
        }

    @Test
    fun `broadcastReceiver handles ACTION_TRY_AGAIN`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt", contentLength = 1000)
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())
        service.downloadJobs[providedDownload.value.state.id]?.job?.join()

        // Simulate a failure
        var downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        service.setDownloadJobStatus(downloadJobState, FAILED)
        service.downloadJobs[providedDownload.value.state.id]?.job?.cancel()

        val tryAgainIntent = Intent(ACTION_TRY_AGAIN).apply {
            setPackage(testContext.applicationContext.packageName)
            putExtra(INTENT_EXTRA_DOWNLOAD_ID, providedDownload.value.state.id)
        }

        CollectionProcessor.withFactCollection { facts ->
            service.broadcastReceiver.onReceive(testContext, tryAgainIntent)

            val tryAgainFact = facts[0]
            assertEquals(Action.TRY_AGAIN, tryAgainFact.action)
            assertEquals(NOTIFICATION, tryAgainFact.item)
        }

        downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState))

        // Make sure the download job is completed (break out of copyInChunks)
        service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()

        verify(service).startDownloadJob(providedDownload.value)
    }

    @Test
    fun `download fails on a bad network response`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            400,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()
        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
    }

    @Test
    fun `makeUniqueFileNameIfNecessary transforms fileName when appending FALSE`() {
        folder.newFile("example.apk")

        val download = DownloadState(
            url = "mozilla.org",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
        )
        val transformedDownload = service.makeUniqueFileNameIfNecessary(download, false)

        assertNotEquals(download.fileName, transformedDownload.fileName)
    }

    @Test
    fun `makeUniqueFileNameIfNecessary does NOT transform fileName when appending TRUE`() {
        folder.newFile("example.apk")

        val download = DownloadState(
            url = "mozilla.org",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
        )
        val transformedDownload = service.makeUniqueFileNameIfNecessary(download, true)

        assertEquals(download, transformedDownload)
    }

    @Test
    fun `notification is shown when download status is ACTIVE`() = runBlocking {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()
        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        service.setDownloadJobStatus(downloadJobState, DOWNLOADING)
        assertEquals(DOWNLOADING, service.getDownloadJobStatus(downloadJobState))

        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()

        // The additional notification is the summary one (the notification group).
        assertEquals(2, shadowNotificationService.size())
    }

    @Test
    fun `WHEN a failed download is tried again, created time is updated`() =
        runTest(testsDispatcher) {
            val downloadId = "cakes"
            val downloadJobState = DownloadJobState(
                state = DownloadState(url = "", id = downloadId),
                status = FAILED,
                createdTime = 0,
            )
            service.downloadJobs[downloadId] = downloadJobState
            val tryAgainIntent = Intent(ACTION_TRY_AGAIN).apply {
                putExtra(INTENT_EXTRA_DOWNLOAD_ID, downloadId)
            }
            service.broadcastReceiver.onReceive(testContext, tryAgainIntent)
            assertTrue(downloadJobState.createdTime > 0)
        }

    @Test
    fun `onStartCommand must change status of INITIATED downloads to DOWNLOADING`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt", status = INITIATED)

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        doNothing().`when`(service).performDownload(any(), anyBoolean())

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.first().job!!

        verify(service).startDownloadJob(any())
        assertEquals(DOWNLOADING, service.downloadJobs.values.first().status)
    }

    @Test
    fun `onStartCommand must change the status only for INITIATED downloads`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt", status = FAILED)

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)

        verify(service, never()).startDownloadJob(any())
        assertEquals(FAILED, service.downloadJobs.values.first().status)
    }

    @Test
    fun `onStartCommand sets the notification foreground`() = runTest(testsDispatcher) {
        val download = DownloadState("https://example.com/file.txt", "file.txt")

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        doNothing().`when`(service).performDownload(any(), anyBoolean())

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)

        verify(service).setForegroundNotification()
    }

    @Test
    fun `sets the notification foreground in devices that support notification group`() = runTest(testsDispatcher) {
        val download = DownloadState(
            id = "1",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = DOWNLOADING,
        )
        val downloadState = DownloadJobState(
            state = download,
            status = DOWNLOADING,
            foregroundServiceId = Random.nextInt(),
        )
        val notification = mock<Notification>()

        doReturn(notification).`when`(service).updateNotificationGroup()

        service.downloadJobs["1"] = downloadState

        service.setForegroundNotification()

        verify(service).startForeground(NOTIFICATION_DOWNLOAD_GROUP_ID, notification)
    }

    @Test
    fun `getForegroundId in devices that support notification group will return NOTIFICATION_DOWNLOAD_GROUP_ID`() {
        val download = DownloadState(id = "1", url = "https://example.com/file.txt", fileName = "file.txt")

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        doNothing().`when`(service).performDownload(any(), anyBoolean())

        service.onStartCommand(downloadIntent, 0, 0)

        assertEquals(NOTIFICATION_DOWNLOAD_GROUP_ID, service.getForegroundId())
    }

    @Test
    fun `removeDownloadJob will update the background notification if there are other pending downloads`() {
        val download = DownloadState(
            id = "1",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = DOWNLOADING,
        )
        val downloadState = DownloadJobState(
            state = download,
            status = DOWNLOADING,
            foregroundServiceId = Random.nextInt(),
        )

        service.downloadJobs["1"] = downloadState
        service.downloadJobs["2"] = mock()

        doNothing().`when`(service).updateForegroundNotificationIfNeeded()

        service.removeDownloadJob(downloadJobState = downloadState)

        assertEquals(1, service.downloadJobs.size)
        verify(service).updateForegroundNotificationIfNeeded()
        verify(service).removeNotification(testContext, downloadState)
    }

    @Test
    fun `WHEN all downloads are completed stopForeground must be called`() {
        val download1 = DownloadState(
            id = "1",
            url = "https://example.com/file1.txt",
            fileName = "file1.txt",
            status = COMPLETED,
        )
        val download2 = DownloadState(
            id = "2",
            url = "https://example.com/file2.txt",
            fileName = "file2.txt",
            status = COMPLETED,
        )
        val downloadState1 = DownloadJobState(
            state = download1,
            status = COMPLETED,
            foregroundServiceId = Random.nextInt(),
        )

        val downloadState2 = DownloadJobState(
            state = download2,
            status = COMPLETED,
            foregroundServiceId = Random.nextInt(),
        )

        service.downloadJobs["1"] = downloadState1
        service.downloadJobs["2"] = downloadState2

        service.updateForegroundNotificationIfNeeded()

        verify(service).stopForegroundCompat(false)
    }

    @Test
    fun `Until all downloads are NOT completed stopForeground must NOT be called`() {
        val download1 = DownloadState(
            id = "1",
            url = "https://example.com/file1.txt",
            fileName = "file1.txt",
            status = COMPLETED,
        )
        val download2 = DownloadState(
            id = "2",
            url = "https://example.com/file2.txt",
            fileName = "file2.txt",
            status = DOWNLOADING,
        )
        val downloadState1 = DownloadJobState(
            state = download1,
            status = COMPLETED,
            foregroundServiceId = Random.nextInt(),
        )

        val downloadState2 = DownloadJobState(
            state = download2,
            status = DOWNLOADING,
            foregroundServiceId = Random.nextInt(),
        )

        service.downloadJobs["1"] = downloadState1
        service.downloadJobs["2"] = downloadState2

        service.updateForegroundNotificationIfNeeded()

        verify(service, never()).stopForeground(Service.STOP_FOREGROUND_DETACH)
    }

    @Test
    fun `removeDownloadJob will stop the service if there are none pending downloads`() {
        val download = DownloadState(
            id = "1",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = DOWNLOADING,
        )
        val downloadState = DownloadJobState(
            state = download,
            status = DOWNLOADING,
            foregroundServiceId = Random.nextInt(),
        )

        doNothing().`when`(service).stopForeground(Service.STOP_FOREGROUND_DETACH)
        doNothing().`when`(service).clearAllDownloadsNotificationsAndJobs()
        doNothing().`when`(service).stopSelf()

        service.downloadJobs["1"] = downloadState

        service.removeDownloadJob(downloadJobState = downloadState)

        assertTrue(service.downloadJobs.isEmpty())
        verify(service).stopSelf()
        verify(service, times(0)).updateForegroundNotificationIfNeeded()
    }

    @Test
    fun `updateForegroundNotification will update the notification group for devices that support it`() {
        doReturn(null).`when`(service).updateNotificationGroup()

        service.updateForegroundNotificationIfNeeded()

        verify(service).updateNotificationGroup()
    }

    @Test
    fun `notification is shown when download status is PAUSED`() = runBlocking {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()
        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        service.setDownloadJobStatus(downloadJobState, DownloadState.Status.PAUSED)
        assertEquals(DownloadState.Status.PAUSED, service.getDownloadJobStatus(downloadJobState))

        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()

        // one of the notifications it is the group notification only for devices the support it
        assertEquals(2, shadowNotificationService.size())
    }

    @Test
    fun `notification is shown when download status is COMPLETED`() = runBlocking {
        performSuccessfulCompleteDownload()

        assertEquals(2, shadowNotificationService.size())
    }

    @Test
    fun `completed download notification avoids notification trampoline restrictions by using an activity based PendingIntent to open the file`() = runBlocking {
        val downloadJobState = performSuccessfulCompleteDownload()

        val notification = shadowNotificationService.getNotification(downloadJobState.foregroundServiceId)
        val shadowNotificationContentPendingIntent = shadowOf(notification.contentIntent)
        assertTrue(shadowNotificationContentPendingIntent.isActivity)
    }

    private suspend fun performSuccessfulCompleteDownload(): DownloadJobState {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()
        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        service.setDownloadJobStatus(downloadJobState, COMPLETED)
        assertEquals(COMPLETED, service.getDownloadJobStatus(downloadJobState))

        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()
        return downloadJobState
    }

    @Test
    fun `notification is shown when download status is FAILED`() = runBlocking {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()
        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        service.setDownloadJobStatus(downloadJobState, FAILED)
        assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))

        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()

        // one of the notifications it is the group notification only for devices the support it
        assertEquals(2, shadowNotificationService.size())
    }

    @Test
    fun `notification is not shown when download status is CANCELLED`() = runBlocking {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        service.downloadJobs[providedDownload.value.state.id]?.job?.join()
        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        service.setDownloadJobStatus(downloadJobState, DownloadState.Status.CANCELLED)
        assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(downloadJobState))

        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()

        // The additional notification is the summary one (the notification group).
        assertEquals(1, shadowNotificationService.size())
    }

    @Test
    fun `job status is set to failed when an Exception is thrown while performDownload`() = runTest(testsDispatcher) {
        doThrow(IOException()).`when`(client).fetch(any())
        val download = DownloadState("https://example.com/file.txt", "file.txt")

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!
        assertEquals(FAILED, service.getDownloadJobStatus(downloadJobState))
    }

    @Test
    fun `WHEN a download is from a private session the request must be private`() = runTest(testsDispatcher) {
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(any())
        val download = DownloadState("https://example.com/file.txt", "file.txt", private = true)
        val downloadJob = DownloadJobState(state = download, status = DOWNLOADING)
        val providedRequest = argumentCaptor<Request>()

        service.performDownload(downloadJob)
        verify(client).fetch(providedRequest.capture())
        assertTrue(providedRequest.value.private)

        downloadJob.state = download.copy(private = false)
        service.performDownload(downloadJob)

        verify(client, times(2)).fetch(providedRequest.capture())

        assertFalse(providedRequest.value.private)
    }

    @Test
    fun `performDownload - use the download response when available`() {
        val responseFromDownloadState = mock<Response>()
        val responseFromClient = mock<Response>()
        val download = DownloadState("https://example.com/file.txt", "file.txt", response = responseFromDownloadState, contentLength = 1000)
        val downloadJob = DownloadJobState(state = download, status = DOWNLOADING)

        doReturn(404).`when`(responseFromDownloadState).status
        doReturn(responseFromClient).`when`(client).fetch(any())

        service.performDownload(downloadJob)

        verify(responseFromDownloadState, atLeastOnce()).status
        verifyNoInteractions(client)
    }

    @Test
    fun `performDownload - use the client response when the download response NOT available`() {
        val responseFromClient = mock<Response>()
        val download = spy(DownloadState("https://example.com/file.txt", "file.txt", response = null, contentLength = 1000))
        val downloadJob = DownloadJobState(state = download, status = DOWNLOADING)

        doReturn(404).`when`(responseFromClient).status
        doReturn(responseFromClient).`when`(client).fetch(any())

        service.performDownload(downloadJob)

        verify(responseFromClient, atLeastOnce()).status
    }

    @Test
    fun `performDownload - use the client response when resuming a download`() {
        val responseFromDownloadState = mock<Response>()
        val responseFromClient = mock<Response>()
        val download = spy(DownloadState("https://example.com/file.txt", "file.txt", response = responseFromDownloadState, contentLength = 1000))
        val downloadJob = DownloadJobState(currentBytesCopied = 100, state = download, status = DOWNLOADING)

        doReturn(404).`when`(responseFromClient).status
        doReturn(responseFromClient).`when`(client).fetch(any())

        service.performDownload(downloadJob)

        verify(responseFromClient, atLeastOnce()).status
        verifyNoInteractions(responseFromDownloadState)
    }

    @Test
    fun `performDownload - don't make a client request when download is completed`() {
        val responseFromDownloadState = mock<Response>()
        val download = spy(DownloadState("https://example.com/file.txt", "file.txt", response = responseFromDownloadState, contentLength = 1000))
        val downloadJob = DownloadJobState(currentBytesCopied = 1000, state = download, status = DOWNLOADING)

        service.performDownload(downloadJob)

        verify(service).verifyDownload(downloadJob)
    }

    @Test
    @Config(sdk = [28])
    fun `onDestroy cancels all running jobs when using legacy file stream`() = runBlocking {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            // Simulate a long running reading operation by sleeping for 5 seconds.
            Response.Body(
                object : InputStream() {
                    override fun read(): Int {
                        Thread.sleep(5000)
                        return 0
                    }
                },
            ),
        )
        // Call the real method to force the reading of the response's body.
        doCallRealMethod().`when`(service).useFileStream(any(), anyBoolean(), any())
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.registerNotificationActionsReceiver()
        service.onStartCommand(downloadIntent, 0, 0)

        service.downloadJobs.values.forEach { assertTrue(it.job!!.isActive) }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        // Advance the clock so that the puller posts a notification.
        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()
        // One of the notifications it is the group notification only for devices the support it
        assertEquals(2, shadowNotificationService.size())

        // Now destroy
        service.onDestroy()

        // Assert that jobs were cancelled rather than completed.
        service.downloadJobs.values.forEach {
            assertTrue(it.job!!.isCancelled)
            assertFalse(it.job!!.isCompleted)
        }

        // Assert that all currently shown notifications are gone.
        assertEquals(0, shadowNotificationService.size())
    }

    @Test
    fun `updateDownloadState must update the download state in the store and in the downloadJobs`() {
        val download = DownloadState(
            "https://example.com/file.txt",
            "file1.txt",
            status = DOWNLOADING,
        )
        val downloadJob = DownloadJobState(state = mock(), status = DOWNLOADING)
        val mockStore = mock<BrowserStore>()
        val service = createService(mockStore)

        service.downloadJobs[download.id] = downloadJob

        service.updateDownloadState(download)

        assertEquals(download, service.downloadJobs[download.id]!!.state)
        verify(mockStore).dispatch(DownloadAction.UpdateDownloadAction(download))
    }

    @Test
    fun `onTaskRemoved cancels all notifications on the shadow notification manager`() = runBlocking {
        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )
        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))

        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.registerNotificationActionsReceiver()
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()
        verify(service).performDownload(providedDownload.capture(), anyBoolean())

        service.setDownloadJobStatus(service.downloadJobs[download.id]!!, DownloadState.Status.PAUSED)

        // Advance the clock so that the poller posts a notification.
        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()
        assertEquals(2, shadowNotificationService.size())

        // Now simulate onTaskRemoved.
        service.onTaskRemoved(null)

        verify(service).stopSelf()
    }

    @Test
    fun `clearAllDownloadsNotificationsAndJobs cancels all running jobs and remove all notifications`() = runTest(testsDispatcher) {
        val download = DownloadState(
            id = "1",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = DOWNLOADING,
        )
        val downloadState = DownloadJobState(
            state = download,
            foregroundServiceId = Random.nextInt(),
            status = DOWNLOADING,
            job = backgroundScope.launch {
                @Suppress("ControlFlowWithEmptyBody")
                while (true) { }
            },
        )

        service.registerNotificationActionsReceiver()
        service.downloadJobs[download.id] = downloadState

        val notificationStyle = AbstractFetchDownloadService.Style()
        val notification = DownloadNotification.createOngoingDownloadNotification(
            context = testContext,
            downloadState = downloadState.state,
            fileSizeFormatter = fakeFileSizeFormatter,
            notificationAccentColor = notificationStyle.notificationAccentColor,
            downloadEstimator = fakeDownloadEstimator,
        )

        NotificationManagerCompat.from(testContext).notify(downloadState.foregroundServiceId, notification)

        // We have a pending notification
        assertEquals(1, shadowNotificationService.size())

        service.clearAllDownloadsNotificationsAndJobs()

        // Assert that all currently shown notifications are gone.
        assertEquals(0, shadowNotificationService.size())

        // Assert that jobs were cancelled rather than completed.
        service.downloadJobs.values.forEach {
            assertTrue(it.job!!.isCancelled)
            assertFalse(it.job!!.isCompleted)
        }
    }

    @Test
    fun `WHEN clearAllDownloadsNotificationsAndJobs is called THEN all non-completed downloads are cancelled`() = runTest(testsDispatcher) {
        val inProgressDownload = DownloadState(
            id = "1",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = DOWNLOADING,
        )
        val inProgressDownloadState = DownloadJobState(
            state = inProgressDownload,
            foregroundServiceId = Random.nextInt(),
            status = DOWNLOADING,
            job = backgroundScope.launch {
                @Suppress("ControlFlowWithEmptyBody")
                while (true) { }
            },
        )

        val pausedDownload = DownloadState(
            id = "2",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = PAUSED,
        )
        val pausedDownloadState = DownloadJobState(
            state = pausedDownload,
            foregroundServiceId = Random.nextInt(),
            status = PAUSED,
            job = backgroundScope.launch {
                @Suppress("ControlFlowWithEmptyBody")
                while (true) { }
            },
        )
        val initiatedDownload = DownloadState(
            id = "3",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = INITIATED,
        )
        val initiatedDownloadState = DownloadJobState(
            state = initiatedDownload,
            foregroundServiceId = Random.nextInt(),
            status = INITIATED,
            job = backgroundScope.launch {
                @Suppress("ControlFlowWithEmptyBody")
                while (true) { }
            },
        )
        val failedDownload = DownloadState(
            id = "4",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = FAILED,
        )
        val failedDownloadState = DownloadJobState(
            state = failedDownload,
            foregroundServiceId = Random.nextInt(),
            status = FAILED,
            job = backgroundScope.launch {
                @Suppress("ControlFlowWithEmptyBody")
                while (true) { }
            },
        )

        service.downloadJobs[inProgressDownload.id] = inProgressDownloadState
        service.downloadJobs[pausedDownload.id] = pausedDownloadState
        service.downloadJobs[initiatedDownload.id] = initiatedDownloadState
        service.downloadJobs[failedDownload.id] = failedDownloadState

        service.clearAllDownloadsNotificationsAndJobs()

        // Assert that jobs were cancelled rather than completed.
        service.downloadJobs.values.forEach {
            assertTrue(it.job!!.isCancelled)
            assertFalse(it.job!!.isCompleted)
            assertTrue(it.state.status == CANCELLED)
            assertTrue(it.status == CANCELLED)
            verify(service).updateDownloadState(it.state.copy(status = CANCELLED))
        }
    }

    @Test
    fun `WHEN clearAllDownloadsNotificationsAndJobs is called THEN all completed and cancelled downloads are unaffected`() = runTest(testsDispatcher) {
        val completedDownload = DownloadState(
            id = "1",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = COMPLETED,
        )
        val completedDownloadState = DownloadJobState(
            state = completedDownload,
            foregroundServiceId = Random.nextInt(),
            status = COMPLETED,
            job = backgroundScope.launch {
                @Suppress("ControlFlowWithEmptyBody")
                while (true) { }
            },
        )

        val cancelledDownload = DownloadState(
            id = "2",
            url = "https://example.com/file.txt",
            fileName = "file.txt",
            status = CANCELLED,
        )
        val cancelledDownloadState = DownloadJobState(
            state = cancelledDownload,
            foregroundServiceId = Random.nextInt(),
            status = CANCELLED,
            job = backgroundScope.launch {
                @Suppress("ControlFlowWithEmptyBody")
                while (true) { }
            },
        )

        service.downloadJobs[completedDownload.id] = completedDownloadState
        service.downloadJobs[cancelledDownload.id] = cancelledDownloadState

        val expected = mapOf(
            Pair(completedDownload.id, completedDownloadState.copy()),
            Pair(cancelledDownload.id, cancelledDownloadState.copy()),
        )

        service.clearAllDownloadsNotificationsAndJobs()

        assertEquals(expected, service.downloadJobs)
    }

    @Test
    fun `onDestroy will remove all download notifications, jobs and will call unregisterNotificationActionsReceiver`() = runTest(testsDispatcher) {
        service.registerNotificationActionsReceiver()

        service.onDestroy()

        verify(service).clearAllDownloadsNotificationsAndJobs()
        verify(service).unregisterNotificationActionsReceiver()
    }

    @Test
    fun `onTimeout will call service stopSelf`() {
        val service = spy(AbstractFetchDownloadService::class.java)
        val startId = 1
        val fgsType = 0

        service.onTimeout(startId, fgsType)

        verify(service).stopSelf()
    }

    @Test
    fun `register and unregister notification actions receiver`() {
        service.onCreate()

        verify(service).registerNotificationActionsReceiver()

        service.onDestroy()

        verify(service).unregisterNotificationActionsReceiver()
    }

    @Test
    @Config(sdk = [28])
    fun `WHEN a download is completed and the scoped storage is not used it MUST be added manually to the download system database`() = runTest(testsDispatcher) {
        val download = DownloadState(
            url = "http://www.mozilla.org",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
            status = DownloadState.Status.COMPLETED,
        )

        val downloadJobState = DownloadJobState(state = download, status = DownloadState.Status.COMPLETED)

        service.updateDownloadNotification(DownloadState.Status.COMPLETED, downloadJobState, this)
        testsDispatcher.scheduler.advanceUntilIdle()

        verify(service).addCompletedDownload(
            title = any(),
            description = any(),
            isMediaScannerScannable = eq(true),
            mimeType = any(),
            path = any(),
            length = anyLong(),
            showNotification = anyBoolean(),
            download = any(),
        )
    }

    @Test
    fun `WHEN a download is completed and the scoped storage is NOT not used it MUST NOT be added manually to the download system database`() = runTest(testsDispatcher) {
        val download = DownloadState(
            url = "http://www.mozilla.org",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
            status = DownloadState.Status.COMPLETED,
        )

        val downloadJobState = DownloadJobState(state = download, status = DownloadState.Status.COMPLETED)

        service.updateDownloadNotification(DownloadState.Status.COMPLETED, downloadJobState, this)

        verify(service, never()).addCompletedDownload(
            title = any(),
            description = any(),
            isMediaScannerScannable = anyBoolean(),
            mimeType = any(),
            path = any(),
            length = anyLong(),
            showNotification = anyBoolean(),
            download = any(),
        )
    }

    @Test
    fun `WHEN a download is completed and the scoped storage is used addToDownloadSystemDatabaseCompat MUST NOT be called`() = runTest(testsDispatcher) {
        val download = DownloadState(
            url = "http://www.mozilla.org",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
            status = DownloadState.Status.COMPLETED,
        )

        val downloadJobState = DownloadJobState(state = download, status = DownloadState.Status.COMPLETED)

        doNothing().`when`(service).addCompletedDownload(
            title = any(),
            description = any(),
            isMediaScannerScannable = eq(true),
            mimeType = any(),
            path = any(),
            length = anyLong(),
            showNotification = anyBoolean(),
            download = any(),
        )
        doReturn(true).`when`(service).shouldUseScopedStorage()

        service.updateDownloadNotification(DownloadState.Status.COMPLETED, downloadJobState, this)

        verify(service, never()).addCompletedDownload(
            title = any(),
            description = any(),
            isMediaScannerScannable = eq(true),
            mimeType = any(),
            path = any(),
            length = anyLong(),
            showNotification = anyBoolean(),
            download = any(),
        )
    }

    @Test
    fun `WHEN we download on devices with version higher than Q THEN we use scoped storage`() {
        val service = createService(browserStore)
        val append = true
        val uniqueFile: DownloadState = mock()
        val qSdkVersion = 29
        doReturn(uniqueFile).`when`(service).makeUniqueFileNameIfNecessary(any(), anyBoolean())
        doNothing().`when`(service).updateDownloadState(uniqueFile)
        doNothing().`when`(service).useFileStreamScopedStorage(eq(uniqueFile), eq(append), any())
        doReturn(qSdkVersion).`when`(service).getSdkVersion()

        service.useFileStream(mock(), append) {}

        verify(service).useFileStreamScopedStorage(eq(uniqueFile), eq(append), any())
    }

    @Test
    fun `WHEN we download on devices with version lower than Q THEN we use legacy file stream`() {
        val service = createService(browserStore)
        val uniqueFile: DownloadState = mock()
        val qSdkVersion = 27
        doReturn(uniqueFile).`when`(service).makeUniqueFileNameIfNecessary(any(), anyBoolean())
        doNothing().`when`(service).updateDownloadState(uniqueFile)
        doNothing().`when`(service).useFileStreamLegacy(eq(uniqueFile), anyBoolean(), any())
        doReturn(qSdkVersion).`when`(service).getSdkVersion()

        service.useFileStream(mock(), true) {}

        verify(service).useFileStreamLegacy(eq(uniqueFile), anyBoolean(), any())
    }

    @Test
    @Suppress("Deprecation")
    @Config(sdk = [28])
    fun `WHEN scoped storage is used do not pass non-http(s) url to addCompletedDownload`() = runTest(testsDispatcher) {
        val download = DownloadState(
            url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
        )

        val spyContext = spy(testContext)
        val downloadManager: DownloadManager = mock()

        doReturn(spyContext).`when`(service).context
        doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()

        service.addToDownloadSystemDatabaseCompat(download, this)
        testsDispatcher.scheduler.advanceUntilIdle()

        verify(downloadManager).addCompletedDownload(anyString(), anyString(), anyBoolean(), anyString(), anyString(), anyLong(), anyBoolean(), isNull(), any())
    }

    @Test
    @Suppress("Deprecation")
    fun `GIVEN a download that throws an exception WHEN adding to the system database THEN handle the exception`() =
        runTest(testsDispatcher) {
            val download = DownloadState(
                url = "url",
                fileName = "example.apk",
                destinationDirectory = folder.root.path,
            )

            val spyContext = spy(testContext)
            val downloadManager: DownloadManager = mock()

            doReturn(spyContext).`when`(service).context
            doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()

            doAnswer { throw IllegalArgumentException() }.`when`(downloadManager)
                .addCompletedDownload(
                    anyString(),
                    anyString(),
                    anyBoolean(),
                    anyString(),
                    anyString(),
                    anyLong(),
                    anyBoolean(),
                    isNull(),
                    any(),
                )

            try {
                service.addToDownloadSystemDatabaseCompat(download, this)
            } catch (e: IOException) {
                fail()
            }
        }

    @Test
    @Suppress("Deprecation")
    @Config(sdk = [28])
    fun `WHEN scoped storage is used pass http(s) url to addCompletedDownload`() = runTest(testsDispatcher) {
        val download = DownloadState(
            url = "https://mozilla.com",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
        )

        val spyContext = spy(testContext)
        val downloadManager: DownloadManager = mock()

        doReturn(spyContext).`when`(service).context
        doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()

        service.addToDownloadSystemDatabaseCompat(download, this)
        testsDispatcher.scheduler.advanceUntilIdle()

        verify(downloadManager).addCompletedDownload(
            eq("example.apk"),
            eq("example.apk"),
            eq(true),
            eq("*/*"),
            anyString(),
            eq(0L),
            eq(false),
            eq("https://mozilla.com".toUri()),
            eq(null),
        )
    }

    @Test
    @Suppress("Deprecation")
    @Config(sdk = [28])
    fun `WHEN scoped storage is used ALWAYS call addCompletedDownload with a not empty or null mimeType`() = runTest(testsDispatcher) {
        val spyContext = spy(testContext)
        var downloadManager: DownloadManager = mock()
        doReturn(spyContext).`when`(service).context
        doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
        val downloadWithNullMimeType = DownloadState(
            url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
            contentType = null,
        )
        val downloadWithEmptyMimeType = downloadWithNullMimeType.copy(contentType = "")
        val defaultMimeType = "*/*"

        service.addToDownloadSystemDatabaseCompat(downloadWithNullMimeType, this)
        testsDispatcher.scheduler.advanceUntilIdle()

        verify(downloadManager).addCompletedDownload(
            anyString(),
            anyString(),
            anyBoolean(),
            eq(defaultMimeType),
            anyString(),
            anyLong(),
            anyBoolean(),
            isNull(),
            any(),
        )

        downloadManager = mock()
        doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
        service.addToDownloadSystemDatabaseCompat(downloadWithEmptyMimeType, this)
        testsDispatcher.scheduler.advanceUntilIdle()

        verify(downloadManager).addCompletedDownload(
            anyString(),
            anyString(),
            anyBoolean(),
            eq(defaultMimeType),
            anyString(),
            anyLong(),
            anyBoolean(),
            isNull(),
            any(),
        )
    }

    @Test
    @Suppress("Deprecation")
    fun `WHEN scoped storage is NOT used NEVER call addCompletedDownload with a not empty or null mimeType`() = runTest(testsDispatcher) {
        val spyContext = spy(testContext)
        var downloadManager: DownloadManager = mock()
        doReturn(spyContext).`when`(service).context
        doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
        val downloadWithNullMimeType = DownloadState(
            url = "blob:moz-extension://d5ea9baa-64c9-4c3d-bb38-49308c47997c/",
            fileName = "example.apk",
            destinationDirectory = folder.root.path,
            contentType = null,
        )
        val downloadWithEmptyMimeType = downloadWithNullMimeType.copy(contentType = "")
        val defaultMimeType = "*/*"

        service.addToDownloadSystemDatabaseCompat(downloadWithNullMimeType, this)
        verify(downloadManager, never()).addCompletedDownload(
            anyString(),
            anyString(),
            anyBoolean(),
            eq(defaultMimeType),
            anyString(),
            anyLong(),
            anyBoolean(),
            isNull(),
            any(),
        )

        downloadManager = mock()
        doReturn(downloadManager).`when`(spyContext).getSystemService<DownloadManager>()
        service.addToDownloadSystemDatabaseCompat(downloadWithEmptyMimeType, this)
        verify(downloadManager, never()).addCompletedDownload(
            anyString(),
            anyString(),
            anyBoolean(),
            eq(defaultMimeType),
            anyString(),
            anyLong(),
            anyBoolean(),
            isNull(),
            any(),
        )
    }

    @Test
    fun `cancelled download does not prevent other notifications`() = runBlocking {
        val cancelledDownload = DownloadState("https://example.com/file.txt", "file.txt")
        val response = Response(
            "https://example.com/file.txt",
            200,
            MutableHeaders(),
            Response.Body(mock()),
        )

        doReturn(response).`when`(client).fetch(Request("https://example.com/file.txt"))
        val cancelledDownloadIntent = Intent("ACTION_DOWNLOAD")
        cancelledDownloadIntent.putExtra(EXTRA_DOWNLOAD_ID, cancelledDownload.id)

        browserStore.dispatch(DownloadAction.AddDownloadAction(cancelledDownload))
        service.onStartCommand(cancelledDownloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }

        val providedDownload = argumentCaptor<DownloadJobState>()

        verify(service).performDownload(providedDownload.capture(), anyBoolean())
        service.downloadJobs[providedDownload.value.state.id]?.job?.join()

        val cancelledDownloadJobState = service.downloadJobs[providedDownload.value.state.id]!!

        service.setDownloadJobStatus(cancelledDownloadJobState, DownloadState.Status.CANCELLED)
        assertEquals(DownloadState.Status.CANCELLED, service.getDownloadJobStatus(cancelledDownloadJobState))
        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()
        // The additional notification is the summary one (the notification group).
        assertEquals(1, shadowNotificationService.size())

        val download = DownloadState("https://example.com/file.txt", "file.txt")
        val downloadIntent = Intent("ACTION_DOWNLOAD")
        downloadIntent.putExtra(EXTRA_DOWNLOAD_ID, download.id)

        // Start another download to ensure its notifications are presented
        browserStore.dispatch(DownloadAction.AddDownloadAction(download))
        service.onStartCommand(downloadIntent, 0, 0)
        service.downloadJobs.values.forEach { it.job?.join() }
        verify(service, times(2)).performDownload(providedDownload.capture(), anyBoolean())
        service.downloadJobs[providedDownload.value.state.id]?.job?.join()

        val downloadJobState = service.downloadJobs[providedDownload.value.state.id]!!

        service.setDownloadJobStatus(downloadJobState, DownloadState.Status.COMPLETED)
        assertEquals(DownloadState.Status.COMPLETED, service.getDownloadJobStatus(downloadJobState))
        mainDispatcher.scheduler.advanceTimeBy(delayTime)
        mainDispatcher.scheduler.runCurrent()
        // one of the notifications it is the group notification only for devices the support it
        assertEquals(2, shadowNotificationService.size())
    }

    @Test
    fun `createDirectoryIfNeeded - MUST create directory when it does not exists`() = runTest(testsDispatcher) {
        val download = DownloadState(destinationDirectory = Environment.DIRECTORY_DOWNLOADS, url = "")

        val file = File(download.directoryPath)
        file.delete()

        assertFalse(file.exists())

        service.createDirectoryIfNeeded(download)

        assertTrue(file.exists())
    }

    @Test
    fun `keeps track of how many seconds have passed since the last update to a notification`() = runBlocking {
        val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
        val oneSecond = 1000L

        downloadJobState.lastNotificationUpdate = System.currentTimeMillis()

        delay(oneSecond)

        var seconds = downloadJobState.getSecondsSinceTheLastNotificationUpdate()

        assertEquals(1, seconds)

        delay(oneSecond)

        seconds = downloadJobState.getSecondsSinceTheLastNotificationUpdate()

        assertEquals(2, seconds)
    }

    @Test
    fun `is a notification under the time limit for updates`() = runBlocking {
        val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
        val oneSecond = 1000L

        downloadJobState.lastNotificationUpdate = System.currentTimeMillis()

        assertFalse(downloadJobState.isUnderNotificationUpdateLimit())

        delay(oneSecond)

        assertTrue(downloadJobState.isUnderNotificationUpdateLimit())
    }

    @Test
    fun `try to update a notification`() = runBlocking {
        val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
        val oneSecond = 1000L

        downloadJobState.lastNotificationUpdate = System.currentTimeMillis()

        // It's over the notification limit
        assertFalse(downloadJobState.canUpdateNotification())

        delay(oneSecond)

        // It's under the notification limit
        assertTrue(downloadJobState.canUpdateNotification())

        downloadJobState.notifiedStopped = true

        assertFalse(downloadJobState.canUpdateNotification())

        downloadJobState.notifiedStopped = false

        assertTrue(downloadJobState.canUpdateNotification())
    }

    @Test
    fun `copyInChunks must alter download currentBytesCopied`() = runTest(testsDispatcher) {
        val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
        val inputStream = mock<InputStream>()

        assertEquals(0, downloadJobState.currentBytesCopied)

        doReturn(15, -1).`when`(inputStream).read(any())
        doNothing().`when`(service).updateDownloadState(any())

        service.copyInChunks(downloadJobState, inputStream, mock())

        assertEquals(15, downloadJobState.currentBytesCopied)
    }

    @Test
    fun `copyInChunks - must return ERROR_IN_STREAM_CLOSED when inStream is closed`() = runTest(testsDispatcher) {
        val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
        val inputStream = mock<InputStream>()

        assertEquals(0, downloadJobState.currentBytesCopied)

        doAnswer { throw IOException() }.`when`(inputStream).read(any())
        doNothing().`when`(service).updateDownloadState(any())
        doNothing().`when`(service).performDownload(any(), anyBoolean())

        val status = service.copyInChunks(downloadJobState, inputStream, mock())

        verify(service).performDownload(downloadJobState, true)
        assertEquals(ERROR_IN_STREAM_CLOSED, status)
    }

    @Test
    fun `copyInChunks - must throw when inStream is closed and download was performed using http client`() = runTest(testsDispatcher) {
        val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
        val inputStream = mock<InputStream>()
        var exceptionWasThrown = false

        assertEquals(0, downloadJobState.currentBytesCopied)

        doAnswer { throw IOException() }.`when`(inputStream).read(any())
        doNothing().`when`(service).updateDownloadState(any())
        doNothing().`when`(service).performDownload(any(), anyBoolean())

        try {
            service.copyInChunks(downloadJobState, inputStream, mock(), true)
        } catch (e: IOException) {
            exceptionWasThrown = true
        }

        verify(service, times(0)).performDownload(downloadJobState, true)
        assertTrue(exceptionWasThrown)
    }

    @Test
    fun `copyInChunks - must return COMPLETED when finish copying bytes`() = runTest(testsDispatcher) {
        val downloadJobState = DownloadJobState(state = mock(), status = DOWNLOADING)
        val inputStream = mock<InputStream>()

        assertEquals(0, downloadJobState.currentBytesCopied)

        doReturn(15, -1).`when`(inputStream).read(any())
        doNothing().`when`(service).updateDownloadState(any())

        val status = service.copyInChunks(downloadJobState, inputStream, mock())

        verify(service, never()).performDownload(any(), anyBoolean())

        assertEquals(15, downloadJobState.currentBytesCopied)
        assertEquals(AbstractFetchDownloadService.CopyInChuckStatus.COMPLETED, status)
    }

    @Test
    fun `getSafeContentType - WHEN the file content type is available THEN use it`() {
        val contentTypeFromFile = "application/pdf; qs=0.001"
        val spyContext = spy(testContext)
        val contentResolver = mock<ContentResolver>()

        doReturn(contentTypeFromFile).`when`(contentResolver).getType(any())
        doReturn(contentResolver).`when`(spyContext).contentResolver

        val result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), "any")

        assertEquals("application/pdf", result)
    }

    @Test
    fun `getSafeContentType - WHEN the file content type is not available THEN use the provided content type`() {
        val contentType = " application/pdf "
        val spyContext = spy(testContext)
        val contentResolver = mock<ContentResolver>()
        doReturn(contentResolver).`when`(spyContext).contentResolver

        doReturn(null).`when`(contentResolver).getType(any())
        var result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), contentType)
        assertEquals("application/pdf", result)

        doReturn("").`when`(contentResolver).getType(any())
        result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), contentType)
        assertEquals("application/pdf", result)
    }

    @Test
    fun `getSafeContentType - WHEN none of the provided content types are available THEN return a generic content type`() {
        val spyContext = spy(testContext)
        val contentResolver = mock<ContentResolver>()
        doReturn(contentResolver).`when`(spyContext).contentResolver

        doReturn(null).`when`(contentResolver).getType(any())
        var result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), null)
        assertEquals("*/*", result)

        doReturn("").`when`(contentResolver).getType(any())
        result = AbstractFetchDownloadService.getSafeContentType(spyContext, mock<Uri>(), null)
        assertEquals("*/*", result)
    }

    // Following 3 tests use the String version of #getSafeContentType while the above 3 tested the Uri version
    // The String version just overloads and delegates the Uri one but being in a companion object we cannot
    // verify the delegation so we are left to verify the result to prevent any regressions.
    @Test
    fun `getSafeContentType2 - WHEN the file content type is available THEN use it`() {
        val contentTypeFromFile = "application/pdf; qs=0.001"
        val spyContext = spy(testContext)
        val contentResolver = mock<ContentResolver>()

        doReturn(contentTypeFromFile).`when`(contentResolver).getType(any())
        doReturn(contentResolver).`when`(spyContext).contentResolver

        val result = AbstractFetchDownloadService.getSafeContentType(
            spyContext,
            fakePackageNameProvider.packageName,
            "any",
            "any",
        )

        assertEquals("application/pdf", result)
    }

    @Test
    fun `getSafeContentType2 - WHEN the file content type is not available THEN use the provided content type`() {
        val contentType = " application/pdf "
        val spyContext = spy(testContext)
        val contentResolver = mock<ContentResolver>()
        doReturn(contentResolver).`when`(spyContext).contentResolver

        doReturn(null).`when`(contentResolver).getType(any())
        var result = AbstractFetchDownloadService.getSafeContentType(
            spyContext,
            fakePackageNameProvider.packageName,
            "any",
            contentType,
        )
        assertEquals("application/pdf", result)

        doReturn("").`when`(contentResolver).getType(any())
        result = AbstractFetchDownloadService.getSafeContentType(
            spyContext,
            fakePackageNameProvider.packageName,
            "any",
            contentType,
        )
        assertEquals("application/pdf", result)
    }

    @Test
    fun `getSafeContentType2 - WHEN none of the provided content types are available THEN return a generic content type`() {
        val spyContext = spy(testContext)
        val contentResolver = mock<ContentResolver>()
        doReturn(contentResolver).`when`(spyContext).contentResolver

        doReturn(null).`when`(contentResolver).getType(any())
        var result = AbstractFetchDownloadService.getSafeContentType(
            spyContext,
            fakePackageNameProvider.packageName,
            "any",
            null,
        )
        assertEquals("*/*", result)

        doReturn("").`when`(contentResolver).getType(any())
        result = AbstractFetchDownloadService.getSafeContentType(
            spyContext,
            fakePackageNameProvider.packageName,
            "any",
            null,
        )
        assertEquals("*/*", result)
    }

    // Hard to test #getFilePathUri since it only returns the result of a certain Android api call.
    // But let's try.
    @Test
    @Config(shadows = [DefaultFileProvider::class]) // use default implementation just for this test
    fun `getFilePathUri - WHEN called without a registered provider THEN exception is thrown`() {
        // There is no app registered provider that could expose a file from the filesystem of the machine running this test.
        // Peeking into the exception would indicate whether the code really called "FileProvider.getUriForFile" as expected.
        var exception: IllegalArgumentException? = null
        try {
            AbstractFetchDownloadService.getFilePathUri(
                testContext,
                fakePackageNameProvider.packageName,
                "test.txt",
            )
        } catch (e: IllegalArgumentException) {
            exception = e
        }

        assertTrue(exception!!.stackTrace[0].fileName.contains("FileProvider"))
        assertTrue(exception.stackTrace[0].methodName == "getUriForFile")
    }

    @Test
    fun `getFilePathUri - WHEN called THEN return a file provider path for the filePath`() {
        // Test that the String filePath is passed to the provider from which we expect a Uri path
        val result = AbstractFetchDownloadService.getFilePathUri(
            testContext,
            fakePackageNameProvider.packageName,
            "location/test.txt",
        )

        assertTrue(result.toString().endsWith("location/test.txt"))
    }

    @Test
    fun `WHEN cancelDownloadJob is called THEN deleteDownloadingFile must be called`() =
        runTest(testsDispatcher) {
            val downloadState = DownloadState(url = "mozilla.org/mozilla.txt")
            val downloadJobState =
                DownloadJobState(job = Job(), state = downloadState, status = DOWNLOADING)

            doNothing().`when`(service)
                .deleteDownloadingFile(downloadState.copy(status = CANCELLED))

            service.downloadJobs[downloadState.id] = downloadJobState

            service.cancelDownloadJob(
                currentDownloadJobState = downloadJobState,
                coroutineScope = CoroutineScope(coroutinesTestRule.testDispatcher),
            )

            verify(service).deleteDownloadingFile(downloadState.copy(status = CANCELLED))
            assertTrue(downloadJobState.downloadDeleted)
        }

    @Test
    fun `WHEN makeUniqueFileNameIfNecessary is called THEN file name should be unique`() =
        runTest(testsDispatcher) {
            val downloadState = DownloadState("https://example.com/file.txt", "file.txt")
            val previousDownloadState = DownloadState("https://example.com/file.txt", "file.txt")

            service.downloadJobs[previousDownloadState.id] = DownloadJobState(job = Job(), state = previousDownloadState, status = DOWNLOADING)

            val transformedDownload = service.makeUniqueFileNameIfNecessary(downloadState, false)

            assertEquals("file(1).txt", transformedDownload.fileName)
        }
}

@Implements(FileProvider::class)
object ShadowFileProvider {
    @Implementation
    @JvmStatic
    @Suppress("UNUSED_PARAMETER")
    fun getUriForFile(
        context: Context?,
        authority: String?,
        file: File,
    ) = "content://authority/random/location/${file.name}".toUri()
}

@Implements(FileProvider::class)
object DefaultFileProvider
