/*
 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

package kotlinx.coroutines.android

import android.os.*
import kotlinx.coroutines.*
import org.junit.Test
import org.junit.runner.*
import org.robolectric.*
import org.robolectric.Shadows.*
import org.robolectric.annotation.*
import org.robolectric.shadows.*
import org.robolectric.util.*
import kotlin.test.*

@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, sdk = [28])
class HandlerDispatcherTest : TestBase() {

    /**
     * Because [Dispatchers.Main] is a singleton, we cannot vary its initialization behavior. As a
     * result we only test its behavior on the newest API level and assert that it uses async
     * messages. We rely on the other tests to exercise the variance of the mechanism that the main
     * dispatcher uses to ensure it has correct behavior on all API levels.
     */
    @Test
    fun mainIsAsync() = runTest {
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)

        val mainLooper = ShadowLooper.getShadowMainLooper()
        mainLooper.pause()
        val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)

        val job = launch(Dispatchers.Main) {
            expect(2)
        }

        val message = mainMessageQueue.head
        assertTrue(message.isAsynchronous)
        job.join(mainLooper)
    }

    @Test
    fun asyncMessagesApi14() = runTest {
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 14)

        val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()

        val mainLooper = ShadowLooper.getShadowMainLooper()
        mainLooper.pause()
        val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)

        val job = launch(main) {
            expect(2)
        }

        val message = mainMessageQueue.head
        assertFalse(message.isAsynchronous)
        job.join(mainLooper)
    }

    @Test
    fun asyncMessagesApi16() = runTest {
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 16)

        val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()

        val mainLooper = ShadowLooper.getShadowMainLooper()
        mainLooper.pause()
        val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)

        val job = launch(main) {
            expect(2)
        }

        val message = mainMessageQueue.head
        assertTrue(message.isAsynchronous)
        job.join(mainLooper)
    }

    @Test
    fun asyncMessagesApi28() = runTest {
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)

        val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher()

        val mainLooper = ShadowLooper.getShadowMainLooper()
        mainLooper.pause()
        val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)

        val job = launch(main) {
            expect(2)
        }

        val message = mainMessageQueue.head
        assertTrue(message.isAsynchronous)
        job.join(mainLooper)
    }

    @Test
    fun noAsyncMessagesIfNotRequested() = runTest {
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)

        val main = Looper.getMainLooper().asHandler(async = false).asCoroutineDispatcher()

        val mainLooper = ShadowLooper.getShadowMainLooper()
        mainLooper.pause()
        val mainMessageQueue = shadowOf(Looper.getMainLooper().queue)

        val job = launch(main) {
            expect(2)
        }

        val message = mainMessageQueue.head
        assertFalse(message.isAsynchronous)
        job.join(mainLooper)
    }

    @Test
    fun testToString() {
        ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", 28)
        val main = Looper.getMainLooper().asHandler(async = true).asCoroutineDispatcher("testName")
        assertEquals("testName", main.toString())
        assertEquals("testName [immediate]", main.immediate.toString())
        assertEquals("testName [immediate]", main.immediate.immediate.toString())
    }

    private suspend fun Job.join(mainLooper: ShadowLooper) {
        expect(1)
        mainLooper.unPause()
        join()
        finish(3)
    }

    // TODO compile against API 23+ so this can be invoked without reflection.
    private val Looper.queue: MessageQueue
        get() = Looper::class.java.getDeclaredMethod("getQueue").invoke(this) as MessageQueue

    // TODO compile against API 22+ so this can be invoked without reflection.
    private val Message.isAsynchronous: Boolean
        get() = Message::class.java.getDeclaredMethod("isAsynchronous").invoke(this) as Boolean
}
