package main

import (
	"fmt"
	"strings"
	"testing"
)

func TestOneOhOneModelDealsCorrectPlayerCount(t *testing.T) {
	for n := 2; n <= 6; n++ {
		m := NewOneOhOneModel(n)
		if len(m.game.Players) != n {
			t.Errorf("ожидалось %d игроков, получено %d", n, len(m.game.Players))
		}
	}
}

func TestOneOhOneModelClampsPlayerCount(t *testing.T) {
	m := NewOneOhOneModel(1)
	if len(m.game.Players) != 2 {
		t.Errorf("ожидалось ограничение снизу до 2, получено %d", len(m.game.Players))
	}
	m = NewOneOhOneModel(10)
	if len(m.game.Players) != 6 {
		t.Errorf("ожидалось ограничение сверху до 6, получено %d", len(m.game.Players))
	}
}

func TestOneOhOneModelPlayCardViaKeys(t *testing.T) {
	m := NewOneOhOneModel(2)
	m.game.Discard = []Card{c(Nine, Clubs)}
	m.game.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
	m.game.DeclaredSuit = nil
	m.game.Players[m.humanIdx].Hand = []Card{c(Ten, Clubs)}
	m.game.CurrentPlayerIdx = m.humanIdx
	m.game.Phase = OneOhOnePhasePlay
	m.cursor = 0

	next, _ := m.Update(key("enter"))
	m = next.(OneOhOneModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка: %s", m.message)
	}
}

func TestOneOhOneModelPendingEightVisibleInView(t *testing.T) {
	m := NewOneOhOneModel(3)
	suit := Diamonds
	m.game.PendingEightSuit = &suit
	view := m.View()
	if !strings.Contains(view, suit.String()) {
		t.Errorf("требование покрыть восьмёрку должно быть видно на экране, получено:\n%s", view)
	}
}

func TestOneOhOneModelNoPendingEightLineWhenNil(t *testing.T) {
	m := NewOneOhOneModel(3)
	m.game.PendingEightSuit = nil
	view := m.View()
	if strings.Contains(view, "покрыть восьмёрку") {
		t.Errorf("строка требования покрытия не должна показываться, когда PendingEightSuit == nil")
	}
}

func TestOneOhOneModelDeclaredSuitVisibleInView(t *testing.T) {
	m := NewOneOhOneModel(3)
	suit := Hearts
	m.game.DeclaredSuit = &suit
	view := m.View()
	if !strings.Contains(view, suit.String()) {
		t.Errorf("объявленная масть должна быть видна на экране, получено:\n%s", view)
	}
}

func TestOneOhOneModelNoDeclaredSuitLineWhenNil(t *testing.T) {
	m := NewOneOhOneModel(3)
	m.game.DeclaredSuit = nil
	view := m.View()
	if strings.Contains(view, "назвала масть") {
		t.Errorf("строка объявленной масти не должна показываться, когда DeclaredSuit == nil")
	}
}

func TestOneOhOneModelBotDeclaredSuitVisibleAfterQueen(t *testing.T) {
	m := NewOneOhOneModel(3)
	// подстраиваем ситуацию: сейчас ход бота, у него на руке дама,
	// и сразу после розыгрыша должна открыться фаза выбора масти
	botIdx := m.game.CurrentPlayerIdx
	if botIdx == m.humanIdx {
		botIdx = (botIdx + 1) % len(m.game.Players)
		m.game.CurrentPlayerIdx = botIdx
	}
	m.game.Players[botIdx].Hand = []Card{c(Queen, Spades), c(Two, Clubs)}
	m.game.Discard = []Card{c(King, Spades)}
	m.game.PendingEightSuit = nil
	m.game.DeclaredSuit = nil

	next, _ := m.Update(botMoveMsg{})
	m = next.(OneOhOneModel)
	if m.game.Phase != OneOhOnePhaseChooseSuit {
		t.Fatalf("ожидалась фаза выбора масти после розыгрыша дамы, получено %v", m.game.Phase)
	}

	next, _ = m.Update(botMoveMsg{})
	m = next.(OneOhOneModel)
	if m.game.DeclaredSuit == nil {
		t.Fatalf("бот должен был назвать масть")
	}
	view := m.View()
	if !strings.Contains(view, m.game.DeclaredSuit.String()) {
		t.Errorf("названная ботом масть должна быть видна человеку на экране, получено:\n%s", view)
	}
}

func TestOneOhOneModelDrawViaKeys(t *testing.T) {
	m := NewOneOhOneModel(2)
	m.game.Discard = []Card{c(Nine, Clubs)}
	m.game.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
	m.game.DeclaredSuit = nil
	m.game.Players[m.humanIdx].Hand = []Card{c(Ten, Hearts)} // не подходит
	m.game.Stock = []Card{c(Two, Diamonds)}
	m.game.CurrentPlayerIdx = m.humanIdx
	m.game.Phase = OneOhOnePhasePlay

	next, _ := m.Update(key("d"))
	m = next.(OneOhOneModel)
	if m.isError {
		t.Fatalf("неожиданная ошибка: %s", m.message)
	}
	if len(m.game.Players[m.humanIdx].Hand) != 2 {
		t.Fatalf("рука должна была вырасти на 1 карту, получено %d карт", len(m.game.Players[m.humanIdx].Hand))
	}
}

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

// TestOneOhOneModelFullAutoPlaythrough прогоняет TUI-модель от
// начала до конца матча, где "человек" отыгрывает свои ходы через
// реальные нажатия клавиш (используя ту же логику, что и бот).
func TestOneOhOneModelFullAutoPlaythrough(t *testing.T) {
	for n := 2; n <= 4; n++ {
		m := NewOneOhOneModel(n)
		const maxRounds = 100
		round := 0
		for ; round < maxRounds; round++ {
			const maxSteps = 200000
			steps := 0
			for m.game.Phase != OneOhOnePhaseRoundOver && steps < maxSteps {
				steps++
				if m.game.CurrentPlayerIdx != m.humanIdx {
					next, _ := m.Update(botMoveMsg{})
					m = next.(OneOhOneModel)
					continue
				}
				if m.game.Phase == OneOhOnePhaseChooseSuit {
					suit := m.bot.DecideSuit(m.game)
					next, _ := m.Update(key(fmt.Sprintf("%d", int(suit)+1)))
					m = next.(OneOhOneModel)
					continue
				}
				if card, ok := m.bot.DecideAction(m.game); ok {
					for i, hc := range m.currentHand() {
						if hc == card {
							m.cursor = i
							break
						}
					}
					next, _ := m.Update(key("enter"))
					m = next.(OneOhOneModel)
				} else {
					next, _ := m.Update(key("d"))
					m = next.(OneOhOneModel)
				}
			}
			if steps >= maxSteps {
				t.Fatalf("игроков %d, раунд %d: раунд через TUI не завершился за %d шагов", n, round, maxSteps)
			}
			if m.game.Result == nil {
				t.Fatalf("игроков %d, раунд %d: раунд завершился без результата", n, round)
			}
			if m.game.Result.MatchOver {
				break
			}
			next, _ := m.Update(key("n"))
			m = next.(OneOhOneModel)
		}
		if round >= maxRounds {
			t.Fatalf("игроков %d: матч не завершился за %d раундов", n, maxRounds)
		}
	}
}
