package main

import (
	"regexp"
	"strings"
	"testing"
)

var ansiEscapeRe = regexp.MustCompile("\x1b\\[[0-9;]*m")

func stripANSI(s string) string {
	return ansiEscapeRe.ReplaceAllString(s, "")
}

// TestKlondikeTableauAlignmentWithMixedCardWidths проверяет
// исправление бага: карта "10" (единственный ранг длиной в 2
// символа) без паддинга оказывалась короче остальных карт и
// закрытой клетки "[?]", из-за чего колонки правее в той же строке
// сдвигались и вертикальное выравнивание таблицы ломалось.
func TestKlondikeTableauAlignmentWithMixedCardWidths(t *testing.T) {
	m := NewKlondikeModel()
	m.game.Tableau[0] = []KlondikeCard{{Card: Card{Rank: Ace, Suit: Spades}, FaceUp: true}}
	m.game.Tableau[1] = []KlondikeCard{{Card: Card{Rank: Ten, Suit: Hearts}, FaceUp: true}}
	m.game.Tableau[2] = []KlondikeCard{{Card: Card{Rank: King, Suit: Clubs}, FaceUp: false}}
	m.game.Tableau[3] = []KlondikeCard{{Card: Card{Rank: Queen, Suit: Diamonds}, FaceUp: true}}
	m.game.Tableau[4] = []KlondikeCard{}
	m.game.Tableau[5] = []KlondikeCard{}
	m.game.Tableau[6] = []KlondikeCard{}

	rendered := stripANSI(m.renderTableau())
	lines := strings.Split(rendered, "\n")
	if len(lines) < 2 {
		t.Fatalf("ожидалось минимум 2 строки (заголовок + ряд карт), получено %d", len(lines))
	}

	headerWidth := len([]rune(lines[0]))
	for i, line := range lines[1:] {
		if w := len([]rune(line)); w != headerWidth {
			t.Errorf("строка %d имеет ширину %d, отличную от заголовка (%d): %q", i+1, w, headerWidth, line)
		}
	}
}

func TestKlondikeModelDrawViaKeys(t *testing.T) {
	m := NewKlondikeModel()
	stockBefore := len(m.game.Stock)

	next, _ := m.Update(key("d"))
	m = next.(KlondikeModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка: %s", m.message)
	}
	if len(m.game.Stock) != stockBefore-1 {
		t.Fatalf("колода должна была уменьшиться на 1")
	}
}

func TestKlondikeModelPartialRunTransferViaKeys(t *testing.T) {
	m := NewKlondikeModel()
	m.game.Tableau[0] = []KlondikeCard{
		{Card: c(King, Spades), FaceUp: true},
		{Card: c(Queen, Diamonds), FaceUp: true},
		{Card: c(Jack, Spades), FaceUp: true},
	}
	m.game.Tableau[1] = []KlondikeCard{
		{Card: c(King, Clubs), FaceUp: true},
	}

	next, _ := m.Update(key("1"))
	m = next.(KlondikeModel)
	if m.runLen != 3 {
		t.Fatalf("сразу после выбора ожидалась вся последовательность (3), получено %d", m.runLen)
	}

	next, _ = m.Update(key("down"))
	m = next.(KlondikeModel)
	if m.runLen != 2 {
		t.Fatalf("после уменьшения ожидалось 2, получено %d", m.runLen)
	}

	next, _ = m.Update(key("2"))
	m = next.(KlondikeModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка: %s", m.message)
	}
	if len(m.game.Tableau[0]) != 1 {
		t.Errorf("в колонне-источнике должна была остаться только одна карта (K♠), получено %d", len(m.game.Tableau[0]))
	}
	if len(m.game.Tableau[1]) != 3 {
		t.Errorf("в колонне назначения ожидалось 3 карты, получено %d", len(m.game.Tableau[1]))
	}
}

func TestKlondikeModelRunLenClampedBetweenOneAndMax(t *testing.T) {
	m := NewKlondikeModel()
	m.game.Tableau[0] = []KlondikeCard{
		{Card: c(King, Spades), FaceUp: true},
		{Card: c(Queen, Diamonds), FaceUp: true},
	}

	next, _ := m.Update(key("1"))
	m = next.(KlondikeModel)

	for i := 0; i < 5; i++ {
		next, _ = m.Update(key("up"))
		m = next.(KlondikeModel)
	}
	if m.runLen != 2 {
		t.Errorf("runLen не должен был превысить полную последовательность (2), получено %d", m.runLen)
	}

	for i := 0; i < 5; i++ {
		next, _ = m.Update(key("down"))
		m = next.(KlondikeModel)
	}
	if m.runLen != 1 {
		t.Errorf("runLen не должен был опуститься ниже 1, получено %d", m.runLen)
	}
}

func TestKlondikeModelSelectAndMoveTableauToTableau(t *testing.T) {
	m := NewKlondikeModel()
	m.game.Tableau[0] = []KlondikeCard{{Card: c(Seven, Hearts), FaceUp: true}}
	m.game.Tableau[1] = []KlondikeCard{{Card: c(Six, Spades), FaceUp: true}}

	next, _ := m.Update(key("2"))
	m = next.(KlondikeModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка выбора: %s", m.message)
	}
	if m.selected != klondikePileRef(1) {
		t.Fatalf("ожидался выбор колонны 1 (индекс), получено %v", m.selected)
	}

	next, _ = m.Update(key("1"))
	m = next.(KlondikeModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка хода: %s", m.message)
	}
	if len(m.game.Tableau[0]) != 2 {
		t.Fatalf("ожидалось 2 карты в целевой колонне, получено %d", len(m.game.Tableau[0]))
	}
	if m.selected != klondikeNoSelection {
		t.Errorf("выбор должен был сброситься после хода")
	}
}

func TestKlondikeModelSelectWasteAndSendToFoundation(t *testing.T) {
	m := NewKlondikeModel()
	m.game.Waste = []Card{c(Ace, Hearts)}

	next, _ := m.Update(key("0"))
	m = next.(KlondikeModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка: %s", m.message)
	}
	if m.selected != klondikeWasteRef {
		t.Fatalf("ожидался выбор отбоя")
	}

	next, _ = m.Update(key("f"))
	m = next.(KlondikeModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка отправки на фундамент: %s", m.message)
	}
	if m.game.Foundations[Hearts] != int(Ace) {
		t.Errorf("туз червей должен был оказаться на фундаменте")
	}
}

func TestKlondikeModelEscClearsSelection(t *testing.T) {
	m := NewKlondikeModel()
	m.game.Tableau[0] = []KlondikeCard{{Card: c(Seven, Hearts), FaceUp: true}}

	next, _ := m.Update(key("1"))
	m = next.(KlondikeModel)
	if m.selected == klondikeNoSelection {
		t.Fatalf("ожидался выбор колонны 1")
	}

	next, _ = m.Update(key("esc"))
	m = next.(KlondikeModel)
	if m.selected != klondikeNoSelection {
		t.Errorf("esc должен был сбросить выбор")
	}
}

func TestKlondikeModelUndoViaKeys(t *testing.T) {
	m := NewKlondikeModel()
	m.game.Waste = []Card{c(Ace, Hearts)}
	next, _ := m.Update(key("0"))
	m = next.(KlondikeModel)
	next, _ = m.Update(key("f"))
	m = next.(KlondikeModel)
	if m.game.Foundations[Hearts] != int(Ace) {
		t.Fatalf("ход должен был примениться")
	}

	next, _ = m.Update(key("u"))
	m = next.(KlondikeModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка отмены: %s", m.message)
	}
	if m.game.Foundations[Hearts] != 0 {
		t.Errorf("после отмены фундамент должен был вернуться в исходное состояние")
	}
}

func TestKlondikeModelNewGameViaKeys(t *testing.T) {
	m := NewKlondikeModel()
	oldStock := m.game.Stock

	next, _ := m.Update(key("n"))
	m = next.(KlondikeModel)
	if len(m.game.Stock)+28 != 52 {
		t.Fatalf("новая партия должна быть корректно раздана")
	}
	same := true
	for i := range oldStock {
		if i >= len(m.game.Stock) || oldStock[i] != m.game.Stock[i] {
			same = false
			break
		}
	}
	if same {
		t.Errorf("новая партия должна была перетасовать колоду заново")
	}
}

func TestKlondikeModelQReturnsToMenu(t *testing.T) {
	m := NewKlondikeModel()
	next, _ := m.Update(key("q"))
	m = next.(KlondikeModel)
	if !m.backToMenu {
		t.Fatalf("ожидался флаг backToMenu после 'q'")
	}
	if m.quitting {
		t.Fatalf("'q' не должен закрывать всё приложение")
	}
}

func TestKlondikeModelWinShowsResultAndAllowsNewGame(t *testing.T) {
	m := NewKlondikeModel()
	for s := Clubs; s <= Spades; s++ {
		m.game.Foundations[s] = int(King)
	}
	m.game.Foundations[Hearts] = int(Queen)
	m.game.Waste = []Card{c(King, Hearts)}

	next, _ := m.Update(key("0"))
	m = next.(KlondikeModel)
	next, _ = m.Update(key("f"))
	m = next.(KlondikeModel)
	if !m.game.Won {
		t.Fatalf("партия должна была завершиться победой")
	}

	next, _ = m.Update(key("n"))
	m = next.(KlondikeModel)
	if m.game.Won {
		t.Errorf("новая партия не должна была начаться выигранной")
	}
}
