package wang.yaojia.webterm.session

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import java.text.BreakIterator
import wang.yaojia.webterm.wire.Tunables

/** Ports iOS `TitleSanitizerTests` — control/bidi/zero-width stripping + grapheme cap. */
class TitleSanitizerTest {

    /** Build a String from raw code points (avoids editor-fragile literals). */
    private fun cp(vararg codePoints: Int): String =
        buildString { codePoints.forEach { appendCodePoint(it) } }

    private fun graphemeCount(s: String): Int {
        val bi = BreakIterator.getCharacterInstance()
        bi.setText(s)
        var count = 0
        while (bi.next() != BreakIterator.DONE) count++
        return count
    }

    @Test
    fun plainTitlePassesThroughUnchanged() {
        assertEquals("web-terminal", TitleSanitizer.sanitize("web-terminal"))
    }

    @Test
    fun stripsC0ControlCharacters() {
        // ESC (0x1B) + BEL (0x07) embedded in "a<ESC>b<BEL>c".
        val raw = "a" + cp(0x1B) + "b" + cp(0x07) + "c"
        assertEquals("abc", TitleSanitizer.sanitize(raw))
    }

    @Test
    fun stripsDelAndC1ControlCharacters() {
        // DEL (0x7F) and a C1 control (0x9B — CSI).
        val raw = "x" + cp(0x7F) + "y" + cp(0x9B) + "z"
        assertEquals("xyz", TitleSanitizer.sanitize(raw))
    }

    @Test
    fun stripsZeroWidthAndBidiSpoofingCharacters() {
        val raw = "a" +
            cp(0x200B) + // zero-width space
            cp(0x200D) + // zero-width joiner
            cp(0x200E) + // LRM
            cp(0x202E) + // RLO override
            cp(0x2066) + // LRI isolate
            cp(0xFEFF) + // BOM
            "b"
        assertEquals("ab", TitleSanitizer.sanitize(raw))
    }

    @Test
    fun trimsSurroundingWhitespace() {
        assertEquals("hello", TitleSanitizer.sanitize("  hello \t "))
    }

    @Test
    fun emptyAfterSanitizationYieldsEmptyString() {
        val raw = cp(0x200B) + "   " + cp(0x1B)
        assertEquals("", TitleSanitizer.sanitize(raw))
        assertEquals("", TitleSanitizer.sanitize(""))
    }

    @Test
    fun truncatesToTitleMaxLengthGraphemes() {
        val raw = "a".repeat(Tunables.TITLE_MAX_LENGTH + 50)
        val out = TitleSanitizer.sanitize(raw)
        assertEquals(Tunables.TITLE_MAX_LENGTH, out.length)
    }

    @Test
    fun emojiFloodCapsAtMaxGlyphsWithoutSplittingASurrogatePair() {
        // Each 😀 (U+1F600) is one grapheme = one surrogate pair (2 UTF-16 units).
        val emoji = cp(0x1F600)
        val raw = emoji.repeat(Tunables.TITLE_MAX_LENGTH + 40)
        val out = TitleSanitizer.sanitize(raw)

        assertEquals(Tunables.TITLE_MAX_LENGTH, graphemeCount(out), "capped at 256 visible glyphs")
        assertEquals(Tunables.TITLE_MAX_LENGTH * 2, out.length, "no half-emoji: even UTF-16 length")
        assertFalse(out.last().isHighSurrogate(), "must not end on a lone high surrogate")
    }

    @Test
    fun sanitizeIsIdempotent() {
        val raw = "  " + cp(0x202E) + "danger" + cp(0x200B) + " " + cp(0x1B) + "  "
        val once = TitleSanitizer.sanitize(raw)
        val twice = TitleSanitizer.sanitize(once)
        assertEquals(once, twice)
    }

    @Test
    fun keepsNonAsciiVisibleText() {
        // CJK + accented text must survive (only control/bidi/zero-width are stripped).
        val raw = "会话 café"
        assertEquals("会话 café", TitleSanitizer.sanitize(raw))
        assertTrue(TitleSanitizer.sanitize(raw).isNotEmpty())
    }
}
