package uno

import (
	"encoding/json"
	"math/rand/v2"
	"testing"
)

const rake = 0.05

func duel() Tier  { t, _ := TierBySlug("duel"); return t }
func full() Tier  { t, _ := TierBySlug("full"); return t }
func table() Tier { t, _ := TierBySlug("table"); return t }

// deal starts a game, failing the test if it can't.
func deal(t *testing.T, tier Tier, bet int64, seed uint64) State {
	t.Helper()
	s, evs, err := New(bet, tier, rake, seed, 0xC0FFEE)
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	if len(evs) != 1 || evs[0].Kind != EvDeal {
		t.Fatalf("New should deal exactly one event, got %+v", evs)
	}
	return s
}

// census counts every card in the game, wherever it is. It is the invariant the
// whole engine has to hold: 108 cards, each of them in exactly one place.
func census(s State) map[Card]int {
	m := map[Card]int{}
	for _, h := range s.Hands {
		for _, c := range h {
			m[c]++
		}
	}
	for _, c := range s.Deck {
		m[c]++
	}
	for _, c := range s.Discard {
		// A wild is stamped with the colour it was played as while it sits on the
		// pile, so it counts as the wild it really is. This asks the face, rather
		// than listing the wilds: No Mercy prints four more of them, and a census
		// that didn't know about them would count a played roulette as a red card
		// and report a deck that balances while the cards don't.
		if c.Value.Wild() {
			c.Color = Wild
		}
		m[c]++
	}
	return m
}

func total(m map[Card]int) int {
	n := 0
	for _, v := range m {
		n += v
	}
	return n
}

func TestNewDeckIsADeck(t *testing.T) {
	m := census(State{Deck: NewDeck()})
	if got := total(m); got != 108 {
		t.Fatalf("deck has %d cards, want 108", got)
	}
	if m[Card{Red, Zero}] != 1 {
		t.Errorf("want one red zero, got %d", m[Card{Red, Zero}])
	}
	if m[Card{Blue, Seven}] != 2 {
		t.Errorf("want two blue sevens, got %d", m[Card{Blue, Seven}])
	}
	if m[Card{Wild, WildDrawFour}] != 4 {
		t.Errorf("want four +4s, got %d", m[Card{Wild, WildDrawFour}])
	}
}

func TestNewDeals(t *testing.T) {
	s := deal(t, full(), 100, 7)
	if len(s.Hands) != 4 {
		t.Fatalf("full house is four seats, got %d", len(s.Hands))
	}
	for i, h := range s.Hands {
		if len(h) != HandSize {
			t.Errorf("seat %d holds %d cards, want %d", i, len(h), HandSize)
		}
	}
	if len(s.Bots) != 3 {
		t.Fatalf("want three bot names, got %v", s.Bots)
	}
	if s.Turn != You {
		t.Errorf("you play first, turn is %d", s.Turn)
	}
	if got := total(census(s)); got != 108 {
		t.Fatalf("the deal lost cards: %d of 108", got)
	}
}

// The card turned over to start is never an action card — see New.
func TestOpeningCardIsANumber(t *testing.T) {
	for seed := uint64(0); seed < 300; seed++ {
		s := deal(t, table(), 50, seed)
		if s.Top().Value.Action() {
			t.Fatalf("seed %d opened on %v", seed, s.Top())
		}
		if s.Color != s.Top().Color {
			t.Fatalf("seed %d: colour in play is %v, top card is %v", seed, s.Color, s.Top())
		}
	}
}

// ---- the rules ------------------------------------------------------------

// rig builds a state by hand, so a rule can be tested without hunting a seed
// that happens to deal it.
//
// The deck is the rest of the deck: every card not in a hand and not the one in
// play. So a rigged game still holds 108 cards, and the census invariant means
// something in these tests too.
func rig(hands [][]Card, top Card, color Color) State {
	left := map[Card]int{}
	for _, c := range NewDeck() {
		left[c]++
	}
	take := func(c Card) {
		if c.IsWild() {
			c.Color = Wild
		}
		left[c]--
	}
	for _, h := range hands {
		for _, c := range h {
			take(c)
		}
	}
	take(top)

	var deck []Card
	for _, c := range NewDeck() {
		key := c
		if left[key] > 0 {
			left[key]--
			deck = append(deck, c)
		}
	}
	return State{
		Tier: full(), Hands: hands, Discard: []Card{top}, Color: color,
		Deck: deck, Dir: 1, Turn: You, Phase: PhasePlay,
		Bet: 100, RakePct: rake, Seed1: 1, Seed2: 2,
	}
}

func TestPlayMustMatch(t *testing.T) {
	s := rig([][]Card{{{Blue, Three}}, {{Red, Five}}}, Card{Red, Nine}, Red)
	if _, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0}); err != ErrCantPlay {
		t.Fatalf("a blue 3 on a red 9 should be refused, got %v", err)
	}
}

func TestPlayMatchesFaceOrColor(t *testing.T) {
	// Same face, different colour: legal.
	s := rig([][]Card{{{Blue, Nine}, {Red, Two}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("a blue 9 on a red 9 is legal: %v", err)
	}
	if next.Color != Blue {
		t.Errorf("colour in play should follow the card: %v", next.Color)
	}
	if evs[0].Kind != EvPlay || evs[0].Seat != You {
		t.Errorf("first event should be your play, got %+v", evs[0])
	}
}

func TestWildNeedsAColor(t *testing.T) {
	s := rig([][]Card{{{Wild, WildCard}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	if _, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0}); err != ErrNeedColor {
		t.Fatalf("a wild with no colour should be refused, got %v", err)
	}
	if _, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0, Color: Wild}); err != ErrNeedColor {
		t.Fatalf("naming 'wild' is not naming a colour, got %v", err)
	}
}

func TestWildNamesTheColor(t *testing.T) {
	s := rig([][]Card{{{Wild, WildCard}, {Green, One}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	next, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0, Color: Green})
	if err != nil {
		t.Fatalf("play wild: %v", err)
	}
	// The bot moved after us, so the colour in play is whatever it left behind —
	// what we can check is that the wild itself went down as green.
	top := next.Discard
	if len(top) < 2 {
		t.Fatalf("expected the wild and the bot's card on the pile: %v", top)
	}
	if top[1] != (Card{Green, WildCard}) {
		t.Errorf("the wild should sit on the pile as green, got %v", top[1])
	}
}

func TestDrawTwoHitsTheNextSeat(t *testing.T) {
	// Two seats, so the +2 lands on the bot and the turn comes straight back.
	s := rig([][]Card{{{Red, DrawTwo}, {Red, One}}, {{Blue, Five}, {Blue, Six}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play +2: %v", err)
	}
	if len(next.Hands[1]) != 4 {
		t.Errorf("the bot should hold 2 + 2 = 4 cards, got %d", len(next.Hands[1]))
	}
	if next.Turn != You {
		t.Errorf("the bot was skipped, so it should be your turn: %d", next.Turn)
	}
	if !hasKind(evs, EvForced) || !hasKind(evs, EvSkip) {
		t.Errorf("a +2 is a forced draw and a skip: %+v", evs)
	}
	if got := total(census(next)); got != 108 {
		t.Fatalf("the +2 lost cards: %d of 108", got)
	}
}

func TestReverseIsASkipHeadsUp(t *testing.T) {
	s := rig([][]Card{{{Red, Reverse}, {Red, One}}, {{Blue, Five}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play reverse: %v", err)
	}
	if next.Dir != 1 {
		t.Errorf("with two at the table a reverse doesn't turn the table around: dir %d", next.Dir)
	}
	if next.Turn != You {
		t.Errorf("the bot should have been skipped, turn is %d", next.Turn)
	}
	if !hasKind(evs, EvSkip) || hasKind(evs, EvReverse) {
		t.Errorf("heads up, a reverse reads as a skip: %+v", evs)
	}
	if len(next.Hands[1]) != 1 {
		t.Errorf("the bot never played, so it still holds one card: %d", len(next.Hands[1]))
	}
}

func TestReverseTurnsTheTableAround(t *testing.T) {
	// Every bot holds a red card, so each of them can play the moment the turn
	// reaches it — which is what makes the *order* they play in observable.
	s := rig([][]Card{
		{{Red, Reverse}, {Red, One}},
		{{Red, Five}, {Blue, Six}},
		{{Red, Six}, {Green, Six}},
		{{Red, Seven}, {Yellow, Six}},
	}, Card{Red, Nine}, Red)
	next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play reverse: %v", err)
	}
	if next.Dir != -1 {
		t.Errorf("four at the table: a reverse turns it around, dir %d", next.Dir)
	}
	if !hasKind(evs, EvReverse) {
		t.Errorf("want a reverse event: %+v", evs)
	}
	if next.Turn != You {
		t.Errorf("the bots should have played round to you, turn is %d", next.Turn)
	}
	// The table now runs anticlockwise: seat 3 plays, then 2, then 1.
	var order []int
	for _, e := range evs {
		if e.Kind == EvPlay && e.Seat != You {
			order = append(order, e.Seat)
		}
	}
	if len(order) != 3 || order[0] != 3 || order[1] != 2 || order[2] != 1 {
		t.Errorf("the bots played in the order %v, want [3 2 1]", order)
	}
}

func TestSkipSkips(t *testing.T) {
	// Both bots hold a playable red, so the only reason either of them doesn't
	// play is that it wasn't asked to.
	s := rig([][]Card{
		{{Red, Skip}, {Red, One}},
		{{Red, Five}, {Blue, Six}},
		{{Red, Six}, {Green, Six}},
	}, Card{Red, Nine}, Red)
	s.Tier = table()
	next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play skip: %v", err)
	}
	if !hasKind(evs, EvSkip) {
		t.Errorf("want a skip event: %+v", evs)
	}
	for _, e := range evs {
		if e.Kind == EvPlay && e.Seat == 1 {
			t.Errorf("seat 1 was skipped and should not have played: %+v", e)
		}
	}
	if len(next.Hands[1]) != 2 {
		t.Errorf("seat 1 was skipped and should still hold two: %d", len(next.Hands[1]))
	}
	if len(next.Hands[2]) != 1 {
		t.Errorf("seat 2 was not skipped and should have played: %d", len(next.Hands[2]))
	}
}

// ---- drawing --------------------------------------------------------------

func TestDrawnPlayableWaitsForYou(t *testing.T) {
	s := rig([][]Card{{{Blue, Three}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	s.Deck = []Card{{Red, Four}} // exactly what you'll draw, and it plays
	next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if next.Phase != PhaseDrawn {
		t.Fatalf("a playable draw should stop and ask, phase is %v", next.Phase)
	}
	if next.Turn != You {
		t.Fatalf("the turn should still be yours: %d", next.Turn)
	}
	if evs[0].Kind != EvDraw || evs[0].Card == nil || *evs[0].Card != (Card{Red, Four}) {
		t.Fatalf("your own drawn card comes face up: %+v", evs[0])
	}
	if got := next.Playable(); len(got) != 1 || got[0] != 1 {
		t.Errorf("the drawn card, and only it, is playable: %v", got)
	}
	// You may not play the *other* card instead — drawing would otherwise be a
	// free look with no cost.
	if _, _, err := ApplyMove(next, Move{Kind: MovePlay, Index: 0}); err != ErrMustPlayNow {
		t.Fatalf("only the drawn card may be played, got %v", err)
	}
	if _, _, err := ApplyMove(next, Move{Kind: MoveDraw}); err != ErrMustPlayNow {
		t.Fatalf("you can't draw twice, got %v", err)
	}
	after, _, err := ApplyMove(next, Move{Kind: MovePass})
	if err != nil {
		t.Fatalf("pass: %v", err)
	}
	if after.Phase != PhasePlay || after.Turn != You {
		t.Fatalf("after passing the bot plays and it comes back to you: phase %v turn %d", after.Phase, after.Turn)
	}
	if len(after.Hands[You]) != 2 {
		t.Errorf("you kept the card you drew: %d", len(after.Hands[You]))
	}
}

func TestUnplayableDrawPassesTheTurn(t *testing.T) {
	s := rig([][]Card{{{Blue, Three}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	s.Deck = []Card{{Blue, Four}, {Red, Two}} // draw a blue 4: it doesn't go on a red 9
	next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if next.Phase != PhasePlay {
		t.Errorf("nothing to decide, so no pause: %v", next.Phase)
	}
	if !hasKind(evs, EvPass) {
		t.Errorf("the turn passed, and the table should be told: %+v", evs)
	}
}

func TestPassOnlyAfterADraw(t *testing.T) {
	s := rig([][]Card{{{Red, Three}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	if _, _, err := ApplyMove(s, Move{Kind: MovePass}); err != ErrCantPass {
		t.Fatalf("you can't pass a turn you haven't drawn on, got %v", err)
	}
}

// dead is a table nobody can move at: the deck is spent, the discard is one card
// deep so there is nothing to reshuffle out of, and not a seat holds a card that
// goes on the pile. Every seat can only pass, forever.
func dead(hands [][]Card) State {
	s := rig(hands, Card{Red, Nine}, Red)
	s.Deck = nil
	return s
}

// The game has to end here. It used to not: the stuck guard counted how many
// bots had passed in a row and asked for more of them than there are seats, so
// it never fired once, and a dead table just handed the turn round and round.
// That is a game the player cannot finish — and a game they cannot finish is
// chips they cannot cash out, because the cage won't let them leave a live hand.
func TestDeadTableEnds(t *testing.T) {
	s := dead([][]Card{{{Blue, Three}}, {{Green, Five}}})

	next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if next.Phase != PhaseDone {
		t.Fatalf("nobody can move and there is nothing to draw: the game is over, not %q", next.Phase)
	}
	if next.Outcome != OutcomeStuck {
		t.Errorf("a tie on the shortest hand is nobody going out: %q", next.Outcome)
	}
	if next.Payout != 0 {
		t.Errorf("a stuck game pays nothing, not %d", next.Payout)
	}
	if !hasKind(evs, EvSettle) {
		t.Errorf("the table has to be told it's over: %+v", evs)
	}
}

// And the shortest hand takes it, which is the one way a stuck table still pays.
func TestDeadTablePaysTheShortestHand(t *testing.T) {
	s := dead([][]Card{{{Blue, Three}}, {{Green, Five}, {Green, Six}}})

	next, _, err := ApplyMove(s, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if next.Outcome != OutcomeWon {
		t.Fatalf("one card against two is a win: %q", next.Outcome)
	}
	if next.Payout != s.Pays() {
		t.Errorf("payout %d, quoted %d — the felt has to honour what it advertised", next.Payout, s.Pays())
	}
}

func TestReshuffleRebuildsTheDeck(t *testing.T) {
	s := rig([][]Card{{{Blue, Three}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	// An empty deck, and a discard with something under the top card to become one.
	// The buried wild went down as green; it has to come back as a wild.
	s.Deck = nil
	s.Discard = []Card{{Green, WildCard}, {Red, Two}, {Red, Nine}}
	next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw on an empty deck: %v", err)
	}
	if !hasKind(evs, EvReshuffle) {
		t.Fatalf("want a reshuffle: %+v", evs)
	}
	if len(next.Discard) == 0 || next.Discard[0] != (Card{Red, Nine}) {
		t.Errorf("the card in play stays on the pile: %v", next.Discard)
	}
	for _, c := range next.Deck {
		if c.Value == WildCard && c.Color != Wild {
			t.Errorf("a wild went back into the deck stamped %v", c.Color)
		}
	}
	for _, h := range next.Hands {
		for _, c := range h {
			if c.Value == WildCard && c.Color != Wild {
				t.Errorf("a wild was dealt out stamped %v", c.Color)
			}
		}
	}
}

// ---- the money ------------------------------------------------------------

// The rule every game in this casino has had to be taught: the number the felt
// quotes and the number the settle lands on are one function, not two.
func TestQuoteIsThePayout(t *testing.T) {
	for _, tier := range Tiers {
		s := rig([][]Card{{{Red, Three}}, {{Green, Five}}}, Card{Red, Nine}, Red)
		s.Tier = tier
		s.Hands = make([][]Card, tier.Bots+1)
		s.Hands[You] = []Card{{Red, Three}}
		for i := 1; i <= tier.Bots; i++ {
			s.Hands[i] = []Card{{Green, Five}, {Green, Six}}
		}
		quoted := s.Pays()

		next, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0}) // your last card
		if err != nil {
			t.Fatalf("%s: go out: %v", tier.Slug, err)
		}
		if next.Outcome != OutcomeWon {
			t.Fatalf("%s: playing your last card wins, got %q", tier.Slug, next.Outcome)
		}
		if next.Payout != quoted {
			t.Errorf("%s: the felt quoted %d, the house paid %d", tier.Slug, quoted, next.Payout)
		}
		if next.Net() != quoted-s.Bet {
			t.Errorf("%s: net is %d, want %d", tier.Slug, next.Net(), quoted-s.Bet)
		}
	}
}

// The rake comes out of the winnings, never the stake.
//
// The arithmetic is derived from the tier rather than written down. It used to be
// written down — payout 214, rake 6 — and those numbers were the 2.2× duel. When
// the tiers were re-measured and repriced, this test failed on a rake that was
// perfectly correct, which is a test asserting a *price* while claiming to assert
// a *rule*. The rule is: the house takes its cut of the profit and never touches
// the stake. That holds at any multiple.
func TestRakeIsOnWinningsOnly(t *testing.T) {
	s := rig([][]Card{{{Red, Three}}, {{Green, Five}, {Green, Six}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	s.Bet = 100

	gross := int64(float64(s.Bet) * s.Tier.Base) // what the tier pays back, before the house
	profit := gross - s.Bet
	wantRake := int64(float64(profit) * rake)
	wantPayout := s.Bet + profit - wantRake

	next, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("go out: %v", err)
	}
	if next.Payout != wantPayout {
		t.Errorf("payout %d, want %d (%d stake + %d winnings - %d rake)",
			next.Payout, wantPayout, s.Bet, profit, wantRake)
	}
	if next.Rake != wantRake {
		t.Errorf("rake %d, want %d — and never a penny of the %d stake",
			next.Rake, wantRake, s.Bet)
	}
	if next.Net() != wantPayout-s.Bet {
		t.Errorf("net %d, want %d", next.Net(), wantPayout-s.Bet)
	}
}

func TestLosingPaysNothingAndIsNotCharged(t *testing.T) {
	// The bot holds one card that plays on the pile, so it goes out the moment the
	// turn reaches it.
	s := rig([][]Card{{{Red, Three}, {Red, Four}}, {{Red, Five}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play: %v", err)
	}
	if next.Outcome != OutcomeLost {
		t.Fatalf("the bot went out, so you lost: %q", next.Outcome)
	}
	if next.Payout != 0 || next.Rake != 0 {
		t.Errorf("a loss pays nothing and is charged nothing: payout %d rake %d", next.Payout, next.Rake)
	}
	if next.Net() != -s.Bet {
		t.Errorf("a loss costs the stake and no more: %d", next.Net())
	}
	last := evs[len(evs)-1]
	if last.Kind != EvSettle || last.Seat != 1 {
		t.Errorf("the settle should name the winner: %+v", last)
	}
}

func TestNoMoveAfterItIsOver(t *testing.T) {
	s := rig([][]Card{{{Red, Three}}, {{Green, Five}, {Green, Six}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	done, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("go out: %v", err)
	}
	if _, _, err := ApplyMove(done, Move{Kind: MoveDraw}); err != ErrGameOver {
		t.Fatalf("a finished game takes no more moves, got %v", err)
	}
}

func TestBadBet(t *testing.T) {
	if _, _, err := New(0, duel(), rake, 1, 2); err != ErrBadBet {
		t.Fatalf("want ErrBadBet, got %v", err)
	}
}

// ---- the whole game -------------------------------------------------------

// playOut plays a game to its end with a simple strategy: play the first legal
// card, otherwise draw, otherwise pass. It asserts the invariants at every step.
func playOut(t *testing.T, s State, maxTurns int) State {
	t.Helper()
	for turn := 0; s.Phase != PhaseDone; turn++ {
		if turn > maxTurns {
			t.Fatalf("the game never ended in %d turns", maxTurns)
		}
		if s.Turn != You {
			t.Fatalf("ApplyMove left the turn with seat %d — the bots should always run out", s.Turn)
		}

		var m Move
		if p := s.Playable(); len(p) > 0 {
			m = Move{Kind: MovePlay, Index: p[0]}
			if s.Hands[You][p[0]].IsWild() {
				m.Color = Green
			}
		} else if s.Phase == PhaseDrawn {
			m = Move{Kind: MovePass}
		} else {
			m = Move{Kind: MoveDraw}
		}

		next, evs, err := ApplyMove(s, m)
		if err != nil {
			t.Fatalf("turn %d: %v (move %+v, phase %v)", turn, err, m, s.Phase)
		}
		if len(evs) == 0 {
			t.Fatalf("turn %d: a move that happened emitted nothing", turn)
		}
		if got := total(census(next)); got != 108 {
			t.Fatalf("turn %d: %d cards of 108 — a card was lost or minted", turn, got)
		}
		for c, n := range census(next) {
			if want := deckCount(c); n != want {
				t.Fatalf("turn %d: %d of %v, want %d — a card was duplicated", turn, n, c, want)
			}
		}
		// No event ever names a bot's card. That is the hole card of this game, and
		// it is most of the deck.
		for _, e := range evs {
			if (e.Kind == EvDraw || e.Kind == EvForced) && e.Seat != You && e.Card != nil {
				t.Fatalf("turn %d: a bot's drawn card crossed the wire: %+v", turn, e)
			}
		}
		s = next
	}
	return s
}

// deckCount is how many of a given card a 108 deck holds.
func deckCount(c Card) int {
	switch {
	case c.Color == Wild:
		return 4
	case c.Value == Zero:
		return 1
	default:
		return 2
	}
}

// A hundred games, played out, with the invariants checked at every step. This
// is the test that would have caught a deck that leaks cards through the
// reshuffle, a turn the bots don't hand back, or a game that can't end.
func TestGamesPlayOut(t *testing.T) {
	wins, losses, stuck := 0, 0, 0
	for seed := uint64(0); seed < 100; seed++ {
		tier := Tiers[seed%3]
		end := playOut(t, deal(t, tier, 100, seed), 500)
		switch end.Outcome {
		case OutcomeWon:
			wins++
			if end.Payout != end.Pays() {
				t.Fatalf("seed %d: paid %d, quoted %d", seed, end.Payout, end.Pays())
			}
		case OutcomeLost:
			losses++
		case OutcomeStuck:
			stuck++
		default:
			t.Fatalf("seed %d ended as %q", seed, end.Outcome)
		}
		if len(end.Hands[end.winnerSeat()]) != 0 && end.Outcome != OutcomeStuck {
			t.Fatalf("seed %d: the winner is still holding cards", seed)
		}
	}
	// Playing the first legal card is a poor strategy against bots that hold their
	// +4s back, so this is not a fairness assertion — it's a check that both
	// outcomes actually happen. A table that never pays is a bug in the bots.
	if wins == 0 || losses == 0 {
		t.Fatalf("100 games gave %d wins, %d losses, %d stuck — one side never happens", wins, losses, stuck)
	}
	t.Logf("100 games: %d won, %d lost, %d stuck", wins, losses, stuck)
}

// winnerSeat is the seat the settle event named — only used by the tests.
func (s State) winnerSeat() int {
	best := 0
	for i := range s.Hands {
		if len(s.Hands[i]) < len(s.Hands[best]) {
			best = i
		}
	}
	return best
}

// The same seed deals the same game and the bots make the same choices — which
// is what lets a disputed game be replayed exactly as it fell.
func TestReplaysFromTheSeed(t *testing.T) {
	a := playOut(t, deal(t, full(), 250, 42), 500)
	b := playOut(t, deal(t, full(), 250, 42), 500)

	ja, _ := json.Marshal(a)
	jb, _ := json.Marshal(b)
	if string(ja) != string(jb) {
		t.Fatal("the same seed played the same way gave two different games")
	}
	if a.Outcome == "" {
		t.Fatal("the replay didn't finish")
	}
}

// A game in progress survives a redeploy: it is a plain value, so it round-trips
// through the JSON it is stored as.
func TestStateSurvivesStorage(t *testing.T) {
	s := deal(t, table(), 100, 9)
	s, _, err := ApplyMove(s, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}

	blob, err := json.Marshal(s)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var back State
	if err := json.Unmarshal(blob, &back); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	again, _ := json.Marshal(back)
	if string(again) != string(blob) {
		t.Fatal("a stored game came back different")
	}
	// And it plays on from there.
	playOut(t, back, 500)
}

// A move the engine refuses leaves the caller's state exactly as it was — no
// card half-played, no turn half-passed.
func TestARefusedMoveChangesNothing(t *testing.T) {
	s := rig([][]Card{{{Blue, Three}, {Wild, WildCard}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	before, _ := json.Marshal(s)

	for _, m := range []Move{
		{Kind: MovePlay, Index: 0},              // doesn't match
		{Kind: MovePlay, Index: 1},              // wild with no colour
		{Kind: MovePlay, Index: 9},              // no such card
		{Kind: MovePass},                        // nothing drawn
		{Kind: "shuffle-the-deck-in-my-favour"}, // no
	} {
		if _, _, err := ApplyMove(s, m); err == nil {
			t.Fatalf("%+v should have been refused", m)
		}
	}
	after, _ := json.Marshal(s)
	if string(before) != string(after) {
		t.Fatal("a refused move touched the state")
	}
}

// The bots choose. Two different seeds should not play the same bot game, or the
// bot is a lookup table you can memorise.
func TestBotsAreNotDeterministicAcrossSeeds(t *testing.T) {
	same := 0
	for seed := uint64(0); seed < 20; seed++ {
		a := playOut(t, deal(t, duel(), 100, seed), 500)
		b := playOut(t, deal(t, duel(), 100, seed+1000), 500)
		if len(a.Discard) == len(b.Discard) {
			same++
		}
	}
	if same == 20 {
		t.Fatal("every seed played out to the same length — the bots aren't choosing")
	}
}

// botPick holds its +4 back while it's comfortable, and reaches for it when
// somebody is about to go out.
func TestBotSavesTheDrawFour(t *testing.T) {
	hand := []Card{{Wild, WildDrawFour}, {Red, Five}}
	top, color := Card{Red, Nine}, Red
	rng := rand.New(rand.NewPCG(1, 2))

	held := 0
	for i := 0; i < 50; i++ {
		if _, idx := botPick(hand, top, color, 5, rng); idx == 1 {
			held++
		}
	}
	if held < 30 {
		t.Errorf("with the table comfortable the bot should mostly play the red 5, held %d/50", held)
	}

	reached := 0
	for i := 0; i < 50; i++ {
		if _, idx := botPick(hand, top, color, 1, rng); idx == 0 {
			reached++
		}
	}
	if reached < 30 {
		t.Errorf("with a player on one card the bot should mostly play the +4, reached %d/50", reached)
	}
}

func TestBotPicksItsBestColor(t *testing.T) {
	rng := rand.New(rand.NewPCG(3, 4))
	hand := []Card{{Blue, One}, {Blue, Two}, {Green, Three}, {Wild, WildCard}}
	if got := botColor(hand, rng); got != Blue {
		t.Errorf("the bot holds two blues: it should call blue, got %v", got)
	}
	// A hand of nothing but wilds still has to name something.
	for i := 0; i < 20; i++ {
		if got := botColor([]Card{{Wild, WildCard}}, rng); !got.Playable() {
			t.Fatalf("botColor named %v, which is not a colour", got)
		}
	}
}

func TestBotHasNothingToPlay(t *testing.T) {
	if _, idx := botPick([]Card{{Blue, Three}}, Card{Red, Nine}, Red, 3, rand.New(rand.NewPCG(1, 1))); idx != -1 {
		t.Errorf("a hand with nothing legal should report -1, got %d", idx)
	}
}

func hasKind(evs []Event, kind string) bool {
	for _, e := range evs {
		if e.Kind == kind {
			return true
		}
	}
	return false
}
