package wang.yaojia.webterm.components

import android.view.KeyEvent
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 wang.yaojia.webterm.session.KeyByteMap

/**
 * A17 core-contract guard (pure JVM — no device). Two things are verified here; the
 * Compose layout, IME-inset behaviour and real key events are device-QA (plan §7):
 *
 *  1. **Key-bar byte contract** — every button funnels its key through [KeyByteMap]
 *     to a fake send sink and produces the EXACT web-parity byte string.
 *  2. **DECCKM split** — [HardwareKeyRouter.resolve] routes Esc / Ctrl-chord / ⇧Tab
 *     app-side, and LEAVES arrows / Enter / plain Tab to Termux's `KeyHandler`.
 *
 * [EXPECTED_BYTES] is transcribed INDEPENDENTLY from public/keybar.ts `KEY_MAP`
 * (ESC rebuilt from `Char(0x1B)` = web `\x1b`), so any drift in [KeyByteMap] or the
 * [KEYBAR_LAYOUT] wiring fails here — not silently on a device.
 */
class KeyBarTest {

    private val esc = Char(0x1B).toString()

    /** KeyByteMap.Key → byte string, transcribed straight from keybar.ts `KEY_MAP`. */
    private val EXPECTED_BYTES: Map<KeyByteMap.Key, String> = mapOf(
        KeyByteMap.Key.ESC to esc, //                 '\x1b'
        KeyByteMap.Key.ESC_ESC to esc + esc, //       '\x1b\x1b'
        KeyByteMap.Key.SHIFT_TAB to esc + "[Z", //    '\x1b[Z'
        KeyByteMap.Key.ARROW_UP to esc + "[A", //     '\x1b[A'
        KeyByteMap.Key.ARROW_DOWN to esc + "[B", //   '\x1b[B'
        KeyByteMap.Key.ARROW_LEFT to esc + "[D", //   '\x1b[D'
        KeyByteMap.Key.ARROW_RIGHT to esc + "[C", //  '\x1b[C'
        KeyByteMap.Key.ENTER to "\r", //              '\r' — NOT '\n'
        KeyByteMap.Key.CTRL_C to Char(0x03).toString(), // '\x03'
        KeyByteMap.Key.CTRL_R to Char(0x12).toString(), // '\x12'
        KeyByteMap.Key.CTRL_O to Char(0x0F).toString(), // '\x0f'
        KeyByteMap.Key.CTRL_L to Char(0x0C).toString(), // '\x0c'
        KeyByteMap.Key.CTRL_T to Char(0x14).toString(), // '\x14'
        KeyByteMap.Key.CTRL_B to Char(0x02).toString(), // '\x02'
        KeyByteMap.Key.CTRL_D to Char(0x04).toString(), // '\x04'
        KeyByteMap.Key.TAB to "\t", //                '\t'
        KeyByteMap.Key.SLASH to "/", //               '/'
    )

    // ── 1. Key-bar byte contract ─────────────────────────────────────────────────

    @Test
    fun `every key-bar button emits its exact web-parity bytes through the sink`() {
        for (button in KEYBAR_LAYOUT) {
            // Arrange: a fake IME-bypass sink capturing every send.
            val captured = mutableListOf<String>()

            // Act: tap the button (exactly what onClick does).
            emitKey(button.key, captured::add)

            // Assert: one send, byte-for-byte the expected sequence.
            val expected = EXPECTED_BYTES[button.key] ?: error("no expected bytes for ${button.key}")
            assertEquals(1, captured.size, "one send per tap for ${button.label}")
            assertEquals(expected, captured.single(), "byte mismatch for ${button.label}")
        }
    }

    @Test
    fun `layout order, glyphs and keys mirror the web key-bar`() {
        val expectedKeys = listOf(
            KeyByteMap.Key.ESC, KeyByteMap.Key.ESC_ESC, KeyByteMap.Key.SHIFT_TAB,
            KeyByteMap.Key.ARROW_UP, KeyByteMap.Key.ARROW_DOWN, KeyByteMap.Key.ENTER,
            KeyByteMap.Key.CTRL_C, KeyByteMap.Key.CTRL_R, KeyByteMap.Key.CTRL_O,
            KeyByteMap.Key.CTRL_L, KeyByteMap.Key.CTRL_T, KeyByteMap.Key.CTRL_B,
            KeyByteMap.Key.CTRL_D, KeyByteMap.Key.TAB, KeyByteMap.Key.ARROW_LEFT,
            KeyByteMap.Key.ARROW_RIGHT, KeyByteMap.Key.SLASH,
        )
        assertEquals(expectedKeys, KEYBAR_LAYOUT.map { it.key })
        assertEquals("Esc", KEYBAR_LAYOUT.first().label)
        assertTrue(KEYBAR_LAYOUT.first().isPrimary, "Esc is the primary (interrupt) key")
    }

    @Test
    fun `enter emits carriage-return not linefeed`() {
        val captured = mutableListOf<String>()
        emitKey(KeyByteMap.Key.ENTER, captured::add)
        assertEquals("\r", captured.single())
    }

    // ── 2. DECCKM split routing (plan §6.3 source #3) ────────────────────────────

    @Test
    fun `esc is app-handled with the ESC byte`() {
        val result = HardwareKeyRouter.resolve(KeyEvent.KEYCODE_ESCAPE, ctrl = false, shift = false, alt = false)
        assertEquals(HardwareKeyResult.AppHandled(esc), result)
    }

    @Test
    fun `arrows are left to Termux KeyHandler so DECCKM stays correct`() {
        val arrows = listOf(
            KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN,
            KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT,
        )
        for (code in arrows) {
            assertEquals(
                HardwareKeyResult.DeferToTerminal,
                HardwareKeyRouter.resolve(code, ctrl = false, shift = false, alt = false),
                "arrow keyCode=$code must defer (DECCKM emits ESC O A, never a hardcoded ESC [ A)",
            )
        }
    }

    @Test
    fun `enter and plain Tab are left to Termux KeyHandler`() {
        assertEquals(
            HardwareKeyResult.DeferToTerminal,
            HardwareKeyRouter.resolve(KeyEvent.KEYCODE_ENTER, ctrl = false, shift = false, alt = false),
        )
        assertEquals(
            HardwareKeyResult.DeferToTerminal,
            HardwareKeyRouter.resolve(KeyEvent.KEYCODE_TAB, ctrl = false, shift = false, alt = false),
        )
    }

    @Test
    fun `shift+Tab is app-handled with the reverse-tab sequence`() {
        val result = HardwareKeyRouter.resolve(KeyEvent.KEYCODE_TAB, ctrl = false, shift = true, alt = false)
        assertEquals(HardwareKeyResult.AppHandled(esc + "[Z"), result)
    }

    @Test
    fun `each mapped Ctrl-letter chord is app-handled with its control byte`() {
        val expected = mapOf(
            KeyEvent.KEYCODE_C to KeyByteMap.Key.CTRL_C,
            KeyEvent.KEYCODE_R to KeyByteMap.Key.CTRL_R,
            KeyEvent.KEYCODE_O to KeyByteMap.Key.CTRL_O,
            KeyEvent.KEYCODE_L to KeyByteMap.Key.CTRL_L,
            KeyEvent.KEYCODE_T to KeyByteMap.Key.CTRL_T,
            KeyEvent.KEYCODE_B to KeyByteMap.Key.CTRL_B,
            KeyEvent.KEYCODE_D to KeyByteMap.Key.CTRL_D,
        )
        for ((code, key) in expected) {
            assertEquals(
                HardwareKeyResult.AppHandled(KeyByteMap.bytes(key)),
                HardwareKeyRouter.resolve(code, ctrl = true, shift = false, alt = false),
                "Ctrl+ keyCode=$code should map to $key",
            )
        }
    }

    @Test
    fun `an unmapped Ctrl-letter defers to Termux`() {
        // Ctrl+A is not one of the web key-bar chords → Termux handles it.
        assertEquals(
            HardwareKeyResult.DeferToTerminal,
            HardwareKeyRouter.resolve(KeyEvent.KEYCODE_A, ctrl = true, shift = false, alt = false),
        )
    }

    @Test
    fun `a plain letter is left to Termux (typed via IME, not a chord)`() {
        assertEquals(
            HardwareKeyResult.DeferToTerminal,
            HardwareKeyRouter.resolve(KeyEvent.KEYCODE_C, ctrl = false, shift = false, alt = false),
        )
    }

    // ── Visibility heuristic (web-parity mobile-only + hardware-keyboard auto-hide) ─

    @Test
    fun `key-bar shows on a narrow phone with no hardware keyboard`() {
        assertTrue(shouldShowKeyBar(screenWidthDp = 360, hardwareKeyboardPresent = false))
        assertTrue(shouldShowKeyBar(screenWidthDp = WIDE_SCREEN_MAX_DP, hardwareKeyboardPresent = false))
    }

    @Test
    fun `key-bar hides on a wide screen or when a hardware keyboard is present`() {
        assertFalse(shouldShowKeyBar(screenWidthDp = 900, hardwareKeyboardPresent = false))
        assertFalse(shouldShowKeyBar(screenWidthDp = 360, hardwareKeyboardPresent = true))
    }
}
