package uno

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

func nmDuel() Tier  { t, _ := TierBySlug("nm-duel"); return t }
func nmTable() Tier { t, _ := TierBySlug("nm-table"); return t }
func nmFull() Tier  { t, _ := TierBySlug("nm-full"); return t }

func TestNoMercyDeckIsADeck(t *testing.T) {
	m := census(State{Deck: NewNoMercyDeck()})
	if got := total(m); got != 168 {
		t.Fatalf("deck has %d cards, want 168", got)
	}
	want := map[Card]int{
		{Red, Zero}:          2, // two of every number, unlike the normal deck's single zero
		{Blue, Seven}:        2,
		{Green, Skip}:        3,
		{Yellow, SkipAll}:    2,
		{Red, Reverse}:       4,
		{Blue, DrawTwo}:      2,
		{Green, DrawFour}:    2, // the *coloured* +4
		{Yellow, DiscardAll}: 3,
		{Wild, WildRevFour}:  8,
		{Wild, WildDrawSix}:  4,
		{Wild, WildDrawTen}:  4,
		{Wild, WildRoulette}: 8,
	}
	for c, n := range want {
		if m[c] != n {
			t.Errorf("%v %v: got %d, want %d", c.Color, c.Value, m[c], n)
		}
	}
	if m[Card{Wild, WildCard}] != 0 || m[Card{Wild, WildDrawFour}] != 0 {
		t.Error("the No Mercy deck should print none of the normal wilds")
	}
}

// TestNoMercyCensus is the load-bearing one: 168 cards, each in exactly one place,
// checked after every move of a hundred hands played to the end.
func TestNoMercyCensus(t *testing.T) {
	for _, tier := range []Tier{nmDuel(), nmTable(), nmFull()} {
		for seed := uint64(0); seed < 100; seed++ {
			s := deal(t, tier, seed)
			if got := total(census(s)); got != 168 {
				t.Fatalf("%s seed %d: dealt %d cards, want 168", tier.Slug, seed, got)
			}
			rng := rand.New(rand.NewPCG(seed, 99))
			for moves := 0; s.playing() && moves < 800; moves++ {
				next, _, err := ApplyMove(s, You, naive(s, rng))
				if err != nil {
					t.Fatalf("%s seed %d: %v (phase %s)", tier.Slug, seed, err, s.Phase)
				}
				s = next
				if got := census(s); total(got) != 168 {
					t.Fatalf("%s seed %d: %d cards after a move, want 168",
						tier.Slug, seed, total(got))
				}
			}
			if s.playing() {
				t.Fatalf("%s seed %d: hand never ended", tier.Slug, seed)
			}
		}
	}
}

// naive is a bad-but-real strategy: play the first legal card, take a stack you
// can't answer, and draw when you have nothing. Always played from the human seat.
func naive(s State, rng *rand.Rand) Move {
	if s.Phase == PhaseStack {
		if p := s.Playable(You); len(p) > 0 {
			return playMove(s, p[0], rng)
		}
		return Move{Kind: MoveTake}
	}
	if p := s.Playable(You); len(p) > 0 {
		return playMove(s, p[0], rng)
	}
	return Move{Kind: MoveDraw}
}

// stack loads a seat's hand up to n cards by taking them off the deck, so the
// table still holds 168 of them.
func stack(s *State, seat, n int) {
	s.Deck = append(s.Deck, s.Hands[seat]...)
	s.Hands[seat] = nil
	s.Color = s.top().Color

	kept := make([]Card, 0, len(s.Deck))
	for _, c := range s.Deck {
		if len(s.Hands[seat]) < n {
			s.Hands[seat] = append(s.Hands[seat], c)
			continue
		}
		kept = append(kept, c)
	}
	s.Deck = kept
}

func playMove(s State, idx int, rng *rand.Rand) Move {
	m := Move{Kind: MovePlay, Index: idx}
	if s.Hands[You][idx].IsWild() {
		m.Color = Red + Color(rng.IntN(4))
	}
	for _, at := range s.UnoAt(You) {
		if at == idx {
			m.Uno = true
		}
	}
	return m
}

// TestAStackIsPassedOnAndPaid walks the rule the whole mode turns on: a draw card
// opens a bill, and the seat that can't answer pays the whole thing.
func TestAStackIsPassedOnAndPaid(t *testing.T) {
	s := deal(t, nmDuel(), 7)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, DrawTwo}, {Blue, One}}
	s.Hands[1] = []Card{{Red, DrawTwo}, {Blue, Nine}}
	s.Turn = You
	s.Phase = PhasePlay

	next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play +2: %v", err)
	}
	if next.Phase != PhaseStack {
		t.Fatalf("phase is %s, want stack: a +2 in No Mercy opens a stack", next.Phase)
	}
	if next.Turn != You {
		t.Fatalf("the stack came back to seat %d, want you", next.Turn)
	}
	if next.Pending != 4 {
		t.Fatalf("the bill is %d, want 4 (your two, plus the bot's two)", next.Pending)
	}
	if !hasKind(evs, EvStack) {
		t.Error("no stack event: the felt has nothing to show the player")
	}
	if _, _, err := ApplyMove(next, You, Move{Kind: MoveDraw}); err != ErrMustStack {
		t.Errorf("drawing out of a stack: %v, want ErrMustStack", err)
	}
	if _, _, err := ApplyMove(next, You, Move{Kind: MovePlay, Index: 0}); err != ErrMustStack {
		t.Errorf("playing a plain card under a stack: %v, want ErrMustStack", err)
	}

	before := len(next.Hands[You])
	paid, evs, err := ApplyMove(next, You, Move{Kind: MoveTake})
	if err != nil {
		t.Fatalf("take: %v", err)
	}
	var forced int
	for _, e := range evs {
		if e.Kind == EvForced && e.Seat == You {
			forced = e.N
		}
	}
	if forced != 4 {
		t.Errorf("the stack made you take %d cards, want 4", forced)
	}
	if len(paid.Hands[You]) < before+4 {
		t.Errorf("hand went %d → %d, want at least four more", before, len(paid.Hands[You]))
	}
	if paid.Pending == 4 && paid.Phase == PhaseStack {
		t.Error("the bill you just paid is still standing")
	}
}

// TestMercyKillsThePlayerButNotTheSession: the mercy limit takes the human out of
// the *hand* — the pot goes to whoever is left, and the table plays on. It is no
// longer game over.
func TestMercyKillsThePlayerButNotTheSession(t *testing.T) {
	s := deal(t, nmDuel(), 3)
	stack(&s, You, 24)
	s.Turn = You
	s.Phase = PhaseStack
	s.Pending = 10

	next, evs, err := ApplyMove(s, You, Move{Kind: MoveTake})
	if err != nil {
		t.Fatalf("take: %v", err)
	}
	if !hasKind(evs, EvMercy) {
		t.Fatal("no mercy event: twenty-five cards should have killed the seat")
	}
	if next.playing() {
		t.Fatalf("the hand should be over once one seat is left: phase %s", next.Phase)
	}
	if next.live(You) || len(next.Hands[You]) != 0 {
		t.Error("a dead seat should hold no cards and be out of the hand")
	}
	// Heads-up, killing the human leaves the bot alone: it takes the pot.
	if next.Winner != 1 {
		t.Errorf("winner is seat %d, want the surviving bot", next.Winner)
	}
	if got := total(census(next)); got != 168 {
		t.Errorf("%d cards after a mercy kill, want 168 — the hand goes back in the deck", got)
	}
}

// TestOutlivingTheTableWins: the deck buries bots too, and a table with every bot
// dead is a pot you have taken.
func TestOutlivingTheTableWins(t *testing.T) {
	s := deal(t, nmDuel(), 11)
	pot := s.Pot
	before := s.Seats[You].Stack
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, DrawTwo}, {Blue, One}}
	s.Hands[1] = make([]Card, 0, 24)
	for i := 0; i < 24; i++ {
		s.Hands[1] = append(s.Hands[1], Card{Blue, Nine}) // nothing it can answer with
	}
	s.Turn = You
	s.Phase = PhasePlay

	next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play +2: %v", err)
	}
	if !hasKind(evs, EvMercy) {
		t.Fatal("the bot should have died taking the stack")
	}
	if next.playing() || next.Winner != You {
		t.Fatalf("phase %s winner %d, want a finished hand won by you", next.Phase, next.Winner)
	}
	profit := pot - s.Tier.Ante
	wantWon := pot - int64(float64(profit)*rake)
	if next.Seats[You].Stack != before+wantWon {
		t.Errorf("your stack is %d, want %d", next.Seats[You].Stack, before+wantWon)
	}
}

func TestYouDrawUntilYouCanPlay(t *testing.T) {
	s := deal(t, nmDuel(), 5)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Blue, One}} // nothing playable
	s.Deck = []Card{{Green, Two}, {Yellow, Three}, {Red, Nine}, {Blue, Four}}
	s.Turn = You
	s.Phase = PhasePlay

	next, _, err := ApplyMove(s, You, Move{Kind: MoveDraw})
	if err != nil {
		t.Fatalf("draw: %v", err)
	}
	if len(next.Hands[You]) != 4 {
		t.Fatalf("hand is %d, want 4: you draw until something plays", len(next.Hands[You]))
	}
	if next.Phase != PhaseDrawn {
		t.Fatalf("phase %s, want drawn: the card you stopped on is one you must play", next.Phase)
	}
	if _, _, err := ApplyMove(next, You, Move{Kind: MovePass}); err != ErrMustPlayNow {
		t.Errorf("passing in No Mercy: %v, want ErrMustPlayNow", err)
	}
}

func TestSkipAllComesBackToYou(t *testing.T) {
	s := deal(t, nmFull(), 13)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, SkipAll}, {Blue, One}}
	s.Turn = You
	s.Phase = PhasePlay

	next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
	if err != nil {
		t.Fatalf("play skip-all: %v", err)
	}
	if next.Turn != You {
		t.Errorf("turn went to seat %d, want you: skip-all skips everyone else", next.Turn)
	}
	if !hasKind(evs, EvSkipAll) {
		t.Error("no skipall event")
	}
}

func TestDiscardAllTakesTheColourWithIt(t *testing.T) {
	s := deal(t, nmDuel(), 17)
	s.Color = Red
	s.Discard = []Card{{Red, Five}}
	s.Hands[You] = []Card{{Red, DiscardAll}, {Red, One}, {Red, Nine}, {Blue, Two}}
	s.Turn = You
	s.Phase = PhasePlay
	before := total(census(s))

	next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0, Uno: true})
	if err != nil {
		t.Fatalf("play discard-all: %v", err)
	}
	if len(next.Hands[You]) != 1 {
		t.Fatalf("hand is %d, want 1: every red should have gone with it", len(next.Hands[You]))
	}
	if next.Hands[You][0] != (Card{Blue, Two}) {
		t.Errorf("kept %v, want the blue two", next.Hands[You][0])
	}
	// The discard-all was played, so it is on the pile — the bot may have played over
	// it since, which is why this looks for it rather than at the very top.
	found := false
	for _, c := range next.Discard {
		if c.Value == DiscardAll {
			found = true
		}
	}
	if !found {
		t.Error("the discard-all isn't on the pile")
	}
	if !hasKind(evs, EvDiscardAll) {
		t.Error("no discard event")
	}
	if got := total(census(next)); got != before {
		t.Errorf("%d cards, want %d: a dumped colour is buried, not destroyed", got, before)
	}
}

func TestRouletteFlipsUntilTheColour(t *testing.T) {
	s := deal(t, nmDuel(), 19)
	s.Color = Blue
	s.Discard = []Card{{Blue, Five}}
	s.Hands[You] = []Card{{Wild, WildRoulette}, {Blue, One}}
	s.Hands[1] = []Card{{Green, Three}}
	s.Deck = []Card{{Blue, Two}, {Green, Four}, {Yellow, Six}, {Red, Seven}, {Blue, Eight}}
	s.Turn = You
	s.Phase = PhasePlay

	next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0, Color: Red})
	if err != nil {
		t.Fatalf("play roulette: %v", err)
	}
	var got int
	for _, e := range evs {
		if e.Kind == EvRoulette {
			got = e.N
		}
	}
	if got != 4 {
		t.Errorf("flipped %d, want 4 — up to and including the first red", got)
	}
	if n := len(next.Hands[1]); n != 5 {
		t.Errorf("the bot holds %d, want 5", n)
	}
	if total(census(next)) != total(census(s)) {
		t.Error("the roulette lost a card")
	}
}
