package ch.threema.app.apptaskexecutor

import ch.threema.app.apptaskexecutor.tasks.AppTask
import ch.threema.app.apptaskexecutor.tasks.PersistableAppTask
import ch.threema.testhelpers.assertSuspendsForever
import ch.threema.testhelpers.testDispatcherProvider
import ch.threema.testhelpers.willThrow
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk
import kotlin.test.Test
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest

@OptIn(ExperimentalCoroutinesApi::class)
class AppTaskExecutorTest {

    @Test
    fun `app task is being run when scheduled early`() = runTest {
        val (appTask, deferred) = getAwaitableAppTask()

        val appTaskExecutor = AppTaskExecutor(
            dispatcherProvider = testDispatcherProvider(),
            appTaskPersistenceProvider = mockk {
                coEvery { getAppTaskPersistence() } coAnswers { awaitCancellation() }
            },
        )

        appTaskExecutor.scheduleTask(appTask)

        backgroundScope.launch {
            appTaskExecutor.run()
        }

        // Assert that the deferred is completed
        deferred.await()
    }

    @Test
    fun `app task is being run when scheduled late`() = runTest {
        val (appTask, deferred) = getAwaitableAppTask()

        val appTaskExecutor = AppTaskExecutor(
            dispatcherProvider = testDispatcherProvider(),
            appTaskPersistenceProvider = mockk {
                coEvery { getAppTaskPersistence() } coAnswers { awaitCancellation() }
            },
        )

        backgroundScope.launch {
            appTaskExecutor.run()
        }

        appTaskExecutor.scheduleTask(appTask)

        // Assert that the deferred is completed
        deferred.await()
    }

    @Test
    fun `several app tasks are being run`() = runTest {
        val (appTasks, deferreds) = List(1000) { getAwaitableAppTask { delay(1000.milliseconds) } }.unzip()

        val appTaskExecutor = AppTaskExecutor(
            dispatcherProvider = testDispatcherProvider(),
            appTaskPersistenceProvider = mockk {
                coEvery { getAppTaskPersistence() } coAnswers { awaitCancellation() }
            },
        )

        backgroundScope.launch {
            appTaskExecutor.run()
        }

        appTasks.map(appTaskExecutor::scheduleTask).forEach { it.await() }

        // Assert that all deferreds are awaited
        deferreds.awaitAll()
    }

    @Test
    fun `tasks can be run in parallel`() = runTest {
        val (appTaskA, deferredA) = getAwaitableAppTask {
            delay(2000)
        }
        // App task B cannot finish before A
        val (appTaskB, deferredB) = getAwaitableAppTask {
            deferredA.await()
        }

        val appTaskExecutor = AppTaskExecutor(
            dispatcherProvider = testDispatcherProvider(),
            appTaskPersistenceProvider = mockk {
                coEvery { getAppTaskPersistence() } coAnswers { awaitCancellation() }
            },
        )

        backgroundScope.launch {
            appTaskExecutor.run()
        }

        // First schedule app task B and then A
        appTaskExecutor.scheduleTask(appTaskB)
        appTaskExecutor.scheduleTask(appTaskA)

        // Assert that app task B finishes which implies that app task A has finished before B despite being scheduled later
        deferredB.await()
    }

    @Test
    fun `persisted tasks are run when app task persistence is ready`() = runTest {
        val persistedTask = mockk<PersistableAppTask>()
        val persistedTaskCompleted = CompletableDeferred<Unit>()
        coEvery { persistedTask.run() } answers {
            persistedTaskCompleted.complete(Unit)
        }
        val deferred = CompletableDeferred<Unit>()
        val mockedPersistence = mockk<AppTaskPersistence>()
        coEvery { mockedPersistence.loadAllPersistedTasks() } returns setOf(persistedTask)
        coEvery { mockedPersistence.removePersistedTask(persistedTask) } just Runs

        val appTaskExecutor = AppTaskExecutor(
            dispatcherProvider = testDispatcherProvider(),
            appTaskPersistenceProvider = mockk {
                coEvery { getAppTaskPersistence() } coAnswers {
                    deferred.await()
                    mockedPersistence
                }
            },
        )

        backgroundScope.launch {
            appTaskExecutor.run()
        }

        advanceUntilIdle()

        // Assert that persisted task has not yet been executed (as AppTaskPersistence isn't available yet)
        coVerify(exactly = 0) { mockedPersistence.loadAllPersistedTasks() }

        // Provide AppTaskPersistence and assert that task have been loaded and run
        deferred.complete(Unit)
        persistedTaskCompleted.await()
        coVerify(exactly = 1) { mockedPersistence.loadAllPersistedTasks() }
    }

    @Test
    fun `persistable tasks are not scheduled when no AppTaskPersistence is available`() = runTest {
        val appTaskExecutor = AppTaskExecutor(
            dispatcherProvider = testDispatcherProvider(),
            appTaskPersistenceProvider = mockk {
                coEvery { getAppTaskPersistence() } coAnswers { awaitCancellation() }
            },
        )

        backgroundScope.launch {
            appTaskExecutor.run()
        }

        assertSuspendsForever {
            appTaskExecutor.persistAndScheduleTask(mockk())
        }
    }

    @Test
    fun `failing task should not stop task executor`() = runTest {
        val appTaskExecutor = AppTaskExecutor(
            dispatcherProvider = testDispatcherProvider(),
            appTaskPersistenceProvider = mockk {
                coEvery { getAppTaskPersistence() } coAnswers { awaitCancellation() }
            },
        )

        backgroundScope.launch {
            appTaskExecutor.run()
        }

        // Assert that an exception throwing task will result in an exceptionally completed deferred
        val failingAppTask = getAwaitableAppTask { throw IllegalStateException("This task throws an exception") }.first
        val scheduleAndAwaitAppTask = suspend { appTaskExecutor.scheduleTask(failingAppTask).await() }
        scheduleAndAwaitAppTask willThrow IllegalStateException::class

        // Assert that the task executor is still able to run other tasks
        val succeedingAppTask = getAwaitableAppTask().first
        appTaskExecutor.scheduleTask(succeedingAppTask).await()
    }

    /**
     * Note that these awaitable app tasks complete a completable deferred before they finish. This allows us checking that the tasks were really run.
     */
    private fun getAwaitableAppTask(body: suspend () -> Unit = { }): Pair<AppTask, Deferred<Unit>> {
        val completableDeferred = CompletableDeferred<Unit>()
        val appTask = object : AppTask {
            override suspend fun run() {
                body()
                completableDeferred.complete(Unit)
            }
        }
        return appTask to completableDeferred
    }
}
