package main

import (
	"math/rand"
	"testing"
)

func newTestPokerGame(numPlayers, stack int) *PokerGameState {
	rnd := rand.New(rand.NewSource(1))
	return NewPokerGame(numPlayers, stack, 10, 20, rnd)
}

func TestNewPokerGamePostsBlindsCorrectly(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	sbIdx := g.nextActiveTournamentSeat(g.DealerIdx)
	bbIdx := g.nextActiveTournamentSeat(sbIdx)
	if g.Players[sbIdx].CurrentBet != 10 {
		t.Errorf("малый блайнд должен был поставить 10, получено %d", g.Players[sbIdx].CurrentBet)
	}
	if g.Players[bbIdx].CurrentBet != 20 {
		t.Errorf("большой блайнд должен был поставить 20, получено %d", g.Players[bbIdx].CurrentBet)
	}
	if g.Players[sbIdx].Stack != 990 || g.Players[bbIdx].Stack != 980 {
		t.Errorf("стеки после блайндов неверны: SB=%d BB=%d", g.Players[sbIdx].Stack, g.Players[bbIdx].Stack)
	}
}

func TestHeadsUpDealerPostsSmallBlindHimself(t *testing.T) {
	g := newTestPokerGame(2, 1000)
	other := g.nextActiveTournamentSeat(g.DealerIdx)
	if g.Players[g.DealerIdx].CurrentBet != g.SmallBlind {
		t.Errorf("в игре один на один дилер должен сам поставить малый блайнд, получено %d", g.Players[g.DealerIdx].CurrentBet)
	}
	if g.Players[other].CurrentBet != g.BigBlind {
		t.Errorf("в игре один на один второй игрок должен поставить большой блайнд, получено %d", g.Players[other].CurrentBet)
	}
	if g.CurrentPlayerIdx != g.DealerIdx {
		t.Errorf("в игре один на один префлоп-ход первым должен ходить дилер (малый блайнд), получено игрока %d", g.CurrentPlayerIdx)
	}
}

func TestEachPlayerGetsTwoHoleCards(t *testing.T) {
	g := newTestPokerGame(4, 1000)
	seen := map[Card]bool{}
	for _, p := range g.Players {
		if p.HoleCards[0] == p.HoleCards[1] {
			t.Fatalf("две карты в руке одного игрока не должны совпадать: %v", p.HoleCards)
		}
		for _, c := range p.HoleCards {
			if seen[c] {
				t.Fatalf("карта %v оказалась у двух разных игроков", c)
			}
			seen[c] = true
		}
	}
}

func TestPreflopActionStartsAfterBigBlind(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	bbIdx := g.nextActiveTournamentSeat(g.nextActiveTournamentSeat(g.DealerIdx))
	want := g.nextToAct(bbIdx)
	if g.CurrentPlayerIdx != want {
		t.Errorf("первый ход префлопа должен быть у игрока после большого блайнда, ожидалось %d, получено %d", want, g.CurrentPlayerIdx)
	}
}

func TestCheckRejectedWhenBetIsOutstanding(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	if err := g.Check(); err != ErrPokerCannotCheck {
		t.Errorf("ожидалась ошибка недопустимого чека против ставки большого блайнда, получено: %v", err)
	}
}

func TestCallMatchesHighestBet(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	actor := g.CurrentPlayerIdx
	if err := g.Call(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Players[actor].CurrentBet != 20 {
		t.Errorf("после колла ставка должна была стать 20, получено %d", g.Players[actor].CurrentBet)
	}
}

func TestBetBelowMinimumRejected(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	if err := g.Bet(25); err != ErrPokerBetTooLow {
		t.Errorf("ожидалась ошибка слишком маленькой ставки (минимум 40 = 20+20), получено: %v", err)
	}
}

func TestBetAboveStackRejected(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	if err := g.Bet(100000); err != ErrPokerNotEnoughChips {
		t.Errorf("ожидалась ошибка нехватки фишек, получено: %v", err)
	}
}

func TestCallWithInsufficientStackGoesAllIn(t *testing.T) {
	g := newTestPokerGame(3, 15)
	actor := g.CurrentPlayerIdx
	if err := g.Call(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if !g.Players[actor].AllIn {
		t.Errorf("колл с недостаточным стеком должен был перевести игрока в олл-ин")
	}
	if g.Players[actor].Stack != 0 {
		t.Errorf("после олл-ина стек должен был обнулиться, получено %d", g.Players[actor].Stack)
	}
}

func TestFoldToOnePlayerEndsHandImmediately(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	for g.Phase != PokerPhaseShowdown && g.countInHand() > 1 {
		if err := g.Fold(); err != nil {
			t.Fatalf("неожиданная ошибка: %v", err)
		}
	}
	if g.Phase != PokerPhaseShowdown {
		t.Fatalf("раздача должна была завершиться после того, как остался один игрок")
	}
	if g.Result == nil || g.Result.WentToShowdown {
		t.Errorf("завершение сбросами не должно доходить до шоудауна с показом карт")
	}
}

func TestBettingRoundAdvancesToFlopWhenAllCall(t *testing.T) {
	g := newTestPokerGame(3, 1000)
	for g.Phase == PokerPhasePreflop {
		if err := g.Call(); err != nil {
			t.Fatalf("неожиданная ошибка: %v", err)
		}
	}
	if g.Phase != PokerPhaseFlop {
		t.Fatalf("после того как все уравняли, должен был начаться флоп, получено %v", g.Phase)
	}
	if len(g.Board) != 3 {
		t.Errorf("на флопе должно быть 3 общие карты, получено %d", len(g.Board))
	}
}

func TestFullHandProgressesThroughAllStreets(t *testing.T) {
	g := newTestPokerGame(2, 1000)
	steps := 0
	for g.Phase != PokerPhaseShowdown && steps < 100 {
		steps++
		if err := g.Check(); err != nil {
			g.Call()
		}
	}
	if g.Phase != PokerPhaseShowdown {
		t.Fatalf("раздача не дошла до шоудауна за %d шагов", steps)
	}
	if len(g.Board) != 5 {
		t.Errorf("на шоудауне должно быть 5 общих карт, получено %d", len(g.Board))
	}
}

func TestAllInSkipsRemainingBettingRounds(t *testing.T) {
	g := newTestPokerGame(2, 100)
	if err := g.AllIn(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if err := g.Call(); err != nil {
		t.Fatalf("неожиданная ошибка: %v", err)
	}
	if g.Phase != PokerPhaseShowdown {
		t.Fatalf("после олл-ина и колла (никто больше не может действовать) должен был сразу настать шоудаун, получено %v", g.Phase)
	}
	if len(g.Board) != 5 {
		t.Errorf("на доске должно было оказаться все 5 карт (доигранных без торгов), получено %d", len(g.Board))
	}
}

func TestShowdownAwardsPotToBestHand(t *testing.T) {
	g := newTestPokerGame(2, 1000)
	g.Players[0].HoleCards = [2]Card{c(Ace, Spades), c(Ace, Hearts)}
	g.Players[1].HoleCards = [2]Card{c(Two, Clubs), c(Seven, Diamonds)}
	g.Board = []Card{c(Ace, Clubs), c(King, Spades), c(Queen, Hearts), c(Four, Diamonds), c(Nine, Clubs)}
	g.Players[0].TotalContributed = 100
	g.Players[1].TotalContributed = 100
	g.resolveShowdown()

	if g.Result.Winners[0] <= 0 {
		t.Errorf("игрок 0 с тройкой тузов должен был выиграть банк, получено %+v", g.Result.Winners)
	}
	if _, lost := g.Result.Winners[1]; lost {
		t.Errorf("игрок 1 не должен был получить долю банка")
	}
}

func TestShowdownSplitsPotOnTie(t *testing.T) {
	g := newTestPokerGame(2, 1000)
	g.Players[0].HoleCards = [2]Card{c(Two, Clubs), c(Three, Diamonds)}
	g.Players[1].HoleCards = [2]Card{c(Two, Hearts), c(Three, Spades)}
	g.Board = []Card{c(Ace, Clubs), c(King, Spades), c(Queen, Hearts), c(Jack, Diamonds), c(Nine, Clubs)}
	g.Players[0].TotalContributed = 100
	g.Players[1].TotalContributed = 100
	g.resolveShowdown()

	if g.Result.Winners[0] != 100 || g.Result.Winners[1] != 100 {
		t.Errorf("одинаковые по силе руки (игра по доске) должны были поровну разделить банк 200, получено %+v", g.Result.Winners)
	}
}

func TestSidePotsWithTwoDifferentAllInAmounts(t *testing.T) {
	players := []*PokerPlayer{
		{TotalContributed: 50},
		{TotalContributed: 100},
		{TotalContributed: 100},
	}
	pots := computeSidePots(players)
	if len(pots) != 2 {
		t.Fatalf("ожидалось 2 банка (основной + один сайд-пот), получено %d: %+v", len(pots), pots)
	}
	if pots[0].Amount != 150 {
		t.Errorf("основной банк должен быть 150 (50 от каждого из трёх), получено %d", pots[0].Amount)
	}
	if len(pots[0].EligiblePlayers) != 3 {
		t.Errorf("на основной банк претендуют все трое, получено %d", len(pots[0].EligiblePlayers))
	}
	if pots[1].Amount != 100 {
		t.Errorf("сайд-пот должен быть 100 (по 50 от двоих, кто пошёл дальше), получено %d", pots[1].Amount)
	}
	if len(pots[1].EligiblePlayers) != 2 {
		t.Errorf("на сайд-пот претендуют только двое, получено %d", len(pots[1].EligiblePlayers))
	}
}

func TestSidePotsExcludeFoldedPlayersFromEligibilityButKeepTheirMoney(t *testing.T) {
	players := []*PokerPlayer{
		{TotalContributed: 100, Folded: true},
		{TotalContributed: 100},
		{TotalContributed: 100},
	}
	pots := computeSidePots(players)
	if len(pots) != 1 {
		t.Fatalf("ожидался один общий банк, получено %d", len(pots))
	}
	if pots[0].Amount != 300 {
		t.Errorf("деньги сбросившего игрока должны были остаться в банке, ожидалось 300, получено %d", pots[0].Amount)
	}
	if len(pots[0].EligiblePlayers) != 2 {
		t.Errorf("сбросивший игрок не должен претендовать на банк, ожидалось 2 претендента, получено %d", len(pots[0].EligiblePlayers))
	}
}

func TestPlayerEliminatedWhenStackHitsZero(t *testing.T) {
	g := newTestPokerGame(2, 1000)
	g.Players[0].HoleCards = [2]Card{c(Two, Clubs), c(Seven, Diamonds)}
	g.Players[1].HoleCards = [2]Card{c(Ace, Spades), c(Ace, Hearts)}
	g.Board = []Card{c(Ace, Clubs), c(King, Spades), c(Queen, Hearts), c(Four, Diamonds), c(Nine, Clubs)}
	g.Players[0].Stack = 0
	g.Players[0].TotalContributed = 1000
	g.Players[1].TotalContributed = 1000
	g.resolveShowdown()

	if !g.Players[0].Eliminated {
		t.Errorf("проигравший всё игрок должен был выбыть из турнира")
	}
}

func TestTournamentOverWhenOnePlayerRemains(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	g := newTestPokerGame(2, 1000)
	g.Players[0].Eliminated = true
	g.Players[0].Stack = 0
	g.Phase = PokerPhaseShowdown
	g.NextHand(rnd)
	if g.Phase != PokerPhaseTournamentOver {
		t.Errorf("турнир должен был завершиться при одном оставшемся игроке, получено %v", g.Phase)
	}
}

func TestDealerButtonRotatesEachHand(t *testing.T) {
	rnd := rand.New(rand.NewSource(1))
	g := newTestPokerGame(3, 1000)
	firstDealer := g.DealerIdx
	for g.Phase != PokerPhaseShowdown {
		g.Fold()
	}
	g.NextHand(rnd)
	if g.DealerIdx == firstDealer {
		t.Errorf("дилер должен был смениться на следующей раздаче")
	}
}
