package web

import (
	"testing"

	"pete/internal/games/uno"
)

// Sitting down is the only time chips leave your stack at this table, and getting
// up is the only time they come back. The ante and the pot move inside the engine.
func TestUnoSitTakesTheBuyIn(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)

	v, code := call(t, s, s.handleUnoSit, as(t, s, "reala", "POST", "/api/games/uno/sit",
		map[string]any{"tier": "full", "buyin": 1000}))
	if code != 200 {
		t.Fatalf("sit = %d, want 200", code)
	}
	if v.Chips != 4000 {
		t.Fatalf("chips after a 1000 buy-in = %d, want 4000", v.Chips)
	}
	if v.Game != gameUno || v.Uno == nil {
		t.Fatalf("sit returned no table: game=%q", v.Game)
	}
	g := v.Uno
	if g.Stack != 1000 {
		t.Errorf("you sat down with %d, want the 1000 you bought in for", g.Stack)
	}
	if len(g.Seats) != 4 {
		t.Fatalf("three bots and you is four seats, got %d", len(g.Seats))
	}
	if g.Phase != "handover" {
		t.Errorf("phase %q — a table you just sat at has no hand on it yet", g.Phase)
	}

	// Deal a hand: chips do not move (the ante is within the blob), and you are dealt
	// seven cards and to act.
	deal, code := call(t, s, s.handleUnoMove, as(t, s, "reala", "POST", "/api/games/uno/move",
		map[string]any{"kind": "deal"}))
	if code != 200 {
		t.Fatalf("deal = %d, want 200", code)
	}
	if deal.Chips != 4000 {
		t.Errorf("chips moved on the deal: %d, want 4000 — the ante is inside the engine", deal.Chips)
	}
	if deal.Uno == nil || len(deal.Uno.Hand) != uno.HandSize {
		t.Fatalf("you were dealt the wrong hand: %+v", deal.Uno)
	}
	if deal.Uno.Turn != 0 {
		t.Errorf("you act first at your own table, turn is %d", deal.Uno.Turn)
	}
	if deal.Uno.Pot != deal.Uno.Ante*int64(len(deal.Uno.Seats)) {
		t.Errorf("pot is %d, want one ante per seat", deal.Uno.Pot)
	}
}

// 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, 5000)

	call(t, s, s.handleUnoSit, as(t, s, "reala", "POST", "/api/games/uno/sit",
		map[string]any{"tier": "table", "buyin": 1000}))
	deal, code := call(t, s, s.handleUnoMove, as(t, s, "reala", "POST", "/api/games/uno/move",
		map[string]any{"kind": "deal"}))
	if code != 200 || deal.Uno == nil {
		t.Fatalf("deal = %d", code)
	}

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

// TestUnoViewNeverLeaksAnotherSeatsCards is the security boundary of the whole
// multiplayer table. There is no showdown in UNO, so the rule is absolute: no
// seat's hand, and no card a seat drew, ever reaches another viewer. Rendered for
// every seat, after every step of a hand played to its end.
//
// After SSE the view a seat renders fans to that seat's stream, so a single missed
// redaction broadcasts a hand to the whole felt. The engine emits every seat's
// cards (it cannot know who a shared stream is for), which makes viewUno and
// viewUnoEvents the only thing between the deck and the network tab.
func TestUnoViewNeverLeaksAnotherSeatsCards(t *testing.T) {
	tier, _ := uno.TierBySlug("full")
	// Two humans (0, 2) and two bots (1, 3), so the test covers a viewer seeing both
	// another human and a bot, and a human at a non-zero index.
	seats := []uno.SeatConfig{
		{Name: "Ana", Stack: 2000},
		{Name: "Bot A", Bot: true, Stack: 2000},
		{Name: "Bo", Stack: 2000},
		{Name: "Bot B", Bot: true, Stack: 2000},
	}
	g, _, err := uno.New(tier, seats, tier.RakePct, 5, 6)
	if err != nil {
		t.Fatal(err)
	}
	g, evs, err := uno.ApplyMove(g, 0, uno.Move{Kind: uno.MoveDeal})
	if err != nil {
		t.Fatal(err)
	}
	assertUnoRedacted(t, g, evs)

	for step := 0; unoPlaying(g) && step < 200; step++ {
		seat := g.Turn
		if g.Seats[seat].Bot {
			t.Fatalf("advance stopped on a bot at seat %d", seat)
		}
		move := unoHumanMove(g, seat)
		var stepEvs []uno.Event
		g, stepEvs, err = uno.ApplyMove(g, seat, move)
		if err != nil {
			t.Fatalf("seat %d %s: %v", seat, move.Kind, err)
		}
		assertUnoRedacted(t, g, stepEvs)
	}
}

// unoHumanMove picks a legal move for the human to act: play a legal card, take a
// stack you can't answer, pass a drawn card, otherwise draw.
func unoHumanMove(g uno.State, seat int) uno.Move {
	if p := g.Playable(seat); len(p) > 0 {
		m := uno.Move{Kind: uno.MovePlay, Index: p[0], Color: uno.Red}
		for _, at := range g.UnoAt(seat) {
			if at == p[0] {
				m.Uno = true
			}
		}
		return m
	}
	switch g.Phase {
	case uno.PhaseStack:
		return uno.Move{Kind: uno.MoveTake}
	case uno.PhaseDrawn:
		return uno.Move{Kind: uno.MovePass}
	default:
		return uno.Move{Kind: uno.MoveDraw}
	}
}

// assertUnoRedacted renders every seat's view and event script and fails if any of
// them carries a card belonging to another seat.
func assertUnoRedacted(t *testing.T, g uno.State, evs []uno.Event) {
	t.Helper()
	for viewer := range g.Seats {
		v := viewUno(g, viewer)
		// The view's hand is the viewer's own, exactly — no more, no fewer.
		if len(v.Hand) != len(g.Hands[viewer]) {
			t.Fatalf("seat %d's view carries %d cards, but the seat holds %d",
				viewer, len(v.Hand), len(g.Hands[viewer]))
		}
		// The event script: a hand rides only the viewer's own beat, and a drawn card
		// only the viewer's own draw. (A card a bot plays is public — it lands on the
		// pile — so only a *drawn* card is a secret, which is why this is scoped to the
		// draw and forced beats.) Undo either guard in viewUnoEvents and this fails.
		for _, e := range viewUnoEvents(evs, viewer) {
			if len(e.Hand) > 0 && e.Seat != viewer {
				t.Fatalf("seat %d's event stream carries seat %d's hand", viewer, e.Seat)
			}
			if e.Card != nil && (e.Kind == uno.EvDraw || e.Kind == uno.EvForced) && e.Seat != viewer {
				t.Fatalf("seat %d's event stream carries seat %d's drawn card", viewer, e.Seat)
			}
		}
	}
}

// A seat the mercy rule has buried holds no cards; so does one that just went out.
// The view tells them apart by asking the engine (Live), not by inferring from a
// count of zero.
func TestABuriedSeatIsRenderedOut(t *testing.T) {
	tier, _ := uno.TierBySlug("nm-full")
	g, _, err := uno.New(tier, uno.SoloSeats(tier, tier.Bots, 1000), 0.05, 7, 9)
	if err != nil {
		t.Fatal(err)
	}
	g, _, err = uno.ApplyMove(g, 0, uno.Move{Kind: uno.MoveDeal})
	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, 0)
	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 != -1 {
		t.Errorf("nobody has taken the pot mid-hand, so there is no winner: got %d", v.Winner)
	}
}

// The bill a stack has run up is the only thing on the table while it stands, so it
// has to reach the browser.
func TestTheStackBillCrossesTheWire(t *testing.T) {
	tier, _ := uno.TierBySlug("nm-duel")
	g, _, err := uno.New(tier, uno.SoloSeats(tier, tier.Bots, 1000), 0.05, 3, 4)
	if err != nil {
		t.Fatal(err)
	}
	g, _, err = uno.ApplyMove(g, 0, uno.Move{Kind: uno.MoveDeal})
	if err != nil {
		t.Fatal(err)
	}
	g.Pending = 6
	g.Phase = uno.PhaseStack

	v := viewUno(g, 0)
	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)
	}
}
