package main

import (
	"strings"
	"testing"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/muesli/termenv"
)

// key — маленький хелпер для конструирования tea.KeyMsg, которые
// соответствуют строкам, разбираемым в handleActionKey/handleDrawKey.
func key(s string) tea.KeyMsg {
	switch s {
	case "left":
		return tea.KeyMsg{Type: tea.KeyLeft}
	case "right":
		return tea.KeyMsg{Type: tea.KeyRight}
	case "up":
		return tea.KeyMsg{Type: tea.KeyUp}
	case "down":
		return tea.KeyMsg{Type: tea.KeyDown}
	case "esc":
		return tea.KeyMsg{Type: tea.KeyEsc}
	case " ":
		return tea.KeyMsg{Type: tea.KeySpace}
	case "enter":
		return tea.KeyMsg{Type: tea.KeyEnter}
	case "ctrl+c":
		return tea.KeyMsg{Type: tea.KeyCtrlC}
	default:
		return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)}
	}
}

func TestTUIDrawAndDiscardCycle(t *testing.T) {
	m := NewModel()
	// делаем раунд предсказуемым: у человека маленькая рука без
	// готовых комбинаций
	m.game.Players[0].Hand = []Card{c(Two, Clubs), c(Nine, Hearts), c(King, Spades)}
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseDraw

	next, _ := m.Update(key("d"))
	m = next.(Model)
	if m.game.Phase != PhaseAction {
		t.Fatalf("после взятия карты ожидалась фаза PhaseAction, получена %v", m.game.Phase)
	}
	if len(m.game.current().Hand) != 4 {
		t.Fatalf("ожидалось 4 карты после взятия, получено %d", len(m.game.current().Hand))
	}

	next, _ = m.Update(key("enter"))
	m = next.(Model)
	if m.game.CurrentPlayer == m.humanIdx && m.game.Phase != PhaseGameOver {
		t.Fatalf("после сброса ход должен был перейти к боту")
	}
}

func TestTUILayMeldViaKeys(t *testing.T) {
	m := NewModel()
	m.game.Players[0].Hand = []Card{
		c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades), c(Two, Diamonds),
	}
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseAction

	// отмечаем три семёрки пробелом, двигая курсор
	for i := 0; i < 3; i++ {
		next, _ := m.Update(key(" "))
		m = next.(Model)
		if i < 2 {
			next, _ = m.Update(key("right"))
			m = next.(Model)
		}
	}
	if len(m.staged) != 3 {
		t.Fatalf("ожидалось 3 отмеченные карты, получено %d", len(m.staged))
	}

	next, _ := m.Update(key("m"))
	m = next.(Model)
	if m.isError {
		t.Fatalf("неожиданная ошибка при выкладке комбинации: %s", m.message)
	}
	if len(m.game.Table) != 1 {
		t.Fatalf("ожидалась 1 комбинация на столе, получено %d", len(m.game.Table))
	}
	if len(m.game.current().Hand) != 1 {
		t.Fatalf("в руке должна была остаться 1 карта, получено %d", len(m.game.current().Hand))
	}
	if len(m.staged) != 0 {
		t.Fatalf("отметки должны сброситься после выкладки комбинации")
	}
}

func TestTUILayMeldRejectsFewerThanThree(t *testing.T) {
	m := NewModel()
	m.game.Players[0].Hand = []Card{c(Seven, Clubs), c(Seven, Hearts), c(Two, Diamonds)}
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseAction

	next, _ := m.Update(key(" "))
	m = next.(Model)
	next, _ = m.Update(key("m"))
	m = next.(Model)
	if !m.isError {
		t.Fatalf("ожидалась ошибка при попытке выложить меньше 3 карт")
	}
}

func TestTUIAddToMeld(t *testing.T) {
	m := NewModel()
	// комбинацию на столе готовит бот (игрок 1) — напрямую через
	// GameState, это лишь подготовка сцены для теста
	m.game.Players[1].Hand = []Card{c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades)}
	m.game.CurrentPlayer = 1
	m.game.Phase = PhaseAction
	if err := m.game.LayNewMeld(m.game.Players[1].Hand); err != nil {
		t.Fatalf("не удалось подготовить стол: %v", err)
	}

	// а вот подкладывает карту уже человек — через реальный путь
	// TUI (Update + нажатие клавиши), именно это здесь и проверяем
	m.game.Players[0].Hand = []Card{c(Seven, Diamonds)}
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseAction
	m.cursor = 0
	next, _ := m.Update(key("1"))
	m = next.(Model)
	if m.isError {
		t.Fatalf("неожиданная ошибка подкладки: %s", m.message)
	}
	if len(m.game.Table[0].Cards) != 4 {
		t.Fatalf("ожидалось 4 карты в комбинации, получено %d", len(m.game.Table[0].Cards))
	}
}

func TestTUIDropSucceeds(t *testing.T) {
	m := NewModel()
	m.game.Players[0].Hand = []Card{c(Two, Clubs)}
	m.game.Players[1].Hand = []Card{c(King, Hearts)}
	m.game.Players[2].Hand = []Card{c(Queen, Spades)}
	m.game.Players[3].Hand = []Card{c(King, Diamonds)}
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseDraw

	next, _ := m.Update(key("k"))
	m = next.(Model)
	if m.isError {
		t.Fatalf("неожиданная ошибка дропа: %s", m.message)
	}
	if m.game.Phase != PhaseGameOver || !m.game.Result.DropCall || m.game.Result.DropCaught {
		t.Fatalf("ожидался успешный дроп: %+v", m.game.Result)
	}
}

func TestTUIDropCaught(t *testing.T) {
	m := NewModel()
	m.game.Players[0].Hand = []Card{c(King, Hearts)}
	m.game.Players[1].Hand = []Card{c(Ace, Clubs)} // реально ниже
	m.game.Players[2].Hand = []Card{c(King, Spades)}
	m.game.Players[3].Hand = []Card{c(King, Diamonds)}
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseDraw

	next, _ := m.Update(key("k"))
	m = next.(Model)
	if m.isError {
		t.Fatalf("неожиданная ошибка дропа: %s", m.message)
	}
	if !m.game.Result.DropCaught {
		t.Fatalf("ожидался неудачный дроп (попался)")
	}
}

func TestTUIDropOnlyInDrawPhase(t *testing.T) {
	m := NewModel()
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseAction

	next, _ := m.Update(key("k"))
	m = next.(Model)
	if m.game.Phase == PhaseGameOver {
		t.Fatalf("дроп после взятия карты должен быть запрещён — клавиша 'k' здесь не должна ни на что влиять")
	}
}

func TestTUIDeclareTonk(t *testing.T) {
	m := NewModel()
	m.game.Players[0].Hand = []Card{c(Ace, Clubs), c(Two, Hearts)}
	m.game.CurrentPlayer = 0
	m.game.Phase = PhaseAction

	next, _ := m.Update(key("t"))
	m = next.(Model)
	if m.isError {
		t.Fatalf("неожиданная ошибка объявления Тонка: %s", m.message)
	}
	if m.game.Phase != PhaseGameOver || !m.game.Result.TonkCall {
		t.Fatalf("раунд должен был завершиться объявлением Тонка")
	}
}

func TestTUINewRoundCarriesOverBalance(t *testing.T) {
	m := NewModel()
	m.game.Players[1].Hand = []Card{c(Ace, Clubs)}
	m.game.CurrentPlayer = 1
	m.game.Phase = PhaseAction
	if err := m.game.Discard(c(Ace, Clubs)); err != nil {
		t.Fatalf("не удалось завершить раунд для теста: %v", err)
	}
	if m.game.Phase != PhaseGameOver {
		t.Fatalf("раунд должен был завершиться")
	}
	balanceAfterRound := m.game.Players[0].Balance

	next, _ := m.Update(key("n"))
	m = next.(Model)
	if m.game.Phase == PhaseGameOver {
		t.Fatalf("новый раунд должен был начаться")
	}
	// капитал переносится, но в новом раунде заново списывается
	// анте — итоговый капитал должен быть меньше на размер ставки
	want := balanceAfterRound - m.game.AnteAmount
	if m.game.Players[0].Balance != want {
		t.Fatalf("капитал должен был перенестись с учётом нового анте: ожидалось %d, получено %d",
			want, m.game.Players[0].Balance)
	}
}

// TestTUIFullAutoPlaythrough прогоняет TUI-модель от начала до
// конца, но подменяет человека ботом на уровне вызовов Update:
// вместо реальных нажатий клавиш скрипт запрашивает у SimpleBot
// решение и переводит его в нажатия клавиш. Так мы проверяем, что
// весь путь через Update (а не только через GameState напрямую)
// стабильно доводит партию до конца без паники и зависаний.
func TestTUIFullAutoPlaythrough(t *testing.T) {
	m := NewModel()

	const maxSteps = 2000
	steps := 0
	for m.game.Phase != PhaseGameOver && steps < maxSteps {
		steps++
		if m.game.CurrentPlayer != m.humanIdx {
			next, _ := m.Update(botMoveMsg{})
			m = next.(Model)
			continue
		}

		// "человек" в этом тесте играет как простой бот, но через
		// реальные нажатия клавиш, а не напрямую через GameState
		switch m.game.Phase {
		case PhaseDraw:
			top, ok := m.game.PeekDiscard()
			k := "d"
			if ok && wouldFormMeld(m.game.current().Hand, top) {
				k = "p"
			}
			next, _ := m.Update(key(k))
			m = next.(Model)
		case PhaseAction:
			discard := chooseDiscardSimple(m.game.current().Hand)
			for i, cardInHand := range m.game.current().Hand {
				if cardInHand == discard {
					m.cursor = i
					break
				}
			}
			next, _ := m.Update(key("enter"))
			m = next.(Model)
		}
	}

	if steps >= maxSteps {
		t.Fatalf("партия через TUI не завершилась за %d шагов", maxSteps)
	}
	if m.game.Result == nil {
		t.Fatalf("партия завершилась без результата")
	}
}

func TestRenderCardBoxContentShowsRankAndSuitSeparately(t *testing.T) {
	box := renderCardBox(c(Ace, Diamonds), CardNormal)
	rankLine := stripANSI(box[1])
	suitLine := stripANSI(box[2])
	if !strings.Contains(rankLine, "A") || strings.Contains(rankLine, "♦") {
		t.Errorf("строка ранга должна содержать только ранг, получено %q", rankLine)
	}
	if !strings.Contains(suitLine, "♦") || strings.Contains(suitLine, "A") {
		t.Errorf("строка масти должна содержать только масть, получено %q", suitLine)
	}
}

func TestRenderCardBoxHasFourLines(t *testing.T) {
	box := renderCardBox(c(Ace, Spades), CardNormal)
	if len(box) != 4 {
		t.Fatalf("ожидалось 4 строки (верх/ранг/масть/низ), получено %d", len(box))
	}
}

func TestRenderCardBoxAlignsTenSameWidthAsSingleDigit(t *testing.T) {
	tenBox := renderCardBox(c(Ten, Spades), CardNormal)
	aceBox := renderCardBox(c(Ace, Spades), CardNormal)
	for row := 0; row < 4; row++ {
		wTen := len([]rune(stripANSI(tenBox[row])))
		wAce := len([]rune(stripANSI(aceBox[row])))
		if wTen != wAce {
			t.Errorf("строка %d: карта с рангом '10' (ширина %d) должна была совпадать по ширине с картой ранга 'A' (ширина %d)", row, wTen, wAce)
		}
	}
}

func TestRenderCardBoxCursorHighlightsBorderNotText(t *testing.T) {
	oldProfile := lipgloss.ColorProfile()
	lipgloss.SetColorProfile(termenv.ANSI256)
	defer lipgloss.SetColorProfile(oldProfile)

	normal := renderCardBox(c(Ace, Spades), CardNormal)
	cursor := renderCardBox(c(Ace, Spades), CardCursor)
	// текст (ранг/масть, строки 1-2) не должен меняться от курсора —
	// подсвечивается только рамка (строки 0 и 3)
	if stripANSI(normal[1]) != stripANSI(cursor[1]) || stripANSI(normal[2]) != stripANSI(cursor[2]) {
		t.Errorf("курсор не должен менять видимый текст ранга/масти, только рамку")
	}
	if normal[0] == cursor[0] {
		t.Errorf("курсор должен был визуально отличать верхнюю рамку (по стилю), получены одинаковые строки")
	}
}

func TestRenderCardBoxStagedUnderlinesTextKeepsBorder(t *testing.T) {
	oldProfile := lipgloss.ColorProfile()
	lipgloss.SetColorProfile(termenv.ANSI256)
	defer lipgloss.SetColorProfile(oldProfile)

	normal := renderCardBox(c(Ace, Spades), CardNormal)
	staged := renderCardBox(c(Ace, Spades), CardStaged)
	if stripANSI(normal[0]) != stripANSI(staged[0]) || stripANSI(normal[3]) != stripANSI(staged[3]) {
		t.Errorf("отложенная карта не должна менять рамку, только текст")
	}
	if normal[1] == staged[1] {
		t.Errorf("отложенная карта должна была визуально отличаться (подчёркивание) в строке ранга")
	}
}

func TestRenderCardBoxRowJoinsMultipleCardsWithEqualWidth(t *testing.T) {
	hand := []Card{c(Ace, Spades), c(Ten, Hearts), c(King, Clubs)}
	row := renderCardBoxRow(hand, nil)
	lines := strings.Split(row, "\n")
	if len(lines) != 4 {
		t.Fatalf("ожидалось 4 строки в ряду карт, получено %d", len(lines))
	}
	width0 := len([]rune(stripANSI(lines[0])))
	for i, line := range lines {
		if w := len([]rune(stripANSI(line))); w != width0 {
			t.Errorf("строка %d ряда карт имеет ширину %d, ожидалась %d (все строки должны быть выровнены по прямоугольнику)", i, w, width0)
		}
	}
}

func TestRenderCardBoxRowEmptyHand(t *testing.T) {
	if got := renderCardBoxRow(nil, nil); got != "" {
		t.Errorf("для пустой руки ожидалась пустая строка, получено %q", got)
	}
}

func TestCursorHighlightsMarksOnlyCursorPosition(t *testing.T) {
	hl := cursorHighlights(5, 2)
	for i, h := range hl {
		want := CardNormal
		if i == 2 {
			want = CardCursor
		}
		if h != want {
			t.Errorf("позиция %d: ожидалось %v, получено %v", i, want, h)
		}
	}
}

func TestRenderCardBoxHandWrapsAfterSixCards(t *testing.T) {
	hand := make([]Card, 8)
	for i := range hand {
		hand[i] = c(Rank(2+i%9), Clubs)
	}
	out := renderCardBoxHand(hand, cursorHighlights(len(hand), 0))
	// два ряда по 4 строки, разделённые пустой строкой между ними
	lines := strings.Split(out, "\n")
	if len(lines) != 9 {
		t.Fatalf("ожидалось 9 строк (4+пустая+4) для переноса 8 карт по 6 в ряд, получено %d:\n%s", len(lines), out)
	}
	if lines[4] != "" {
		t.Errorf("между рядами должна быть пустая строка-разделитель, получено %q", lines[4])
	}
}

func TestRenderCardBoxHandNoWrapWithinLimit(t *testing.T) {
	hand := make([]Card, cardsPerBoxRow)
	for i := range hand {
		hand[i] = c(Rank(2+i), Clubs)
	}
	out := renderCardBoxHand(hand, cursorHighlights(len(hand), 0))
	lines := strings.Split(out, "\n")
	if len(lines) != 4 {
		t.Fatalf("ровно %d карт должны были уместиться в один ряд (4 строки), получено %d строк", cardsPerBoxRow, len(lines))
	}
}

func TestRenderCardBoxHandEmpty(t *testing.T) {
	if got := renderCardBoxHand(nil, nil); got != "" {
		t.Errorf("для пустой руки ожидалась пустая строка, получено %q", got)
	}
}
