package uno

import "testing"

// The UNO call, and the catch on the other side of it. See call.go.

// oneCardAway sets you up holding two cards, both of which go on the pile, so
// playing either one takes you to UNO.
//
// The bot is given a hand that can't touch a red pile and a deck that can't help
// it, so whatever it does on its turn, it does not make you draw. That matters:
// a hand that grows spends the call (see tidyCalls), which is correct and would
// otherwise make these tests flap on the seeds where the bot happens to turn up
// a +2.
func oneCardAway(t *testing.T, seed uint64) State {
	t.Helper()
	s := deal(t, duel(), seed)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, One}, {Red, Two}}
	s.Hands[1] = []Card{{Blue, Three}, {Green, Four}, {Yellow, Six}}
	s.Deck = make([]Card, 24)
	for i := range s.Deck {
		s.Deck[i] = Card{Blue, Nine} // nothing here plays on a red one, and nothing bites
	}
	s.Turn = You
	s.Phase = PhasePlay
	return s
}

// TestCallingUnoKeepsYouSafe — say the word and the table has nothing on you.
// Across a spread of seeds, not one bot ever gets a catch.
func TestCallingUnoKeepsYouSafe(t *testing.T) {
	for seed := uint64(0); seed < 200; seed++ {
		s := oneCardAway(t, seed)
		next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0, Uno: true})
		if err != nil {
			t.Fatalf("seed %d: play: %v", seed, err)
		}
		if hasKind(evs, EvCaught) {
			t.Fatalf("seed %d: caught after calling UNO", seed)
		}
		if !hasKind(evs, EvUno) {
			t.Fatalf("seed %d: called UNO and the table never said so", seed)
		}
		if !next.Called[You] {
			t.Fatalf("seed %d: the call wasn't recorded", seed)
		}
	}
}

// TestForgettingUnoGetsYouCaught — stay quiet on one card and the bot takes you
// for two. It gets one look, so it misses sometimes; over 400 games it should
// land near botAlert, and the two cards should actually arrive.
func TestForgettingUnoGetsYouCaught(t *testing.T) {
	caught, games := 0, 400
	for seed := uint64(0); seed < uint64(games); seed++ {
		s := oneCardAway(t, seed)
		next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
		if err != nil {
			t.Fatalf("seed %d: play: %v", seed, err)
		}
		if !hasKind(evs, EvCaught) {
			continue
		}
		caught++
		// One card left after the play, plus the two the catch cost.
		if n := len(next.Hands[You]); n != 3 {
			t.Fatalf("seed %d: caught and holding %d, want 3", seed, n)
		}
		if hasKind(evs, EvUno) {
			t.Fatalf("seed %d: an UNO was announced by a seat that never called", seed)
		}
	}
	rate := float64(caught) / float64(games)
	if rate < botAlert-0.08 || rate > botAlert+0.08 {
		t.Errorf("one bot caught you %.0f%% of the time, want about %.0f%% (botAlert)",
			rate*100, botAlert*100)
	}
}

// TestMoreBotsMeansLessGettingAwayWithIt — every seat gets its own look, so
// forgetting at a full table is very nearly always punished.
func TestMoreBotsMeansLessGettingAwayWithIt(t *testing.T) {
	away := func(tier Tier, seed uint64) bool {
		s := deal(t, tier, seed)
		s.Color = Red
		s.Discard = []Card{{Red, Five}}
		s.Hands[You] = []Card{{Red, One}, {Red, Two}}
		s.Turn = You
		s.Phase = PhasePlay
		_, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
		if err != nil {
			t.Fatalf("play: %v", err)
		}
		return !hasKind(evs, EvCaught)
	}
	var got [2]float64
	for i, tier := range []Tier{duel(), table()} {
		escapes := 0
		for seed := uint64(0); seed < 400; seed++ {
			if away(tier, seed) {
				escapes++
			}
		}
		got[i] = float64(escapes) / 400
	}
	if got[1] >= got[0] {
		t.Errorf("you got away with it %.0f%% of the time against one bot and %.0f%% against two; "+
			"two pairs of eyes should catch you more often, not less", got[0]*100, got[1]*100)
	}
}

// quietBot puts a bot on one card it never called, with the turn back on you.
func quietBot(t *testing.T, called bool) State {
	t.Helper()
	s := deal(t, duel(), 21)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, One}, {Blue, Two}, {Green, Three}}
	s.Hands[1] = []Card{{Yellow, Nine}}
	s.Called = []bool{false, called}
	s.Turn = You
	s.Phase = PhasePlay
	return s
}

// TestCatchingAQuietBot — it's on one card and it never said so. Two cards to it,
// and the turn is still yours: catching is not a move you spend a turn on.
func TestCatchingAQuietBot(t *testing.T) {
	s := quietBot(t, false)
	before := total(census(s))

	next, evs, err := ApplyMove(s, You, Move{Kind: MoveCatch, Seat: 1})
	if err != nil {
		t.Fatalf("catch: %v", err)
	}
	if !hasKind(evs, EvCaught) {
		t.Fatal("no catch event")
	}
	if n := len(next.Hands[1]); n != 3 {
		t.Errorf("the bot holds %d, want 3: one card, plus the two it just took", n)
	}
	if n := len(next.Hands[You]); n != 3 {
		t.Errorf("your hand is %d, want 3: a catch costs you nothing", n)
	}
	if next.Turn != You {
		t.Errorf("the turn went to seat %d: a catch is not a turn", next.Turn)
	}
	if total(census(next)) != before {
		t.Error("the catch lost a card")
	}
}

// TestCatchingACleanBotCostsYou — it called, or it isn't on one card at all.
// Either way you've accused it of nothing, and that is two cards to you.
func TestCatchingACleanBotCostsYou(t *testing.T) {
	for _, tc := range []struct {
		name  string
		state State
	}{
		{"it called", quietBot(t, true)},
		{"it isn't even close", func() State {
			s := quietBot(t, false)
			s.Hands[1] = []Card{{Yellow, Nine}, {Yellow, Eight}}
			return s
		}()},
	} {
		t.Run(tc.name, func(t *testing.T) {
			next, evs, err := ApplyMove(tc.state, You, Move{Kind: MoveCatch, Seat: 1})
			if err != nil {
				t.Fatalf("catch: %v", err)
			}
			if !hasKind(evs, EvMiscall) {
				t.Fatal("no miscall event")
			}
			if n := len(next.Hands[You]); n != 5 {
				t.Errorf("your hand is %d, want 5: three, plus the two a bad call cost", n)
			}
			if next.Turn != You {
				t.Errorf("the turn went to seat %d: even a bad catch isn't a turn", next.Turn)
			}
		})
	}
}

// TestYouCannotCatchYourself, or a seat that isn't at the table.
func TestYouCannotCatchYourself(t *testing.T) {
	s := quietBot(t, false)
	for _, seat := range []int{You, -1, 9} {
		if _, _, err := ApplyMove(s, You, Move{Kind: MoveCatch, Seat: seat}); err != ErrNoCatch {
			t.Errorf("catching seat %d: got %v, want ErrNoCatch", seat, err)
		}
	}
}

// TestACallIsSpentWhenTheHandGrows. Call on one card, get made to draw, and work
// your way back down to one: that is a new call you owe, not the old one still
// standing. Without this a seat could be caught out once and never again.
func TestACallIsSpentWhenTheHandGrows(t *testing.T) {
	s := deal(t, duel(), 5)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, One}}
	s.Called = []bool{true, false}
	s.Turn = You
	s.Phase = PhasePlay

	// Draw, and the hand is two: the word you said was about a card you no longer
	// hold on its own.
	next, _, err := ApplyMove(s, You, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if next.Called[You] {
		t.Error("the call survived the hand growing; it should be spent")
	}
}

// TestCatchableIsWhatTheTableCanSee — a quiet bot on one card, and nobody else.
func TestCatchable(t *testing.T) {
	s := quietBot(t, false)
	if got := s.Catchable(You); len(got) != 1 || got[0] != 1 {
		t.Errorf("Catchable() = %v, want [1]", got)
	}
	clean := quietBot(t, true)
	if got := clean.Catchable(You); len(got) != 0 {
		t.Errorf("Catchable() = %v on a bot that called, want none", got)
	}
	// And not on somebody else's turn: you can only call it out when it's on you.
	off := quietBot(t, false)
	off.Turn = 1
	if got := off.Catchable(You); len(got) != 0 {
		t.Errorf("Catchable() = %v off-turn, want none", got)
	}
}

// TestUnoAtSeesThroughDiscardAll — the whole reason the table asks the engine
// which cards take you to one, rather than counting your hand itself. "Discard
// all" takes every card of its colour with it, so a six-card hand can land on
// one, and a browser subtracting one from six gets a player caught.
func TestUnoAtSeesThroughDiscardAll(t *testing.T) {
	s := deal(t, nmDuel(), 3)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, DiscardAll}, {Red, One}, {Red, Nine}, {Red, Seven}, {Blue, Two}}
	s.Turn = You
	s.Phase = PhasePlay

	// Index 0 dumps itself and the three other reds: five cards become one.
	// Index 4 is an ordinary play: five become four.
	got := s.UnoAt(You)
	if len(got) != 1 || got[0] != 0 {
		t.Errorf("UnoAt() = %v, want [0]: only the discard-all lands you on one card", got)
	}
}

// TestUnoAtIsTheOrdinaryCaseToo — two cards in hand, and either of them is a call.
func TestUnoAtIsTheOrdinaryCaseToo(t *testing.T) {
	s := oneCardAway(t, 1)
	got := s.UnoAt(You)
	if len(got) != 2 {
		t.Errorf("UnoAt() = %v, want both cards: either one leaves you holding one", got)
	}
}

// TestGoingOutNeedsNoCall — your last card is not one card, it's none. Nobody
// owes the table a word for winning.
func TestGoingOutNeedsNoCall(t *testing.T) {
	s := deal(t, duel(), 9)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, One}}
	s.Turn = You
	s.Phase = PhasePlay

	next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play the last card: %v", err)
	}
	if next.Winner != You {
		t.Fatalf("winner %d outcome %q, want a win for you", next.Winner, next.Outcome)
	}
	if hasKind(evs, EvCaught) {
		t.Error("caught for not calling UNO on the card that won the game")
	}
}

// TestABotThatForgetsSaysNothing — the tell is the absence of the badge, and the
// count beside the fan. If a quiet bot emitted anything at all there'd be nothing
// to spot.
func TestABotThatForgetsSaysNothing(t *testing.T) {
	quiet := 0
	for seed := uint64(0); seed < 300 && quiet < 1; seed++ {
		s := deal(t, duel(), seed)
		s.Color = Red
		s.Discard = []Card{{Red, Five}}
		s.Hands[You] = []Card{{Blue, Two}, {Blue, Three}, {Blue, Four}}
		s.Hands[1] = []Card{{Red, One}, {Red, Nine}}
		s.Turn = 1
		s.Phase = PhasePlay
		s.Turn = You // the bot plays on the back of your move

		// Draw, handing the turn over: the bot plays a red and lands on one card.
		next, evs, err := ApplyMove(s, You, Move{Kind: MoveDraw})
		if err != nil {
			t.Fatalf("draw: %v", err)
		}
		if len(next.Hands[1]) != 1 || next.Called[1] {
			continue // it either didn't get down to one, or it remembered
		}
		quiet++
		if hasKind(evs, EvUno) {
			t.Error("a bot that forgot to call still announced it")
		}
		if len(next.Catchable(You)) != 1 {
			t.Error("a quiet bot on one card isn't catchable")
		}
	}
	if quiet == 0 {
		t.Skip("no bot forgot in 300 games; botForget may have been turned down")
	}
}
