package wang.yaojia.webterm.components

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

/**
 * A25 core-contract guard (pure JVM — no device). The Compose chip layout, the
 * float animation and real taps are device-QA (plan §7); what is load-bearing and
 * verified here:
 *
 *  1. **Palette CRUD** — [QuickReplyStore] add / edit / remove / reorder each return
 *     a NEW list (immutable), with sensible defaults, blank-input guards and a
 *     persistence round-trip through a fake `DataStore<Preferences>`.
 *  2. **Codec** — [QuickReplyCodec] round-trips (incl. quotes / backslash / newline)
 *     and is defensive against a corrupt blob at rest.
 *  3. **Tap → exact bytes** — [sendQuickReply] emits the chip text VERBATIM;
 *     [shouldShowQuickReply] floats the row only while a gate is held with chips.
 */
class QuickReplyTest {

    // ── An in-memory DataStore<Preferences> double (no Android Context needed) ─────
    private class FakePreferencesDataStore(
        initial: Preferences = emptyPreferences(),
    ) : DataStore<Preferences> {
        private val state = MutableStateFlow(initial)
        override val data: Flow<Preferences> = state
        override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences {
            val updated = transform(state.value)
            state.value = updated
            return updated
        }
    }

    private var idSeq = 0
    private fun sequentialId(): () -> String = { "id-${idSeq++}" }

    // ── 1. Defaults ───────────────────────────────────────────────────────────────

    @Test
    fun `first read yields the sensible defaults`() = runTest {
        val store = DataStoreQuickReplyStore(FakePreferencesDataStore())
        assertEquals(QuickReplyDefaults.chips, store.loadAll())
        assertTrue(QuickReplyDefaults.chips.isNotEmpty(), "defaults must not be empty")
    }

    // ── 1. CRUD returns new lists (immutable) + persistence ───────────────────────

    @Test
    fun `add appends a chip, persists, and returns a new list`() = runTest {
        val backing = FakePreferencesDataStore()
        val store = DataStoreQuickReplyStore(backing, newId = sequentialId())

        val before = store.loadAll()
        val after = store.add("run tests")

        assertEquals(before.size + 1, after.size)
        assertEquals(QuickReplyChip("id-0", "run tests"), after.last())
        // A fresh store over the SAME backing sees the persisted list (round-trip).
        assertEquals(after, DataStoreQuickReplyStore(backing).loadAll())
    }

    @Test
    fun `add stores text verbatim (surrounding whitespace preserved)`() = runTest {
        val store = DataStoreQuickReplyStore(FakePreferencesDataStore(), newId = sequentialId())
        val after = store.add("  spaced  ")
        assertEquals("  spaced  ", after.last().text)
    }

    @Test
    fun `blank add is a no-op`() = runTest {
        val store = DataStoreQuickReplyStore(FakePreferencesDataStore())
        val before = store.loadAll()
        assertEquals(before, store.add("   "))
        assertEquals(before, store.add(""))
    }

    @Test
    fun `edit replaces text in place by id, leaving others untouched`() = runTest {
        val store = DataStoreQuickReplyStore(FakePreferencesDataStore())
        val after = store.edit("qr-yes", "yes please")

        val edited = after.single { it.id == "qr-yes" }
        assertEquals("yes please", edited.text)
        // Position preserved and siblings unchanged.
        assertEquals(QuickReplyDefaults.chips.map { it.id }, after.map { it.id })
        assertEquals("continue", after.first { it.id == "qr-continue" }.text)
    }

    @Test
    fun `edit of an unknown id or blank text is a no-op`() = runTest {
        val store = DataStoreQuickReplyStore(FakePreferencesDataStore())
        val defaults = store.loadAll()
        assertEquals(defaults, store.edit("does-not-exist", "x"))
        assertEquals(defaults, store.edit("qr-yes", "  "))
    }

    @Test
    fun `remove drops the chip by id, persists, and unknown id is a no-op`() = runTest {
        val backing = FakePreferencesDataStore()
        val store = DataStoreQuickReplyStore(backing)

        val after = store.remove("qr-no")
        assertFalse(after.any { it.id == "qr-no" })
        assertEquals(QuickReplyDefaults.chips.size - 1, after.size)
        assertEquals(after, DataStoreQuickReplyStore(backing).loadAll())

        // Removing again (now unknown) changes nothing.
        assertEquals(after, store.remove("qr-no"))
    }

    @Test
    fun `deleting every chip persists an empty list and does NOT re-seed defaults`() = runTest {
        val backing = FakePreferencesDataStore()
        val store = DataStoreQuickReplyStore(backing)

        var list = store.loadAll()
        list.forEach { store.remove(it.id) }

        assertTrue(store.loadAll().isEmpty(), "delete-all must persist as empty")
        assertTrue(DataStoreQuickReplyStore(backing).loadAll().isEmpty(), "empty must not re-seed defaults")
    }

    @Test
    fun `move reorders immutably and out-of-range is a no-op`() = runTest {
        val store = DataStoreQuickReplyStore(FakePreferencesDataStore())
        val ids = store.loadAll().map { it.id }

        val moved = store.move(0, ids.lastIndex)
        val expected = ids.drop(1) + ids.first()
        assertEquals(expected, moved.map { it.id })

        // Out-of-range indices leave the list untouched.
        assertEquals(moved, store.move(-1, 0))
        assertEquals(moved, store.move(0, 99))
    }

    // ── 1. In-memory double parity ────────────────────────────────────────────────

    @Test
    fun `in-memory double supports the same CRUD and defaults`() = runTest {
        val store = InMemoryQuickReplyStore(newId = sequentialId())
        assertEquals(QuickReplyDefaults.chips, store.loadAll())

        val added = store.add("deploy")
        assertEquals("deploy", added.last().text)
        val removed = store.remove(added.last().id)
        assertFalse(removed.any { it.text == "deploy" })

        // Seeding with an explicit initial list is honoured (no default re-seed).
        val empty = InMemoryQuickReplyStore(initial = emptyList())
        assertTrue(empty.loadAll().isEmpty())
    }

    // ── 2. Codec round-trip + defensiveness ───────────────────────────────────────

    @Test
    fun `codec round-trips including quotes, backslashes and newlines`() = runTest {
        val chips = listOf(
            QuickReplyChip("a", "plain"),
            QuickReplyChip("b", "he said \"hi\""),
            QuickReplyChip("c", "path\\to\\thing"),
            QuickReplyChip("d", "line1\nline2\t/slash"),
        )
        assertEquals(chips, QuickReplyCodec.decode(QuickReplyCodec.encode(chips)))
    }

    @Test
    fun `codec is defensive against corrupt or malformed blobs`() {
        assertTrue(QuickReplyCodec.decode(null).isEmpty())
        assertTrue(QuickReplyCodec.decode("").isEmpty())
        assertTrue(QuickReplyCodec.decode("not json {").isEmpty())
        // A non-array top level is rejected wholesale.
        assertTrue(QuickReplyCodec.decode("""{"id":"a","text":"b"}""").isEmpty())
        // Chips missing a string id/text are dropped; valid ones survive.
        val mixed = QuickReplyCodec.decode(
            """[{"text":"noid"},{"id":"ok","text":"keep"},{"id":"x"},{"id":5,"text":"num"}]""",
        )
        assertEquals(listOf(QuickReplyChip("ok", "keep")), mixed)
    }

    // ── 3. Tap → exact bytes, and the float-visibility heuristic ───────────────────

    @Test
    fun `tapping a chip emits its exact text verbatim, once`() {
        val captured = mutableListOf<String>()
        sendQuickReply(QuickReplyChip("id", "git status\n"), captured::add)
        assertEquals(1, captured.size)
        assertEquals("git status\n", captured.single())
    }

    @Test
    fun `the row floats only while a gate is held with at least one chip`() {
        assertTrue(shouldShowQuickReply(gateHeld = true, hasChips = true))
        assertFalse(shouldShowQuickReply(gateHeld = false, hasChips = true))
        assertFalse(shouldShowQuickReply(gateHeld = true, hasChips = false))
    }

    // ── Pure transforms never mutate the receiver ─────────────────────────────────

    @Test
    fun `pure transforms return fresh lists and never mutate the receiver`() {
        val original = QuickReplyDefaults.chips

        assertEquals(original.size + 1, original.adding("x", "nid").size)
        assertEquals("nid", original.adding("x", "nid").last().id)
        assertEquals(original, original.adding("  ", "nid")) // blank guarded

        assertEquals("edited", original.editing("qr-yes", "edited").single { it.id == "qr-yes" }.text)
        assertEquals(original.size - 1, original.removingId("qr-yes").size)
        // move(first → end): the original first chip becomes the last.
        assertEquals(original.first().id, original.moving(0, original.lastIndex).last().id)

        // The receiver is unchanged after every transform above.
        assertEquals(QuickReplyDefaults.chips, original)
        assertNull(original.firstOrNull { it.text == "edited" })
    }
}
