package dev.privacyllc.period.core.notifications

import dev.privacyllc.period.core.datastore.NotificationPrivacy
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test

/**
 * The privacy modes, checked exhaustively.
 *
 * This is the only surface in the app whose mistakes are visible to somebody who
 * is not the user, so the tests are exhaustive rather than representative: every
 * kind × every mode, not a couple of examples. It runs as a plain JUnit test —
 * no Robolectric, no emulator — which is the reason the copy is a pure function
 * in the first place.
 */
class NotificationCopyTest {

    private val privateModes = listOf(NotificationPrivacy.DISCREET, NotificationPrivacy.MAXIMUM_PRIVACY)

    private fun NotificationText.lockScreenText() = (publicTitle + " " + (publicBody ?: "")).lowercase()

    // -----------------------------------------------------------------------
    // The rule the whole feature exists for
    // -----------------------------------------------------------------------

    @Test fun `no health word ever reaches a lock screen except in Direct`() {
        ReminderKind.entries.forEach { kind ->
            privateModes.forEach { mode ->
                val text = NotificationCopy.textFor(kind, mode, daysUntil = 2).lockScreenText()
                NotificationCopy.SENSITIVE_WORDS.forEach { word ->
                    assertTrue(
                        "$kind in $mode put \"$word\" on the lock screen: \"$text\"",
                        !text.contains(word),
                    )
                }
            }
        }
    }

    @Test fun `maximum privacy says nothing but the word Reminder`() {
        // §28: no health information. Not vague health information — none. The
        // body is omitted rather than made bland, because a body is a second
        // chance to leak and this mode exists for people who cannot afford one.
        ReminderKind.entries.forEach { kind ->
            val text = NotificationCopy.textFor(kind, NotificationPrivacy.MAXIMUM_PRIVACY, 2)
            assertEquals("Reminder", text.publicTitle)
            assertNull("$kind supplied a body in maximum privacy", text.publicBody)
        }
    }

    @Test fun `Direct is the only mode that says anything on the lock screen`() {
        val direct = NotificationCopy.textFor(
            ReminderKind.PERIOD_APPROACHING, NotificationPrivacy.DIRECT, 2,
        )
        assertTrue(direct.lockScreenText().contains("period"))
        assertEquals(LockScreenVisibility.PUBLIC, direct.visibility)

        privateModes.forEach {
            assertEquals(
                LockScreenVisibility.PRIVATE,
                NotificationCopy.textFor(ReminderKind.PERIOD_APPROACHING, it, 2).visibility,
            )
        }
    }

    @Test fun `every kind and mode supplies a public version`() {
        // Android falls back to showing the PRIVATE text when a notification is
        // marked private and no public version was set. A missing public title
        // is therefore not an empty lock screen — it is a full one.
        ReminderKind.entries.forEach { kind ->
            NotificationPrivacy.entries.forEach { mode ->
                val text = NotificationCopy.textFor(kind, mode, 2)
                assertNotNull("$kind/$mode", text.publicTitle)
                assertTrue("$kind/$mode public title was blank", text.publicTitle.isNotBlank())
            }
        }
    }

    // -----------------------------------------------------------------------
    // Action labels are visible text too — §31
    // -----------------------------------------------------------------------

    @Test fun `action labels carry no health information outside Direct`() {
        // The body can be perfectly discreet while a button underneath says
        // "Started my period". §31 calls this out and it is easy to miss.
        ReminderKind.entries.forEach { kind ->
            privateModes.forEach { mode ->
                NotificationCopy.actionLabels(kind, mode).forEach { label ->
                    NotificationCopy.SENSITIVE_WORDS.forEach { word ->
                        assertTrue(
                            "$kind in $mode had action label \"$label\"",
                            !label.lowercase().contains(word),
                        )
                    }
                }
            }
        }
    }

    @Test fun `the check-in offers a yes and a not yet`() {
        val labels = NotificationCopy.actionLabels(
            ReminderKind.PERIOD_EXPECTED_TODAY, NotificationPrivacy.DISCREET,
        )
        assertEquals(2, labels.size)
        assertTrue(labels.any { it.lowercase().contains("not yet") })
    }

    @Test fun `kinds with nothing to answer have no actions`() {
        assertTrue(
            NotificationCopy.actionLabels(
                ReminderKind.OVULATION_ESTIMATED, NotificationPrivacy.DISCREET,
            ).isEmpty(),
        )
    }

    // -----------------------------------------------------------------------
    // The private side is allowed to be plain, and still has rules
    // -----------------------------------------------------------------------

    @Test fun `the in-app text is specific about days`() {
        val two = NotificationCopy.textFor(ReminderKind.PERIOD_APPROACHING, NotificationPrivacy.DISCREET, 2)
        assertTrue(two.privateBody.contains("2 days"))

        val one = NotificationCopy.textFor(ReminderKind.PERIOD_APPROACHING, NotificationPrivacy.DISCREET, 1)
        assertTrue("one day should read as tomorrow", one.privateBody.contains("tomorrow"))

        val unknown = NotificationCopy.textFor(ReminderKind.PERIOD_APPROACHING, NotificationPrivacy.DISCREET, null)
        assertTrue(unknown.privateBody.isNotBlank())
    }

    @Test fun `fertility notifications always say estimated`() {
        // §17. The word is load-bearing, and a notification is the easiest place
        // to drop it for brevity.
        listOf(ReminderKind.FERTILE_WINDOW_APPROACHING, ReminderKind.OVULATION_ESTIMATED).forEach { kind ->
            val text = NotificationCopy.textFor(kind, NotificationPrivacy.DIRECT, 3)
            assertTrue(
                "$kind said it without \"estimated\": ${text.privateTitle} / ${text.privateBody}",
                (text.privateTitle + text.privateBody).lowercase().contains("estimated"),
            )
        }
    }

    @Test fun `no copy anywhere claims certainty about fertility`() {
        ReminderKind.entries.forEach { kind ->
            NotificationPrivacy.entries.forEach { mode ->
                val t = NotificationCopy.textFor(kind, mode, 2)
                val all = (t.publicTitle + " " + (t.publicBody ?: "") + " " + t.privateTitle + " " + t.privateBody)
                    .lowercase()
                listOf("you are ovulating", "safe", "unsafe", "guaranteed").forEach { phrase ->
                    assertTrue("$kind/$mode contained \"$phrase\"", !all.contains(phrase))
                }
            }
        }
    }
}
