package web

import (
	"strings"
	"testing"

	"pete/internal/storage"
)

// The one thing this table cannot get wrong: the stake leaves the stack, and the
// phrase does not leave the server.
func TestHangmanStartTakesTheStakeAndKeepsThePhrase(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	v, code := call(t, s, s.handleHangmanStart, as(t, s, "reala", "POST", "/api/games/hangman/start",
		map[string]any{"bet": 100, "tier": "short"}))
	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.Hangman == nil {
		t.Fatal("start returned no game")
	}
	if v.Game != gameHangman {
		t.Errorf("game = %q, want hangman", v.Game)
	}
	if v.Hangman.Phrase != "" {
		t.Fatalf("the phrase was sent to the browser before it was won: %q", v.Hangman.Phrase)
	}
	// Nothing is revealed at the start except the scaffolding, and a space is not
	// a letter you have to earn.
	for _, c := range v.Hangman.Cells {
		if c.Slot && c.Ch != "" {
			t.Fatalf("a letter was face up before it was guessed: %+v", c)
		}
	}
	if v.Hangman.Lives != 6 {
		t.Errorf("lives = %d, want 6", v.Hangman.Lives)
	}
}

// A win pays what the felt said it would, and the rake comes out of the winnings.
func TestHangmanWinPaysWhatTheFeltQuoted(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	v, _ := call(t, s, s.handleHangmanStart, as(t, s, "reala", "POST", "/api/games/hangman/start",
		map[string]any{"bet": 100, "tier": "short"}))
	quoted := v.Hangman.Stands

	// The server holds the phrase, so read it out of the live row — which is the
	// only place it exists — and solve it.
	phrase := livePhrase(t)
	v, code := call(t, s, s.handleHangmanGuess, as(t, s, "reala", "POST", "/api/games/hangman/guess",
		map[string]string{"solve": phrase}))
	if code != 200 {
		t.Fatalf("solve = %d, want 200", code)
	}
	if v.Hangman.Outcome != "solved" {
		t.Fatalf("outcome = %q, want solved", v.Hangman.Outcome)
	}
	// No wrong guesses, so the full 2.6×: 260 gross, 160 profit, 8 rake, 252 back.
	if v.Hangman.Payout != quoted {
		t.Errorf("felt quoted %d, house paid %d", quoted, v.Hangman.Payout)
	}
	if v.Hangman.Payout != 252 || v.Hangman.Rake != 8 {
		t.Errorf("payout/rake = %d/%d, want 252/8", v.Hangman.Payout, v.Hangman.Rake)
	}
	if got := chipsNow(t); got != 900+252 {
		t.Errorf("chips = %d, want %d", got, 900+252)
	}
	// And the phrase is finally allowed out, now that it decides nothing.
	if v.Hangman.Phrase == "" {
		t.Error("a finished game never told the player what the phrase was")
	}
	// The game is off the felt.
	if _, err := storage.LoadLiveHand(testPlayer); err == nil {
		t.Error("a settled game is still sitting in game_live_hands")
	}
}

// Six wrong guesses take the stake and nothing more.
func TestHangmanHangingCostsExactlyTheStake(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	call(t, s, s.handleHangmanStart, as(t, s, "reala", "POST", "/api/games/hangman/start",
		map[string]any{"bet": 100, "tier": "short"}))

	// Six solves that are certainly wrong — a wrong solve costs a life, same as a
	// wrong letter, and this needs no knowledge of the phrase.
	var v tableView
	for i := 0; i < 6; i++ {
		v, _ = call(t, s, s.handleHangmanGuess, as(t, s, "reala", "POST", "/api/games/hangman/guess",
			map[string]string{"solve": "definitely not the phrase at all"}))
	}
	if v.Hangman == nil || v.Hangman.Outcome != "hung" {
		t.Fatalf("outcome = %+v, want hung", v.Hangman)
	}
	if v.Hangman.Payout != 0 {
		t.Errorf("payout = %d, want 0", v.Hangman.Payout)
	}
	if got := chipsNow(t); got != 900 {
		t.Errorf("chips = %d, want 900 — a loss costs the stake and no more", got)
	}
	if v.Hangman.Phrase == "" {
		t.Error("hung without being told the answer")
	}
}

// One game at a time, across games: you cannot walk from a hangman into a hand of
// blackjack with chips still riding on a phrase.
func TestHangmanHoldsTheSeatAgainstBlackjack(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	call(t, s, s.handleHangmanStart, as(t, s, "reala", "POST", "/api/games/hangman/start",
		map[string]any{"bet": 100, "tier": "short"}))

	_, code := call(t, s, s.handleDeal, as(t, s, "reala", "POST", "/api/games/blackjack/deal",
		map[string]int64{"bet": 100}))
	if code != 409 {
		t.Fatalf("dealt blackjack on top of a live hangman: %d, want 409", code)
	}
	// And the stake that was refused came back: 1000 - 100 (the hangman) and not a
	// chip more.
	if got := chipsNow(t); got != 900 {
		t.Errorf("chips = %d, want 900 — the refused deal kept the stake", got)
	}
	if _, err := storage.LoadLiveHand(testPlayer); err != nil {
		t.Errorf("the hangman was evicted by the deal it refused: %v", err)
	}
}

// Cashing out mid-phrase is refused, for the same reason as mid-hand.
func TestCannotCashOutMidPhrase(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	call(t, s, s.handleHangmanStart, as(t, s, "reala", "POST", "/api/games/hangman/start",
		map[string]any{"bet": 100, "tier": "short"}))

	_, code := call(t, s, s.handleCashOut, as(t, s, "reala", "POST", "/api/games/cashout",
		map[string]int64{"amount": 0}))
	if code != 409 {
		t.Fatalf("cash-out mid-phrase = %d, want 409", code)
	}
}

// A tier the browser made up is refused, and costs nothing.
func TestHangmanRefusesAnInventedTier(t *testing.T) {
	s := newCasino(t)
	fund(t, 1000)

	_, code := call(t, s, s.handleHangmanStart, as(t, s, "reala", "POST", "/api/games/hangman/start",
		map[string]any{"bet": 100, "tier": "impossible"}))
	if code != 400 {
		t.Fatalf("start on a made-up tier = %d, want 400", code)
	}
	if got := chipsNow(t); got != 1000 {
		t.Errorf("chips = %d, want 1000 — a refused game must not take a stake", got)
	}
}

// livePhrase digs the phrase out of the live row. Only a test may do this: it is
// reaching past the wire on purpose, to prove the wire doesn't carry it.
func livePhrase(t *testing.T) string {
	t.Helper()
	live, err := storage.LoadLiveHand(testPlayer)
	if err != nil {
		t.Fatal(err)
	}
	blob := string(live.State)
	const key = `"phrase":"`
	i := strings.Index(blob, key)
	if i < 0 {
		t.Fatalf("no phrase in the live row: %s", blob)
	}
	rest := blob[i+len(key):]
	j := strings.Index(rest, `"`)
	if j < 0 {
		t.Fatal("unterminated phrase in the live row")
	}
	return rest[:j]
}
