current commit
261024.3 · fix(resize): size the nested parser on a window size report
// Copyright 2026 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !js && !wasm
// +build !js,!wasm

package tcell

import (
	"bytes"
	"fmt"
	"testing"
	"time"

	"github.com/gdamore/tcell/v3/vt"
)

// TestInputNullByte tests that null byte (0x00) is correctly handled
// as Ctrl+Space per the fix in the scan() function.
func TestInputNullByte(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	// Send null byte
	ip.ScanUTF8([]byte{0x00})

	// Wait briefly for processing
	select {
	case ev := <-evch:
		if kev, ok := ev.(*EventKey); ok {
			if kev.Key() != KeyRune {
				t.Errorf("Expected KeyRune for null byte, got %v", kev.Key())
			}
			if kev.Str() != " " {
				t.Errorf("Expected space character ' ' for null byte, got %q", kev.Str())
			}
			if kev.Modifiers() != ModCtrl {
				t.Errorf("Expected ModCtrl for null byte, got %v", kev.Modifiers())
			}
		} else {
			t.Errorf("Expected EventKey, got %T", ev)
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout waiting for null byte event")
	}
}

// TestInputControlKeys tests control key handling for bytes 1-31.
// Note: NewEventKey converts control characters to KeyCtrlA-Z for 0x01-0x1A,
// and KeyRune for 0x1C-0x1F with the character and ModCtrl.
func TestInputControlKeys(t *testing.T) {
	tests := []struct {
		name  string
		input byte
		key   Key
		str   string
		mod   ModMask
	}{
		{"Ctrl+A", 0x01, KeyCtrlA, "", ModCtrl},
		{"Ctrl+B", 0x02, KeyCtrlB, "", ModCtrl},
		{"Ctrl+C", 0x03, KeyCtrlC, "", ModCtrl},
		{"Ctrl+D", 0x04, KeyCtrlD, "", ModCtrl},
		{"Ctrl+E", 0x05, KeyCtrlE, "", ModCtrl},
		{"Ctrl+F", 0x06, KeyCtrlF, "", ModCtrl},
		{"Ctrl+G", 0x07, KeyCtrlG, "", ModCtrl},
		{"Ctrl+H", 0x08, KeyBackspace, "", ModNone}, // BS is special
		{"Ctrl+I", 0x09, KeyTab, "", ModNone},       // Tab is special
		{"Ctrl+J", 0x0A, KeyCtrlJ, "", ModCtrl},
		{"Ctrl+K", 0x0B, KeyCtrlK, "", ModCtrl},
		{"Ctrl+L", 0x0C, KeyCtrlL, "", ModCtrl},
		{"Ctrl+M", 0x0D, KeyEnter, "", ModNone}, // CR is Enter
		{"Ctrl+N", 0x0E, KeyCtrlN, "", ModCtrl},
		{"Ctrl+O", 0x0F, KeyCtrlO, "", ModCtrl},
		{"Ctrl+P", 0x10, KeyCtrlP, "", ModCtrl},
		{"Ctrl+Q", 0x11, KeyCtrlQ, "", ModCtrl},
		{"Ctrl+R", 0x12, KeyCtrlR, "", ModCtrl},
		{"Ctrl+S", 0x13, KeyCtrlS, "", ModCtrl},
		{"Ctrl+T", 0x14, KeyCtrlT, "", ModCtrl},
		{"Ctrl+U", 0x15, KeyCtrlU, "", ModCtrl},
		{"Ctrl+V", 0x16, KeyCtrlV, "", ModCtrl},
		{"Ctrl+W", 0x17, KeyCtrlW, "", ModCtrl},
		{"Ctrl+X", 0x18, KeyCtrlX, "", ModCtrl},
		{"Ctrl+Y", 0x19, KeyCtrlY, "", ModCtrl},
		{"Ctrl+Z", 0x1A, KeyCtrlZ, "", ModCtrl},
		{"Ctrl+[", 0x1B, KeyEscape, "", ModNone},  // ESC is special
		{"Ctrl+\\", 0x1C, KeyRune, "\\", ModCtrl}, // becomes KeyRune with string
		{"Ctrl+]", 0x1D, KeyRune, "]", ModCtrl},   // becomes KeyRune with string
		{"Ctrl+^", 0x1E, KeyRune, "^", ModCtrl},   // becomes KeyRune with string
		{"Ctrl+_", 0x1F, KeyRune, "_", ModCtrl},   // becomes KeyRune with string
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			// Skip ESC (0x1B) as it has special timeout handling
			if tt.input == 0x1B {
				t.Skip("ESC byte triggers escape sequence state machine with timeout")
				return
			}

			ip.ScanUTF8([]byte{tt.input})

			select {
			case ev := <-evch:
				if kev, ok := ev.(*EventKey); ok {
					if kev.Key() != tt.key {
						t.Errorf("Expected key %v, got %v", tt.key, kev.Key())
					}
					if kev.Str() != tt.str {
						t.Errorf("Expected string %q, got %q", tt.str, kev.Str())
					}
					if kev.Modifiers() != tt.mod {
						t.Errorf("Expected modifiers %v, got %v", tt.mod, kev.Modifiers())
					}
				} else {
					t.Errorf("Expected EventKey, got %T", ev)
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatal("Timeout waiting for control key event")
			}
		})
	}
}

func TestInputEscapeTimeouts(t *testing.T) {
	evch := make(chan Event, 2)
	ip := newInputParser(evch)

	ip.ScanUTF8([]byte{'\x1b'})
	if got := ip.WaitDuration(); got != platformEscapeTimeout() {
		t.Fatalf("bare legacy ESC timeout = %v, want %v", got, platformEscapeTimeout())
	}
	ip.SetEscapeTimeout(125 * time.Millisecond)
	if got := ip.WaitDuration(); got != 125*time.Millisecond {
		t.Fatalf("configured bare ESC timeout = %v, want %v", got, 125*time.Millisecond)
	}
	ip.SetEscapeTimeout(0)
	if got := ip.WaitDuration(); got != platformEscapeTimeout() {
		t.Fatalf("reset bare ESC timeout = %v, want %v", got, platformEscapeTimeout())
	}

	// A byte that starts a control sequence gets the longer deadline, so it
	// can arrive well after the bare-ESC deadline without being discarded.
	ip.ScanUTF8([]byte{'['})
	if got := ip.WaitDuration(); got != escapeSequenceTimeout {
		t.Fatalf("CSI timeout = %v, want %v", got, escapeSequenceTimeout)
	}
	ip.keyTime = time.Now().Add(-platformEscapeTimeout() * 2)
	ip.Scan()
	select {
	case ev := <-evch:
		t.Fatalf("CSI timed out at the bare-ESC deadline: %v", ev)
	default:
	}
	ip.ScanUTF8([]byte{'A'})
	if ev := <-evch; ev.(*EventKey).Key() != KeyUp {
		t.Fatalf("delayed CSI key = %v, want %v", ev, KeyUp)
	}

	// A bare ESC stays ambiguous under every keyboard protocol, so it keeps the
	// short deadline even when the negotiated protocol is not the legacy one.
	ip.ScanUTF8([]byte{'\x1b'})
	ip.SetKeyboardProtocol(KittyKeyboard)
	if got := ip.WaitDuration(); got != platformEscapeTimeout() {
		t.Fatalf("bare kitty ESC timeout = %v, want %v", got, platformEscapeTimeout())
	}

	// The longer deadline still applies once an introducer has arrived,
	// regardless of protocol.
	ip.ScanUTF8([]byte{'['})
	if got := ip.WaitDuration(); got != escapeSequenceTimeout {
		t.Fatalf("kitty CSI timeout = %v, want %v", got, escapeSequenceTimeout)
	}
	ip.ScanUTF8([]byte{'A'})
	if ev := <-evch; ev.(*EventKey).Key() != KeyUp {
		t.Fatalf("kitty CSI key = %v, want %v", ev, KeyUp)
	}

	nested := newInputParser(make(chan Event, 1))
	ip.nested = nested
	ip.SetEscapeTimeout(175 * time.Millisecond)
	if got := nested.escapeTimeout; got != 175*time.Millisecond {
		t.Fatalf("nested ESC timeout = %v, want %v", got, 175*time.Millisecond)
	}
	ip.SetKeyboardProtocol(Win32Keyboard)
	if nested.legacy {
		t.Fatal("nested parser retained legacy keyboard protocol")
	}

	win := newInputParser(make(chan Event, 1))
	win.SetKeyboardProtocol(KittyKeyboard)
	win.handleWinKey([]int{0, 0, 'A', 1})
	if win.nested == nil || win.nested.legacy {
		t.Fatal("Win32 nested parser did not inherit keyboard protocol")
	}
}

func TestEscapeTimeoutForPlatform(t *testing.T) {
	tests := []struct {
		goos string
		want time.Duration
	}{
		{"darwin", defaultEscapeTimeout},
		{"linux", defaultEscapeTimeout},
		{"windows", windowsEscapeTimeout},
	}
	for _, test := range tests {
		t.Run(test.goos, func(t *testing.T) {
			if got := escapeTimeoutForPlatform(test.goos); got != test.want {
				t.Fatalf("escape timeout = %v, want %v", got, test.want)
			}
		})
	}
}

// TestInputNullVsOtherControlChars tests the boundary between
// null byte and other control characters.
func TestInputNullVsOtherControlChars(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	// Test null (0x00) - should be KeyRune with Ctrl+Space
	ip.ScanUTF8([]byte{0x00})
	select {
	case ev := <-evch:
		if kev, ok := ev.(*EventKey); ok {
			if kev.Key() != KeyRune {
				t.Errorf("Null byte: expected KeyRune, got %v", kev.Key())
			}
			if kev.Str() != " " {
				t.Errorf("Null byte: expected ' ', got %q", kev.Str())
			}
			if kev.Modifiers() != ModCtrl {
				t.Errorf("Null byte: expected ModCtrl, got %v", kev.Modifiers())
			}
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout for null byte")
	}

	// Test 0x01 (Ctrl+A) - should be KeyCtrlA
	ip.ScanUTF8([]byte{0x01})
	select {
	case ev := <-evch:
		if kev, ok := ev.(*EventKey); ok {
			if kev.Key() != KeyCtrlA {
				t.Errorf("Byte 0x01: expected KeyCtrlA, got %v", kev.Key())
			}
			if kev.Modifiers() != ModCtrl {
				t.Errorf("Byte 0x01: expected ModCtrl, got %v", kev.Modifiers())
			}
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout for 0x01")
	}
}

// TestInputPrintableCharacters tests that printable characters
// are handled correctly without control modifiers.
func TestInputPrintableCharacters(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected string
		mod      ModMask
	}{
		{"Space", " ", " ", ModNone},
		{"ExclamationMark", "!", "!", ModNone},
		{"LetterA", "A", "A", ModNone},
		{"LetterZ", "Z", "Z", ModNone},
		{"Lowera", "a", "a", ModNone},
		{"Lowerz", "z", "z", ModNone},
		{"Digit0", "0", "0", ModNone},
		{"Digit9", "9", "9", ModNone},
		{"At", "@", "@", ModNone},
		{"Tilde", "~", "~", ModNone},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			ip.ScanUTF8([]byte(tt.input))

			select {
			case ev := <-evch:
				if kev, ok := ev.(*EventKey); ok {
					if kev.Key() != KeyRune {
						t.Errorf("Expected KeyRune, got %v", kev.Key())
					}
					if kev.Str() != tt.expected {
						t.Errorf("Expected %q, got %q", tt.expected, kev.Str())
					}
					if kev.Modifiers() != tt.mod {
						t.Errorf("Expected modifiers %v, got %v", tt.mod, kev.Modifiers())
					}
				} else {
					t.Errorf("Expected EventKey, got %T", ev)
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatal("Timeout waiting for character event")
			}
		})
	}
}

// TestInputSpecialKeys tests special key handling (tab, backspace, enter).
func TestInputSpecialKeys(t *testing.T) {
	tests := []struct {
		name     string
		input    byte
		expected Key
		mod      ModMask
	}{
		{"Tab", '\t', KeyTab, ModNone},
		{"Backspace_BS", '\b', KeyBackspace, ModNone},
		{"Backspace_DEL", 0x7F, KeyBackspace, ModNone},
		{"Enter_LF", '\n', KeyCtrlJ, ModCtrl},
		{"Enter_CR", '\r', KeyEnter, ModNone},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			ip.ScanUTF8([]byte{tt.input})

			select {
			case ev := <-evch:
				if kev, ok := ev.(*EventKey); ok {
					if kev.Key() != tt.expected {
						t.Errorf("Expected key %v, got %v", tt.expected, kev.Key())
					}
					if kev.Modifiers() != tt.mod {
						t.Errorf("Expected modifiers %v, got %v", tt.mod, kev.Modifiers())
					}
				} else {
					t.Errorf("Expected EventKey, got %T", ev)
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatal("Timeout waiting for special key event")
			}
		})
	}
}

// TestInputSequentialInput tests handling multiple inputs in sequence.
func TestInputSequentialInput(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	// Send: null, Ctrl+A, 'B', space
	inputs := []byte{0x00, 0x01, 'B', ' '}
	expected := []struct {
		key Key
		str string
		mod ModMask
	}{
		{KeyRune, " ", ModCtrl}, // null -> Ctrl+Space
		{KeyCtrlA, "", ModCtrl}, // 0x01 -> KeyCtrlA with ModCtrl
		{KeyRune, "B", ModNone}, // 'B'
		{KeyRune, " ", ModNone}, // space
	}

	for i, b := range inputs {
		ip.ScanUTF8([]byte{b})

		select {
		case ev := <-evch:
			if kev, ok := ev.(*EventKey); ok {
				if kev.Key() != expected[i].key {
					t.Errorf("Input %d: expected key %v, got %v", i, expected[i].key, kev.Key())
				}
				if kev.Str() != expected[i].str {
					t.Errorf("Input %d: expected %q, got %q", i, expected[i].str, kev.Str())
				}
				if kev.Modifiers() != expected[i].mod {
					t.Errorf("Input %d: expected modifiers %v, got %v", i, expected[i].mod, kev.Modifiers())
				}
			} else {
				t.Errorf("Input %d: expected EventKey, got %T", i, ev)
			}
		case <-time.After(100 * time.Millisecond):
			t.Fatalf("Timeout waiting for event %d", i)
		}
	}
}

// TestInputUTF8Characters tests UTF-8 multibyte character handling.
func TestInputUTF8Characters(t *testing.T) {
	tests := []struct {
		name     string
		input    []byte
		expected string
	}{
		{"Euro", []byte("€"), "€"},
		{"CJK", []byte("中"), "中"},
		{"Emoji", []byte("😀"), "😀"},
		{"Cyrillic", []byte("Ж"), "Ж"},
		{"Arabic", []byte("ع"), "ع"},
		{"SMP", []byte("🝁"), "🝁"}, // needs full 4 character UTF-8
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			ip.ScanUTF8(tt.input)
			time.Sleep(time.Millisecond * 100)
			ip.Scan()

			select {
			case ev := <-evch:
				if kev, ok := ev.(*EventKey); ok {
					if kev.Str() != tt.expected {
						t.Errorf("Expected %q, got %q", tt.expected, kev.Str())
					}
					if kev.Modifiers() != ModNone {
						t.Errorf("Expected ModNone, got %v", kev.Modifiers())
					}
				} else {
					t.Errorf("Expected EventKey, got %T", ev)
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatal("Timeout waiting for UTF-8 character event")
			}
		})
	}
}

// TestInputEdgeCases tests edge cases and boundary conditions.
func TestInputEdgeCases(t *testing.T) {
	t.Run("EmptyInput", func(t *testing.T) {
		evch := make(chan Event, 10)
		ip := newInputParser(evch)

		ip.ScanUTF8([]byte{})
		time.Sleep(time.Millisecond * 100)
		ip.Scan()

		select {
		case ev := <-evch:
			t.Errorf("Expected no event for empty input, got %T", ev)
		case <-time.After(50 * time.Millisecond):
			// Success - no event generated
		}
	})

	t.Run("MultipleNullBytes", func(t *testing.T) {
		evch := make(chan Event, 10)
		ip := newInputParser(evch)

		ip.ScanUTF8([]byte{0x00, 0x00, 0x00})

		for i := range 3 {
			select {
			case ev := <-evch:
				if kev, ok := ev.(*EventKey); ok {
					if kev.Key() != KeyRune || kev.Str() != " " || kev.Modifiers() != ModCtrl {
						t.Errorf("Null byte %d: expected KeyRune with Ctrl+Space, got key=%v str=%q mod=%v",
							i, kev.Key(), kev.Str(), kev.Modifiers())
					}
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatalf("Timeout waiting for null byte %d", i)
			}
		}
	})

	t.Run("BoundaryByte0x1F", func(t *testing.T) {
		evch := make(chan Event, 10)
		ip := newInputParser(evch)

		// 0x1F is the last control character before space (0x20)
		// NewEventKey transforms it to KeyRune with "_" and ModCtrl
		ip.ScanUTF8([]byte{0x1F})

		select {
		case ev := <-evch:
			if kev, ok := ev.(*EventKey); ok {
				if kev.Key() != KeyRune {
					t.Errorf("Expected KeyRune for 0x1F, got %v", kev.Key())
				}
				if kev.Str() != "_" {
					t.Errorf("Expected '_' for 0x1F, got %q", kev.Str())
				}
				if kev.Modifiers() != ModCtrl {
					t.Errorf("Expected ModCtrl for 0x1F, got %v", kev.Modifiers())
				}
			}
		case <-time.After(100 * time.Millisecond):
			t.Fatal("Timeout for 0x1F")
		}
	})

	t.Run("BoundaryByte0x20", func(t *testing.T) {
		evch := make(chan Event, 10)
		ip := newInputParser(evch)

		// 0x20 is space - first printable character
		ip.ScanUTF8([]byte{0x20})

		select {
		case ev := <-evch:
			if kev, ok := ev.(*EventKey); ok {
				if kev.Key() != KeyRune {
					t.Errorf("Expected KeyRune for 0x20, got %v", kev.Key())
				}
				if kev.Str() != " " {
					t.Errorf("Expected ' ' for 0x20, got %q", kev.Str())
				}
				if kev.Modifiers() != ModNone {
					t.Errorf("Expected ModNone for 0x20, got %v", kev.Modifiers())
				}
			}
		case <-time.After(100 * time.Millisecond):
			t.Fatal("Timeout for 0x20")
		}
	})
}

// TestInputConcurrentAccess tests that the parser is safe
// for concurrent access (it uses a mutex internally).
func TestInputConcurrentAccess(t *testing.T) {
	evch := make(chan Event, 100)
	ip := newInputParser(evch)

	done := make(chan bool)

	// Goroutine 1: Send null bytes
	go func() {
		for range 10 {
			ip.ScanUTF8([]byte{0x00})
			time.Sleep(time.Millisecond)
		}
		done <- true
	}()

	// Goroutine 2: Send regular characters
	go func() {
		for range 10 {
			ip.ScanUTF8([]byte{'A'})
			time.Sleep(time.Millisecond)
		}
		done <- true
	}()

	// Wait for both goroutines
	<-done
	<-done

	// Verify we got events (exact order not guaranteed)
	eventCount := 0
	timeout := time.After(200 * time.Millisecond)
	for eventCount < 20 {
		select {
		case <-evch:
			eventCount++
		case <-timeout:
			t.Fatalf("Expected 20 events, got %d", eventCount)
		}
	}
}

// TestInputStateTransitions tests that the null byte fix doesn't
// interfere with state machine transitions.
func TestInputStateTransitions(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	// Send null in initial state
	ip.ScanUTF8([]byte{0x00})

	select {
	case ev := <-evch:
		if kev, ok := ev.(*EventKey); ok {
			if kev.Key() != KeyRune || kev.Str() != " " || kev.Modifiers() != ModCtrl {
				t.Errorf("Expected KeyRune with Ctrl+Space, got key=%v str=%q mod=%v",
					kev.Key(), kev.Str(), kev.Modifiers())
			}
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout")
	}

	// Verify state machine returns to initial state by sending a normal char
	ip.ScanUTF8([]byte{'X'})

	select {
	case ev := <-evch:
		if kev, ok := ev.(*EventKey); ok {
			if kev.Key() != KeyRune || kev.Str() != "X" || kev.Modifiers() != ModNone {
				t.Errorf("Expected KeyRune 'X' with ModNone after null, got key=%v str=%q mod=%v",
					kev.Key(), kev.Str(), kev.Modifiers())
			}
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout")
	}
}

// TestSpecialKeys tests that special keys (F-keys, home, del, etc. work as expected)
func TestSpecialKeys(t *testing.T) {
	tests := []struct {
		name        string
		input       []byte
		expectedKey Key
		expectedMod ModMask
		expectedStr string
	}{
		{"Esc", []byte{'\x1b'}, KeyEscape, ModNone, ""},
		{"Esc-Esc", []byte{'\x1b', '\x1b'}, KeyEscape, ModAlt, ""},
		{"Esc-Y", []byte{'\x1b', 'Y'}, KeyRune, ModAlt, "Y"},
		{"Esc-Ctrl-B", []byte{'\x1b', '\x02'}, KeyRune, ModAlt | ModCtrl, "b"},
		{"Esc-[", []byte{'\x1b', '['}, KeyRune, ModAlt, "["},
		{"Tab", []byte{'\t'}, KeyTab, ModNone, ""},
		{"NL", []byte{'\n'}, KeyCtrlJ, ModCtrl, ""},
		{"CR", []byte{'\r'}, KeyEnter, ModNone, ""},
		{"Backspace", []byte{'\b'}, KeyBackspace, ModNone, ""},
		{"Delete", []byte{'\x7f'}, KeyBackspace, ModNone, ""},
		{"CSI-A", []byte{'\x1b', '[', 'A'}, KeyUp, ModNone, ""},
		{"CSI-B", []byte{'\x1b', '[', 'B'}, KeyDown, ModNone, ""},
		{"CSI-C", []byte{'\x1b', '[', 'C'}, KeyRight, ModNone, ""},
		{"CSI-D", []byte{'\x1b', '[', 'D'}, KeyLeft, ModNone, ""},
		{"CSI-E", []byte{'\x1b', '[', 'E'}, KeyClear, ModNone, ""},
		{"CSI-F", []byte{'\x1b', '[', 'F'}, KeyEnd, ModNone, ""},
		{"CSI-H", []byte{'\x1b', '[', 'H'}, KeyHome, ModNone, ""},
		{"CSI-L", []byte{'\x1b', '[', 'L'}, KeyInsert, ModNone, ""},
		{"CSI-P", []byte{'\x1b', '[', 'P'}, KeyF1, ModNone, ""},
		{"CSI-Q", []byte{'\x1b', '[', 'Q'}, KeyF2, ModNone, ""},
		{"CSI-R", []byte{'\x1b', '[', 'R'}, KeyF3, ModNone, ""},
		{"CSI-S", []byte{'\x1b', '[', 'S'}, KeyF4, ModNone, ""},
		{"CSI-Z", []byte{'\x1b', '[', 'Z'}, KeyBacktab, ModNone, ""},
		{"CSI-a", []byte{'\x1b', '[', 'a'}, KeyUp, ModShift, ""},
		{"CSI-b", []byte{'\x1b', '[', 'b'}, KeyDown, ModShift, ""},
		{"CSI-c", []byte{'\x1b', '[', 'c'}, KeyRight, ModShift, ""},
		{"CSI-d", []byte{'\x1b', '[', 'd'}, KeyLeft, ModShift, ""},
		{"CSI-15~", []byte{'\x1b', '[', '1', '5', '~'}, KeyF5, ModNone, ""},
		{"CSI-17~", []byte{'\x1b', '[', '1', '7', '~'}, KeyF6, ModNone, ""},
		{"CSI-18~", []byte{'\x1b', '[', '1', '8', '~'}, KeyF7, ModNone, ""},
		{"CSI-19~", []byte{'\x1b', '[', '1', '9', '~'}, KeyF8, ModNone, ""},
		{"CSI-20~", []byte{'\x1b', '[', '2', '0', '~'}, KeyF9, ModNone, ""},
		{"CSI-21~", []byte{'\x1b', '[', '2', '1', '~'}, KeyF10, ModNone, ""},
		{"CSI-23~", []byte{'\x1b', '[', '2', '3', '~'}, KeyF11, ModNone, ""},
		{"CSI-24~", []byte{'\x1b', '[', '2', '4', '~'}, KeyF12, ModNone, ""},
		{"CSI-1-$", []byte{'\x1b', '[', '1', '$'}, KeyHome, ModShift, ""}, // rxvt bs
		{"SS3-F1", []byte{'\x1b', 'O', 'P'}, KeyF1, ModNone, ""},
		{"SS3-F2", []byte{'\x1b', 'O', 'Q'}, KeyF2, ModNone, ""},
		{"SS3-F3", []byte{'\x1b', 'O', 'R'}, KeyF3, ModNone, ""},
		{"SS3-F4", []byte{'\x1b', 'O', 'S'}, KeyF4, ModNone, ""},
		{"SS3-F4-Shift", []byte{'\x1b', 'O', '1', ';', '2', 'S'}, KeyF4, ModShift, ""},
		{"SS3-F4-Ctrl", []byte{'\x1b', 'O', '1', ';', '5', 'S'}, KeyF4, ModCtrl, ""},
		{"SS3-F4-Ctrl-Short", []byte{'\x1b', 'O', '5', 'S'}, KeyF4, ModCtrl, ""},
		{"SS3-F4-Ctrl-Shift", []byte{'\x1b', 'O', ';', '6', 'S'}, KeyF4, ModCtrl | ModShift, ""},
		{"SS3-F2-Meta", []byte{'\x1b', 'O', ';', '9', 'Q'}, KeyF2, ModMeta, ""},
		{"SS3-Keypad-0", []byte{'\x1b', 'O', 'p'}, KeyInsert, ModNone, ""},
		{"SS3-Keypad-1", []byte{'\x1b', 'O', 'q'}, KeyEnd, ModNone, ""},
		{"SS3-Keypad-2", []byte{'\x1b', 'O', 'r'}, KeyDown, ModNone, ""},
		{"SS3-Keypad-3", []byte{'\x1b', 'O', 's'}, KeyPgDn, ModNone, ""},
		{"SS3-Keypad-4", []byte{'\x1b', 'O', 't'}, KeyLeft, ModNone, ""},
		{"SS3-Keypad-5", []byte{'\x1b', 'O', 'u'}, KeyClear, ModNone, ""},
		{"SS3-Keypad-6", []byte{'\x1b', 'O', 'v'}, KeyRight, ModNone, ""},
		{"SS3-Keypad-7", []byte{'\x1b', 'O', 'w'}, KeyHome, ModNone, ""},
		{"SS3-Keypad-8", []byte{'\x1b', 'O', 'x'}, KeyUp, ModNone, ""},
		{"SS3-Keypad-9", []byte{'\x1b', 'O', 'y'}, KeyPgUp, ModNone, ""},
		{"SS3-Keypad-Enter", []byte{'\x1b', 'O', 'M'}, KeyEnter, ModNone, ""},
		{"SS3-Keypad-Decimal", []byte{'\x1b', 'O', 'n'}, KeyDelete, ModNone, ""},
		{"SS3-Keypad-Multiply", []byte{'\x1b', 'O', 'j'}, KeyRune, ModNone, "*"},
		{"SS3-Keypad-Add", []byte{'\x1b', 'O', 'k'}, KeyRune, ModNone, "+"},
		{"SS3-Keypad-Comma", []byte{'\x1b', 'O', 'l'}, KeyRune, ModNone, ","},
		{"SS3-Keypad-Subtract", []byte{'\x1b', 'O', 'm'}, KeyRune, ModNone, "-"},
		{"SS3-Keypad-Divide", []byte{'\x1b', 'O', 'o'}, KeyRune, ModNone, "/"},
		{"SS3-Keypad-Equal", []byte{'\x1b', 'O', 'X'}, KeyRune, ModNone, "="},
		// Legacy key reporting normalizes Shift away for printable runes.
		{"SS3-Keypad-Multiply-Shift", []byte{'\x1b', 'O', '1', ';', '2', 'j'}, KeyRune, ModNone, "*"},
		{"SS3-Keypad-Multiply-Shift-Short", []byte{'\x1b', 'O', '2', 'j'}, KeyRune, ModNone, "*"},
		{"CSI-SS3-Keypad-Multiply-Shift", []byte{'\x1b', '[', '1', ';', '2', 'j'}, KeyRune, ModNone, "*"},
		{"CSI-F2-Alt", []byte{'\x1b', '[', '1', ';', '3', 'Q'}, KeyF2, ModAlt, ""},
		{"CSI-F2-Hyper", []byte{'\x1b', '[', '1', ';', '1', '7', 'Q'}, KeyF2, ModHyper, ""},
		{"CSI-F2-Super", []byte{'\x1b', '[', '1', ';', '3', '3', 'Q'}, KeyF2, ModMeta, ""},
		{"Ctrl-Home", []byte{'\x1b', '[', '1', ';', '5', '~'}, KeyHome, ModCtrl, ""},
		{"SS3-Home", []byte{'\x1b', 'O', 'H'}, KeyHome, ModNone, ""},
		{"SS3-Clear", []byte{'\x1b', 'O', 'E'}, KeyClear, ModNone, ""},
		{"ESC-Tab", []byte{'\x1b', '\t'}, KeyBacktab, ModNone, ""}, // linux console special
		{"Linux-F1", []byte{'\x1b', '[', '[', 'A'}, KeyF1, ModNone, ""},
		{"Linux-F2", []byte{'\x1b', '[', '[', 'B'}, KeyF2, ModNone, ""},
		{"Linux-F3", []byte{'\x1b', '[', '[', 'C'}, KeyF3, ModNone, ""},
		{"Linux-F4", []byte{'\x1b', '[', '[', 'D'}, KeyF4, ModNone, ""},
		{"Linux-F5", []byte{'\x1b', '[', '[', 'E'}, KeyF5, ModNone, ""},
		{"XTerm-Alt-Tab", []byte{'\x1b', '[', '2', '7', ';', '3', ';', '9', '~'}, KeyTab, ModAlt, ""}, // modifyOtherKeys == 1
		{"Alt-F7", []byte{'\x1b', '[', '1', '8', ';', '3', '~'}, KeyF7, ModAlt, ""},
		{"XTerm-Shift-Tab", []byte{'\x1b', '[', '2', '7', ';', '2', ';', '9', '~'}, KeyBacktab, ModNone, ""}, // modifyOtherKeys == 2
		{"XTerm-Space", []byte{'\x1b', '[', '2', '7', ';', '1', ';', '3', '2', '~'}, KeyRune, ModNone, " "},  // modifyOtherKeys == 3
		{"XTerm-Alt-CJK", []byte{'\x1b', '[', '2', '7', ';', '3', ';', '2', '0', '3', '2', '0', '~'}, KeyRune, ModAlt, "你"},
		{"Kitty-Esc", []byte{'\x1b', '[', '2', '7', 'u'}, KeyEsc, ModNone, ""},
		{"Kitty-Control-I", []byte{'\x1b', '[', '1', '0', '5', ';', '5', 'u'}, 'I', ModCtrl, ""},
		{"Kitty-Text-Empty-Mod", []byte{'\x1b', '[', '0', ';', ';', '2', '2', '9', 'u'}, KeyRune, ModNone, "å"},
		{"Kitty-Text-Invalid-Codepoints", []byte{
			'\x1b', '[', '2', '2', '9', ';', ';', '5', '5', '2', '9', '6',
			':', '1', '1', '1', '4', '1', '1', '2',
			':', '6', '5', 'u',
		}, KeyRune, ModNone, "A"},
		{"Kitty-Text-C0-Rejected", []byte{'\x1b', '[', '9', '7', ';', '1', ';', '9', 'u'}, KeyRune, ModNone, "a"},
		{"Kitty-Text-C1-Rejected", []byte{'\x1b', '[', '9', '7', ';', '1', ';', '1', '4', '0', 'u'}, KeyRune, ModNone, "a"},
		{"Win-Shift-A", []byte{'\x1b', '[', '6', '5', ';', '0', ';', '6', '5', ';', '1', ';', '1', '6', '_'}, KeyRune, ModNone, "A"},
		{"Win-Ctrl-1", []byte{'\x1b', '[', '4', '9', ';', '0', ';', '4', '9', ';', '1', ';', '8', '_'}, KeyRune, ModCtrl, "1"},
		{"Win-Ctrl-A", []byte{'\x1b', '[', '6', '5', ';', '0', ';', '1', ';', '1', ';', '8', '_'}, KeyCtrlA, ModCtrl, ""},
		{"Win-Ctrl-Up", []byte{'\x1b', '[', '3', '8', ';', '0', ';', '0', ';', '1', ';', '8', '_'}, KeyUp, ModCtrl, ""},
		{"Win-Ctrl-Up-2", []byte{'\x1b', '[', '3', '8', ';', '0', ';', '0', ';', '1', ';', '4', '_'}, KeyUp, ModCtrl, ""},
		{"Win-Alt-F1", []byte{'\x1b', '[', '1', '1', '2', ';', '0', ';', '0', ';', '1', ';', '1', '_'}, KeyF1, ModAlt, ""},
		{"Win-Alt-F1-2", []byte{'\x1b', '[', '1', '1', '2', ';', '0', ';', '0', ';', '1', ';', '2', '_'}, KeyF1, ModAlt, ""},
		{"Win-AltGr-E", []byte{'\x1b', '[', '6', '9', ';', '0', ';', '6', '9', ';', '1', ';', '5', '_'}, KeyRune, ModNone, "E"},
		{"Win-Ignore-Release", []byte{'\x1b', '[', '6', '5', ';', '0', ';', '6', '5', ';', '0', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"},
		{"Win-Mod-Ignore-Shift", []byte{'\x1b', '[', '1', '6', ';', '0', ';', '1', '1', ';', '1', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"},
		{"Win-Mod-Ignore-Ctrl", []byte{'\x1b', '[', '1', '7', ';', '0', ';', '1', '3', ';', '1', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"},
		{"Win-Mod-Ignore-Alt", []byte{'\x1b', '[', '1', '8', ';', '0', ';', '1', '4', ';', '1', ';', '1', '6', '_', 'C'}, KeyRune, ModNone, "C"},
		{"Win-Surrogates", []byte{
			'\x1b', '[', '0', ';', '0', ';', '5', '5', '3', '5', '6', ';', '1', ';', '0', ';', '0', '_',
			'\x1b', '[', '0', ';', '0', ';', '5', '7', '2', '5', '6', ';', '1', ';', '0', ';', '0', '_',
		}, KeyRune, ModNone, "🎨"},
		{"Win-Nested-Shift-B", []byte{
			'\x1b', '[', '0', ';', '0', ';', '2', '7', ';', '1', ';', '0', ';', '0', '_', // ESC
			'\x1b', '[', '0', ';', '0', ';', '9', '1', ';', '1', ';', '0', ';', '0', '_', // [
			'\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6
			'\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6
			'\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ;
			'\x1b', '[', '0', ';', '0', ';', '4', '8', ';', '1', ';', '0', ';', '0', '_', // 0
			'\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ;
			'\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6
			'\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6
			'\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ;
			'\x1b', '[', '0', ';', '0', ';', '4', '9', ';', '1', ';', '0', ';', '0', '_', // 1
			'\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ;
			'\x1b', '[', '0', ';', '0', ';', '4', '9', ';', '1', ';', '0', ';', '0', '_', // 1
			'\x1b', '[', '0', ';', '0', ';', '5', '4', ';', '1', ';', '0', ';', '0', '_', // 6
			'\x1b', '[', '0', ';', '0', ';', '5', '9', ';', '1', ';', '0', ';', '0', '_', // ;
			'\x1b', '[', '0', ';', '0', ';', '4', '9', ';', '1', ';', '0', ';', '0', '_', // 1
			'\x1b', '[', '0', ';', '0', ';', '9', '5', ';', '1', ';', '0', ';', '0', '_', // _
		}, KeyRune, ModNone, "B"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			ip.ScanUTF8(tt.input)

			expected := NewEventKey(tt.expectedKey, "", tt.expectedMod)

			select {
			case ev := <-evch:
				if kev, ok := ev.(*EventKey); ok {
					if kev.Key() != tt.expectedKey || kev.Modifiers() != tt.expectedMod || kev.Str() != string(tt.expectedStr) {
						t.Errorf("Expected %q, got %q (rune expected %q got %q)", expected.Name(), kev.Name(), string(tt.expectedStr), kev.Str())
					} else {
						t.Logf("Key %s ok", kev.Name())
					}
				} else {
					t.Errorf("Expected EventKey, got %T", ev)
				}

			case <-time.After(100 * time.Millisecond):
				// Force any incomplete escape sequence to expire without making
				// every legacy-key test wait for its full timeout.
				ip.keyTime = time.Now().Add(-2 * escapeSequenceTimeout)
				ip.Scan()
				select {
				case ev := <-evch:
					if kev, ok := ev.(*EventKey); ok {
						if kev.Key() != tt.expectedKey || kev.Modifiers() != tt.expectedMod {
							t.Errorf("Expected %q, got %q", expected.Name(), kev.Name())
						}
					} else {
						t.Errorf("Expected EventKey, got %T", ev)
					}

				default:
					t.Fatalf("Timeout waiting for key event")
				}
			}
		})
	}
}

// TestDecPrivateModeResponse tests responses to various DEC private mode queries
func TestDecPrivateModeResponse(t *testing.T) {
	tests := []struct {
		bytes  string
		result eventPrivateMode
		usable bool
	}{
		{"\x1b[?1001;0$y", eventPrivateMode{Mode: 1001, Status: vt.ModeNA}, false},
		{"\x1b[?1004;2$y", eventPrivateMode{Mode: 1004, Status: vt.ModeOff}, true},
		{"\x1b[?7;1$y", eventPrivateMode{Mode: vt.PmAutoMargin, Status: vt.ModeOn}, true},
		{"\x1b[?25;3$y", eventPrivateMode{Mode: vt.PmShowCursor, Status: vt.ModeOnLocked}, false},
		{"\x1b[?12;4$y", eventPrivateMode{Mode: vt.PmBlinkCursor, Status: vt.ModeOffLocked}, false},
		{"\x1b[?990;$y", eventPrivateMode{Mode: 990, Status: vt.ModeNA}, false},
		{"\x1b[?991$y", eventPrivateMode{Mode: 991, Status: vt.ModeNA}, false},
	}
	for i, tt := range tests {
		t.Run(fmt.Sprintf("case #%d", i), func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			// Send null in initial state
			ip.ScanUTF8([]byte(tt.bytes))

			select {
			case ev := <-evch:
				if pev, ok := ev.(*eventPrivateMode); ok {
					if pev.Status.Changeable() != tt.usable {
						t.Errorf("Private mode usability wrong %v != %v", pev.Status.Changeable(), tt.usable)
					}
					if *pev != tt.result {
						t.Errorf("Private mode mismatch for %d", tt.result.Mode)
					}
				} else {
					t.Errorf("Got unexpected event %T", ev)
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatal("Timeout")
			}
		})
	}
}

func TestIgnoredSequences(t *testing.T) {
	tests := []struct {
		name  string
		bytes string
	}{
		{"LoneST", "\x9c"}, // 7 bit version would be confused with Alt-\
		{"SoS", "\x1bXdata\x1b\\"},
		{"SoS-Bell", "\x1bXdata\x07"},
		{"SoS-Embed-ESC", "\x1bXab\x1bcde\x1b\\"},
		{"PM", "\x1b^data\x07"},
		{"PM8", "\x9edata\x07"},
		{"PM-Bell", "\x1b^data\x07"},
		{"APC", "\x1b_data\x07"},
		{"APC8", "\x9fdata\x07"},
		{"APC-Bell", "\x1b_data\x07"},
		{"OSC", "\x1b]junk\x1b\\"},
		{"OSC8", "\x9djunk\x1b\\"},
		{"OSC-Bell", "\x1b]junk\x07"},
		{"DCS", "\x1bPjunk\x1b\\"},
		{"DCS8", "\x90junk\x1b\\"},
		{"DCS-Bell", "\x1bPjunk\x07"},
		{"SS2", "\x1bN1"},
		{"SS28", "\x8e1"},
		{"BadCSI", "\x1b[\x07"},
		{"BadUTF8", "\xe0\xff"},
		{"Win32Shift", "\x1b[16;0;0;1;1;1_"},
		{"Win32Ctrl", "\x1b[17;0;0;1;1;1_"},
		{"Win32Alt", "\x1b[18;0;0;1;1;1_"},
		{"Win32CapsLock", "\x1b[20;0;0;1;1;1_"},
		{"Win32KeyUp", "\x1b[13;0;13;0;1;1_"},
		{"RuntDA1", "\x1b[?c"},
		{"RuntWindowNotice", "\x1b[t"},
		{"OtherIntermediates", "\x1b[1 ~"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			// send event plus DECID so we get a final event
			ip.ScanUTF8(append([]byte(test.bytes), '\x1B', '[', '?', '6', 'c'))
			select {
			case ev := <-evch:
				if _, ok := ev.(*eventPrimaryAttributes); ok {
					return
				} else {
					t.Errorf("Got unexpected event %T", ev)
					if ev, ok := ev.(*EventKey); ok {
						t.Logf("Key %s", ev.Name())
					}
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatal("Timeout")
			}
		})
	}
}

func TestStringSequenceLimit(t *testing.T) {
	tests := []struct {
		name   string
		prefix string
	}{
		{"OSC", "\x1b]"},
		{"XDA", "\x1bP"},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			input := append([]byte(test.prefix), bytes.Repeat([]byte{'x'}, defaultControlStringLimit+1)...)
			ip.ScanUTF8(input)

			if ip.state == istInit {
				t.Fatalf("parser state = %v, want string discard state", ip.state)
			}
			if len(ip.strBuf) != 0 {
				t.Fatalf("string buffer length = %d, want 0", len(ip.strBuf))
			}
			if !ip.discardString {
				t.Fatal("parser is not discarding over-limit string")
			}

			ip.ScanUTF8([]byte("tail\x07a"))
			select {
			case ev := <-evch:
				kev, ok := ev.(*EventKey)
				if !ok {
					t.Fatalf("got %T, want *EventKey", ev)
				}
				if kev.Key() != KeyRune || kev.Str() != "a" {
					t.Fatalf("got %v %q, want %v %q", kev.Key(), kev.Str(), KeyRune, "a")
				}
			case <-time.After(100 * time.Millisecond):
				t.Fatal("timeout waiting for parser recovery")
			}
		})
	}
}

func TestStringSequenceLimitAfterEmbeddedEscape(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	input := append([]byte("\x1b]"), bytes.Repeat([]byte{'x'}, defaultControlStringLimit-1)...)
	input = append(input, '\x1b', 'x')
	ip.ScanUTF8(input)

	if ip.state != istOsc {
		t.Fatalf("parser state = %v, want %v", ip.state, istOsc)
	}
	if len(ip.strBuf) != 0 {
		t.Fatalf("string buffer length = %d, want 0", len(ip.strBuf))
	}
	if !ip.discardString {
		t.Fatal("parser is not discarding over-limit string")
	}

	ip.ScanUTF8([]byte("tail\x1b\\a"))
	select {
	case ev := <-evch:
		kev, ok := ev.(*EventKey)
		if !ok {
			t.Fatalf("got %T, want *EventKey", ev)
		}
		if kev.Key() != KeyRune || kev.Str() != "a" {
			t.Fatalf("got %v %q, want %v %q", kev.Key(), kev.Str(), KeyRune, "a")
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("timeout waiting for parser recovery")
	}
}

func TestStringSequenceCustomLimit(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.controlStringMax = 4

	ip.ScanUTF8([]byte("\x1b]12345"))

	if ip.state != istOsc {
		t.Fatalf("parser state = %v, want %v", ip.state, istOsc)
	}
	if len(ip.strBuf) != 0 {
		t.Fatalf("string buffer length = %d, want 0", len(ip.strBuf))
	}
	if !ip.discardString {
		t.Fatal("parser is not discarding over-limit string")
	}
}

func TestStringSequenceExactLimit(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.controlStringMax = 4

	ip.ScanUTF8([]byte("\x1b]1234"))

	if ip.state != istOsc {
		t.Fatalf("parser state = %v, want %v", ip.state, istOsc)
	}
	if got := string(ip.strBuf); got != "1234" {
		t.Fatalf("string buffer = %q, want %q", got, "1234")
	}
}

func TestStringSequenceUnlimited(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.controlStringMax = 0

	payload := bytes.Repeat([]byte{'x'}, defaultControlStringLimit+1)
	ip.ScanUTF8(append([]byte("\x1b]"), payload...))

	if ip.state != istOsc {
		t.Fatalf("parser state = %v, want %v", ip.state, istOsc)
	}
	if !bytes.Equal(ip.strBuf, payload) {
		t.Fatalf("string buffer length = %d, want %d", len(ip.strBuf), len(payload))
	}
}

// TestKeyboardMode tests the responses to various keyboard modes
func TestKeyboardMode(t *testing.T) {
	tests := []struct {
		bytes  string
		result Event
	}{
		{"\x1b[?9001;0$y", &eventPrivateMode{Mode: 9001, Status: vt.ModeNA}},
		{"\x1b[?9001;2$y", &eventPrivateMode{Mode: 9001, Status: vt.ModeOff}},
		{"\x1b[>4;0m", &eventXTermKbdMode{Mode: XtermKbdModeOff}},
		{"\x1b[?0u", &eventKittyKbdMode{Mode: KittyKbdModeOff}},
	}
	for i, tt := range tests {
		t.Run(fmt.Sprintf("case #%d", i), func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)

			// Send null in initial state
			ip.ScanUTF8([]byte(tt.bytes))

			select {
			case ev := <-evch:
				switch expect := tt.result.(type) {
				case *eventPrivateMode:
					if actual, ok := ev.(*eventPrivateMode); ok {
						if actual.Mode != expect.Mode || actual.Status != expect.Status {
							t.Errorf("Wrong mode or status: %v %v != %v %v", actual.Mode, actual.Status, expect.Mode, expect.Status)
						}
						return
					}
				case *eventXTermKbdMode:
					if actual, ok := ev.(*eventXTermKbdMode); ok {
						if actual.Mode != expect.Mode {
							t.Errorf("Wrong mode: %v != %v", actual.Mode, expect.Mode)
						}
						return
					}
				case *eventKittyKbdMode:
					if actual, ok := ev.(*eventKittyKbdMode); ok {
						if actual.Mode != expect.Mode {
							t.Errorf("Wrong mode: %v != %v", actual.Mode, expect.Mode)
						}
						return
					}
				}
				t.Errorf("Wrong type, expected %T got %T", tt.result, ev)
			case <-time.After(100 * time.Millisecond):
				t.Fatal("Timeout")
			}
		})
	}
}

// firstKey drains the event channel and returns the first EventKey received,
// or nil if none arrives within 100ms.
func firstKey(evch chan Event) *EventKey {
	var got *EventKey
	for {
		select {
		case ev := <-evch:
			if got == nil {
				if kev, ok := ev.(*EventKey); ok {
					got = kev
				}
			}
			continue
		case <-time.After(100 * time.Millisecond):
		}
		break
	}
	return got
}

func nextKey(evch chan Event) *EventKey {
	for {
		select {
		case ev := <-evch:
			if kev, ok := ev.(*EventKey); ok {
				return kev
			}
		case <-time.After(100 * time.Millisecond):
			return nil
		}
	}
}

func TestAdvancedControlKeys(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.advanced = true

	ip.ScanUTF8([]byte{0x01, '\t'})

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected Ctrl-A event")
	}
	if got.Key() != KeyRune || got.Str() != "a" || got.Modifiers() != ModCtrl || got.Physical() != Key('a') {
		t.Fatalf("expected Ctrl-A as KeyRune a + ModCtrl, got key=%v str=%q mod=%v physical=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected Tab event")
	}
	if got.Key() != KeyTab || got.Modifiers() != ModNone {
		t.Fatalf("expected ambiguous Ctrl-I/Tab to remain Tab, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers())
	}
}

func TestAdvancedBacktabReportsShiftTab(t *testing.T) {
	tests := []struct {
		name  string
		input []byte
	}{
		{"CSI-Z", []byte("\x1b[Z")},
		{"ESC-Tab", []byte{'\x1b', '\t'}},
		{"XTerm-Shift-Tab", []byte("\x1b[27;2;9~")},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)
			ip.advanced = true

			ip.ScanUTF8(tt.input)

			got := nextKey(evch)
			if got == nil {
				t.Fatal("expected Shift-Tab event")
			}
			if got.Key() != KeyTab || got.Modifiers() != ModShift {
				t.Fatalf("expected KeyTab + ModShift, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers())
			}
		})
	}
}

func TestAdvancedKittyTextKeepsShift(t *testing.T) {
	// kitty spec's own shift+a example: CSI 97 ; 2 ; 65 u - the associated
	// text 'A' is delivered together with the shift modifier, which must not
	// be dropped.
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.advanced = true

	ip.ScanUTF8([]byte("\x1b[97;2;65u"))

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected shifted A press")
	}
	if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift {
		t.Fatalf("expected shifted A with ModShift, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers())
	}
}

func TestAdvancedKittyKeyMetadata(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.advanced = true

	ip.ScanUTF8([]byte("\x1b[65:97;2u\x1b[65:97;2:3u\x1b[57448;1:3u"))

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected shifted A press")
	}
	if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || !got.Pressed() || got.Repeat() != 1 {
		t.Fatalf("unexpected shifted A press: key=%v str=%q mod=%v physical=%v pressed=%v repeat=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed(), got.Repeat())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected shifted A release")
	}
	if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || got.Pressed() {
		t.Fatalf("unexpected shifted A release: key=%v str=%q mod=%v physical=%v pressed=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected right Ctrl release")
	}
	if got.Key() != KeyCtrl || got.Modifiers()&ModRCtrl != ModRCtrl || got.Modifiers()&ModCtrl == 0 || got.Pressed() {
		t.Fatalf("unexpected right Ctrl release: key=%v mod=%v pressed=%v", got.Key(), got.Modifiers(), got.Pressed())
	}
}

func TestAdvancedKittyParameterParseErrorKeepsSubparametersAligned(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.advanced = true

	ip.ScanUTF8([]byte("\x1b[65;999999999999999999999999999999999:3u"))

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected key event")
	}
	if got.Key() != KeyRune || got.Str() != "A" || got.Pressed() {
		t.Fatalf("expected shifted A release despite malformed modifier parameter, got key=%v str=%q pressed=%v", got.Key(), got.Str(), got.Pressed())
	}
}

func TestKeyConversionBounds(t *testing.T) {
	if key, ok := keyFromInt(32767); !ok || key != Key(32767) {
		t.Fatalf("keyFromInt max = %v, %v", key, ok)
	}
	if _, ok := keyFromInt(32768); ok {
		t.Fatal("keyFromInt accepted overflow")
	}
	if _, ok := keyFromInt(-1); ok {
		t.Fatal("keyFromInt accepted negative")
	}
	if key, ok := keyFromRune(32767); !ok || key != Key(32767) {
		t.Fatalf("keyFromRune max = %v, %v", key, ok)
	}
	if _, ok := keyFromRune(32768); ok {
		t.Fatal("keyFromRune accepted overflow")
	}
	if b, ok := asciiByteFromInt(0x7f); !ok || b != 0x7f {
		t.Fatalf("asciiByteFromInt max = %v, %v", b, ok)
	}
	if _, ok := asciiByteFromInt(0); ok {
		t.Fatal("asciiByteFromInt accepted zero")
	}
	if _, ok := asciiByteFromInt(0x80); ok {
		t.Fatal("asciiByteFromInt accepted non-ASCII")
	}
}

func TestAdvancedPhysicalKeyOverflowIsUnknown(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.advanced = true

	ip.ScanUTF8([]byte("\x1b[65:40000;2u"))

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected key event")
	}
	if got.Key() != KeyRune || got.Str() != "A" || got.Physical() != 0 {
		t.Fatalf("expected overflow physical key to be unknown, got key=%v str=%q physical=%v", got.Key(), got.Str(), got.Physical())
	}
}

func TestLargeUnicodePhysicalKeyIsUnknown(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.advanced = true

	ip.ScanUTF8([]byte("😀"))

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected key event")
	}
	if got.Key() != KeyRune || got.Str() != "😀" || got.Physical() != 0 {
		t.Fatalf("expected large Unicode physical key to be unknown, got key=%v str=%q physical=%v", got.Key(), got.Str(), got.Physical())
	}
}

func TestAdvancedWinKeyMetadata(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.advanced = true

	ip.ScanUTF8([]byte("\x1b[65;0;65;1;16;3_\x1b[65;0;65;0;16;1_\x1b[17;0;0;1;8;1_\x1b[65;0;1;1;4;1_\x1b[160;0;0;1;20;1_\x1b[76;0;108;1;512;1_"))

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected Win32 shifted A press")
	}
	if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || !got.Pressed() || got.Repeat() != 3 {
		t.Fatalf("unexpected Win32 shifted A press: key=%v str=%q mod=%v physical=%v pressed=%v repeat=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed(), got.Repeat())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected Win32 shifted A release")
	}
	if got.Key() != KeyRune || got.Str() != "A" || got.Modifiers() != ModShift || got.Physical() != Key('a') || got.Pressed() {
		t.Fatalf("unexpected Win32 shifted A release: key=%v str=%q mod=%v physical=%v pressed=%v", got.Key(), got.Str(), got.Modifiers(), got.Physical(), got.Pressed())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected Win32 Ctrl modifier press")
	}
	if got.Key() != KeyCtrl || got.Modifiers()&ModLCtrl != ModLCtrl || got.Modifiers()&ModCtrl == 0 || !got.Pressed() {
		t.Fatalf("unexpected Win32 Ctrl press: key=%v mod=%v pressed=%v", got.Key(), got.Modifiers(), got.Pressed())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected Win32 right-Ctrl A press")
	}
	if got.Key() != KeyRune || got.Str() != "a" || got.Modifiers()&ModRCtrl != ModRCtrl || got.Modifiers()&ModCtrl == 0 {
		t.Fatalf("unexpected Win32 right-Ctrl A press: key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected Win32 left-Shift press with right-Ctrl held")
	}
	if got.Key() != KeyShift || got.Modifiers()&ModLShift != ModLShift ||
		got.Modifiers()&ModRCtrl != ModRCtrl || got.Modifiers()&ModCtrl == 0 {
		t.Fatalf("unexpected Win32 left-Shift press with right-Ctrl held: key=%v mod=%v", got.Key(), got.Modifiers())
	}

	got = nextKey(evch)
	if got == nil {
		t.Fatal("expected Win32 left-Meta l press")
	}
	if got.Key() != KeyRune || got.Str() != "l" || got.Modifiers()&ModLMeta != ModLMeta || got.Modifiers()&ModMeta == 0 {
		t.Fatalf("unexpected Win32 left-Meta l press: key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers())
	}
}

func TestWinKeyWithoutChar(t *testing.T) {
	for _, tc := range []struct {
		name string
		seq  string
		str  string
		mod  ModMask
	}{
		{"CtrlComma", "\x1b[188;51;0;1;8;1_", ",", ModCtrl},
		{"CtrlMinus", "\x1b[189;12;0;1;8;1_", "-", ModCtrl},
		{"CtrlPeriod", "\x1b[190;52;0;1;8;1_", ".", ModCtrl},
		{"CtrlAltPeriod", "\x1b[190;52;0;1;10;1_", ".", ModCtrl | ModAlt},
		{"CtrlAltDigit", "\x1b[50;3;0;1;10;1_", "2", ModCtrl | ModAlt},
		{"AltGrChar", "\x1b[50;3;64;1;9;1_", "@", ModNone},
	} {
		t.Run(tc.name, func(t *testing.T) {
			evch := make(chan Event, 10)
			ip := newInputParser(evch)
			ip.ScanUTF8([]byte(tc.seq))

			got := nextKey(evch)
			if got == nil {
				t.Fatal("expected key event")
			}

			if got.Key() != KeyRune || got.Str() != tc.str || got.Modifiers() != tc.mod {
				t.Fatalf("got key=%v str=%q mod=%v, want str=%q mod=%v", got.Key(), got.Str(), got.Modifiers(), tc.str, tc.mod)
			}
		})
	}
}

func TestWinKeyASCIIFallbackRangeCheck(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	ip.ScanUTF8([]byte("\x1b[0;0;65;1;0;1_"))

	got := nextKey(evch)
	if got == nil {
		t.Fatal("expected Win32 ASCII fallback key")
	}
	if got.Key() != KeyRune || got.Str() != "A" {
		t.Fatalf("unexpected Win32 ASCII fallback key: key=%v str=%q", got.Key(), got.Str())
	}

	ip.ScanUTF8([]byte("\x1b[0;0;0;1;0;1_"))

	if got = nextKey(evch); got != nil {
		t.Fatalf("unexpected Win32 NUL fallback key: key=%v str=%q", got.Key(), got.Str())
	}
}

func TestWinKeySurrogateErrorsArePreserved(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	ip.ScanUTF8([]byte("\x1b[1;0;55357;1;0;1_\x1b[1;0;32;1;0;1_"))

	first := nextKey(evch)
	if first == nil || first.Str() != "�" {
		t.Fatalf("expected orphaned high surrogate as replacement rune, got %#v", first)
	}

	second := nextKey(evch)
	if second == nil || second.Str() != " " {
		t.Fatalf("expected following BMP rune to be preserved, got %#v", second)
	}

	ip.ScanUTF8([]byte("\x1b[1;0;56832;1;0;1_"))

	third := nextKey(evch)
	if third == nil || third.Str() != "�" {
		t.Fatalf("expected orphaned low surrogate as replacement rune, got %#v", third)
	}

	ip.ScanUTF8([]byte("\x1b[1;0;55357;1;0;1_\x1b[1;0;55357;1;0;1_\x1b[1;0;56832;1;0;1_"))

	fourth := nextKey(evch)
	if fourth == nil || fourth.Str() != "�" {
		t.Fatalf("expected superseded high surrogate as replacement rune, got %#v", fourth)
	}

	fifth := nextKey(evch)
	if fifth == nil || fifth.Str() != "😀" {
		t.Fatalf("expected replacement high surrogate to pair with following low surrogate, got %#v", fifth)
	}
}

func TestAdvancedModifierHelpers(t *testing.T) {
	winCases := []struct {
		vk      int
		wantKey Key
		wantMod ModMask
	}{
		{0x10, KeyShift, ModShift},
		{0xa0, KeyShift, ModLShift},
		{0xa1, KeyShift, ModRShift},
		{0x11, KeyCtrl, ModCtrl},
		{0xa2, KeyCtrl, ModLCtrl},
		{0xa3, KeyCtrl, ModRCtrl},
		{0x12, KeyAlt, ModAlt},
		{0xa4, KeyAlt, ModLAlt},
		{0xa5, KeyAlt, ModRAlt},
		{0x5b, KeyMeta, ModLMeta},
		{0x5c, KeyMeta, ModRMeta},
		{0x14, KeyCapsLock, ModNone},
	}
	for _, tt := range winCases {
		key, mod, ok := winModifierKey(tt.vk)
		if !ok || key != tt.wantKey || mod != tt.wantMod {
			t.Fatalf("winModifierKey(%#x) = %v, %v, %v; want %v, %v, true", tt.vk, key, mod, ok, tt.wantKey, tt.wantMod)
		}
	}
	if _, _, ok := winModifierKey(0xff); ok {
		t.Fatal("winModifierKey accepted unknown key")
	}

	kittyCases := map[int]ModMask{
		57441: ModLShift,
		57447: ModRShift,
		57442: ModLCtrl,
		57448: ModRCtrl,
		57443: ModLAlt,
		57449: ModRAlt,
		57444: ModLMeta,
		57450: ModRMeta,
	}
	for code, want := range kittyCases {
		if got := kittyModifierKey(code); got != want {
			t.Fatalf("kittyModifierKey(%d) = %v, want %v", code, got, want)
		}
	}
	if got := kittyModifierKey(12345); got != ModNone {
		t.Fatalf("kittyModifierKey unknown = %v, want ModNone", got)
	}
}

func TestWinModifierState(t *testing.T) {
	if got := calcWinModifier(0x71f, true); got != ModShift|ModLCtrl|ModRCtrl|ModLAlt|ModRAlt|ModLMeta|ModRMeta {
		t.Fatalf("advanced win modifier state = %v", got)
	}
	if got := calcWinModifier(0xdf, false); got != ModShift|ModCtrl|ModAlt {
		t.Fatalf("legacy win modifier state = %v", got)
	}
	if got := calcWinModifier(0xc0, true); got&ModMeta != 0 {
		t.Fatalf("win lock bits reported Meta: %v", got)
	}
}

// TestEscDuringCsiResetsParser verifies that an ESC byte received while
// the parser is in CSI state correctly transitions to escape state per
// ECMA-48 §5.3.1, rather than being swallowed as a "bad parse".
func TestEscDuringCsiResetsParser(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	// Feed a partial CSI (ESC [) followed immediately by a new escape
	// sequence for Down arrow (ESC [ B). Without the fix, the second
	// ESC would be swallowed and 'B' would be emitted as a literal key.
	ip.ScanUTF8([]byte("\x1b[\x1b[B"))

	got := firstKey(evch)
	if got == nil {
		t.Fatal("expected a key event, got none")
	}
	if got.Key() != KeyDown {
		t.Errorf("expected KeyDown, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers())
	}
}

// TestEscDuringSs3ResetsParser verifies that an ESC byte received while
// the parser is accumulating SS3 parameters correctly transitions to
// escape state per ECMA-48 §5.3.1.
func TestEscDuringSs3ResetsParser(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	// Feed a partial SS3 with parameter (ESC O 1) followed immediately
	// by a new escape sequence for Down arrow (ESC [ B). Without the
	// fix, the ESC would be lost inside the SS3 parameter accumulation.
	ip.ScanUTF8([]byte("\x1bO1\x1b[B"))

	got := firstKey(evch)
	if got == nil {
		t.Fatal("expected a key event, got none")
	}
	if got.Key() != KeyDown {
		t.Errorf("expected KeyDown, got key=%v str=%q mod=%v", got.Key(), got.Str(), got.Modifiers())
	}
}

// firstMouse drains the next EventMouse off the channel.
func firstMouse(ch <-chan Event) *EventMouse {
	for {
		select {
		case ev := <-ch:
			if me, ok := ev.(*EventMouse); ok {
				return me
			}
		case <-time.After(100 * time.Millisecond):
			return nil
		}
	}
}

// TestInputMouseSgrClipsToScreen verifies that legacy SGR mouse reports
// (CSI ?1006h) have their coordinates clamped to the cell-based screen
// size, matching long-standing tcell behavior for terminals that report
// out-of-range coordinates during click-drag.
func TestInputMouseSgrClipsToScreen(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.cols = 80
	ip.rows = 24

	// SGR press at column 500, row 300 (well off-screen) — should clip.
	ip.ScanUTF8([]byte("\x1b[<0;500;300M"))

	me := firstMouse(evch)
	if me == nil {
		t.Fatal("expected a mouse event")
	}
	if x, y := me.Position(); x != 79 || y != 23 {
		t.Errorf("expected clipped position (79,23), got (%d,%d)", x, y)
	}
}

// TestInputMouseSgrPixelNoClip verifies that when the input parser is
// in pixel-reporting mode (CSI ?1016h), mouse coordinates pass through
// unchanged so applications can resolve sub-cell positions.
func TestInputMouseSgrPixelNoClip(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.cols = 80
	ip.rows = 24
	ip.SetPixelMouse(true)

	// Same press at pixel (500, 300). Screen is 80x24 cells, so cell-mode
	// would clip; pixel-mode must not.
	ip.ScanUTF8([]byte("\x1b[<0;500;300M"))

	me := firstMouse(evch)
	if me == nil {
		t.Fatal("expected a mouse event")
	}
	if x, y := me.Position(); x != 499 || y != 299 {
		t.Errorf("expected unclipped position (499,299), got (%d,%d)", x, y)
	}

	// Toggling pixel mode off should restore clipping behavior.
	ip.SetPixelMouse(false)
	ip.ScanUTF8([]byte("\x1b[<0;500;300M"))
	me = firstMouse(evch)
	if me == nil {
		t.Fatal("expected a mouse event after disabling pixel mode")
	}
	if x, y := me.Position(); x != 79 || y != 23 {
		t.Errorf("expected re-clipped position (79,23), got (%d,%d)", x, y)
	}
}

// TestInputInBandResizePixels tests that an in-band resize report carries the size in pixels.
func TestInputInBandResizePixels(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)

	ip.ScanUTF8([]byte("\x1b[48;25;80;400;640t"))

	select {
	case ev := <-evch:
		rev, ok := ev.(*EventResize)
		if !ok {
			t.Fatalf("Expected EventResize, got %T", ev)
		}
		if w, h := rev.Size(); w != 80 || h != 25 {
			t.Errorf("Expected size 80x25, got %dx%d", w, h)
		}
		if w, h := rev.PixelSize(); w != 640 || h != 400 {
			t.Errorf("Expected pixel size 640x400, got %dx%d", w, h)
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout waiting for resize event")
	}
}

// TestInputPixelSizeReport tests that a text area size report carries the size in pixels.
func TestInputPixelSizeReport(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.rows, ip.cols = 25, 80

	ip.ScanUTF8([]byte("\x1b[4;400;640t"))

	select {
	case ev := <-evch:
		rev, ok := ev.(*EventResize)
		if !ok {
			t.Fatalf("Expected EventResize, got %T", ev)
		}
		if w, h := rev.Size(); w != 80 || h != 25 {
			t.Errorf("Expected size 80x25, got %dx%d", w, h)
		}
		if w, h := rev.PixelSize(); w != 640 || h != 400 {
			t.Errorf("Expected pixel size 640x400, got %dx%d", w, h)
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout waiting for resize event")
	}
}

// TestInputWindowAndPixelSizeReports tests that a pixel size report is paired with the window size reported just before it.
func TestInputWindowAndPixelSizeReports(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.rows, ip.cols = 25, 80

	ip.ScanUTF8([]byte("\x1b[8;30;100t\x1b[4;600;1000t"))

	for _, want := range []WindowSize{{Width: 100, Height: 30}, {Width: 100, Height: 30, PixelWidth: 1000, PixelHeight: 600}} {
		select {
		case ev := <-evch:
			rev, ok := ev.(*EventResize)
			if !ok {
				t.Fatalf("Expected EventResize, got %T", ev)
			}
			width, height := rev.Size()
			pixelWidth, pixelHeight := rev.PixelSize()
			if got := (WindowSize{Width: width, Height: height, PixelWidth: pixelWidth, PixelHeight: pixelHeight}); got != want {
				t.Errorf("Expected %+v, got %+v", want, got)
			}
		case <-time.After(100 * time.Millisecond):
			t.Fatal("Timeout waiting for resize event")
		}
	}
}

// TestInputWindowSizeReportNested tests that a window size report also sizes the nested Win32 parser, without a second resize event.
func TestInputWindowSizeReportNested(t *testing.T) {
	evch := make(chan Event, 10)
	ip := newInputParser(evch)
	ip.rows, ip.cols = 25, 80
	nested := newInputParser(evch)
	nested.rows, nested.cols = 25, 80
	ip.nested = nested

	ip.ScanUTF8([]byte("\x1b[8;30;100t"))

	nested.l.Lock()
	rows, cols := nested.rows, nested.cols
	nested.l.Unlock()
	if rows != 30 || cols != 100 {
		t.Errorf("Expected nested size 100x30, got %dx%d", cols, rows)
	}

	select {
	case ev := <-evch:
		if width, height := ev.(*EventResize).Size(); width != 100 || height != 30 {
			t.Errorf("Expected size 100x30, got %dx%d", width, height)
		}
	case <-time.After(100 * time.Millisecond):
		t.Fatal("Timeout waiting for resize event")
	}

	select {
	case ev := <-evch:
		t.Errorf("Expected one resize event, got another %T", ev)
	case <-time.After(50 * time.Millisecond):
	}
}