package dev.privacyllc.period.core.notifications

import android.app.Notification
import android.app.NotificationManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import androidx.test.platform.app.InstrumentationRegistry
import dev.privacyllc.period.core.datastore.NotificationPrivacy
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/**
 * What a locked phone would actually show, checked on a real device.
 *
 * The unit tests prove the *copy* is right. This proves the copy reaches Android
 * intact — that the public version is attached, that the visibility flag is set,
 * and that the thing a stranger could read is the public one.
 *
 * Those are different claims, and only the second one catches the mistake that
 * matters: a notification marked private with no public version does not show a
 * blank lock screen, it shows the private text.
 */
@RunWith(AndroidJUnit4::class)
class NotificationPrivacyTest {

    /**
     * The test APK is its own package, so the app's grant does not cover it.
     * Granted here rather than with `adb shell pm grant` out of band, because a
     * test that only passes after a manual command is a test nobody can re-run.
     */
    @get:org.junit.Rule
    val permission: GrantPermissionRule =
        GrantPermissionRule.grant(android.Manifest.permission.POST_NOTIFICATIONS)

    private val context = InstrumentationRegistry.getInstrumentation().targetContext
    private val manager = context.getSystemService(NotificationManager::class.java)
    private val notifier = PeriodNotifier(context)

    @Before fun clear() = manager.cancelAll()

    @After fun tearDown() = manager.cancelAll()

    /**
     * Posting is asynchronous. Polled rather than read once — a bare read
     * failed intermittently and the failure looked like "nothing was posted",
     * which is the same symptom as a missing permission and sent me looking in
     * the wrong place.
     */
    private fun postedNotification(): Notification? {
        repeat(50) {
            manager.activeNotifications
                .firstOrNull { it.id == PeriodNotifier.NOTIFICATION_ID }
                ?.let { return it.notification }
            Thread.sleep(20)
        }
        return null
    }

    private fun Notification.textOf(): String {
        val e = extras
        return listOfNotNull(
            e.getCharSequence(Notification.EXTRA_TITLE)?.toString(),
            e.getCharSequence(Notification.EXTRA_TEXT)?.toString(),
        ).joinToString(" ").lowercase()
    }

    @Test
    fun everyPrivateModeAttachesAPublicVersionThatLeaksNothing() {
        listOf(NotificationPrivacy.DISCREET, NotificationPrivacy.MAXIMUM_PRIVACY).forEach { mode ->
            ReminderKind.entries.forEach { kind ->
                manager.cancelAll()

                val text = NotificationCopy.textFor(kind, mode, daysUntil = 2)
                val posted = notifier.notify(text, mode, contentIntent = null)
                assertTrue("nothing was posted for $kind/$mode — is the permission granted?", posted)

                val notification = postedNotification()
                assertNotNull("$kind/$mode was not delivered", notification)

                // The whole point: a private notification with no public version
                // falls back to showing its private text on the lock screen.
                val public = notification!!.publicVersion
                assertNotNull("$kind/$mode attached no public version", public)

                assertEquals(
                    "$kind/$mode was not marked private",
                    Notification.VISIBILITY_PRIVATE,
                    notification.visibility,
                )

                val visible = public!!.textOf()
                NotificationCopy.SENSITIVE_WORDS.forEach { word ->
                    assertTrue(
                        "$kind/$mode would have shown \"$word\" on a lock screen: \"$visible\"",
                        !visible.contains(word),
                    )
                }
            }
        }
    }

    @Test
    fun directModeIsTheOnlyOneMarkedPublic() {
        val text = NotificationCopy.textFor(
            ReminderKind.PERIOD_APPROACHING, NotificationPrivacy.DIRECT, 2,
        )
        notifier.notify(text, NotificationPrivacy.DIRECT, contentIntent = null)

        val notification = postedNotification()
        assertNotNull(notification)
        assertEquals(Notification.VISIBILITY_PUBLIC, notification!!.visibility)
        assertTrue(notification.textOf().contains("period"))
    }

    @Test
    fun noChannelNameSaysAnythingAboutPeriods() {
        // Visible in Settings > Apps > Notifications, on a phone somebody else
        // may be holding.
        NotificationPrivacy.entries.forEach { mode ->
            notifier.ensureChannel(mode)
            val channel = manager.getNotificationChannel(PeriodNotifier.channelId(mode))
            assertNotNull("no channel for $mode", channel)
            val name = channel!!.name.toString().lowercase()
            NotificationCopy.SENSITIVE_WORDS.forEach {
                assertTrue("the $mode channel is called \"$name\"", !name.contains(it))
            }
        }
    }

    @Test
    fun discreetModeNeverPopsOverTheScreen() {
        // A heads-up notification draws its content over whatever is showing,
        // lock or no lock. High importance and Discreet are a contradiction.
        notifier.ensureChannel(NotificationPrivacy.DISCREET)
        val channel = manager.getNotificationChannel(
            PeriodNotifier.channelId(NotificationPrivacy.DISCREET),
        )!!
        assertTrue(
            "importance was ${channel.importance}",
            channel.importance <= NotificationManager.IMPORTANCE_DEFAULT,
        )

        // And the separate channel per mode is what makes this stay true: a
        // single channel keeps its first importance forever, so switching to
        // Direct and back would leave Discreet popping over the screen.
        notifier.ensureChannel(NotificationPrivacy.DIRECT)
        assertEquals(
            NotificationManager.IMPORTANCE_HIGH,
            manager.getNotificationChannel(PeriodNotifier.channelId(NotificationPrivacy.DIRECT))!!.importance,
        )
    }
}
