package org.mozilla.geckoview.test

import android.os.Parcel
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import org.hamcrest.Matchers.endsWith
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.not
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.GeckoSession.PermissionDelegate
import org.mozilla.geckoview.WebNotification
import org.mozilla.geckoview.WebNotificationDelegate
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule

const val VERY_LONG_IMAGE_URL = "https://example.com/this/is/a/very/long/address/that/is/meant/to/be/longer/than/is/one/hundred/and/fifth/characters/long/for/testing/imageurl/length.ico"

@RunWith(AndroidJUnit4::class)
@MediumTest
class WebNotificationTest : BaseSessionTest() {

    @Before fun setup() {
        mainSession.loadTestPath(HELLO_HTML_PATH)
        mainSession.waitForPageStop()

        sessionRule.setPrefsUntilTestEnd(mapOf("dom.webnotifications.requireuserinteraction" to false))

        // Grant "desktop notification" permission
        mainSession.delegateUntilTestEnd(object : PermissionDelegate {
            override fun onContentPermissionRequest(session: GeckoSession, perm: PermissionDelegate.ContentPermission): GeckoResult<Int>? {
                assertThat("Should grant DESKTOP_NOTIFICATIONS permission", perm.permission, equalTo(PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION))
                return GeckoResult.fromValue(PermissionDelegate.ContentPermission.VALUE_ALLOW)
            }
        })

        val result = mainSession.waitForJS("Notification.requestPermission()")
        assertThat(
            "Permission should be granted",
            result as String,
            equalTo("granted"),
        )
    }

    @Test fun onSilentNotification() {
        sessionRule.setPrefsUntilTestEnd(mapOf("dom.webnotifications.silent.enabled" to true))
        val notificationResult = GeckoResult<Void>()

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                assertThat("Title should match", notification.title, equalTo("The Title"))
                assertThat("Silent should match", notification.silent, equalTo(true))
                assertThat("Vibrate should match", notification.vibrate, equalTo(intArrayOf()))
                assertThat("Source should match", notification.source, equalTo(createTestUrl(HELLO_HTML_PATH)))
                assertThat("Origin should match", notification.origin, equalTo(GeckoSessionTestRule.TEST_ENDPOINT))
                notification.show()
                notificationResult.complete(null)
            }
        })

        mainSession.evaluateJS(
            """
            new Notification('The Title', { body: 'The Text', silent: true });
            """.trimIndent(),
        )

        sessionRule.waitForResult(notificationResult)
    }

    fun assertNotificationData(notification: WebNotification, requireInteraction: Boolean) {
        assertThat("Title should match", notification.title, equalTo("The Title"))
        assertThat("Body should match", notification.text, equalTo("The Text"))
        assertThat("ImageUrl should match", notification.imageUrl, endsWith("icon.png"))
        assertThat("Language should match", notification.lang, equalTo("en-US"))
        assertThat("Direction should match", notification.textDirection, equalTo("ltr"))
        assertThat(
            "Require Interaction should match",
            notification.requireInteraction,
            equalTo(requireInteraction),
        )
        assertThat("Vibrate should match", notification.vibrate, equalTo(intArrayOf(1, 2, 3, 4)))
        assertThat("Silent should match", notification.silent, equalTo(false))
        assertThat("Source should match", notification.source, equalTo(createTestUrl(HELLO_HTML_PATH)))

        val origin = if (notification.privateBrowsing) {
            GeckoSessionTestRule.TEST_ENDPOINT + "^privateBrowsingId=1"
        } else {
            GeckoSessionTestRule.TEST_ENDPOINT
        }
        assertThat("Origin should match", notification.origin, equalTo(origin))
    }

    @GeckoSessionTestRule.Setting.List(
        GeckoSessionTestRule.Setting(
            key = GeckoSessionTestRule.Setting.Key.USE_PRIVATE_MODE,
            value = "true",
        ),
    )
    @Test
    fun onShowNotification() {
        sessionRule.setPrefsUntilTestEnd(mapOf("dom.webnotifications.vibrate.enabled" to true))
        val notificationResult = GeckoResult<Void>()
        val requireInteraction =
            sessionRule.getPrefs("dom.webnotifications.requireinteraction.enabled")[0] as Boolean

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                assertNotificationData(notification, requireInteraction)
                assertThat("privateBrowsing should match", notification.privateBrowsing, equalTo(true))
                notification.show()
                notificationResult.complete(null)
            }
        })

        mainSession.evaluateJS(
            """
            new Notification('The Title', { body: 'The Text', cookie: 'Cookie',
                icon: 'icon.png', tag: 'Tag', dir: 'ltr', lang: 'en-US',
                requireInteraction: true, vibrate: [1,2,3,4] });
            """.trimIndent(),
        )

        sessionRule.waitForResult(notificationResult)
    }

    @Test
    fun onShowNotificationSameTag() {
        val tags = mutableListOf<String>()
        val notificationResult = GeckoResult<Void>()

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                tags += notification.tag
                if (tags.size == 2) {
                    notificationResult.complete(null)
                }
            }
        })

        mainSession.evaluateJS(
            """
            new Notification('The Title', { tag: 'Tag' });
            new Notification('The Title', { tag: 'Tag' });
            """.trimIndent(),
        )

        sessionRule.waitForResult(notificationResult)

        assertThat("tags should be same", tags[0], equalTo(tags[1]))
    }

    @Test
    fun onShowNotificationDifferentTag() {
        val tags = mutableListOf<String>()
        val notificationResult = GeckoResult<Void>()

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                tags += notification.tag
                if (tags.size == 2) {
                    notificationResult.complete(null)
                }
            }
        })

        mainSession.evaluateJS(
            """
            new Notification('The Title', { tag: 'Tag1' });
            new Notification('The Title', { tag: 'Tag2' });
            """.trimIndent(),
        )

        sessionRule.waitForResult(notificationResult)

        assertThat("tags should be different", tags[0], not(equalTo(tags[1])))
    }

    @Test fun onCloseNotification() {
        val closeCalled = GeckoResult<Void>()

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onCloseNotification(notification: WebNotification) {
                closeCalled.complete(null)
            }
        })

        mainSession.evaluateJS(
            """
            const notification = new Notification('The Title', { body: 'The Text'});
            notification.close();
            """.trimIndent(),
        )

        sessionRule.waitForResult(closeCalled)
    }

    @Test fun clickNotificationParceled() {
        sessionRule.setPrefsUntilTestEnd(mapOf("dom.webnotifications.vibrate.enabled" to true))
        val notificationResult = GeckoResult<WebNotification>()
        val requireInteraction =
            sessionRule.getPrefs("dom.webnotifications.requireinteraction.enabled")[0] as Boolean

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                notification.show()
                notificationResult.complete(notification)
            }
        })

        val promiseResult = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                const notification = new Notification('The Title', {
                   body: 'The Text',
                   cookie: 'Cookie',
                   icon: 'icon.png',
                   tag: 'Tag',
                   dir: 'ltr',
                   lang: 'en-US',
                   requireInteraction: true,
                   vibrate: [1,2,3,4]
                });
                notification.onclick = function() {
                    resolve(1);
                }
            });
            """.trimIndent(),
        )

        val notification = sessionRule.waitForResult(notificationResult)
        assertNotificationData(notification, requireInteraction)
        assertThat("privateBrowsing should match", notification.privateBrowsing, equalTo(false))

        // Test that we can click from a deserialized notification
        val parcel = Parcel.obtain()
        notification.writeToParcel(parcel, 0)
        parcel.setDataPosition(0)

        val deserialized = WebNotification.CREATOR.createFromParcel(parcel)
        assertNotificationData(deserialized, requireInteraction)
        assertThat("privateBrowsing should match", deserialized.privateBrowsing, equalTo(false))
        assertThat("tag should match", deserialized.tag, equalTo(notification.tag))

        deserialized!!.click()
        assertThat("Promise should have been resolved.", promiseResult.value as Double, equalTo(1.0))
    }

    @GeckoSessionTestRule.Setting.List(
        GeckoSessionTestRule.Setting(
            key = GeckoSessionTestRule.Setting.Key.USE_PRIVATE_MODE,
            value = "true",
        ),
    )
    @Test
    fun clickPrivateNotificationParceled() {
        sessionRule.setPrefsUntilTestEnd(mapOf("dom.webnotifications.vibrate.enabled" to true))
        val notificationResult = GeckoResult<WebNotification>()
        val requireInteraction =
            sessionRule.getPrefs("dom.webnotifications.requireinteraction.enabled")[0] as Boolean

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                notification.show()
                notificationResult.complete(notification)
            }
        })

        val promiseResult = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                const notification = new Notification('The Title', {
                   body: 'The Text',
                   cookie: 'Cookie',
                   icon: 'icon.png',
                   tag: 'Tag',
                   dir: 'ltr',
                   lang: 'en-US',
                   requireInteraction: true,
                   vibrate: [1,2,3,4]
                });
                notification.onclick = function() {
                    resolve(1);
                }
            });
            """.trimIndent(),
        )

        val notification = sessionRule.waitForResult(notificationResult)
        assertNotificationData(notification, requireInteraction)
        assertThat("privateBrowsing should match", notification.privateBrowsing, equalTo(true))

        // Test that we can click from a deserialized notification
        val parcel = Parcel.obtain()
        notification.writeToParcel(parcel, 0)
        parcel.setDataPosition(0)

        val deserialized = WebNotification.CREATOR.createFromParcel(parcel)
        assertNotificationData(deserialized, requireInteraction)
        assertThat("privateBrowsing should match", deserialized.privateBrowsing, equalTo(true))

        deserialized!!.click()
        assertThat("Promise should have been resolved.", promiseResult.value as Double, equalTo(1.0))
    }

    @Test fun clickNotification() {
        val notificationResult = GeckoResult<Void>()
        var notificationShown: WebNotification? = null

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                notificationShown = notification
                notification.show()
                notificationResult.complete(null)
            }
        })

        val promiseResult = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                const notification = new Notification('The Title', { body: 'The Text' });
                notification.onclick = function() {
                    resolve(1);
                }
            });
            """.trimIndent(),
        )

        sessionRule.waitForResult(notificationResult)
        notificationShown!!.click()

        assertThat("Promise should have been resolved.", promiseResult.value as Double, equalTo(1.0))
    }

    @Test fun dismissNotification() {
        val notificationResult = GeckoResult<Void>()
        var notificationShown: WebNotification? = null

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                notificationShown = notification
                notification.show()
                notificationResult.complete(null)
            }
        })

        val promiseResult = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                const notification = new Notification('The Title', { body: 'The Text'});
                notification.onclose = function() {
                    resolve(1);
                }
            });
            """.trimIndent(),
        )

        sessionRule.waitForResult(notificationResult)
        notificationShown!!.dismiss()

        assertThat("Promise should have been resolved", promiseResult.value as Double, equalTo(1.0))
    }

    @Test fun writeToParcel() {
        val notificationResult = GeckoResult<WebNotification>()

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                notification.show()
                notificationResult.complete(notification)
            }
        })

        val promiseResult = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                const notification = new Notification('The Title', { body: 'The Text' });
                notification.onclose = function() {
                    resolve(1);
                }
            });
            """.trimIndent(),
        )

        val notification = sessionRule.waitForResult(notificationResult)
        notification.dismiss()

        // Ensure we always have a non-null URL from js.
        assertNotNull(notification.imageUrl)

        // Test that we can serialize a notification
        val parcel = Parcel.obtain()
        notification.writeToParcel(
            parcel,
            // ignored
            -1,
        )

        assertThat("Promise should have been resolved.", promiseResult.value as Double, equalTo(1.0))
    }

    @Test fun writeToParcelLongImageUrl() {
        val notificationResult = GeckoResult<WebNotification>()

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                notification.show()
                notificationResult.complete(notification)
            }
        })

        val promiseResult = mainSession.evaluatePromiseJS(
            """
            new Promise(resolve => {
                const notification = new Notification('The Title',
                    {
                        body: 'The Text',
                        icon: '$VERY_LONG_IMAGE_URL'
                    });
                notification.onclose = function() {
                    resolve(1);
                }
            });
            """.trimIndent(),
        )

        val notification = sessionRule.waitForResult(notificationResult)
        notification.dismiss()

        // Ensure we have an imageUrl longer than our max to start with.
        assertNotNull(notification.imageUrl)
        assertTrue(notification.imageUrl!!.length > 150)

        // Test that we can serialize a notification with an imageUrl.length >= 150
        val parcel = Parcel.obtain()
        notification.writeToParcel(
            parcel,
            // ignored
            -1,
        )
        parcel.setDataPosition(0)

        val serializedNotification = WebNotification.CREATOR.createFromParcel(parcel)
        assertTrue(serializedNotification.imageUrl!!.isBlank())

        assertThat("Promise should have been resolved.", promiseResult.value as Double, equalTo(1.0))
    }

    private fun openNotificationAction(): GeckoResult<WebNotification> {
        mainSession.loadTestPath(CLICK_ACTION_PATH)
        mainSession.waitForPageStop()

        val notificationResult = GeckoResult<WebNotification>()

        sessionRule.delegateDuringNextWait(object : WebNotificationDelegate {
            @GeckoSessionTestRule.AssertCalled
            override fun onShowNotification(notification: WebNotification) {
                notificationResult.complete(notification)
            }
        })

        return notificationResult
    }

    @Test fun openWithActionsAndClickAction() {
        assumeThat(sessionRule.env.isNightly, equalTo(true))

        val notificationResult = openNotificationAction()
        val promiseResult = mainSession.evaluatePromiseJS("showNotification()")
        sessionRule.waitForResult(notificationResult)

        notificationResult.accept { it!!.click("action1") }

        assertThat("Promise should have been resolved.", promiseResult.value as String, equalTo("action1"))
    }

    @Test fun openWithActionsAndClickTitle() {
        assumeThat(sessionRule.env.isNightly, equalTo(true))

        val notificationResult = openNotificationAction()
        val promiseResult = mainSession.evaluatePromiseJS("showNotification()")
        sessionRule.waitForResult(notificationResult)

        notificationResult.accept { it!!.click() }

        assertThat("Promise should have been resolved.", promiseResult.value as String, equalTo(""))
    }

    @Test fun openWithActionsAndClickUnknownAction() {
        assumeThat(sessionRule.env.isNightly, equalTo(true))

        val notificationResult = openNotificationAction()
        val promiseResult = mainSession.evaluatePromiseJS("showNotification()")
        sessionRule.waitForResult(notificationResult)

        // This may happen when a tagged notification is replaced with a different set of actions
        // but at the same time the user clicks the existing notification action just before the
        // system replaces it.
        notificationResult.accept { it!!.click("action-unknown") }

        assertThat("Promise should have been resolved.", promiseResult.value as String, equalTo(""))
    }
}
