package web

import (
	"testing"

	"pete/internal/games/holdem"
	"pete/internal/storage"
)

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

	v, code := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 2, "buyin": 600}))
	if code != 200 {
		t.Fatalf("sit = %d, want 200", code)
	}
	if v.Chips != 4400 {
		t.Fatalf("chips after a 600 buy-in = %d, want 4400", v.Chips)
	}
	if v.Game != gameHoldem || v.Holdem == nil {
		t.Fatalf("sit returned no table: game=%q", v.Game)
	}

	g := v.Holdem
	if g.Stack != 600 {
		t.Errorf("you sat down with %d, want the 600 you bought in for", g.Stack)
	}
	if len(g.Seats) != 3 {
		t.Fatalf("two bots and you is three 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)
	}

	// Play a whole hand, then check that not one chip has crossed the border.
	deal, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
		map[string]any{"move": "deal"}))
	if code != 200 {
		t.Fatalf("deal = %d, want 200", code)
	}
	for i := 0; deal.Holdem != nil && deal.Holdem.Phase == "betting" && i < 60; i++ {
		move := "check"
		if deal.Holdem.Owed > 0 {
			move = "fold"
		}
		deal, code = call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
			map[string]any{"move": move}))
		if code != 200 {
			t.Fatalf("%s = %d, want 200", move, code)
		}
	}
	if deal.Chips != 4400 {
		t.Errorf("chips moved during a hand: %d, want the 4400 that were left after the buy-in — "+
			"a pot is settled inside the engine, not across the border", deal.Chips)
	}
}

// The wall. A bot's hole cards are the game; a browser that held them would make
// this unplayable, and the payload is where that has to be true.
func TestHoldemNeverSendsABotsCards(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)

	v, _ := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "micro", "bots": 3, "buyin": 200}))
	if v.Holdem == nil {
		t.Fatal("no table")
	}

	// Play hands until one of them ends without a showdown, checking every payload
	// on the way. Folding is the case that matters: nobody has earned the right to
	// see anybody's cards, so nobody's may be in there.
	for hand := 0; hand < 8; hand++ {
		v, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
			map[string]any{"move": "deal"}))
		if code != 200 {
			t.Fatalf("deal = %d", code)
		}
		noBotCards(t, v)

		for i := 0; v.Holdem != nil && v.Holdem.Phase == "betting" && i < 60; i++ {
			move := "check"
			if v.Holdem.Owed > 0 {
				move = "call"
			}
			v, code = call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
				map[string]any{"move": move}))
			if code != 200 {
				t.Fatalf("%s = %d", move, code)
			}
			noBotCards(t, v)
		}
		if v.Holdem == nil || v.Holdem.Phase == "done" {
			return // busted out; the wall held all the way
		}
	}
}

// noBotCards checks a payload. A bot's cards may appear in exactly one place: a
// seat that is being shown down, on a board that reached a showdown.
func noBotCards(t *testing.T, v tableView) {
	t.Helper()
	g := v.Holdem
	if g == nil {
		return
	}
	shown := g.Street == "showdown"
	for i, seat := range g.Seats {
		if i == 0 || len(seat.Cards) == 0 {
			continue
		}
		if !shown {
			t.Fatalf("seat %d (%s) sent %d cards on the %s — nobody has shown down",
				i, seat.Name, len(seat.Cards), g.Street)
		}
		if seat.State == "folded" {
			t.Fatalf("seat %d (%s) folded and its cards were sent anyway", i, seat.Name)
		}
	}
	for _, e := range v.HoldemEvents {
		if e.Seat > 0 && len(e.Cards) > 0 && e.Kind != "show" {
			t.Fatalf("a %q event carries seat %d's cards — that's a bot's hand", e.Kind, e.Seat)
		}
	}
}

// Getting up is what pays. Everything the session did lands in one movement.
func TestHoldemLeavingBringsTheStackBack(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)

	v, _ := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 1, "buyin": 500}))
	if v.Chips != 4500 || v.Holdem == nil {
		t.Fatalf("sit: chips=%d holdem=%v", v.Chips, v.Holdem)
	}
	stack := v.Holdem.Stack

	v, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
		map[string]any{"move": "leave"}))
	if code != 200 {
		t.Fatalf("leave = %d, want 200", code)
	}
	if v.Chips != 4500+stack {
		t.Errorf("chips after getting up = %d, want %d (the %d that was in front of us)",
			v.Chips, 4500+stack, stack)
	}
	if v.Game != "" {
		t.Errorf("still at a table after getting up: %q", v.Game)
	}

	// And there is no table left to play at.
	_, code = call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
		map[string]any{"move": "deal"}))
	if code != 409 {
		t.Errorf("dealt a hand at a table we got up from: %d, want 409", code)
	}
}

// You cannot walk out on a hand you have chips riding on.
func TestHoldemCannotLeaveMidHand(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)

	call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 2, "buyin": 500}))
	v, _ := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
		map[string]any{"move": "deal"}))
	if v.Holdem == nil || v.Holdem.Phase != "betting" {
		t.Skip("the hand ended before we could act")
	}

	_, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
		map[string]any{"move": "leave"}))
	if code != 400 {
		t.Errorf("left in the middle of a hand: %d, want 400", code)
	}

	// And the chips are still on the table, not back on the stack.
	after, err := storage.Chips("@reala:parodia.dev")
	if err != nil {
		t.Fatal(err)
	}
	if after.Chips != 4500 {
		t.Errorf("chips = %d, want 4500 — the buy-in is still on the table", after.Chips)
	}
}

// A buy-in outside the table's range is not a buy-in, and it must not take chips.
func TestHoldemRefusesABadBuyIn(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)

	tier, _ := holdem.TierBySlug("low")
	for _, amount := range []int64{tier.MinBuy - 1, tier.MaxBuy + 1, 0} {
		_, code := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
			map[string]any{"tier": "low", "bots": 2, "buyin": amount}))
		if code != 400 {
			t.Errorf("buy-in of %d at a %d–%d table = %d, want 400", amount, tier.MinBuy, tier.MaxBuy, code)
		}
	}

	st, err := storage.Chips("@reala:parodia.dev")
	if err != nil {
		t.Fatal(err)
	}
	if st.Chips != 5000 {
		t.Errorf("a refused buy-in took %d chips", 5000-st.Chips)
	}
}

// A top-up is real money crossing the border. If the engine refuses it, the chips
// come straight back — the same order, and the same reason, as doubling down.
func TestHoldemTopUpRefundsWhenRefused(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)

	// Sitting down at the maximum means there is no room to top up into.
	call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 1, "buyin": 1000}))

	_, code := call(t, s, s.handleHoldemMove, as(t, s, "reala", "POST", "/api/games/holdem/move",
		map[string]any{"move": "topup", "amount": 100}))
	if code != 400 {
		t.Errorf("topped up over the table maximum: %d, want 400", code)
	}

	st, err := storage.Chips("@reala:parodia.dev")
	if err != nil {
		t.Fatal(err)
	}
	if st.Chips != 4000 {
		t.Errorf("chips = %d, want 4000 — a refused top-up must give the chips back", st.Chips)
	}
}
