package main

import (
	"strings"
	"testing"

	tea "github.com/charmbracelet/bubbletea"
)

// newTestAppModel создаёт AppModel и сразу подтверждает язык по
// умолчанию (первый пункт — русский), чтобы остальные тесты меню
// могли считать, что стартуют сразу в stateMenu, как и раньше.
// TestQReturnsToMenuForAllGames проверяет, что после запуска КАЖДОЙ
// игры коллекции нажатие 'q' возвращает в главное меню. Существует
// специально из-за реальной регрессии: переключатель типов,
// проверяющий backToMenu после m.activeGame.Update(...), несколько
// раз "забывал" недавно добавленные игры (крестики-нолики, нарды,
// Сенет, Ур) — сама модель корректно выставляла backToMenu=true, но
// AppModel никогда этого не замечал, так что 'q' в этих играх молча
// ничего не делал.
func TestQReturnsToMenuForAllGames(t *testing.T) {
	games := []struct {
		name  string
		build func() tea.Model
	}{
		{"tonk", func() tea.Model { return NewModel() }},
		{"durak", func() tea.Model { return NewDurakModel(2) }},
		{"blackjack", func() tea.Model { return NewBlackjackModel(1, 500, 10) }},
		{"101", func() tea.Model { return NewOneOhOneModel(2) }},
		{"1000", func() tea.Model { return NewThousandModel() }},
		{"klondike", func() tea.Model { return NewKlondikeModel() }},
		{"checkers", func() tea.Model { return NewCheckersModel(CheckersDifficultyEasy, ColorChoiceA) }},
		{"chess", func() tea.Model { return NewChessModel(CheckersDifficultyEasy, ColorChoiceA) }},
		{"corpsestarch", func() tea.Model { return NewCorpseStarchModel() }},
		{"go", func() tea.Model { return NewGoModel(9, GoDifficultyEasy, ColorChoiceA) }},
		{"minesweeper", func() tea.Model { return NewMinesweeperModel(9, 9, 10) }},
		{"wayfarer", func() tea.Model { return NewWayfarerModel() }},
		{"tictactoe", func() tea.Model { return NewTicTacToeModel(TicTacToeDifficultyEasy) }},
		{"nardy", func() tea.Model { return NewNardyModel(NardyShort, NardyDifficultyEasy, ColorChoiceA) }},
		{"senet", func() tea.Model { return NewSenetModel(SenetDifficultyEasy, ColorChoiceA) }},
		{"ur", func() tea.Model { return NewUrModel(UrDifficultyEasy, ColorChoiceA) }},
		{"poker", func() tea.Model { return NewPokerModel(3) }},
	}

	for _, g := range games {
		t.Run(g.name, func(t *testing.T) {
			m := newTestAppModel()
			m.state = statePlaying
			m.activeGame = g.build()

			next, _ := m.Update(key("q"))
			m2 := next.(AppModel)
			if m2.state != stateMenu {
				t.Errorf("%s: 'q' должен был вернуть в главное меню, получено state=%v", g.name, m2.state)
			}
		})
	}
}

func newTestAppModel() AppModel {
	m := NewAppModel()
	next, _ := m.Update(key("enter"))
	return next.(AppModel)
}

func TestLanguageScreenIsInitialState(t *testing.T) {
	SetLanguage(LangRU)
	m := NewAppModel()
	if m.state != stateLanguage {
		t.Fatalf("ожидалось начальное состояние stateLanguage, получено %v", m.state)
	}
}

func TestLanguageScreenSelectEnglish(t *testing.T) {
	defer SetLanguage(LangRU) // не влияем на остальные тесты пакета
	m := NewAppModel()
	if m.langCursor != 0 {
		t.Fatalf("ожидался курсор на первом пункте (русский), получено %d", m.langCursor)
	}

	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	if m.langCursor != 1 {
		t.Fatalf("ожидался курсор на английском после down, получено %d", m.langCursor)
	}

	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("ожидался переход в stateMenu после выбора языка")
	}
	if CurrentLang() != LangEN {
		t.Fatalf("ожидался установленный язык LangEN")
	}
	if T("menu.quit") != "Quit" {
		t.Errorf("после выбора английского T(\"menu.quit\") должен вернуть 'Quit', получено %q", T("menu.quit"))
	}
}

func TestLanguageScreenDefaultsToRussian(t *testing.T) {
	defer SetLanguage(LangRU)
	m := NewAppModel()
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("ожидался переход в stateMenu")
	}
	if CurrentLang() != LangRU {
		t.Fatalf("ожидался язык по умолчанию LangRU")
	}
}

func TestMenuHasLanguageOption(t *testing.T) {
	m := newTestAppModel()
	found := false
	for _, opt := range m.options {
		if opt.kind == "language" {
			found = true
		}
	}
	if !found {
		t.Fatalf("ожидался пункт меню для смены языка")
	}
}

func TestMenuScrollFollowsCursorDown(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 20 // тесный терминал — гарантированно меньше пунктов, чем видно целиком
	visible := m.menuVisibleItems()
	if visible >= len(m.options) {
		t.Fatalf("тест предполагает, что не все пункты помещаются сразу")
	}

	for i := 0; i < len(m.options)-1; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
		if m.cursor < m.menuScroll || m.cursor >= m.menuScroll+visible {
			t.Fatalf("курсор %d выпал из видимого окна [%d,%d) после шага %d",
				m.cursor, m.menuScroll, m.menuScroll+visible, i)
		}
	}
}

func TestMenuScrollFollowsCursorUp(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 20
	visible := m.menuVisibleItems()

	for i := 0; i < len(m.options)-1; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	for i := 0; i < len(m.options)-1; i++ {
		next, _ := m.Update(key("up"))
		m = next.(AppModel)
		if m.cursor < m.menuScroll || m.cursor >= m.menuScroll+visible {
			t.Fatalf("курсор %d выпал из видимого окна [%d,%d) при прокрутке вверх",
				m.cursor, m.menuScroll, m.menuScroll+visible)
		}
	}
	if m.menuScroll != 0 {
		t.Errorf("после возврата к самому первому пункту прокрутка должна была вернуться к 0, получено %d", m.menuScroll)
	}
}

func TestMenuScrollIndicatorsAppearWhenScrolled(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 20

	view := m.viewMenu()
	if strings.Contains(view, "▲") {
		t.Errorf("в самом начале списка индикатор 'выше' не должен показываться")
	}
	if !strings.Contains(view, "▼") {
		t.Errorf("при неполном списке индикатор 'ниже' должен показываться")
	}

	for i := 0; i < len(m.options)-1; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	view = m.viewMenu()
	if !strings.Contains(view, "▲") {
		t.Errorf("в конце списка индикатор 'выше' должен показываться")
	}
	if strings.Contains(view, "▼") {
		t.Errorf("в конце списка индикатор 'ниже' не должен показываться")
	}
}

func TestMenuCompactBannerOnSmallTerminal(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 20
	if !m.menuUsesCompactBanner() {
		t.Errorf("на тесном терминале должен использоваться компактный баннер")
	}
	view := m.viewMenu()
	if !strings.Contains(view, "GO GAMES COLLECTION") {
		t.Errorf("компактный заголовок должен присутствовать в рендере")
	}
}

func TestMenuFullBannerOnLargeTerminal(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 60
	if m.menuUsesCompactBanner() {
		t.Errorf("на просторном терминале должен использоваться полный баннер")
	}
	if m.menuVisibleItems() < len(m.options) {
		t.Errorf("на просторном терминале весь список должен помещаться целиком")
	}
}

func TestRulesMenuScrollFollowsCursor(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 12
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)
	if m.state != stateRulesMenu {
		t.Fatalf("ожидалось состояние stateRulesMenu")
	}

	visible := m.rulesMenuVisibleItems()
	if visible >= len(gameRegistry) {
		t.Fatalf("тест предполагает, что не все игры помещаются сразу при высоте 12")
	}

	for i := 0; i < len(gameRegistry)-1; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
		if m.rulesCursor < m.rulesMenuScroll || m.rulesCursor >= m.rulesMenuScroll+visible {
			t.Fatalf("курсор правил %d выпал из видимого окна [%d,%d)",
				m.rulesCursor, m.rulesMenuScroll, m.rulesMenuScroll+visible)
		}
	}
}

func TestMenuScrollRecalculatedOnResize(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 50 // просторный терминал — весь список виден, курсор можно увести глубоко
	for i := 0; i < len(m.options)-1; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	if m.cursor != len(m.options)-1 {
		t.Fatalf("ожидался курсор на последнем пункте")
	}

	// пользователь на лету уменьшает терминал — курсор должен остаться видимым
	next, _ := m.Update(tea.WindowSizeMsg{Width: 80, Height: 15})
	m = next.(AppModel)
	visible := m.menuVisibleItems()
	if m.cursor < m.menuScroll || m.cursor >= m.menuScroll+visible {
		t.Errorf("после уменьшения терминала курсор %d должен был остаться в видимом окне [%d,%d)",
			m.cursor, m.menuScroll, m.menuScroll+visible)
	}
}

func TestMenuNavigation(t *testing.T) {
	m := newTestAppModel()
	if m.state != stateMenu {
		t.Fatalf("ожидалось начальное состояние stateMenu")
	}
	if len(m.options) != len(gameRegistry)+3 {
		t.Fatalf("ожидалось %d пунктов меню, получено %d", len(gameRegistry)+3, len(m.options))
	}

	next, _ := m.Update(key("down"))
	nm := next.(AppModel)
	if nm.cursor != 1 {
		t.Fatalf("ожидался курсор на позиции 1, получено %d", nm.cursor)
	}

	next, _ = nm.Update(key("up"))
	nm = next.(AppModel)
	if nm.cursor != 0 {
		t.Fatalf("ожидался курсор на позиции 0 после возврата, получено %d", nm.cursor)
	}
}

func TestMenuLeadsToStakeSettings(t *testing.T) {
	m := newTestAppModel()
	// первый пункт меню — первая (и пока единственная) игра, которая
	// использует ставки -> должен открыться экран настроек, а не игра
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateSettings {
		t.Fatalf("ожидалось состояние stateSettings после выбора игры со ставками")
	}
	if m.activeGame != nil {
		t.Fatalf("игра не должна была запуститься до подтверждения настроек")
	}
}

func TestStakeSettingsAdjustAndStart(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("enter")) // входим в настройки
	m = next.(AppModel)

	initialCapital := m.settings.capital()
	next, _ = m.Update(key("right")) // увеличиваем капитал на шаг пресета
	m = next.(AppModel)
	if m.settings.capital() == initialCapital {
		t.Fatalf("значение капитала должно было измениться после [right]")
	}

	next, _ = m.Update(key("down")) // переходим к полю анте
	m = next.(AppModel)
	if m.settings.fieldCursor != 1 {
		t.Fatalf("ожидался курсор на поле анте (1), получено %d", m.settings.fieldCursor)
	}

	chosenCapital := m.settings.capital()
	chosenAnte := m.settings.ante()
	chosenMult := m.settings.multiplier()

	next, _ = m.Update(key("enter")) // подтверждаем и запускаем игру
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось состояние statePlaying после подтверждения настроек")
	}
	tm, ok := m.activeGame.(Model)
	if !ok {
		t.Fatalf("активная игра должна быть моделью Тонка")
	}
	if tm.game.StartingCapital != chosenCapital {
		t.Errorf("стартовый капитал в игре (%d) не совпадает с выбранным (%d)",
			tm.game.StartingCapital, chosenCapital)
	}
	if tm.game.AnteAmount != chosenAnte {
		t.Errorf("анте в игре (%d) не совпадает с выбранным (%d)", tm.game.AnteAmount, chosenAnte)
	}
	if tm.game.TonkMultiplier != chosenMult {
		t.Errorf("множитель в игре (%d) не совпадает с выбранным (%d)", tm.game.TonkMultiplier, chosenMult)
	}
}

func TestStakeSettingsEscReturnsToMenu(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateSettings {
		t.Fatalf("ожидался экран настроек")
	}
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("esc на экране настроек должен вернуть в меню")
	}
}

func TestStakeSettingsCycleClampsAtBounds(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)

	// уводим значение влево много раз — не должно уйти за границу
	// списка пресетов (индекс не может стать отрицательным)
	for i := 0; i < 10; i++ {
		next, _ = m.Update(key("left"))
		m = next.(AppModel)
	}
	if m.settings.capitalIdx != 0 {
		t.Errorf("индекс капитала должен был остановиться на 0, получено %d", m.settings.capitalIdx)
	}
}

func TestMenuShowsRules(t *testing.T) {
	m := newTestAppModel()
	// переходим к пункту "Правила" (сразу после списка игр)
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRulesMenu {
		t.Fatalf("ожидалось состояние stateRulesMenu (подменю выбора игры)")
	}

	// в подменю правил курсор по умолчанию на первой игре (Тонк)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "ТОНК") {
		t.Fatalf("текст правил должен содержать описание Тонка")
	}

	// esc из текста правил возвращает в подменю выбора игры, а не сразу в меню
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateRulesMenu {
		t.Fatalf("ожидалось возвращение в stateRulesMenu после esc из текста правил")
	}

	// esc из подменю возвращает в главное меню
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("ожидалось возвращение в stateMenu после esc из подменю правил")
	}
}

func TestMenuShowsDurakRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	next, _ = m.Update(key("down")) // выбираем вторую игру (Дурак)
	m = next.(AppModel)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "ДУРАК") {
		t.Fatalf("текст правил должен содержать описание Дурака, получено начало: %.50s", m.rulesText)
	}
}

func TestMenuLeadsToPlayerCountForDurak(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down")) // второй пункт меню — Дурак
	m = next.(AppModel)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlayerCount {
		t.Fatalf("ожидалось состояние statePlayerCount после выбора Дурака")
	}
	if m.activeGame != nil {
		t.Fatalf("игра не должна была запуститься до подтверждения числа игроков")
	}
}

func TestPlayerCountAdjustAndStart(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // экран выбора числа игроков
	m = next.(AppModel)

	if m.playerCount.count != 4 {
		t.Fatalf("ожидалось значение по умолчанию 4, получено %d", m.playerCount.count)
	}
	next, _ = m.Update(key("right"))
	m = next.(AppModel)
	if m.playerCount.count != 5 {
		t.Fatalf("ожидалось 5 после [right], получено %d", m.playerCount.count)
	}

	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying после подтверждения числа игроков")
	}
	dm, ok := m.activeGame.(DurakModel)
	if !ok {
		t.Fatalf("активная игра должна быть моделью Дурака")
	}
	if len(dm.game.Players) != 5 {
		t.Errorf("ожидалось 5 игроков в запущенной партии, получено %d", len(dm.game.Players))
	}
}

func TestPlayerCountClampsAtBounds(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)

	for i := 0; i < 10; i++ {
		next, _ = m.Update(key("left"))
		m = next.(AppModel)
	}
	if m.playerCount.count != 2 {
		t.Errorf("число игроков должно было остановиться на 2, получено %d", m.playerCount.count)
	}
	for i := 0; i < 10; i++ {
		next, _ = m.Update(key("right"))
		m = next.(AppModel)
	}
	if m.playerCount.count != 6 {
		t.Errorf("число игроков должно было остановиться на 6, получено %d", m.playerCount.count)
	}
}

func TestPlayerCountEscReturnsToMenu(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("esc на экране выбора числа игроков должен вернуть в меню")
	}
}

func TestDurakQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // экран числа игроков
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // запускаем Дурака
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsBlackjackRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Блэкджек — третья запись в реестре (индекс 2)
	next, _ = m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "БЛЭКДЖЕК") {
		t.Fatalf("текст правил должен содержать описание Блэкджека, получено начало: %.50s", m.rulesText)
	}
}

func TestMenuLeadsToBlackjackSettings(t *testing.T) {
	m := newTestAppModel()
	// Блэкджек — третий пункт меню (после Тонка и Дурака)
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateBlackjackSettings {
		t.Fatalf("ожидалось состояние stateBlackjackSettings после выбора Блэкджека")
	}
	if m.activeGame != nil {
		t.Fatalf("игра не должна была запуститься до подтверждения настроек")
	}
}

func TestBlackjackSettingsAdjustAndStart(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // экран настроек
	m = next.(AppModel)

	if m.blackjack.playerCount != 1 {
		t.Fatalf("ожидалось значение по умолчанию 1 игрок, получено %d", m.blackjack.playerCount)
	}
	next, _ = m.Update(key("right"))
	m = next.(AppModel)
	if m.blackjack.playerCount != 2 {
		t.Fatalf("ожидалось 2 после [right], получено %d", m.blackjack.playerCount)
	}

	next, _ = m.Update(key("down")) // поле капитала
	m = next.(AppModel)
	if m.blackjack.fieldCursor != 1 {
		t.Fatalf("ожидался курсор на поле капитала (1), получено %d", m.blackjack.fieldCursor)
	}
	chosenPlayers := m.blackjack.playerCount
	chosenCapital := m.blackjack.capital()
	chosenBet := m.blackjack.bet()

	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying после подтверждения настроек")
	}
	bm, ok := m.activeGame.(BlackjackModel)
	if !ok {
		t.Fatalf("активная игра должна быть моделью Блэкджека")
	}
	if len(bm.game.Players) != chosenPlayers {
		t.Errorf("число игроков в игре (%d) не совпадает с выбранным (%d)", len(bm.game.Players), chosenPlayers)
	}
	if bm.game.StartingCapital != chosenCapital {
		t.Errorf("капитал в игре (%d) не совпадает с выбранным (%d)", bm.game.StartingCapital, chosenCapital)
	}
	if bm.game.Bet != chosenBet {
		t.Errorf("ставка в игре (%d) не совпадает с выбранной (%d)", bm.game.Bet, chosenBet)
	}
}

func TestBlackjackSettingsEscReturnsToMenu(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("esc на экране настроек Блэкджека должен вернуть в меню")
	}
}

func TestBlackjackQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("down"))
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // настройки
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // запускаем игру
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsOneOhOneRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// 101 — четвёртая запись в реестре (индекс 3)
	for i := 0; i < 3; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "101") {
		t.Fatalf("текст правил должен содержать описание 101, получено начало: %.50s", m.rulesText)
	}
}

func TestMenuLeadsToPlayerCountForOneOhOne(t *testing.T) {
	m := newTestAppModel()
	// 101 — четвёртый пункт меню
	for i := 0; i < 3; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlayerCount {
		t.Fatalf("ожидалось состояние statePlayerCount после выбора 101")
	}
}

func TestOneOhOneQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 3; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран числа игроков
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // запускаем игру
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsThousandRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// 1000 — пятая запись в реестре (индекс 4)
	for i := 0; i < 4; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "ТЫСЯЧА") {
		t.Fatalf("текст правил должен содержать описание Тысячи, получено начало: %.50s", m.rulesText)
	}
}

func TestThousandStartsDirectlyWithoutSettingsScreen(t *testing.T) {
	m := newTestAppModel()
	// 1000 — пятый пункт меню, запускается сразу (без экрана настроек)
	for i := 0; i < 4; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось немедленное statePlaying для 1000, получено %v", m.state)
	}
	if _, ok := m.activeGame.(ThousandModel); !ok {
		t.Fatalf("активная игра должна быть моделью 1000")
	}
}

func TestThousandQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 4; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsKlondikeRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Косынка — шестая запись в реестре (индекс 5)
	for i := 0; i < 5; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "КОСЫНКА") {
		t.Fatalf("текст правил должен содержать описание Косынки, получено начало: %.50s", m.rulesText)
	}
}

func TestKlondikeStartsDirectlyWithoutSettingsScreen(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 5; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось немедленное statePlaying для Косынки, получено %v", m.state)
	}
	if _, ok := m.activeGame.(KlondikeModel); !ok {
		t.Fatalf("активная игра должна быть моделью Косынки")
	}
}

func TestKlondikeQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 5; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsCheckersRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Шашки — седьмая запись в реестре (индекс 6)
	for i := 0; i < 6; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "ШАШКИ") {
		t.Fatalf("текст правил должен содержать описание Шашек, получено начало: %.50s", m.rulesText)
	}
}

func TestCheckersLeadsToDifficultySettings(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 6; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateCheckersSettings {
		t.Fatalf("ожидалось состояние stateCheckersSettings для Шашек, получено %v", m.state)
	}
	if m.activeGame != nil {
		t.Fatalf("игра не должна была запуститься до подтверждения уровня сложности")
	}
}

func TestCheckersDifficultySelectionAndStart(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 6; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран выбора сложности
	m = next.(AppModel)
	if m.checkers.levelIdx != 0 {
		t.Fatalf("ожидался уровень по умолчанию 0 (Простой), получено %d", m.checkers.levelIdx)
	}

	next, _ = m.Update(key("right")) // переключаемся на "Сильный"
	m = next.(AppModel)
	if m.checkers.levelIdx != 1 {
		t.Fatalf("ожидался уровень 1 (Сильный) после переключения, получено %d", m.checkers.levelIdx)
	}

	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying после подтверждения уровня")
	}
	cm, ok := m.activeGame.(CheckersModel)
	if !ok {
		t.Fatalf("активная игра должна быть моделью Шашек")
	}
	if cm.bot.Depth != CheckersDifficultyHard {
		t.Errorf("ожидалась глубина бота %d (Сильный), получено %d", CheckersDifficultyHard, cm.bot.Depth)
	}
}

func TestCheckersDifficultyEscReturnsToMenu(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 6; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("esc на экране сложности должен вернуть в меню")
	}
}

func TestCheckersQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 6; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран сложности
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // запускаем игру
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsCorpseStarchRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Трупные батончики — девятая запись в реестре (индекс 8)
	for i := 0; i < 8; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "ТРУПНЫЕ БАТОНЧИКИ") {
		t.Fatalf("текст правил должен содержать описание игры, получено начало: %.50s", m.rulesText)
	}
}

func TestCorpseStarchStartsDirectlyWithoutSettingsScreen(t *testing.T) {
	withTempConfigDir(t)
	m := newTestAppModel()
	for i := 0; i < 8; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось немедленное statePlaying, получено %v", m.state)
	}
	if _, ok := m.activeGame.(CorpseStarchModel); !ok {
		t.Fatalf("активная игра должна быть моделью Трупных батончиков")
	}
}

func TestCorpseStarchQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	withTempConfigDir(t)
	m := newTestAppModel()
	for i := 0; i < 8; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsWayfarerRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Странник — двенадцатая запись в реестре (индекс 11)
	for i := 0; i < 11; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "СТРАННИК") {
		t.Fatalf("текст правил должен содержать описание Странника, получено начало: %.50s", m.rulesText)
	}
}

func TestWayfarerStartsDirectlyWithoutSettingsScreen(t *testing.T) {
	withTempConfigDir(t)
	m := newTestAppModel()
	for i := 0; i < 11; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось немедленное statePlaying, получено %v", m.state)
	}
	if _, ok := m.activeGame.(WayfarerModel); !ok {
		t.Fatalf("активная игра должна быть моделью Странника")
	}
}

func TestWayfarerQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	withTempConfigDir(t)
	m := newTestAppModel()
	for i := 0; i < 11; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsMinesweeperRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Сапёр — одиннадцатая запись в реестре (индекс 10)
	for i := 0; i < 10; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "САПЁР") {
		t.Fatalf("текст правил должен содержать описание Сапёра, получено начало: %.50s", m.rulesText)
	}
}

func TestMinesweeperLeadsToSettings(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 10; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateMinesweeperSettings {
		t.Fatalf("ожидалось состояние stateMinesweeperSettings, получено %v", m.state)
	}
	if m.activeGame != nil {
		t.Fatalf("игра не должна была запуститься до подтверждения пресета")
	}
}

func TestMinesweeperSettingsSelectionAndStart(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 10; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран настроек
	m = next.(AppModel)
	if m.minesweeper.presetIdx != 0 {
		t.Fatalf("ожидался пресет по умолчанию 0 (Новичок), получено %d", m.minesweeper.presetIdx)
	}

	next, _ = m.Update(key("down")) // Новичок -> Любитель
	m = next.(AppModel)
	if m.minesweeper.presetIdx != 1 {
		t.Fatalf("ожидался пресет 1 (Любитель) после переключения, получено %d", m.minesweeper.presetIdx)
	}

	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying после подтверждения пресета")
	}
	mm, ok := m.activeGame.(MinesweeperModel)
	if !ok {
		t.Fatalf("активная игра должна быть моделью Сапёра")
	}
	if mm.game.Width != 16 || mm.game.Height != 16 || mm.game.MineCount != 40 {
		t.Errorf("ожидался пресет 'Любитель' (16x16, 40 мин) в запущенной игре, получено %dx%d/%d",
			mm.game.Width, mm.game.Height, mm.game.MineCount)
	}
}

func TestMinesweeperSettingsEscReturnsToMenu(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 10; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("esc на экране настроек должен вернуть в меню")
	}
}

func TestMinesweeperQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 10; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран настроек
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // запускаем игру
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsGoRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Го — десятая запись в реестре (индекс 9)
	for i := 0; i < 9; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "ГО (GO)") {
		t.Fatalf("текст правил должен содержать описание Го, получено начало: %.50s", m.rulesText)
	}
}

func TestGoLeadsToSettings(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 9; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateGoSettings {
		t.Fatalf("ожидалось состояние stateGoSettings для Го, получено %v", m.state)
	}
	if m.activeGame != nil {
		t.Fatalf("игра не должна была запуститься до подтверждения настроек")
	}
}

func TestGoSettingsSelectionAndStart(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 9; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран настроек
	m = next.(AppModel)
	if m.goSetup.size() != 9 {
		t.Fatalf("ожидался размер доски по умолчанию 9, получено %d", m.goSetup.size())
	}

	next, _ = m.Update(key("right")) // 9 -> 13
	m = next.(AppModel)
	if m.goSetup.size() != 13 {
		t.Fatalf("ожидался размер доски 13 после переключения, получено %d", m.goSetup.size())
	}

	next, _ = m.Update(key("down")) // переходим к полю сложности
	m = next.(AppModel)
	if m.goSetup.fieldCursor != 1 {
		t.Fatalf("ожидался курсор на поле сложности (1), получено %d", m.goSetup.fieldCursor)
	}
	next, _ = m.Update(key("right")) // Простой -> Средний
	m = next.(AppModel)
	if m.goSetup.difficulty() != GoDifficultyMedium {
		t.Fatalf("ожидалась сложность Средний после переключения")
	}

	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying после подтверждения настроек")
	}
	gm, ok := m.activeGame.(GoModel)
	if !ok {
		t.Fatalf("активная игра должна быть моделью Го")
	}
	if gm.game.Size != 13 {
		t.Errorf("ожидался размер доски 13 в запущенной игре, получено %d", gm.game.Size)
	}
	if gm.bot.Difficulty != GoDifficultyMedium {
		t.Errorf("ожидалась сложность Средний в запущенной игре")
	}
}

func TestGoSettingsEscReturnsToMenu(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 9; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("esc на экране настроек должен вернуть в меню")
	}
}

func TestGoQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 9; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран настроек
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // запускаем игру
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestMenuShowsChessRules(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < len(gameRegistry); i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // подменю правил
	m = next.(AppModel)

	// Шахматы — восьмая запись в реестре (индекс 7)
	for i := 0; i < 7; i++ {
		next, _ = m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}
	if !strings.Contains(m.rulesText, "ШАХМАТЫ") {
		t.Fatalf("текст правил должен содержать описание Шахмат, получено начало: %.50s", m.rulesText)
	}
}

func TestChessLeadsToDifficultySettings(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 7; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != stateChessSettings {
		t.Fatalf("ожидалось состояние stateChessSettings для Шахмат, получено %v", m.state)
	}
	if m.activeGame != nil {
		t.Fatalf("игра не должна была запуститься до подтверждения уровня сложности")
	}
}

func TestChessDifficultySelectionAndStart(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 7; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран выбора сложности
	m = next.(AppModel)
	if m.chess.levelIdx != 0 {
		t.Fatalf("ожидался уровень по умолчанию 0 (Простой), получено %d", m.chess.levelIdx)
	}

	next, _ = m.Update(key("right")) // переключаемся на "Сильный"
	m = next.(AppModel)
	if m.chess.levelIdx != 1 {
		t.Fatalf("ожидался уровень 1 (Сильный) после переключения, получено %d", m.chess.levelIdx)
	}

	next, _ = m.Update(key("enter"))
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying после подтверждения уровня")
	}
	cm, ok := m.activeGame.(ChessModel)
	if !ok {
		t.Fatalf("активная игра должна быть моделью Шахмат")
	}
	if cm.bot.Depth != ChessDifficultyHard {
		t.Errorf("ожидалась глубина бота %d (Сильный), получено %d", ChessDifficultyHard, cm.bot.Depth)
	}
}

func TestChessDifficultyEscReturnsToMenu(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 7; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter"))
	m = next.(AppModel)
	next, _ = m.Update(key("esc"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("esc на экране сложности должен вернуть в меню")
	}
}

func TestChessQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	for i := 0; i < 7; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, _ := m.Update(key("enter")) // экран сложности
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // запускаем игру
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, _ = m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться")
	}
}

func TestRulesScrollDownAndUp(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 10 // маленький экран, чтобы правила точно не поместились
	m.rulesText = strings.Repeat("строка\n", 50)
	m.state = stateRules
	m.rulesScroll = 0

	next, _ := m.Update(key("down"))
	m = next.(AppModel)
	if m.rulesScroll != 1 {
		t.Fatalf("ожидалась прокрутка на 1 строку вниз, получено %d", m.rulesScroll)
	}

	next, _ = m.Update(key("up"))
	m = next.(AppModel)
	if m.rulesScroll != 0 {
		t.Fatalf("ожидался возврат к строке 0, получено %d", m.rulesScroll)
	}

	next, _ = m.Update(key("up"))
	m = next.(AppModel)
	if m.rulesScroll != 0 {
		t.Errorf("прокрутка не должна была уйти в отрицательные значения, получено %d", m.rulesScroll)
	}
}

func TestRulesScrollClampsAtBottom(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 10
	m.rulesText = strings.Repeat("строка\n", 20)
	m.state = stateRules

	for i := 0; i < 100; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	maxScroll := m.rulesMaxScroll()
	if m.rulesScroll != maxScroll {
		t.Errorf("прокрутка должна была остановиться на максимуме %d, получено %d", maxScroll, m.rulesScroll)
	}
}

func TestRulesPageDownMovesByVisibleLines(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 14
	m.rulesText = strings.Repeat("строка\n", 100)
	m.state = stateRules

	next, _ := m.Update(key("pgdown"))
	m = next.(AppModel)
	if m.rulesScroll != m.rulesVisibleLines() {
		t.Errorf("ожидалась прокрутка на %d строк за страницу, получено %d", m.rulesVisibleLines(), m.rulesScroll)
	}
}

func TestRulesHomeAndEndKeys(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 10
	m.rulesText = strings.Repeat("строка\n", 50)
	m.state = stateRules

	next, _ := m.Update(key("G"))
	m = next.(AppModel)
	if m.rulesScroll != m.rulesMaxScroll() {
		t.Fatalf("'G' должна была прокрутить в самый конец")
	}

	next, _ = m.Update(key("g"))
	m = next.(AppModel)
	if m.rulesScroll != 0 {
		t.Errorf("'g' должна была прокрутить в самое начало")
	}
}

func TestRulesScrollResetsWhenReopened(t *testing.T) {
	m := newTestAppModel()
	m.termHeight = 10
	for i := 0; i < len(gameRegistry); i++ {
		n, _ := m.Update(key("down"))
		m = n.(AppModel)
	}
	n, _ := m.Update(key("enter")) // подменю правил
	m = n.(AppModel)
	n, _ = m.Update(key("enter")) // открываем правила первой игры
	m = n.(AppModel)
	if m.state != stateRules {
		t.Fatalf("ожидалось состояние stateRules")
	}

	n, _ = m.Update(key("down"))
	m = n.(AppModel)
	if m.rulesScroll == 0 {
		t.Fatalf("ожидалась прокрутка вниз перед проверкой сброса")
	}

	n, _ = m.Update(key("esc")) // назад в подменю правил
	m = n.(AppModel)
	n, _ = m.Update(key("enter")) // снова открываем те же правила
	m = n.(AppModel)
	if m.rulesScroll != 0 {
		t.Errorf("прокрутка должна была сброситься при повторном открытии правил, получено %d", m.rulesScroll)
	}
}

func TestWindowSizeMsgUpdatesDimensions(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
	m = next.(AppModel)
	if m.termWidth != 100 || m.termHeight != 40 {
		t.Errorf("ожидались обновлённые размеры терминала 100x40, получено %dx%d", m.termWidth, m.termHeight)
	}
}

func TestMenuQuit(t *testing.T) {
	m := newTestAppModel()
	// последний пункт меню — "Выход"
	for i := 0; i < len(m.options)-1; i++ {
		next, _ := m.Update(key("down"))
		m = next.(AppModel)
	}
	next, cmd := m.Update(key("enter"))
	m = next.(AppModel)
	if !m.quitting {
		t.Fatalf("ожидался флаг quitting после выбора пункта 'Выход'")
	}
	if cmd == nil {
		t.Fatalf("ожидалась команда tea.Quit")
	}
}

func TestGameQKeyReturnsToMenuNotQuitsApp(t *testing.T) {
	m := newTestAppModel()
	next, _ := m.Update(key("enter")) // открываем настройки ставки
	m = next.(AppModel)
	next, _ = m.Update(key("enter")) // подтверждаем настройки и запускаем Тонк
	m = next.(AppModel)
	if m.state != statePlaying {
		t.Fatalf("ожидалось statePlaying")
	}

	next, cmd := m.Update(key("q"))
	m = next.(AppModel)
	if m.state != stateMenu {
		t.Fatalf("нажатие 'q' в игре должно вернуть в меню, а не закрыть приложение")
	}
	if m.quitting {
		t.Fatalf("приложение не должно закрываться при возврате в меню")
	}
	if cmd != nil {
		// допустима nil-команда — важно лишь, что это не tea.Quit
		_ = cmd
	}
}

func TestAppModelImplementsTeaModel(t *testing.T) {
	var _ tea.Model = AppModel{}
}
