@ 261030.1
current
// 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.

package tcell

import (
	"bytes"
	"runtime"
	"strings"
	"sync"
	"testing"
	"time"

	gencoding "github.com/gdamore/encoding"
	"github.com/gdamore/tcell/v3/tty"
	"github.com/gdamore/tcell/v3/vt"
	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/charmap"
	"golang.org/x/text/encoding/japanese"
	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
)

// This just offers some very basic tests that do not require a full mock.

// drainInput just does a very primitive sleep to allow input to drain.
// We need this because otherwise the application will close too soon before
// consuming characters from input, including sequences that are returned in
// response to queries.
func drainInput() {
	time.Sleep(time.Millisecond * 30)
}

// TestInitScreen just tries to initialize the default screen.
// It requires a working tty.
func TestInitScreen(t *testing.T) {
	s, err := NewTerminfoScreen()
	if err != nil {
		t.Skip("failed to get screen", err)
	}
	if err := s.Init(); err != nil {
		t.Skip("failed to initialize screen", err)
	}
	defer s.Fini()

	if s.CharacterSet() != "UTF-8" {
		t.Fatalf("Character Set (%v) not UTF-8", s.CharacterSet())
	}

	drainInput()
}

type spyTty struct {
	vt.MockTerm
	writes bytes.Buffer
}

func (t *spyTty) Write(b []byte) (int, error) {
	_, _ = t.writes.Write(b)
	return t.MockTerm.Write(b)
}

func (t *spyTty) Output() string {
	return t.writes.String()
}

func TestOptAltScreenDisable(t *testing.T) {
	t.Setenv("TCELL_ALTSCREEN", "")

	tty := &spyTty{MockTerm: vt.NewMockTerm()}
	s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	s.Fini()

	out := tty.Output()
	if strings.Contains(out, enterCA) {
		t.Fatalf("alternate screen enter escape was emitted")
	}
	if strings.Contains(out, exitCA) {
		t.Fatalf("alternate screen exit escape was emitted")
	}
}

func TestOptAltScreenDefault(t *testing.T) {
	t.Setenv("TCELL_ALTSCREEN", "")

	tty := &spyTty{MockTerm: vt.NewMockTerm()}
	s, err := NewTerminfoScreenFromTty(tty)
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	s.Fini()

	out := tty.Output()
	if !strings.Contains(out, enterCA) {
		t.Fatalf("alternate screen enter escape was not emitted")
	}
	if !strings.Contains(out, exitCA) {
		t.Fatalf("alternate screen exit escape was not emitted")
	}
}

func TestOptEscapeTimeout(t *testing.T) {
	tscreen := &tScreen{escapeTimeout: platformEscapeTimeout()}
	OptEscapeTimeout(125 * time.Millisecond).apply(tscreen)
	if got := tscreen.escapeTimeout; got != 125*time.Millisecond {
		t.Fatalf("option timeout = %v, want %v", got, 125*time.Millisecond)
	}
	OptEscapeTimeout(0).apply(tscreen)
	if got := tscreen.escapeTimeout; got != 125*time.Millisecond {
		t.Fatalf("zero option changed timeout to %v, want %v", got, 125*time.Millisecond)
	}

	// A valid environment override lets users tune applications that do not
	// explicitly pass the option.
	t.Setenv("TCELL_ESCDELAY", "175")
	tscreen.applyEnvironmentOverrides()
	if got := tscreen.escapeTimeout; got != 175*time.Millisecond {
		t.Fatalf("environment timeout = %v, want %v", got, 175*time.Millisecond)
	}
}

func TestMainLoopEscapeTimeout(t *testing.T) {
	evch := make(chan Event, 2)
	tscreen := &tScreen{
		keyQ:    make(chan []byte, 2),
		quit:    make(chan struct{}),
		resizeQ: make(chan bool),
		input:   newInputParser(evch),
		decoder: transform.Nop,
	}
	stopQ := make(chan struct{})
	done := make(chan struct{})
	tscreen.wg.Add(1)
	go func() {
		tscreen.mainLoop(stopQ)
		close(done)
	}()

	tscreen.keyQ <- []byte{'\x1b'}
	deadline := time.After(time.Second)
	for tscreen.input.WaitDuration() != platformEscapeTimeout() {
		select {
		case <-deadline:
			t.Fatal("input timeout was not scheduled")
		default:
			time.Sleep(time.Millisecond)
		}
	}
	// Refresh the parser timestamp before the first timer fires. This verifies
	// that mainLoop re-arms its timer when parsing remains incomplete.
	time.Sleep(platformEscapeTimeout() / 2)
	tscreen.input.l.Lock()
	tscreen.input.keyTime = time.Now()
	tscreen.input.l.Unlock()
	select {
	case ev := <-evch:
		if key, ok := ev.(*EventKey); !ok || key.Key() != KeyEscape {
			t.Fatalf("expired ESC event = %T %v, want KeyEscape", ev, ev)
		}
	case <-time.After(platformEscapeTimeout() * 4):
		t.Fatal("bare ESC did not expire")
	}

	// Verify that the loop clears its timer after a complete key is parsed.
	tscreen.keyQ <- []byte{'A'}
	select {
	case ev := <-evch:
		if key, ok := ev.(*EventKey); !ok || key.Str() != "A" {
			t.Fatalf("plain key event = %T %v, want A", ev, ev)
		}
	case <-time.After(time.Second):
		t.Fatal("plain key was not processed")
	}

	close(stopQ)
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("input main loop did not stop")
	}
}

// TestMainLoopKittyEscapeTimeout measures, through the real mainLoop event
// delivery path, how long a bare Escape takes to arrive when the negotiated
// keyboard protocol is not the legacy one.  Terminals reach this state either
// by advertising the capability or, as WezTerm does, by having the terminal
// profile force it on.  It then verifies that an escape sequence which has
// already begun still gets the longer inter-byte budget.
func TestMainLoopKittyEscapeTimeout(t *testing.T) {
	evch := make(chan Event, 4)
	tscreen := &tScreen{
		keyQ:         make(chan []byte, 2),
		quit:         make(chan struct{}),
		resizeQ:      make(chan bool),
		input:        newInputParser(evch),
		decoder:      transform.Nop,
		haveKittyKbd: true,
	}
	// Same wiring engageLocked performs, so the mapping from the negotiated
	// capability to the parser is exercised rather than assumed.
	tscreen.input.SetKeyboardProtocol(tscreen.keyboardProtocol())
	if tscreen.keyboardProtocol() != KittyKeyboard {
		t.Fatalf("keyboard protocol = %v, want %v", tscreen.keyboardProtocol(), KittyKeyboard)
	}

	stopQ := make(chan struct{})
	done := make(chan struct{})
	tscreen.wg.Add(1)
	go func() {
		tscreen.mainLoop(stopQ)
		close(done)
	}()

	// A lone ESC with nothing behind it must resolve on the bare-ESC deadline.
	// The bound matches the one TestMainLoopEscapeTimeout already allows, which
	// leaves a slow machine plenty of room while still catching a regression to
	// the sequence deadline, since that one cannot deliver before a full second.
	tooSlow := platformEscapeTimeout() * 4
	start := time.Now()
	tscreen.keyQ <- []byte{'\x1b'}
	select {
	case ev := <-evch:
		elapsed := time.Since(start)
		if key, ok := ev.(*EventKey); !ok || key.Key() != KeyEscape {
			t.Fatalf("expired ESC event = %T %v, want KeyEscape", ev, ev)
		}
		if elapsed > tooSlow {
			t.Fatalf("bare ESC took %v, want no more than %v", elapsed, tooSlow)
		}
		if elapsed < platformEscapeTimeout()/2 {
			t.Fatalf("bare ESC took %v, want at least %v", elapsed, platformEscapeTimeout()/2)
		}
	case <-time.After(escapeSequenceTimeout * 2):
		t.Fatal("bare ESC did not expire")
	}

	// An escape sequence that has already started keeps the longer budget, so a
	// stall past the bare-ESC deadline must not split it into a bare Escape.
	tscreen.keyQ <- []byte{'\x1b', '['}
	deadline := time.After(time.Second)
	for tscreen.input.WaitDuration() != escapeSequenceTimeout {
		select {
		case <-deadline:
			t.Fatal("CSI timeout was not scheduled")
		default:
			time.Sleep(time.Millisecond)
		}
	}
	time.Sleep(platformEscapeTimeout() * 2)
	select {
	case ev := <-evch:
		t.Fatalf("started sequence expired at the bare-ESC deadline: %v", ev)
	default:
	}
	tscreen.keyQ <- []byte{'A'}
	select {
	case ev := <-evch:
		if key, ok := ev.(*EventKey); !ok || key.Key() != KeyUp {
			t.Fatalf("delayed sequence event = %T %v, want KeyUp", ev, ev)
		}
	case <-time.After(time.Second):
		t.Fatal("delayed sequence was not processed")
	}

	close(stopQ)
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("input main loop did not stop")
	}
}

func TestFiniPreventsSetStyleMutation(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
	scr, err := NewTerminfoScreenFromTty(mt)
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	bs, ok := scr.(*baseScreen)
	if !ok {
		t.Fatalf("expected *baseScreen, got %T", scr)
	}
	ts, ok := bs.screenImpl.(*tScreen)
	if !ok {
		t.Fatalf("expected *tScreen, got %T", bs.screenImpl)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}

	before := StyleDefault.Foreground(ColorRed)
	after := StyleDefault.Foreground(ColorBlue)
	scr.SetStyle(before)
	scr.Fini()
	scr.SetStyle(after)

	ts.Lock()
	defer ts.Unlock()
	if !ts.fini {
		t.Fatal("screen was not marked finished")
	}
	if ts.style != before {
		t.Fatal("SetStyle mutated the screen after Fini")
	}
}

func TestInitWhileInitializedFailsWithoutReplacingEventQ(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})}
	scr, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer scr.Fini()

	eventQ := scr.EventQ()
	if err := scr.Init(); err == nil {
		t.Fatal("second Init succeeded while screen was initialized")
	}
	if scr.EventQ() != eventQ {
		t.Fatal("failed Init replaced the event queue")
	}
}

func TestInitAfterFiniFails(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})}
	scr, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	scr.Fini()

	if err := scr.Init(); err == nil {
		t.Fatal("Init succeeded after Fini")
	}
}

func TestConcurrentFiniIsIdempotent(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})}
	scr, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}

	var wg sync.WaitGroup
	for range 4 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			scr.Fini()
		}()
	}

	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("concurrent Fini calls did not return")
	}
}

func TestConcurrentInitAndFini(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})}
	scr, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}

	start := make(chan struct{})
	finiDone := make(chan struct{})
	initDone := make(chan error, 1)

	go func() {
		<-start
		scr.Fini()
		close(finiDone)
	}()
	go func() {
		<-start
		initDone <- scr.Init()
	}()

	close(start)
	select {
	case err := <-initDone:
		if err == nil {
			t.Fatal("concurrent Init succeeded")
		}
	case <-time.After(time.Second):
		t.Fatal("concurrent Init did not return")
	}
	select {
	case <-finiDone:
	case <-time.After(time.Second):
		t.Fatal("concurrent Fini did not return")
	}
}

func TestFiniPreventsFurtherTerminalMutation(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})}
	scr, err := NewTerminfoScreenFromTty(tty)
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	bs, ok := scr.(*baseScreen)
	if !ok {
		t.Fatalf("expected *baseScreen, got %T", scr)
	}
	ts, ok := bs.screenImpl.(*tScreen)
	if !ok {
		t.Fatalf("expected *tScreen, got %T", bs.screenImpl)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}

	scr.ShowCursor(1, 1)
	scr.SetCursorStyle(CursorStyleSteadyBlock, ColorRed)
	scr.EnableMouse()
	scr.EnablePaste()
	scr.EnableFocus()
	scr.Fini()

	beforeOutput := tty.Output()
	ts.Lock()
	beforeCursorX, beforeCursorY := ts.cursorx, ts.cursory
	beforeCursorStyle, beforeCursorColor := ts.cursorStyle, ts.cursorColor
	beforeMouseFlags := ts.mouseFlags
	beforePasteEnabled, beforeFocusEnabled := ts.pasteEnabled, ts.focusEnabled
	ts.Unlock()

	scr.ShowCursor(2, 1)
	scr.SetCursorStyle(CursorStyleBlinkingBar, ColorBlue)
	scr.EnableMouse(MouseMotionEvents)
	scr.DisableMouse()
	scr.EnablePaste()
	scr.DisablePaste()
	scr.EnableFocus()
	scr.DisableFocus()
	scr.SetSize(20, 10)
	if err := scr.Suspend(); err != nil {
		t.Fatalf("Suspend after Fini failed: %v", err)
	}
	if err := scr.Resume(); err != nil {
		t.Fatalf("Resume after Fini failed: %v", err)
	}
	if err := scr.Beep(); err != nil {
		t.Fatalf("Beep after Fini failed: %v", err)
	}
	scr.SetTitle("after")
	scr.SetClipboard([]byte("after"))
	scr.GetClipboard()
	scr.ShowNotification("after", "after")

	if got := tty.Output(); got != beforeOutput {
		t.Fatal("terminal output changed after Fini")
	}

	ts.Lock()
	defer ts.Unlock()
	if ts.cursorx != beforeCursorX || ts.cursory != beforeCursorY {
		t.Fatal("cursor position changed after Fini")
	}
	if ts.cursorStyle != beforeCursorStyle || ts.cursorColor != beforeCursorColor {
		t.Fatal("cursor style changed after Fini")
	}
	if ts.mouseFlags != beforeMouseFlags {
		t.Fatal("mouse flags changed after Fini")
	}
	if ts.pasteEnabled != beforePasteEnabled {
		t.Fatal("paste state changed after Fini")
	}
	if ts.focusEnabled != beforeFocusEnabled {
		t.Fatal("focus state changed after Fini")
	}
}

func TestFilterEventsStopsWhileEventQBlocked(t *testing.T) {
	ts := &tScreen{
		quit:   make(chan struct{}),
		eventQ: make(chan Event),
	}
	inQ := ts.filterEvents()

	inQ <- NewEventResize(1, 1)

	done := make(chan struct{})
	go func() {
		ts.eventWg.Wait()
		close(done)
	}()

	select {
	case <-done:
		t.Fatal("filterEvents exited before shutdown")
	case <-time.After(10 * time.Millisecond):
	}

	close(ts.quit)
	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("filterEvents did not stop after shutdown")
	}
}

func TestOptAdvancedKeys(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
	scr, err := NewTerminfoScreenFromTty(mt, OptAdvancedKeys(true))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	bs, ok := scr.(*baseScreen)
	if !ok {
		t.Fatalf("expected *baseScreen, got %T", scr)
	}
	ts, ok := bs.screenImpl.(*tScreen)
	if !ok {
		t.Fatalf("expected *tScreen, got %T", bs.screenImpl)
	}
	if !ts.advancedKeys {
		t.Fatal("advanced keys option was not applied")
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer scr.Fini()
	if !ts.input.advanced {
		t.Fatal("advanced keys option was not propagated to input parser")
	}
}

func TestOptKeyboardProtocol(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
	scr, err := NewTerminfoScreenFromTty(mt, OptKeyboardProtocol(KittyKeyboard))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	bs, ok := scr.(*baseScreen)
	if !ok {
		t.Fatalf("expected *baseScreen, got %T", scr)
	}
	ts, ok := bs.screenImpl.(*tScreen)
	if !ok {
		t.Fatalf("expected *tScreen, got %T", bs.screenImpl)
	}
	if !ts.forceKbd || ts.forcedKbd != KittyKeyboard {
		t.Fatalf("forced keyboard protocol = (%v, %v), want (true, %v)", ts.forceKbd, ts.forcedKbd, KittyKeyboard)
	}
}

func TestOptNegotiation(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
	scr, err := NewTerminfoScreenFromTty(mt, OptNegotiation(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	bs, ok := scr.(*baseScreen)
	if !ok {
		t.Fatalf("expected *baseScreen, got %T", scr)
	}
	ts, ok := bs.screenImpl.(*tScreen)
	if !ok {
		t.Fatalf("expected *tScreen, got %T", bs.screenImpl)
	}
	if ts.negotiate {
		t.Fatal("negotiation option was not applied")
	}
}

func TestSetSizeTakesScreenLock(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
	ts := &tScreen{tty: mt, w: 8, h: 2}

	done := make(chan struct{})
	ts.Lock()
	go func() {
		ts.SetSize(8, 2)
		close(done)
	}()

	select {
	case <-done:
		ts.Unlock()
		t.Fatal("SetSize returned while the screen lock was held")
	case <-time.After(10 * time.Millisecond):
	}

	ts.Unlock()

	select {
	case <-done:
	case <-time.After(time.Second):
		t.Fatal("SetSize did not return after the screen lock was released")
	}
}

func TestOptControlStringLimit(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
	scr, err := NewTerminfoScreenFromTty(mt, OptControlStringLimit(4096))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	bs, ok := scr.(*baseScreen)
	if !ok {
		t.Fatalf("expected *baseScreen, got %T", scr)
	}
	ts, ok := bs.screenImpl.(*tScreen)
	if !ok {
		t.Fatalf("expected *tScreen, got %T", bs.screenImpl)
	}
	if ts.controlStringLimit != 4096 {
		t.Fatalf("control string limit = %d, want %d", ts.controlStringLimit, 4096)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer scr.Fini()
	if ts.input.controlStringMax != 4096 {
		t.Fatalf("input parser control string limit = %d, want %d", ts.input.controlStringMax, 4096)
	}
}

func TestOptControlStringLimitUnlimited(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
	scr, err := NewTerminfoScreenFromTty(mt, OptControlStringLimit(0))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	bs, ok := scr.(*baseScreen)
	if !ok {
		t.Fatalf("expected *baseScreen, got %T", scr)
	}
	ts, ok := bs.screenImpl.(*tScreen)
	if !ok {
		t.Fatalf("expected *tScreen, got %T", bs.screenImpl)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer scr.Fini()

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

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

func TestOptSanitizeContent(t *testing.T) {
	t.Run("disabled by default", func(t *testing.T) {
		mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
		scr, err := NewTerminfoScreenFromTty(mt)
		if err != nil {
			t.Fatalf("failed to get screen: %v", err)
		}
		if err := scr.Init(); err != nil {
			t.Fatalf("failed to initialize screen: %v", err)
		}
		defer scr.Fini()

		scr.PutStr(0, 0, "\x1bA\x07B")
		if got, _, _ := scr.Get(0, 0); !strings.Contains(got, "\x1b") {
			t.Fatalf("expected control bytes to remain when sanitizer is disabled, got %q", got)
		}
		if got, _, _ := scr.Get(1, 0); !strings.Contains(got, "\x07") {
			t.Fatalf("expected control bytes to remain when sanitizer is disabled, got %q", got)
		}
	})

	t.Run("enabled", func(t *testing.T) {
		mt := vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})
		scr, err := NewTerminfoScreenFromTty(mt, OptSanitizeContent(true))
		if err != nil {
			t.Fatalf("failed to get screen: %v", err)
		}
		if err := scr.Init(); err != nil {
			t.Fatalf("failed to initialize screen: %v", err)
		}
		defer scr.Fini()

		scr.PutStr(0, 0, "\x1bA\x07B")
		if got, _, _ := scr.Get(0, 0); got != "A" {
			t.Fatalf("unexpected sanitized cell content at 0,0: %q", got)
		}
		if got, _, _ := scr.Get(1, 0); got != "B" {
			t.Fatalf("unexpected sanitized cell content at 1,0: %q", got)
		}
	})
}

func TestNewScreenSanitizeContentOption(t *testing.T) {
	scr, err := NewScreen(OptSanitizeContent(true))
	if err != nil {
		t.Skipf("failed to get screen: %v", err)
	}
	if err := scr.Init(); err != nil {
		t.Skipf("failed to initialize screen: %v", err)
	}
	defer scr.Fini()

	scr.PutStr(0, 0, "\x1bA\x07B")
	if got, _, _ := scr.Get(0, 0); got != "A" {
		t.Fatalf("unexpected sanitized cell content at 0,0: %q", got)
	}
	if got, _, _ := scr.Get(1, 0); got != "B" {
		t.Fatalf("unexpected sanitized cell content at 1,0: %q", got)
	}
}

func TestNewScreenShimScreen(t *testing.T) {
	_, scr := NewMockScreen(t)
	ShimScreen(scr)

	got, err := NewScreen()
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if got != scr {
		t.Fatalf("unexpected shimmed screen: got %T, want %T", got, scr)
	}
}

func TestUnlockRegionRedrawsUntouchedBlankCell(t *testing.T) {
	mt, scr := NewMockScreen(t, vt.MockOptSize{X: 1, Y: 1})
	defer scr.Fini()

	mt.Backend().Put(vt.Coord{X: 0, Y: 0}, vt.Cell{C: "X", S: vt.BaseStyle, W: 1})
	scr.LockRegion(0, 0, 1, 1, true)
	scr.LockRegion(0, 0, 1, 1, false)
	scr.Show()

	if got := mt.GetCell(vt.Coord{X: 0, Y: 0}); got.C != " " || got.W != 1 {
		t.Fatalf("unlocking an untouched blank cell should repaint a space, got %#v", got)
	}
}

// Redrawing an unchanged wide character after Clear, as immediate-mode
// apps do every frame, must not send it again; a changed one must be sent.
func TestWideCharAfterClear(t *testing.T) {
	t.Setenv("LC_ALL", "en_US.UTF-8")
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 2})}
	s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	frame := func(wide string) string {
		before := len(tty.Output())
		s.Clear()
		s.Put(0, 0, wide, StyleDefault)
		s.Put(2, 0, "a", StyleDefault)
		s.Show()
		return tty.Output()[before:]
	}

	frame("你")
	if out := frame("你"); strings.Contains(out, "你") {
		t.Fatalf("unchanged wide character was sent again: %q", out)
	}
	if out := frame("好"); !strings.Contains(out, "好") {
		t.Fatalf("changed wide character was not sent: %q", out)
	}
	if got := tty.GetCell(vt.Coord{}).C; got != "好" {
		t.Fatalf("terminal shows %q, want %q", got, "好")
	}
	if got := tty.GetCell(vt.Coord{X: 2}).C; got != "a" {
		t.Fatalf("terminal shows %q after the wide character, want %q", got, "a")
	}
}

// Content the UTF-8 fast path must not emit raw still goes through the
// encoder and comes out as "?".  Put strips SUB, but Fill does not.
func TestUnencodableContentDrawsFallback(t *testing.T) {
	t.Setenv("LANG", "en_US.UTF-8")
	t.Setenv("LC_ALL", "en_US.UTF-8")

	for _, tc := range []struct {
		str  string
		draw func(Screen)
	}{
		{"\xff", func(s Screen) { s.Put(0, 0, "\xff", StyleDefault) }},
		{"\xe2\x82", func(s Screen) { s.Put(0, 0, "\xe2\x82", StyleDefault) }},
		{"\x1a", func(s Screen) { s.Fill('\x1a', StyleDefault) }},
	} {
		str := tc.str
		tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
		s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
		if err != nil {
			t.Fatalf("failed to get screen: %v", err)
		}
		if err := s.Init(); err != nil {
			t.Fatalf("failed to initialize screen: %v", err)
		}
		if s.CharacterSet() != "UTF-8" {
			t.Fatalf("Character Set (%v) not UTF-8", s.CharacterSet())
		}

		tc.draw(s)
		s.Show()
		s.Fini()

		if out := tty.Output(); strings.Contains(out, str) {
			t.Errorf("%q was emitted raw: %q", str, out)
		}
		if got := string(tty.GetCell(vt.Coord{}).C); got != "?" {
			t.Errorf("%q drew %q, want %q", str, got, "?")
		}
	}
}

func TestIsASCIISuperset(t *testing.T) {
	for _, tc := range []struct {
		name string
		enc  encoding.Encoding
		want bool
	}{
		{"ASCII", gencoding.ASCII, true},
		{"ISO8859-1", gencoding.ISO8859_1, true},
		{"KOI8-R", charmap.KOI8R, true},
		{"Shift_JIS", japanese.ShiftJIS, true},
		{"GB18030", simplifiedchinese.GB18030, true},
		{"EBCDIC", gencoding.EBCDIC, false},
		{"HZ-GB2312", simplifiedchinese.HZGB2312, false}, // "~" encodes as "~~"
	} {
		if got := isASCIISuperset(tc.enc); got != tc.want {
			t.Errorf("%s: isASCIISuperset = %v, want %v", tc.name, got, tc.want)
		}
	}
}

// On an ASCII screen, ASCII skips the encoder but must come out exactly as
// the encoder would emit it.
func TestASCIIScreenDrawsLikeEncoder(t *testing.T) {
	t.Setenv("LC_ALL", "C")
	if getCharset() != "US-ASCII" {
		t.Skip("no ASCII locale on this platform") // Windows and Plan 9 are always UTF-8
	}

	for _, tc := range []struct {
		str, want string
		draw      func(Screen)
	}{
		{"x", "x", func(s Screen) { s.Put(0, 0, "x", StyleDefault) }},
		{"é", "?", func(s Screen) { s.Put(0, 0, "é", StyleDefault) }},
		{"─", "\x0eq\x0f", func(s Screen) { s.Put(0, 0, "─", StyleDefault) }},
		{"\x1a", "?", func(s Screen) { s.Fill('\x1a', StyleDefault) }},
	} {
		tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
		s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
		if err != nil {
			t.Fatalf("failed to get screen: %v", err)
		}
		if err := s.Init(); err != nil {
			t.Fatalf("failed to initialize screen: %v", err)
		}
		if s.CharacterSet() != "US-ASCII" {
			t.Fatalf("Character Set (%v) not US-ASCII", s.CharacterSet())
		}

		tc.draw(s)
		s.Show()
		s.Fini()

		if out := tty.Output(); !strings.Contains(out, "\x1b[1;1H\x1b[m"+tc.want) {
			t.Errorf("%q: want %q emitted, got %q", tc.str, tc.want, out)
		}
	}
}

func TestOSC8ControlsAreStrippedFromOutput(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	style := StyleDefault.
		Url("http://exa\x07mple.com/\x1b\\path").
		UrlId("id\x00\x1f\x7f\x80\x9fend")

	s.PutStrStyled(0, 0, "X", style)
	s.Show()

	out := tty.Output()
	const prefix = "\x1b]8;id=idend;"
	_, link, ok := strings.Cut(out, prefix)
	if !ok {
		t.Fatalf("missing OSC 8 link open sequence in output: %q", out)
	}
	link, _, ok = strings.Cut(link, "\x1b\\")
	if !ok {
		t.Fatalf("missing OSC 8 terminator in output: %q", out)
	}
	if link != "http://example.com/\\path" {
		t.Fatalf("unexpected emitted URL payload: %q", link)
	}
	if _, afterLink, ok := strings.Cut(out, "X"); !ok || !strings.Contains(afterLink, "\x1b]8;;\x1b\\") {
		t.Fatalf("missing OSC 8 link close sequence after linked content: %q", out)
	}
	for i := 0; i < len(link); i++ {
		c := link[i]
		if c <= 0x1f || c == 0x7f || (c >= 0x80 && c <= 0x9f) {
			t.Fatalf("control characters survived in emitted URL payload: %q", link)
		}
	}
}

func TestOSC8IdWithoutUrlDoesNotEmitClose(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	tty.writes.Reset()
	s.PutStrStyled(0, 0, "X", StyleDefault.UrlId("orphan"))
	s.Show()

	if out := tty.Output(); strings.Contains(out, "\x1b]8;;\x1b\\") {
		t.Fatalf("unexpected OSC 8 close sequence for id-only style: %q", out)
	}
}

func TestKeyboardProtocol(t *testing.T) {
	tests := []struct {
		name  string
		setup func(*tScreen)
		want  KeyProtocol
	}{
		{
			name: "Legacy",
			want: LegacyKeyboard,
		},
		{
			name: "Xterm",
			setup: func(s *tScreen) {
				s.haveXTermKbd = true
			},
			want: XTermKeyboard,
		},
		{
			name: "Kitty",
			setup: func(s *tScreen) {
				s.haveKittyKbd = true
			},
			want: KittyKeyboard,
		},
		{
			name: "Win32",
			setup: func(s *tScreen) {
				s.haveWin32Kbd = true
			},
			want: Win32Keyboard,
		},
		{
			name: "KittyBeforeXterm",
			setup: func(s *tScreen) {
				s.haveKittyKbd = true
				s.haveXTermKbd = true
			},
			want: KittyKeyboard,
		},
		{
			name: "Win32BeforeKittyAndXterm",
			setup: func(s *tScreen) {
				s.haveWin32Kbd = true
				s.haveKittyKbd = true
				s.haveXTermKbd = true
			},
			want: Win32Keyboard,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &tScreen{}
			if tt.setup != nil {
				tt.setup(s)
			}
			if got := s.KeyboardProtocol(); got != tt.want {
				t.Fatalf("KeyboardProtocol() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestApplyKnownTerminalProfile(t *testing.T) {
	tests := []struct {
		name         string
		goos         string
		termProgram  string
		wantKnown    bool
		wantInitted  bool
		wantMouse    bool
		wantMouseSgr bool
		wantKittyKbd bool
		wantWin32Kbd bool
		term         string
		wantLegacy   bool
	}{
		{
			name:         "AppleTerminal",
			goos:         "darwin",
			term:         "xterm-256color",
			termProgram:  "Apple_Terminal",
			wantKnown:    true,
			wantMouse:    true,
			wantMouseSgr: true,
		},
		{
			name:         "LocalWindowsWezTerm",
			goos:         "windows",
			term:         "wezterm",
			termProgram:  "WezTerm",
			wantKnown:    true,
			wantInitted:  true,
			wantMouse:    true,
			wantMouseSgr: true,
			wantWin32Kbd: true,
		},
		{
			name:         "LocalUnixWezTerm",
			goos:         "linux",
			term:         "wezterm",
			termProgram:  "WezTerm",
			wantKnown:    true,
			wantInitted:  true,
			wantMouse:    true,
			wantMouseSgr: true,
			wantKittyKbd: true,
		},
		{
			name:        "UnknownTerminal",
			goos:        "linux",
			term:        "xterm-256color",
			termProgram: "Other",
		},
		{
			name:         "St",
			goos:         "linux",
			term:         "st",
			wantKnown:    true,
			wantLegacy:   true,
			wantMouse:    true,
			wantMouseSgr: true,
		},
		{
			name:         "St256Color",
			goos:         "linux",
			term:         "st-256color",
			wantKnown:    true,
			wantLegacy:   true,
			wantMouse:    true,
			wantMouseSgr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &tScreen{}
			if got := s.applyKnownTerminalProfile(tt.goos, tt.term, tt.termProgram); got != tt.wantKnown {
				t.Fatalf("applyKnownTerminalProfile() = %v, want %v", got, tt.wantKnown)
			}
			if s.initted != tt.wantInitted {
				t.Fatalf("initted = %v, want %v", s.initted, tt.wantInitted)
			}
			if s.haveMouse != tt.wantMouse {
				t.Fatalf("haveMouse = %v, want %v", s.haveMouse, tt.wantMouse)
			}
			if s.haveMouseSgr != tt.wantMouseSgr {
				t.Fatalf("haveMouseSgr = %v, want %v", s.haveMouseSgr, tt.wantMouseSgr)
			}
			if s.haveKittyKbd != tt.wantKittyKbd {
				t.Fatalf("haveKittyKbd = %v, want %v", s.haveKittyKbd, tt.wantKittyKbd)
			}
			if s.haveWin32Kbd != tt.wantWin32Kbd {
				t.Fatalf("haveWin32Kbd = %v, want %v", s.haveWin32Kbd, tt.wantWin32Kbd)
			}
			if s.legacy != tt.wantLegacy {
				t.Fatalf("legacy = %v, want %v", s.legacy, tt.wantLegacy)
			}
		})
	}
}

func TestStProfileSkipsUnsupportedOperations(t *testing.T) {
	t.Setenv("TERM", "st-256color")
	t.Setenv("TERM_PROGRAM", "")

	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	s.EnableMouse()
	s.EnableFocus()
	out := tty.Output()
	for _, seq := range []string{
		requestWindowSize,
		vt.PmResizeReports.Query(),
		vt.PmMouseButton.Query(),
		vt.PmMouseSgr.Query(),
		vt.PmWin32Input.Query(),
		queryKittyKbd,
		queryXTermKbd,
		requestExtAttr,
		// st does not implement pixel-precision (SGR-pixel) mouse
		// reporting, so neither the enable nor the disable sequence for
		// mode 1016 should ever be sent to it.
		vt.PmMouseSgrPixel.Enable(),
		vt.PmMouseSgrPixel.Disable(),
	} {
		if strings.Contains(out, seq) {
			t.Fatalf("st profile emitted unsupported sequence %q", seq)
		}
	}
	if !strings.Contains(out, requestPrimaryDA) {
		t.Fatal("st profile did not emit primary DA")
	}
	// st does implement ordinary SGR mouse (1006) and focus (1004)
	// reporting, so tcell should enable them rather than suppressing them.
	for _, seq := range []string{
		vt.PmMouseButton.Enable(),
		vt.PmMouseSgr.Enable(),
		vt.PmFocusReports.Enable(),
	} {
		if !strings.Contains(out, seq) {
			t.Fatalf("st profile did not emit expected sequence %q", seq)
		}
	}
	name, version := s.Terminal()
	if name != "st" || version != "" {
		t.Fatalf("Terminal() = %q, %q, want %q, %q", name, version, "st", "")
	}

	tScreen := s.(*baseScreen).screenImpl.(*tScreen)
	start := len(tty.Output())
	tScreen.emitUnderline(UnderlineStyleCurly, ColorDefault)
	if got := tty.Output()[start:]; got != underline {
		t.Fatalf("st underline escape = %q, want %q", got, underline)
	}

	start = len(tty.Output())
	s.SetTitle("title")
	s.GetClipboard()
	s.ShowNotification("title", "body")
	if got, want := tty.Output()[start:], "\x1b]2;title\x1b\\"; got != want {
		t.Fatalf("st title, clipboard and notification escapes = %q, want %q", got, want)
	}
}

func TestAppleTerminalProfileSkipsStartupQueries(t *testing.T) {
	t.Setenv("TERM_PROGRAM", "Apple_Terminal")
	t.Setenv("TERM_PROGRAM_VERSION", "999")

	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	out := tty.Output()
	for _, seq := range []string{
		vt.PmResizeReports.Query(),
		vt.PmMouseButton.Query(),
		vt.PmMouseSgr.Query(),
		vt.PmWin32Input.Query(),
		queryKittyKbd,
		queryXTermKbd,
		requestExtAttr,
	} {
		if strings.Contains(out, seq) {
			t.Fatalf("Apple Terminal profile emitted startup query %q", seq)
		}
	}
	if !strings.Contains(out, requestPrimaryDA) {
		t.Fatal("Apple Terminal profile did not emit primary DA")
	}
	name, version := s.Terminal()
	if name != "Terminal.app" || version != "999" {
		t.Fatalf("Terminal() = %q, %q, want %q, %q", name, version, "Terminal.app", "999")
	}
}

func TestKeyboardProbePolicy(t *testing.T) {
	tests := []struct {
		name                string
		goos                string
		wantWindowSizeQuery bool
		wantXTermQuery      bool
	}{
		{
			name:                "Unix",
			goos:                "linux",
			wantWindowSizeQuery: true,
			wantXTermQuery:      true,
		},
		{
			name: "Windows",
			goos: "windows",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := useVTWindowSizeQuery(tt.goos); got != tt.wantWindowSizeQuery {
				t.Fatalf("useVTWindowSizeQuery() = %v, want %v", got, tt.wantWindowSizeQuery)
			}
			if got := useXTermKeyboardQuery(tt.goos); got != tt.wantXTermQuery {
				t.Fatalf("useXTermKeyboardQuery() = %v, want %v", got, tt.wantXTermQuery)
			}
		})
	}
}

func TestKeyboardProtocolHelpers(t *testing.T) {
	tests := []struct {
		name      string
		value     string
		want      KeyProtocol
		wantValid bool
	}{
		{name: "legacy", value: "legacy", want: LegacyKeyboard, wantValid: true},
		{name: "kitty", value: "kitty", want: KittyKeyboard, wantValid: true},
		{name: "win32", value: "win32", want: Win32Keyboard, wantValid: true},
		{name: "xterm", value: "xterm", want: XTermKeyboard, wantValid: true},
		{name: "invalid", value: "bogus"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, ok := parseKeyboardProtocol(tt.value)
			if got != tt.want || ok != tt.wantValid {
				t.Fatalf("parseKeyboardProtocol(%q) = (%v, %v), want (%v, %v)", tt.value, got, ok, tt.want, tt.wantValid)
			}
		})
	}

	if validKeyboardProtocol(KeyProtocol(99)) {
		t.Fatal("invalid keyboard protocol was accepted")
	}
}

func TestForceKeyboardProtocol(t *testing.T) {
	s := &tScreen{
		haveKittyKbd: true,
		haveWin32Kbd: true,
		haveXTermKbd: true,
	}
	if s.forceKeyboardProtocol(KeyProtocol(99)) {
		t.Fatal("invalid keyboard protocol was forced")
	}
	if s.forceKbd {
		t.Fatal("invalid keyboard protocol changed override state")
	}
	if !s.forceKeyboardProtocol(XTermKeyboard) {
		t.Fatal("valid keyboard protocol was not forced")
	}
	s.applyKeyboardProtocolOverride()
	if s.haveKittyKbd || s.haveWin32Kbd || !s.haveXTermKbd {
		t.Fatalf("forced XTerm protocol state = kitty:%v win32:%v xterm:%v", s.haveKittyKbd, s.haveWin32Kbd, s.haveXTermKbd)
	}
}

func TestApplyEnvironmentOverrides(t *testing.T) {
	tests := []struct {
		name             string
		keyboardProtocol string
		negotiate        string
		mouse            string
		startForceKbd    bool
		startForcedKbd   KeyProtocol
		startNegotiate   bool
		wantForceKbd     bool
		wantForcedKbd    KeyProtocol
		wantNegotiate    bool
		wantMouseOff     bool
	}{
		{
			name:           "defaults",
			startNegotiate: true,
			wantNegotiate:  true,
			wantForcedKbd:  LegacyKeyboard,
		},
		{
			name:             "force all",
			keyboardProtocol: "win32",
			negotiate:        "disable",
			mouse:            "disable",
			startNegotiate:   true,
			wantForceKbd:     true,
			wantForcedKbd:    Win32Keyboard,
			wantMouseOff:     true,
		},
		{
			name:             "auto resets options",
			keyboardProtocol: "auto",
			negotiate:        "auto",
			startForceKbd:    true,
			startForcedKbd:   KittyKeyboard,
			wantNegotiate:    true,
			wantForcedKbd:    KittyKeyboard,
		},
		{
			name:             "invalid leaves options",
			keyboardProtocol: "bogus",
			negotiate:        "bogus",
			startForceKbd:    true,
			startForcedKbd:   XTermKeyboard,
			startNegotiate:   false,
			wantForceKbd:     true,
			wantForcedKbd:    XTermKeyboard,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Setenv("TCELL_KEYBOARD_PROTOCOL", tt.keyboardProtocol)
			t.Setenv("TCELL_NEGOTIATE", tt.negotiate)
			t.Setenv("TCELL_MOUSE", tt.mouse)
			s := &tScreen{
				forceKbd:  tt.startForceKbd,
				forcedKbd: tt.startForcedKbd,
				negotiate: tt.startNegotiate,
			}
			s.applyEnvironmentOverrides()
			if s.forceKbd != tt.wantForceKbd {
				t.Fatalf("forceKbd = %v, want %v", s.forceKbd, tt.wantForceKbd)
			}
			if s.forcedKbd != tt.wantForcedKbd {
				t.Fatalf("forcedKbd = %v, want %v", s.forcedKbd, tt.wantForcedKbd)
			}
			if s.negotiate != tt.wantNegotiate {
				t.Fatalf("negotiate = %v, want %v", s.negotiate, tt.wantNegotiate)
			}
			if s.mouseDisabled != tt.wantMouseOff {
				t.Fatalf("mouseDisabled = %v, want %v", s.mouseDisabled, tt.wantMouseOff)
			}
		})
	}
}

func TestForcedKeyboardProtocolSkipsKeyboardQueries(t *testing.T) {
	t.Setenv("TERM_PROGRAM", "")
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptKeyboardProtocol(KittyKeyboard), OptAdvancedKeys(true), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	out := tty.Output()
	for _, seq := range []string{vt.PmWin32Input.Query(), queryKittyKbd, queryXTermKbd} {
		if strings.Contains(out, seq) {
			t.Fatalf("forced keyboard protocol emitted competing query %q", seq)
		}
	}
	for _, seq := range []string{vt.PmResizeReports.Query(), vt.PmMouseButton.Query(), vt.PmMouseSgr.Query(), requestExtAttr} {
		if !strings.Contains(out, seq) {
			t.Fatalf("forced keyboard protocol suppressed unrelated query %q", seq)
		}
	}
	if !strings.Contains(out, enableKittyKbdAdv) {
		t.Fatalf("forced Kitty protocol did not emit advanced enable sequence")
	}
}

func TestNegotiationDisabledSkipsStartupQueries(t *testing.T) {
	t.Setenv("TERM_PROGRAM", "")
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	out := tty.Output()
	for _, seq := range []string{
		requestWindowSize,
		vt.PmResizeReports.Query(),
		vt.PmMouseButton.Query(),
		vt.PmMouseSgr.Query(),
		vt.PmWin32Input.Query(),
		queryKittyKbd,
		queryXTermKbd,
		requestExtAttr,
		requestPrimaryDA,
	} {
		if strings.Contains(out, seq) {
			t.Fatalf("disabled negotiation emitted startup query %q", seq)
		}
	}
}

func TestMouseDisabledPreventsEnablement(t *testing.T) {
	t.Setenv("TCELL_MOUSE", "disable")
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	s.EnableMouse()
	out := tty.Output()
	for _, seq := range []string{vt.PmMouseButton.Enable(), vt.PmMouseDrag.Enable(), vt.PmMouseMotion.Enable(), vt.PmMouseSgr.Enable()} {
		if strings.Contains(out, seq) {
			t.Fatalf("disabled mouse emitted enable sequence %q", seq)
		}
	}
}

func TestMouseWithoutSgrPreventsEnablement(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	ts := s.(*baseScreen).screenImpl.(*tScreen)
	ts.haveMouse = true
	ts.haveMouseSgr = false
	s.EnableMouse()

	out := tty.Output()
	for _, seq := range []string{vt.PmMouseButton.Enable(), vt.PmMouseDrag.Enable(), vt.PmMouseMotion.Enable(), vt.PmMouseSgr.Enable()} {
		if strings.Contains(out, seq) {
			t.Fatalf("mouse without SGR support emitted enable sequence %q", seq)
		}
	}
}

func TestProcessInitQKeyboardProtocol(t *testing.T) {
	tests := []struct {
		name string
		evs  []Event
		want KeyProtocol
	}{
		{
			name: "Legacy",
			want: LegacyKeyboard,
		},
		{
			name: "Xterm",
			evs:  []Event{&eventXTermKbdMode{Mode: XtermKbdModeExt}},
			want: XTermKeyboard,
		},
		{
			name: "Kitty",
			evs:  []Event{&eventKittyKbdMode{Mode: KittyKbdModeBase}},
			want: KittyKeyboard,
		},
		{
			name: "Win32",
			evs:  []Event{&eventPrivateMode{Mode: vt.PmWin32Input, Status: vt.ModeOff}},
			want: Win32Keyboard,
		},
		{
			name: "KittyPreferredOverXterm",
			evs: []Event{
				&eventXTermKbdMode{Mode: XtermKbdModeExt},
				&eventKittyKbdMode{Mode: KittyKbdModeBase},
			},
			want: KittyKeyboard,
		},
		{
			name: "Win32PreferredOverKittyAndXterm",
			evs: []Event{
				&eventXTermKbdMode{Mode: XtermKbdModeExt},
				&eventKittyKbdMode{Mode: KittyKbdModeBase},
				&eventPrivateMode{Mode: vt.PmWin32Input, Status: vt.ModeOff},
			},
			want: Win32Keyboard,
		},
		{
			name: "UnchangeableWin32IsIgnored",
			evs:  []Event{&eventPrivateMode{Mode: vt.PmWin32Input, Status: vt.ModeNA}},
			want: LegacyKeyboard,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &tScreen{
				initQ: make(chan Event, len(tt.evs)+1),
			}
			for _, ev := range tt.evs {
				s.initQ <- ev
			}
			s.initQ <- &eventPrimaryAttributes{}
			s.Lock()
			s.processInitQ()
			s.Unlock()

			if got := s.KeyboardProtocol(); got != tt.want {
				t.Fatalf("KeyboardProtocol() = %v, want %v", got, tt.want)
			}
		})
	}
}

// TestInitScreenStdio just tries to initialize the default screen using standard I/O.
// It requires a working tty.
func TestInitScreenStdio(t *testing.T) {
	tty, err := tty.NewStdIoTty()
	if err != nil {
		t.Skip("maybe stdin is not a tty?")
		return
	}
	s, err := NewTerminfoScreenFromTty(tty)
	if err := s.Init(); err != nil {
		t.Skip("failed to initialize screen", err)
		tty.Close()
		return
	}
	defer s.Fini()

	if s.CharacterSet() != "UTF-8" {
		t.Fatalf("Character Set (%v) not UTF-8", s.CharacterSet())
	}
	drainInput()
}

func TestNotDevNull(t *testing.T) {
	tty, err := tty.NewDevTtyFromDev("/dev/null")
	if err == nil {
		tty.Close()
		t.Error("open /dev/null as tty should not have passed")
	}
}

func TestNoColorEnv(t *testing.T) {
	t.Setenv("NO_COLOR", "1")
	s, err := NewTerminfoScreen()
	if err != nil {
		t.Skip("failed to get screen")
	}
	if err := s.Init(); err != nil {
		t.Skip("failed to initialize screen", err)
	}
	defer s.Fini()

	if s.Colors() != 0 {
		t.Errorf("screen should not have color but had %d", s.Colors())
	}

	drainInput()
}

func NewMockScreen(t *testing.T, opts ...vt.MockOpt) (vt.MockTerm, Screen) {
	t.Helper()
	if runtime.GOOS == "js" {
		t.Skip("not supported on webasm")
		return nil, nil
	}
	mt := vt.NewMockTerm(opts...)
	scr, err := NewTerminfoScreenFromTty(mt)
	if err != nil {
		t.Fatalf("failed to get terminal: %v", err)
		return nil, nil
	}
	if err = scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
		return nil, nil
	}
	return mt, scr
}

func TestSetTitleStripsOSCControls(t *testing.T) {
	mt := vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})
	scr, err := NewTerminfoScreenFromTty(mt)
	if err != nil {
		t.Fatalf("failed to get terminal: %v", err)
	}

	scr.SetTitle("good\x07title\x1b\\end")
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer scr.Fini()

	if got := mt.GetTitle(); got != "goodtitle\\end" {
		t.Fatalf("title not sanitized: %q", got)
	}
}

func TestShowNotificationStripsOSCControls(t *testing.T) {
	mt := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})}
	scr, err := NewTerminfoScreenFromTty(mt)
	if err != nil {
		t.Fatalf("failed to get terminal: %v", err)
	}
	if err := scr.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer scr.Fini()

	before := mt.Output()
	scr.ShowNotification("tit\x07le", "bo\x1b\\dy")
	delta := mt.Output()[len(before):]

	if strings.Contains(delta, "tit\x07le") || strings.Contains(delta, "bo\x1b\\dy") {
		t.Fatalf("notification payload still contains control characters: %q", delta)
	}
	if !strings.Contains(delta, "title") || !strings.Contains(delta, "bo\\dy") {
		t.Fatalf("notification payload missing sanitized strings: %q", delta)
	}
}

// A Show on a suspended screen must return without emitting anything: the
// terminal has been handed back to the caller on disengage, and the released
// cell buffer disagrees with the screen's remembered width and height, which
// used to make the draw scan loop spin forever — while holding the screen
// lock, so a concurrent Resume would deadlock. Show runs in a goroutine so
// that a regression fails the test instead of hanging the suite.
func TestShowWhileSuspendedEmitsNothing(t *testing.T) {
	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 80, Y: 24})}
	s, err := NewTerminfoScreenFromTty(tty)
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	if err := s.Suspend(); err != nil {
		t.Fatalf("failed to suspend screen: %v", err)
	}

	baseline := len(tty.Output())

	done := make(chan struct{})
	go func() {
		s.Show()
		close(done)
	}()
	select {
	case <-done:
	case <-time.After(5 * time.Second):
		t.Fatal("Show hung on a suspended screen")
	}

	if emitted := tty.Output()[baseline:]; emitted != "" {
		t.Fatalf("Show on a suspended screen emitted %q", emitted)
	}

	if err := s.Resume(); err != nil {
		t.Fatalf("failed to resume screen: %v", err)
	}
	s.Fini()
}

func TestScreenFillArea(t *testing.T) {
	_, s := NewMockScreen(t, vt.MockOptSize{X: 20, Y: 10})
	defer s.Fini()

	style := StyleDefault.Foreground(ColorGreen).Background(ColorBlack)
	s.FillArea(2, 3, 4, 2, '*', style)

	// Inside the region.
	if got, gotStyle, _ := s.Get(3, 3); got != "*" || gotStyle != style {
		t.Errorf("filled cell wrong: got %q style=%v", got, gotStyle)
	}
	if got, _, _ := s.Get(5, 4); got != "*" {
		t.Errorf("bottom-right corner not filled: got %q", got)
	}
	// Just outside the region should still be blank.
	if got, _, _ := s.Get(6, 3); got != " " {
		t.Errorf("cell past the region should be blank, got %q", got)
	}
	if got, _, _ := s.Get(3, 5); got != " " {
		t.Errorf("cell below the region should be blank, got %q", got)
	}

	// Coordinates that overflow the screen must not panic and must be clipped.
	s.FillArea(18, 8, 100, 100, '#', style)
	if got, _, _ := s.Get(19, 9); got != "#" {
		t.Errorf("overflowing fill should paint in-bounds cells, got %q", got)
	}
}

type fakeConsoleMouse struct {
	startErr error
	started  int
	stopped  int
	flags    MouseFlags
	sizes    []tty.WindowSize
}

func (m *fakeConsoleMouse) Start(flags MouseFlags) error {
	if m.startErr != nil {
		return m.startErr
	}
	m.started++
	m.flags = flags
	return nil
}

func (m *fakeConsoleMouse) Stop() {
	m.stopped++
}

func (m *fakeConsoleMouse) SetSize(w, h int) {
	m.sizes = append(m.sizes, tty.WindowSize{Width: w, Height: h})
}

func withConsoleMouseFactory(t *testing.T, f consoleMouseFactory) {
	t.Helper()
	old := newConsoleMouse
	newConsoleMouse = f
	t.Cleanup(func() { newConsoleMouse = old })
}

func TestNativeConsoleMouseSuppressesVTMouseEnable(t *testing.T) {
	fm := &fakeConsoleMouse{}
	withConsoleMouseFactory(t, func(chan<- Event) (consoleMouse, error) {
		return fm, nil
	})

	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	tty.writes.Reset()
	s.EnableMouse(MouseButtonEvents)
	if fm.started != 1 {
		t.Fatalf("native console mouse was not started: %d", fm.started)
	}
	if fm.flags != MouseButtonEvents {
		t.Fatalf("native console mouse got flags %x", fm.flags)
	}
	out := tty.Output()
	for _, seq := range []string{vt.PmMouseButton.Enable(), vt.PmMouseDrag.Enable(), vt.PmMouseMotion.Enable(), vt.PmMouseSgr.Enable(), vt.PmMouseSgrPixel.Enable()} {
		if strings.Contains(out, seq) {
			t.Fatalf("native console mouse emitted VT mouse enable sequence %q", seq)
		}
	}
}

func TestNativeConsoleMouseFailureFallsBackToVTMouse(t *testing.T) {
	withConsoleMouseFactory(t, func(chan<- Event) (consoleMouse, error) {
		return nil, errNoConsoleMouse
	})

	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	tty.writes.Reset()
	s.EnableMouse(MouseButtonEvents)
	out := tty.Output()
	for _, seq := range []string{vt.PmMouseButton.Enable(), vt.PmMouseSgr.Enable()} {
		if !strings.Contains(out, seq) {
			t.Fatalf("native console mouse failure did not fall back to VT sequence %q", seq)
		}
	}
}

func TestNativeConsoleMouseStopsOnDisable(t *testing.T) {
	fm := &fakeConsoleMouse{}
	withConsoleMouseFactory(t, func(chan<- Event) (consoleMouse, error) {
		return fm, nil
	})

	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	s.EnableMouse(MouseMotionEvents)
	s.DisableMouse()
	if fm.started != 1 {
		t.Fatalf("native console mouse starts = %d", fm.started)
	}
	if fm.stopped != 1 {
		t.Fatalf("native console mouse stops = %d", fm.stopped)
	}
}

func TestMouseDisabledPreventsNativeConsoleMouse(t *testing.T) {
	t.Setenv("TCELL_MOUSE", "disable")
	fm := &fakeConsoleMouse{}
	withConsoleMouseFactory(t, func(chan<- Event) (consoleMouse, error) {
		return fm, nil
	})

	tty := &spyTty{MockTerm: vt.NewMockTerm(vt.MockOptSize{X: 8, Y: 5})}
	s, err := NewTerminfoScreenFromTty(tty, OptNegotiation(false), OptAltScreen(false))
	if err != nil {
		t.Fatalf("failed to get screen: %v", err)
	}
	if err := s.Init(); err != nil {
		t.Fatalf("failed to initialize screen: %v", err)
	}
	defer s.Fini()

	s.EnableMouse()
	if fm.started != 0 {
		t.Fatalf("native console mouse started despite TCELL_MOUSE=disable")
	}
}