package main

import "testing"

func bc(rank Rank, suit Suit) Card { return Card{Rank: rank, Suit: suit} }

// resetForManualBout приводит только что созданную игру в чистое
// состояние перед ручной подстановкой рук в тесте. Это нужно,
// потому что NewBlackjackGame раздаёт случайные карты — и если
// кому-то случайно выпадает натуральный блэкджек, конструктор сам
// доигрывает и разрешает раунд ещё до того, как тест успевает
// подставить нужные карты. Без сброса такой "случайный" исход
// остаётся в g.Result и в Balance, и повторный вызов
// advanceToNextPlayerOrResolve в тесте задваивает выплату.
func resetForManualBout(g *BlackjackGameState) {
	g.Result = nil
	g.Phase = BJPhasePlayerTurn
	for i, p := range g.Players {
		p.Status = BJStatusPlaying
		p.Balance = g.preRoundBalance[i] - p.Bet
	}
}

func TestHandTotalBasic(t *testing.T) {
	cases := []struct {
		hand []Card
		want int
	}{
		{[]Card{bc(King, Clubs), bc(Seven, Hearts)}, 17},
		{[]Card{bc(Ace, Clubs), bc(King, Hearts)}, 21},
		{[]Card{bc(Ace, Clubs), bc(Ace, Hearts)}, 12},                     // 11+1
		{[]Card{bc(Ace, Clubs), bc(Ace, Hearts), bc(Nine, Diamonds)}, 21}, // 1+1+9+... нет: 11+1+9=21
		{[]Card{bc(King, Clubs), bc(Queen, Hearts), bc(Two, Diamonds)}, 22},
	}
	for i, tc := range cases {
		got := handTotal(tc.hand)
		if got != tc.want {
			t.Errorf("случай %d: ожидалось %d, получено %d (рука %v)", i, tc.want, got, tc.hand)
		}
	}
}

func TestIsNaturalBlackjack(t *testing.T) {
	if !isNaturalBlackjack([]Card{bc(Ace, Clubs), bc(King, Hearts)}) {
		t.Errorf("туз+король должен быть натуральным блэкджеком")
	}
	if isNaturalBlackjack([]Card{bc(Seven, Clubs), bc(Seven, Hearts), bc(Seven, Diamonds)}) {
		t.Errorf("21 из трёх карт не должен считаться натуральным блэкджеком")
	}
	if isNaturalBlackjack([]Card{bc(King, Clubs), bc(Queen, Hearts)}) {
		t.Errorf("20 не должен считаться блэкджеком")
	}
}

func TestNewBlackjackGameDealsAndCollectsBet(t *testing.T) {
	g := NewBlackjackGame([]string{"A", "B"}, -1, 100, 10)
	for i, p := range g.Players {
		// ровно 2 карты игроку раздаются всегда, независимо от
		// исхода — даже если у кого-то мгновенно блэкджек и раунд
		// уже разрешился, руки игроков от этого не меняются
		if len(p.Hand) != 2 {
			t.Errorf("игрок %d: ожидалось 2 карты, получено %d", i, len(p.Hand))
		}
		if p.Bet != 10 {
			t.Errorf("игрок %d: ожидалась ставка 10, получено %d", i, p.Bet)
		}
		// капитал сразу после ставки (90) проверяем только пока
		// раунд ещё не разрешился сам — если кому-то выпал
		// натуральный блэкджек, конструктор уже доиграл дилера и
		// начислил выплату, и Balance закономерно отличается от 90
		if p.Status == BJStatusPlaying && g.Phase == BJPhasePlayerTurn {
			if p.Balance != 90 {
				t.Errorf("игрок %d: ожидался капитал 90 после ставки, получено %d", i, p.Balance)
			}
		}
	}
	if len(g.Dealer) < 2 {
		t.Errorf("у дилера должно быть минимум 2 карты, получено %d", len(g.Dealer))
	}
}

func TestHitAddsCardAndBustEndsTurn(t *testing.T) {
	g := NewBlackjackGame([]string{"A", "B"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(King, Clubs), bc(Queen, Hearts)} // 20
	g.Players[0].Status = BJStatusPlaying
	g.CurrentPlayerIdx = 0
	g.Phase = BJPhasePlayerTurn
	// подменяем колоду, чтобы гарантированно добрать карту с перебором
	g.Deck = &Deck{cards: []Card{bc(Five, Diamonds)}}

	if err := g.Hit(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Players[0].Status != BJStatusBusted {
		t.Fatalf("ожидался перебор, статус: %v", g.Players[0].Status)
	}
	if g.CurrentPlayerIdx != 1 {
		t.Fatalf("ход должен был перейти к игроку 1, получено %d", g.CurrentPlayerIdx)
	}
}

func TestStandMovesToNextPlayer(t *testing.T) {
	g := NewBlackjackGame([]string{"A", "B"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Status = BJStatusPlaying
	g.Players[1].Status = BJStatusPlaying
	g.CurrentPlayerIdx = 0
	g.Phase = BJPhasePlayerTurn

	if err := g.Stand(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Players[0].Status != BJStatusStood {
		t.Fatalf("ожидался статус stood")
	}
	if g.CurrentPlayerIdx != 1 {
		t.Fatalf("ход должен был перейти к игроку 1")
	}
}

func TestDoubleDownDoublesbetAndDrawsOneCard(t *testing.T) {
	g := NewBlackjackGame([]string{"A", "B"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(Five, Clubs), bc(Six, Hearts)} // 11
	g.Players[0].Status = BJStatusPlaying
	g.Players[0].Balance = 90
	g.Players[0].Bet = 10
	g.CurrentPlayerIdx = 0
	g.Phase = BJPhasePlayerTurn
	g.Deck = &Deck{cards: []Card{bc(King, Diamonds)}}

	if err := g.DoubleDown(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Players[0].Bet != 20 {
		t.Errorf("ставка должна была удвоиться, получено %d", g.Players[0].Bet)
	}
	if g.Players[0].Balance != 80 {
		t.Errorf("ожидался капитал 80 после доплаты ставки, получено %d", g.Players[0].Balance)
	}
	if len(g.Players[0].Hand) != 3 {
		t.Errorf("после удвоения должна была добавиться ровно 1 карта, получено %d карт", len(g.Players[0].Hand))
	}
	if g.Players[0].Status != BJStatusStood {
		t.Errorf("после удвоения (11+король=21, не перебор) статус должен быть stood, получено %v", g.Players[0].Status)
	}
}

func TestDoubleDownRejectsWithMoreThanTwoCards(t *testing.T) {
	g := NewBlackjackGame([]string{"A", "B"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(Five, Clubs), bc(Six, Hearts), bc(Two, Diamonds)}
	g.Players[0].Status = BJStatusPlaying
	g.CurrentPlayerIdx = 0
	g.Phase = BJPhasePlayerTurn

	if err := g.DoubleDown(); err != ErrBJCannotDouble {
		t.Errorf("ожидалась ошибка ErrBJCannotDouble, получено: %v", err)
	}
}

func TestDoubleDownRejectsInsufficientFunds(t *testing.T) {
	g := NewBlackjackGame([]string{"A", "B"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(Five, Clubs), bc(Six, Hearts)}
	g.Players[0].Status = BJStatusPlaying
	g.Players[0].Balance = 5 // меньше ставки
	g.Players[0].Bet = 10
	g.CurrentPlayerIdx = 0
	g.Phase = BJPhasePlayerTurn

	if err := g.DoubleDown(); err != ErrBJNotEnoughFunds {
		t.Errorf("ожидалась ошибка ErrBJNotEnoughFunds, получено: %v", err)
	}
}

func TestPlayerBlackjackBeatsDealerNonBlackjack(t *testing.T) {
	g := NewBlackjackGame([]string{"A"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(Ace, Clubs), bc(King, Hearts)}
	g.Players[0].Status = BJStatusBlackjack
	g.Dealer = []Card{bc(Nine, Clubs), bc(Seven, Hearts)} // 16, дилер должен добрать
	g.Deck = &Deck{cards: []Card{bc(Two, Diamonds)}}      // 16+2=18, останов
	g.CurrentPlayerIdx = len(g.Players)
	g.advanceToNextPlayerOrResolve()

	if g.Phase != BJPhaseRoundOver {
		t.Fatalf("раунд должен был завершиться")
	}
	res := g.Result.PlayerResults[0]
	if res.Outcome != "blackjack" {
		t.Fatalf("ожидался исход blackjack, получено %s", res.Outcome)
	}
	// ставка 10: блэкджек платит 3:2 -> получает 10+15=25, было 90 (после
	// списания ставки) -> дельта относительно ДО раунда (100): +15
	if res.Delta != 15 {
		t.Errorf("ожидалась дельта +15, получено %d", res.Delta)
	}
}

func TestPushWhenBothBlackjack(t *testing.T) {
	g := NewBlackjackGame([]string{"A"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(Ace, Clubs), bc(King, Hearts)}
	g.Players[0].Status = BJStatusBlackjack
	g.Dealer = []Card{bc(Ace, Diamonds), bc(Queen, Spades)}
	g.CurrentPlayerIdx = len(g.Players)
	g.advanceToNextPlayerOrResolve()

	res := g.Result.PlayerResults[0]
	if res.Outcome != "push" {
		t.Fatalf("ожидался push при блэкджеке у обоих, получено %s", res.Outcome)
	}
	if res.Delta != 0 {
		t.Errorf("ожидалась нулевая дельта при push, получено %d", res.Delta)
	}
}

func TestDealerBustPaysStandingPlayer(t *testing.T) {
	g := NewBlackjackGame([]string{"A"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(King, Clubs), bc(Nine, Hearts)} // 19
	g.Players[0].Status = BJStatusStood
	g.Dealer = []Card{bc(King, Diamonds), bc(Six, Spades)} // 16, обязан добрать
	g.Deck = &Deck{cards: []Card{bc(King, Hearts)}}        // 16+10=26, перебор
	g.CurrentPlayerIdx = len(g.Players)
	g.advanceToNextPlayerOrResolve()

	if !g.Result.DealerBusted {
		t.Fatalf("дилер должен был перебрать")
	}
	res := g.Result.PlayerResults[0]
	if res.Outcome != "win" {
		t.Fatalf("ожидался win при переборе дилера, получено %s", res.Outcome)
	}
	if res.Delta != 10 {
		t.Errorf("ожидалась дельта +10 (выигрыш 1:1), получено %d", res.Delta)
	}
}

func TestBustedPlayerLosesRegardlessOfDealer(t *testing.T) {
	g := NewBlackjackGame([]string{"A"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(King, Clubs), bc(Queen, Hearts), bc(Five, Diamonds)} // 25
	g.Players[0].Status = BJStatusBusted
	g.Dealer = []Card{bc(King, Diamonds), bc(Nine, Spades)} // 19
	g.CurrentPlayerIdx = len(g.Players)
	g.advanceToNextPlayerOrResolve()

	res := g.Result.PlayerResults[0]
	if res.Outcome != "bust" {
		t.Fatalf("ожидался исход bust, получено %s", res.Outcome)
	}
	if res.Delta != -10 {
		t.Errorf("ожидалась дельта -10 (потеря ставки), получено %d", res.Delta)
	}
}

func TestPushOnEqualTotals(t *testing.T) {
	g := NewBlackjackGame([]string{"A"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(King, Clubs), bc(Nine, Hearts)} // 19
	g.Players[0].Status = BJStatusStood
	g.Dealer = []Card{bc(King, Diamonds), bc(Nine, Spades)} // 19, останов без добора
	g.CurrentPlayerIdx = len(g.Players)
	g.advanceToNextPlayerOrResolve()

	res := g.Result.PlayerResults[0]
	if res.Outcome != "push" {
		t.Fatalf("ожидался push при равных суммах, получено %s", res.Outcome)
	}
	if res.Delta != 0 {
		t.Errorf("ожидалась нулевая дельта, получено %d", res.Delta)
	}
}

func TestDealerStandsOnAllSeventeens(t *testing.T) {
	g := NewBlackjackGame([]string{"A"}, -1, 100, 10)
	resetForManualBout(g)
	g.Players[0].Hand = []Card{bc(King, Clubs), bc(Nine, Hearts)} // 19
	g.Players[0].Status = BJStatusStood
	// мягкие 17: туз + 6 = 17. Дилер должен остановиться, не добирая.
	g.Dealer = []Card{bc(Ace, Diamonds), bc(Six, Spades)}
	g.Deck = &Deck{cards: []Card{bc(King, Hearts)}} // если бы дилер тянул — перебор
	g.CurrentPlayerIdx = len(g.Players)
	g.advanceToNextPlayerOrResolve()

	if len(g.Dealer) != 2 {
		t.Fatalf("дилер не должен был тянуть карту на мягких 17, рука: %v", g.Dealer)
	}
	if g.Result.DealerTotal != 17 {
		t.Errorf("ожидалась сумма дилера 17, получено %d", g.Result.DealerTotal)
	}
}

func TestNextRoundCarriesOverBalance(t *testing.T) {
	g := NewBlackjackGame([]string{"A", "B"}, 0, 100, 10)
	g.Players[0].Balance = 150
	g.Players[1].Balance = 42

	next := g.NextRound()
	// Капитал должен перенестись, а новая ставка — списаться заново.
	// Проверяем именно preRoundBalance (капитал ДО ставки нового
	// раунда), а не текущий Balance: если кому-то в новом раунде
	// случайно выпадет блэкджек, конструктор сам мгновенно разрешит
	// раунд и Balance уже отразит и ставку, и выплату — это ожидаемое
	// поведение движка, но не то, что здесь проверяется.
	if next.preRoundBalance[0] != 150 {
		t.Errorf("ожидался перенесённый капитал 150 до ставки, получено %d", next.preRoundBalance[0])
	}
	if next.preRoundBalance[1] != 42 {
		t.Errorf("ожидался перенесённый капитал 42 до ставки, получено %d", next.preRoundBalance[1])
	}
	if next.Players[0].Balance != 140 && next.Phase == BJPhasePlayerTurn {
		t.Errorf("до какого-либо разрешения раунда ожидался капитал 140 (150-10), получено %d", next.Players[0].Balance)
	}
	if !next.Players[0].IsHuman {
		t.Errorf("флаг IsHuman должен был сохраниться при переносе")
	}
}

// TestBlackjackFullSimulation прогоняет много полных раундов с
// ботами разного числа игроков, проверяя отсутствие паник и
// корректность (капитал не должен непредсказуемо телепортироваться:
// сумма всех дельт равна сумме (dealer-neutral, т.е. каждый платится
// независимо от банка, поэтому проверяем только отсутствие паник и
// что результат всегда присутствует).
func TestBlackjackFullSimulation(t *testing.T) {
	bot := BlackjackBot{}
	for numPlayers := 1; numPlayers <= 6; numPlayers++ {
		for round := 0; round < 50; round++ {
			names := make([]string, numPlayers)
			for i := range names {
				names[i] = "Bot"
			}
			g := NewBlackjackGame(names, -1, 1000, 10)

			const maxSteps = 100
			steps := 0
			for ; steps < maxSteps && g.Phase != BJPhaseRoundOver; steps++ {
				if err := bot.PlayFullTurn(g); err != nil {
					t.Fatalf("игроков %d, раунд %d: ошибка бота: %v", numPlayers, round, err)
				}
			}
			if steps >= maxSteps {
				t.Fatalf("игроков %d, раунд %d: раунд не завершился за %d шагов", numPlayers, round, maxSteps)
			}
			if g.Result == nil {
				t.Fatalf("игроков %d, раунд %d: раунд завершился без результата", numPlayers, round)
			}
			if len(g.Result.PlayerResults) != numPlayers {
				t.Fatalf("игроков %d, раунд %d: ожидалось %d результатов, получено %d",
					numPlayers, round, numPlayers, len(g.Result.PlayerResults))
			}
		}
	}
}

func TestNewBlackjackRoundClampsInsufficientBet(t *testing.T) {
	g := NewBlackjackRound([]string{"Вы", "Бот"}, 0, 100, 10, []int{5, 100})
	if g.Players[0].Bet != 5 {
		t.Errorf("ставка должна была урезаться до всего оставшегося баланса (5), получено %d", g.Players[0].Bet)
	}
	if g.Players[0].Balance != 0 {
		t.Errorf("после урезанной ставки баланс должен был стать ровно 0, получено %d", g.Players[0].Balance)
	}
	if !g.RoundBetReduced[0] {
		t.Errorf("ожидался флаг RoundBetReduced[0]=true")
	}
	if g.RoundBetReduced[1] {
		t.Errorf("у игрока с достаточным балансом флаг урезания ставки быть не должно")
	}
}

func TestNewBlackjackRoundNeverGoesNegative(t *testing.T) {
	g := NewBlackjackRound([]string{"Вы"}, 0, 100, 10, []int{0})
	if g.Players[0].Balance < 0 {
		t.Fatalf("баланс не должен был уйти в минус, получено %d", g.Players[0].Balance)
	}
	if g.Players[0].Balance != 0 {
		t.Errorf("баланс при отсутствии денег должен был остаться 0, получено %d", g.Players[0].Balance)
	}
	if g.Players[0].Bet != 0 {
		t.Errorf("ставка при отсутствии денег должна была стать 0, получено %d", g.Players[0].Bet)
	}
}

func TestBlackjackZeroBetRoundNeverChangesBalance(t *testing.T) {
	g := NewBlackjackRound([]string{"Вы"}, 0, 100, 10, []int{0})
	// доигрываем раунд ботом (человек тоже отыгрывает автоматически через Stand)
	for g.Phase == BJPhasePlayerTurn {
		if err := g.Stand(); err != nil {
			t.Fatalf("неожиданная ошибка: %v", err)
		}
	}
	if g.Players[0].Balance != 0 {
		t.Errorf("при нулевой ставке баланс не должен был измениться ни при каком исходе, получено %d", g.Players[0].Balance)
	}
}

func TestBlackjackMultipleRoundsNeverGoNegativeWithLowCapital(t *testing.T) {
	names := []string{"Вы", "Бот1", "Бот2"}
	g := NewBlackjackGame(names, 0, 15, 10) // капитала хватит всего на 1-2 полные ставки
	bot := BlackjackBot{}
	for round := 0; round < 20; round++ {
		for steps := 0; steps < 100 && g.Phase != BJPhaseRoundOver; steps++ {
			if err := bot.PlayFullTurn(g); err != nil {
				t.Fatalf("раунд %d: неожиданная ошибка бота: %v", round, err)
			}
		}
		for _, p := range g.Players {
			if p.Balance < 0 {
				t.Fatalf("раунд %d: баланс игрока %s ушёл в минус: %d", round, p.Name, p.Balance)
			}
		}
		g = g.NextRound()
	}
}
