package uno

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

const rake = 0.05

// You is the human's seat in the solo tests, a test-local alias for what the
// engine no longer hardcodes: a table is a list of seats, and seat zero being the
// human is a convention only these fixtures keep.
const You = 0

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

// openSolo opens a solo table (one human, the tier's bots) without dealing.
func openSolo(t *testing.T, tier Tier, seed uint64) State {
	t.Helper()
	s, _, err := New(tier, SoloSeats(tier, tier.Bots, 1000), rake, seed, 0xC0FFEE)
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	return s
}

// deal opens a solo table and deals the first hand, so the human is to act.
func deal(t *testing.T, tier Tier, seed uint64) State {
	t.Helper()
	s := openSolo(t, tier, seed)
	s, evs, err := ApplyMove(s, You, Move{Kind: MoveDeal})
	if err != nil {
		t.Fatalf("deal: %v", err)
	}
	if !hasKind(evs, EvDeal) {
		t.Fatalf("the deal emitted no deal event: %+v", evs)
	}
	if s.Turn != You {
		t.Fatalf("the human should act first at a solo table, turn is %d", s.Turn)
	}
	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 {
		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(), 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)
		}
	}
	bots := 0
	for _, seat := range s.Seats {
		if seat.Bot {
			bots++
			if seat.Name == "" {
				t.Error("a bot seat has no name")
			}
		}
	}
	if bots != 3 {
		t.Fatalf("want three bots, got %d", bots)
	}
	if got := total(census(s)); got != 108 {
		t.Fatalf("the deal lost cards: %d of 108", got)
	}
	// Every seat anted, so the pot is one ante per seat.
	if want := s.Tier.Ante * int64(len(s.Seats)); s.Pot != want {
		t.Errorf("pot is %d, want %d (one ante each)", s.Pot, want)
	}
}

// The card turned over to start is never an action card — see dealHand.
func TestOpeningCardIsANumber(t *testing.T) {
	for seed := uint64(0); seed < 300; seed++ {
		s := deal(t, table(), 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 live hand by hand, so a rule can be tested without hunting a seed
// that happens to deal it. Every seat has anted, so the pot is set and a win
// settles for real.
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() {
		if left[c] > 0 {
			left[c]--
			deck = append(deck, c)
		}
	}
	ante := full().Ante
	seats := make([]Seat, len(hands))
	for i := range seats {
		// The stack is what is left after this seat anted: a real deal moves the ante
		// out of the stack and into the pot, so a refund or a win returns it here.
		seats[i] = Seat{Name: botPool[i], Bot: i != You, Stack: 1000 - ante, Ante: ante}
	}
	seats[You].Name = "You"
	return State{
		Tier: full(), Seats: seats, Hands: hands, Discard: []Card{top}, Color: color,
		Deck: deck, Dir: 1, Turn: You, Dealer: len(hands) - 1, Phase: PhasePlay,
		Pot: ante * int64(len(hands)), Winner: -1,
		Out: make([]bool, len(hands)), Called: make([]bool, len(hands)),
		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, You, 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) {
	s := rig([][]Card{{{Blue, Nine}, {Red, Two}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	next, evs, err := ApplyMove(s, You, 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, You, Move{Kind: MovePlay, Index: 0}); err != ErrNeedColor {
		t.Fatalf("a wild with no colour should be refused, got %v", err)
	}
	if _, _, err := ApplyMove(s, You, 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, You, Move{Kind: MovePlay, Index: 0, Color: Green})
	if err != nil {
		t.Fatalf("play wild: %v", err)
	}
	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) {
	s := rig([][]Card{{{Red, DrawTwo}, {Red, One}}, {{Blue, Five}, {Blue, Six}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	next, evs, err := ApplyMove(s, You, 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, You, 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) {
	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, You, 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)
	}
	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) {
	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, You, 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, You, 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(You); len(got) != 1 || got[0] != 1 {
		t.Errorf("the drawn card, and only it, is playable: %v", got)
	}
	if _, _, err := ApplyMove(next, You, Move{Kind: MovePlay, Index: 0}); err != ErrMustPlayNow {
		t.Fatalf("only the drawn card may be played, got %v", err)
	}
	if _, _, err := ApplyMove(next, You, Move{Kind: MoveDraw}); err != ErrMustPlayNow {
		t.Fatalf("you can't draw twice, got %v", err)
	}
	after, _, err := ApplyMove(next, You, 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, You, 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, You, 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, and not a seat holds a card that goes on the pile.
func dead(hands [][]Card) State {
	s := rig(hands, Card{Red, Nine}, Red)
	s.Deck = nil
	return s
}

// A dead table ends the hand rather than passing the turn round forever. It no
// longer ends the *session* — a shared table plays another hand — so it lands on
// PhaseHandOver, not PhaseDone.
func TestDeadTableEnds(t *testing.T) {
	s := dead([][]Card{{{Blue, Three}}, {{Green, Five}}}) // level: one card each

	next, evs, err := ApplyMove(s, You, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if next.playing() {
		t.Fatalf("nobody can move and there is nothing to draw: the hand is over, not %q", next.Phase)
	}
	if next.Phase != PhaseHandOver {
		t.Fatalf("a dead hand at a shared table returns to handover, not %q", next.Phase)
	}
	if next.Outcome != OutcomeTie {
		t.Errorf("level on the shortest hand is a tie, got %q", next.Outcome)
	}
	// A tie hands the antes back: every seat is whole again.
	for i := range next.Seats {
		if next.Seats[i].Stack != 1000 {
			t.Errorf("seat %d wasn't refunded: stack %d, want 1000", i, next.Seats[i].Stack)
		}
	}
	if !hasKind(evs, EvSettle) {
		t.Errorf("the table has to be told the hand is over: %+v", evs)
	}
}

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

	next, _, err := ApplyMove(s, You, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if next.Outcome != OutcomeStuck || next.Winner != You {
		t.Fatalf("one card against two is a win for you: outcome %q winner %d", next.Outcome, next.Winner)
	}
	profit := pot - s.Tier.Ante
	wantRake := int64(float64(profit) * rake)
	wantWon := pot - wantRake
	if next.Seats[You].Won != wantWon {
		t.Errorf("you took %d, want %d (pot %d less rake %d)", next.Seats[You].Won, wantWon, pot, wantRake)
	}
	if next.Seats[You].Stack != before+wantWon {
		t.Errorf("your stack is %d, want %d", next.Seats[You].Stack, before+wantWon)
	}
}

func TestReshuffleRebuildsTheDeck(t *testing.T) {
	s := rig([][]Card{{{Blue, Three}}, {{Green, Five}}}, Card{Red, Nine}, Red)
	s.Deck = nil
	s.Discard = []Card{{Green, WildCard}, {Red, Two}, {Red, Nine}}
	next, evs, err := ApplyMove(s, You, 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 pot --------------------------------------------------------------

// The winner takes the pot, and the house's rake comes out of the winnings, never
// out of a seat's own ante.
func TestWinnerTakesThePotLessRake(t *testing.T) {
	s := rig([][]Card{{{Red, Three}}, {{Green, Five}, {Green, Six}}, {{Blue, One}, {Blue, Two}}}, Card{Red, Nine}, Red)
	pot := s.Pot
	before := s.Seats[You].Stack

	next, _, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0}) // your last card
	if err != nil {
		t.Fatalf("go out: %v", err)
	}
	if next.Outcome != OutcomeWon || next.Winner != You {
		t.Fatalf("playing your last card wins: outcome %q winner %d", next.Outcome, next.Winner)
	}
	profit := pot - s.Tier.Ante
	wantRake := int64(float64(profit) * rake)
	wantWon := pot - wantRake
	if next.Rake != wantRake {
		t.Errorf("rake %d, want %d — and never a penny of an ante", next.Rake, wantRake)
	}
	if next.LastPot != pot {
		t.Errorf("last pot %d, want %d", next.LastPot, pot)
	}
	if next.Seats[You].Stack != before+wantWon {
		t.Errorf("your stack is %d, want %d (+%d)", next.Seats[You].Stack, before+wantWon, wantWon)
	}
	if next.Paid != wantRake {
		t.Errorf("the session rake tally is %d, want %d", next.Paid, wantRake)
	}
}

// A bot winning rakes nothing: the house already keeps the whole pot when its own
// seat takes it, so there is nothing to charge.
func TestABotWinningRakesNothing(t *testing.T) {
	// The bot at seat 1 holds one card that plays; 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()
	pot := s.Pot

	next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play: %v", err)
	}
	if next.Outcome != OutcomeWon || next.Winner != 1 {
		t.Fatalf("the bot went out: outcome %q winner %d", next.Outcome, next.Winner)
	}
	if next.Rake != 0 {
		t.Errorf("a bot winning rakes nothing, got %d", next.Rake)
	}
	if next.Seats[1].Won != pot {
		t.Errorf("the bot took %d, want the whole pot %d", next.Seats[1].Won, pot)
	}
	// You anted and lost it: your stack is down exactly one ante.
	if next.Seats[You].Stack != 1000-s.Tier.Ante {
		t.Errorf("your stack is %d, want %d (one ante gone)", next.Seats[You].Stack, 1000-s.Tier.Ante)
	}
	last := evs[len(evs)-1]
	if last.Kind != EvSettle || last.Seat != 1 {
		t.Errorf("the settle should name the winner: %+v", last)
	}
}

// A hand ending returns the table to handover, ready to deal again — it does not
// take a hand move.
func TestNoHandMoveBetweenHands(t *testing.T) {
	s := rig([][]Card{{{Red, Three}}, {{Green, Five}, {Green, Six}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	over, _, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("go out: %v", err)
	}
	if over.Phase != PhaseHandOver {
		t.Fatalf("a finished hand returns to handover, got %q", over.Phase)
	}
	if _, _, err := ApplyMove(over, You, Move{Kind: MoveDraw}); err != ErrNoHand {
		t.Fatalf("no hand is in progress between hands, got %v", err)
	}
}

// You can deal the next hand, ante again, and play on — the session shape.
func TestDealTheNextHand(t *testing.T) {
	s := rig([][]Card{{{Red, Three}}, {{Green, Five}, {Green, Six}}}, Card{Red, Nine}, Red)
	s.Tier = duel()
	over, _, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("go out: %v", err)
	}
	again, evs, err := ApplyMove(over, You, Move{Kind: MoveDeal})
	if err != nil {
		t.Fatalf("deal the next hand: %v", err)
	}
	if again.HandNo != over.HandNo+1 {
		t.Errorf("hand number didn't advance: %d then %d", over.HandNo, again.HandNo)
	}
	if !again.playing() {
		t.Fatalf("the next hand should be live, phase %q", again.Phase)
	}
	if !hasKind(evs, EvAnte) || !hasKind(evs, EvDeal) {
		t.Errorf("the deal antes and turns a card: %+v", evs)
	}
}

func TestBadBuyIn(t *testing.T) {
	if _, _, err := New(duel(), SoloSeats(duel(), 1, 10), rake, 1, 2); err != ErrBadBuyIn {
		t.Fatalf("a buy-in under the minimum should be refused, got %v", err)
	}
}

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

// playOut plays one hand to its end with a simple strategy: play the first legal
// card, take a stack you can't answer, otherwise draw, otherwise pass.
func playOut(t *testing.T, s State, maxTurns int) State {
	t.Helper()
	for turn := 0; s.playing(); turn++ {
		if turn > maxTurns {
			t.Fatalf("the hand 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(You); len(p) > 0 {
			m = Move{Kind: MovePlay, Index: p[0]}
			if s.Hands[You][p[0]].IsWild() {
				m.Color = Green
			}
		} else if s.Phase == PhaseStack {
			m = Move{Kind: MoveTake}
		} else if s.Phase == PhaseDrawn {
			m = Move{Kind: MovePass}
		} else {
			m = Move{Kind: MoveDraw}
		}

		next, evs, err := ApplyMove(s, You, 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)
			}
		}
		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 hands, played out, with the invariants checked at every step.
func TestGamesPlayOut(t *testing.T) {
	yous, others, ties := 0, 0, 0
	for seed := uint64(0); seed < 100; seed++ {
		tier := Tiers[seed%3]
		end := playOut(t, deal(t, tier, seed), 500)
		if end.Phase != PhaseHandOver {
			t.Fatalf("seed %d ended in phase %q", seed, end.Phase)
		}
		switch {
		case end.Winner == You:
			yous++
		case end.Winner >= 0:
			others++
		case end.Outcome == OutcomeTie:
			ties++
		default:
			t.Fatalf("seed %d ended with winner %d outcome %q", seed, end.Winner, end.Outcome)
		}
		if end.Winner >= 0 && end.Outcome == OutcomeWon && len(end.Hands[end.Winner]) != 0 {
			t.Fatalf("seed %d: the winner is still holding cards", seed)
		}
	}
	if yous == 0 || others == 0 {
		t.Fatalf("100 hands gave %d to you, %d to others, %d tied — one side never happens", yous, others, ties)
	}
	t.Logf("100 hands: %d to you, %d to others, %d tied", yous, others, ties)
}

// The same seed deals the same hand and the bots make the same choices.
func TestReplaysFromTheSeed(t *testing.T) {
	a := playOut(t, deal(t, full(), 42), 500)
	b := playOut(t, deal(t, full(), 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.Winner < 0 && a.Outcome != OutcomeTie {
		t.Fatal("the replay didn't finish")
	}
}

// A game in progress survives a redeploy: it round-trips through its JSON.
func TestStateSurvivesStorage(t *testing.T) {
	s := deal(t, table(), 9)
	s, _, err := ApplyMove(s, You, 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")
	}
	playOut(t, back, 500)
}

// A move the engine refuses leaves the caller's state exactly as it was.
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, You, 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 game.
func TestBotsAreNotDeterministicAcrossSeeds(t *testing.T) {
	same := 0
	for seed := uint64(0); seed < 20; seed++ {
		a := playOut(t, deal(t, duel(), seed), 500)
		b := playOut(t, deal(t, duel(), 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")
	}
}

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)
	}
	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
}
