package web

import (
	"encoding/json"
	"strings"
	"testing"

	"pete/internal/games/holdem"
)

// The security boundary of the whole multiplayer table, tested the way the plan
// insists it must be: render every seat's view at every street, and string-search
// the JSON for any card that belongs to another seat. Zero hits, for every seat,
// at every point before a showdown turns cards over.
//
// This is not belt-and-braces. After SSE the view a seat renders fans to that
// seat's stream, so a single missed redaction does not leak one card to one
// player through one handler bug — it broadcasts a hole card to the whole felt,
// continuously. The engine deliberately emits every seat's cards now (it cannot
// know who a shared stream is for), which makes this function the only thing
// standing between the deck and the network tab.

// holeLabel is a seat's two cards as they would be rendered — the exact strings a
// leak would put in the JSON.
func holeLabels(g holdem.State, seat int) []string {
	h := g.Seats[seat].Hole
	return []string{viewCard(h[0]).Label, viewCard(h[1]).Label}
}

func TestHoldemViewNeverLeaksAnotherSeatsCards(t *testing.T) {
	tier := holdem.Tiers[0]
	// Two humans (0, 2) and two bots (1, 3), so the test covers a seat seeing both
	// another human and a bot, and a human sitting at a non-zero index.
	seats := []holdem.SeatConfig{
		{Name: "Ana", Stack: tier.MaxBuy},
		{Name: "Bot A", Bot: true, Stack: tier.MaxBuy},
		{Name: "Bo", Stack: tier.MaxBuy},
		{Name: "Bot B", Bot: true, Stack: tier.MaxBuy},
	}
	g, _, err := holdem.New(tier, seats, tier.RakePct, 5, 6)
	if err != nil {
		t.Fatal(err)
	}

	// Deal, then walk the hand to the river without anyone folding — every seat
	// stays in, so every seat has cards that must not leak to the others. Checking
	// and calling keeps everyone live; the redaction is checked after every step.
	g, dealEvs, err := holdem.ApplyMove(g, 0, holdem.Move{Kind: holdem.Deal})
	if err != nil {
		t.Fatal(err)
	}

	// The deal script carries every hole, so it is the sharpest test of event
	// redaction: for each viewer, no other seat's cards may survive.
	assertEventsRedacted(t, g, dealEvs)
	assertViewsRedacted(t, g)

	for step := 0; g.Phase == holdem.PhaseBetting && step < 80; step++ {
		seat := g.ToAct
		if g.Seats[seat].Bot {
			t.Fatalf("advance stopped on a bot at seat %d", seat)
		}
		move := holdem.Move{Kind: holdem.Check}
		if g.Owed(seat) > 0 {
			move = holdem.Move{Kind: holdem.Call}
		}
		var evs []holdem.Event
		g, evs, err = holdem.ApplyMove(g, seat, move)
		if err != nil {
			t.Fatalf("seat %d %s: %v", seat, move.Kind, err)
		}
		assertEventsRedacted(t, g, evs)
		assertViewsRedacted(t, g)
	}
}

// assertViewsRedacted renders every seat's table view and fails if any of them
// carries a card belonging to a seat that is still hiding it.
func assertViewsRedacted(t *testing.T, g holdem.State) {
	t.Helper()
	shown := g.Street == holdem.Showdown
	for viewer := range g.Seats {
		blob, err := json.Marshal(viewHoldem(g, viewer))
		if err != nil {
			t.Fatal(err)
		}
		payload := string(blob)
		for other := range g.Seats {
			if other == viewer {
				continue // your own cards are yours to see
			}
			// A seat's cards are legitimately visible only at a showdown, and then only
			// if they did not fold. Everywhere else, a hit is a leak.
			if shown && g.Seats[other].State != holdem.Folded {
				continue
			}
			if g.Seats[other].State == holdem.Out {
				continue
			}
			for _, label := range holeLabels(g, other) {
				if strings.Contains(payload, label) {
					t.Fatalf("seat %d's view (%s street) leaks seat %d's card %q",
						viewer, g.Street, other, label)
				}
			}
		}
	}
}

// assertEventsRedacted renders the event script for every viewer and fails if a
// beat carries another seat's card outside a showdown "show".
func assertEventsRedacted(t *testing.T, g holdem.State, evs []holdem.Event) {
	t.Helper()
	for viewer := range g.Seats {
		blob, err := json.Marshal(viewHoldemEvents(evs, viewer))
		if err != nil {
			t.Fatal(err)
		}
		payload := string(blob)
		// The engine only puts a non-viewer's cards in a "show" beat and in the hole
		// beats it emits for everyone. If any card label of another seat survives the
		// redaction *and* there is no show event in this batch, it leaked.
		hasShow := false
		for _, e := range evs {
			if e.Kind == "show" {
				hasShow = true
			}
		}
		if hasShow {
			continue // a showdown legitimately turns cards over
		}
		for other := range g.Seats {
			if other == viewer || g.Seats[other].State == holdem.Out {
				continue
			}
			for _, label := range holeLabels(g, other) {
				if strings.Contains(payload, label) {
					t.Fatalf("seat %d's event stream leaks seat %d's card %q", viewer, other, label)
				}
			}
		}
	}
}
