package web

import (
	"testing"

	"pete/internal/games/uno"
	"pete/internal/storage"
)

// The one thing this table cannot get wrong: the stake leaves the stack, and no
// card a player is not entitled to see leaves the server. At UNO that is not one
// hole card — it is the deck and every bot's hand, which is most of the game.
func TestUnoStartTakesTheStakeAndKeepsTheCards(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	v, code := call(t, s, s.handleUnoStart, as(t, s, "reala", "POST", "/api/games/uno/start",
		map[string]any{"bet": 100, "tier": "full"}))
	if code != 200 {
		t.Fatalf("start = %d, want 200", code)
	}
	if v.Chips != 900 {
		t.Fatalf("chips after a 100 bet = %d, want 900", v.Chips)
	}
	if v.Game != gameUno || v.Uno == nil {
		t.Fatalf("start returned no uno game: game=%q", v.Game)
	}

	g := v.Uno
	if len(g.Hand) != uno.HandSize {
		t.Errorf("you were dealt %d cards, want %d", len(g.Hand), uno.HandSize)
	}
	if len(g.Seats) != 4 {
		t.Fatalf("a full house is four seats, got %d", len(g.Seats))
	}
	// A bot is a name and a number. There is no field here that could carry a
	// card, which is the point — this is a compile-time guarantee, and the test
	// exists to make deleting it loud.
	for i, seat := range g.Seats {
		if i == 0 {
			if !seat.You || seat.Name != "You" {
				t.Errorf("seat 0 should be you: %+v", seat)
			}
			continue
		}
		if seat.You || seat.Name == "" {
			t.Errorf("seat %d should be a named bot: %+v", i, seat)
		}
		if seat.Cards != uno.HandSize {
			t.Errorf("seat %d holds %d cards, want %d", i, seat.Cards, uno.HandSize)
		}
	}
	if g.Top.Value == "" {
		t.Error("no card in play")
	}
	if g.Turn != 0 {
		t.Errorf("you play first, turn is %d", g.Turn)
	}
}

// A move plays your turn and every bot turn behind it, and the script that comes
// back never carries a bot's drawn card.
func TestUnoMovePlaysTheWholeLap(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	v, _ := call(t, s, s.handleUnoStart, as(t, s, "reala", "POST", "/api/games/uno/start",
		map[string]any{"bet": 100, "tier": "table"}))
	if v.Uno == nil {
		t.Fatal("no game")
	}

	// Draw, which is legal from any hand: the turn passes to the bots and comes
	// back, unless the card drawn happens to be playable.
	v, code := call(t, s, s.handleUnoMove, as(t, s, "reala", "POST", "/api/games/uno/move",
		map[string]any{"kind": "draw"}))
	if code != 200 {
		t.Fatalf("draw = %d, want 200", code)
	}
	if v.Uno == nil {
		t.Fatal("the move returned no game")
	}
	if len(v.UnoEvents) == 0 {
		t.Fatal("a move that happened sent back no events")
	}
	if v.Uno.Phase != "drawn" && v.Uno.Turn != 0 {
		t.Errorf("the bots should have played back round to you, turn is %d", v.Uno.Turn)
	}
	for _, e := range v.UnoEvents {
		if (e.Kind == uno.EvDraw || e.Kind == uno.EvForced) && e.Seat != 0 && e.Card != nil {
			t.Fatalf("a bot's drawn card crossed the wire: %+v", e)
		}
	}
}

// You cannot play a wild without naming a colour — and the zero value of the
// colour field is not red, so a move that simply omits it is refused rather than
// quietly played as one.
func TestUnoWildWithNoColourIsRefused(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	// Deal until a wild lands in the opening hand: it's four cards in 108, so it
	// doesn't take long, and this is the only way to get one without rigging the
	// deck through a door the server doesn't have.
	var wild = -1
	for try := 0; try < 40 && wild < 0; try++ {
		v, _ := call(t, s, s.handleUnoStart, as(t, s, "reala", "POST", "/api/games/uno/start",
			map[string]any{"bet": 10, "tier": "duel"}))
		if v.Uno == nil {
			t.Fatal("no game")
		}
		for i, c := range v.Uno.Hand {
			if c.Wild {
				wild = i
				break
			}
		}
		if wild < 0 {
			// Abandon this deal and take another. The live row is keyed on the
			// player, so it has to go before the next start can be seated.
			if err := storage.ClearLiveHand(testPlayer); err != nil {
				t.Fatal(err)
			}
		}
	}
	if wild < 0 {
		t.Skip("40 deals and not one wild — vanishingly unlikely, but not a failure")
	}

	_, code := call(t, s, s.handleUnoMove, as(t, s, "reala", "POST", "/api/games/uno/move",
		map[string]any{"kind": "play", "index": wild}))
	if code != 400 {
		t.Fatalf("a wild with no colour = %d, want 400", code)
	}

	v, code := call(t, s, s.handleUnoMove, as(t, s, "reala", "POST", "/api/games/uno/move",
		map[string]any{"kind": "play", "index": wild, "color": int(uno.Green)}))
	if code != 200 {
		t.Fatalf("a wild played as green = %d, want 200", code)
	}
	if v.Uno != nil && v.Uno.Phase != "done" && v.Uno.Color != "green" && v.Uno.Color != "" {
		// The bots have played since, so the colour may have moved on — what must
		// not happen is the wild going down as anything we didn't ask for.
		t.Logf("colour in play after the bots: %s", v.Uno.Color)
	}
}

// A seat the mercy rule has buried holds no cards. So does a seat that has just
// gone out and won. The view has to tell them apart, because everything it says
// afterwards hangs off it: who won, whose fan of cards to rub out, and whether the
// player is looking at a payout or a grave.
//
// This is the whole reason the view asks the engine (Live) instead of inferring it
// from a count of zero.
func TestABuriedSeatIsNotTheWinner(t *testing.T) {
	g, _, err := uno.New(100, mustTier(t, "nm-full"), 0.05, 7, 9)
	if err != nil {
		t.Fatal(err)
	}

	// Bury seat 2, the way the engine does: no cards, and out.
	g.Hands[2] = nil
	g.Out[2] = true

	v := viewUno(g)
	if !v.Seats[2].Out {
		t.Error("a buried seat must say so; the felt draws it as a grave")
	}
	if v.Seats[2].Uno {
		t.Error("a buried seat is not one card away from going out")
	}
	if v.Winner == 2 {
		t.Fatal("the buried seat was reported as the winner — an empty hand is not a win")
	}
	if v.Winner != -1 {
		t.Errorf("nobody has gone out yet, so there is no winner: got %d", v.Winner)
	}

	// And the other ending only this deck has: you win by outliving the table, with
	// a hand still in front of you.
	g.Phase, g.Outcome, g.Payout = uno.PhaseDone, uno.OutcomeWon, g.Pays()
	if v := viewUno(g); v.Winner != uno.You {
		t.Errorf("you outlived the table; winner = %d, want you (%d)", v.Winner, uno.You)
	}
}

// The bill a stack has run up is the only thing on the table while it stands, so it
// has to reach the browser. It is a No Mercy field on a struct the normal game
// shares, which is exactly the kind of field that quietly never gets copied across.
func TestTheStackBillCrossesTheWire(t *testing.T) {
	g, _, err := uno.New(100, mustTier(t, "nm-duel"), 0.05, 3, 4)
	if err != nil {
		t.Fatal(err)
	}
	g.Pending = 6
	g.Phase = uno.PhaseStack

	v := viewUno(g)
	if v.Pending != 6 {
		t.Errorf("the felt was told the bill is %d, want 6", v.Pending)
	}
	if v.Phase != string(uno.PhaseStack) {
		t.Errorf("phase = %q, want %q", v.Phase, uno.PhaseStack)
	}
}

func mustTier(t *testing.T, slug string) uno.Tier {
	t.Helper()
	tier, err := uno.TierBySlug(slug)
	if err != nil {
		t.Fatalf("no tier %q: %v", slug, err)
	}
	return tier
}
