package web

import (
	"testing"
	"time"

	"pete/internal/storage"
)

const bobPlayer = "@bob:parodia.dev"

// fundUser puts chips in front of a named player the way the border really does.
func fundUser(t *testing.T, user string, chips int64) {
	t.Helper()
	e, err := storage.RequestBuyIn(user, chips)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := storage.ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	if _, err := storage.SettleEscrow(e.GUID, true, "", 0); err != nil {
		t.Fatal(err)
	}
}

func chipsOf(t *testing.T, user string) int64 {
	t.Helper()
	st, err := storage.Chips(user)
	if err != nil {
		t.Fatal(err)
	}
	return st.Chips
}

// A second person can sit down at a table somebody else opened, taking a chair a
// bot was keeping warm — which is the whole point of the thing being multiplayer.
func TestHoldemJoinTakesAnOpenSeat(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000) // reala
	fundUser(t, bobPlayer, 5000)

	if _, code := call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 2, "buyin": 500})); code != 200 {
		t.Fatalf("reala sit = %d, want 200", code)
	}
	tableID, err := storage.TableOf(testPlayer)
	if err != nil {
		t.Fatal(err)
	}

	v, code := call(t, s, s.handleHoldemSit, as(t, s, "bob", "POST", "/api/games/holdem/sit",
		map[string]any{"table": tableID, "buyin": 500}))
	if code != 200 {
		t.Fatalf("bob join = %d, want 200", code)
	}
	if v.Chips != 4500 {
		t.Errorf("bob's chips after a 500 buy-in = %d, want 4500", v.Chips)
	}
	if v.Holdem == nil {
		t.Fatal("join returned no table")
	}

	_, bobSeat, err := storage.PlayerSeat(bobPlayer)
	if err != nil {
		t.Fatal(err)
	}
	if bobSeat == 0 {
		t.Errorf("bob took reala's seat %d", bobSeat)
	}
	if v.Holdem.YourSeat != bobSeat {
		t.Errorf("the view says bob is at seat %d, storage says %d", v.Holdem.YourSeat, bobSeat)
	}

	_, seats, err := storage.LoadTable(tableID)
	if err != nil {
		t.Fatal(err)
	}
	humans := 0
	for _, seat := range seats {
		if seat.MatrixUser != "" {
			humans++
		}
	}
	if humans != 2 {
		t.Errorf("two people at the table, storage says %d humans", humans)
	}
}

// The felt tells each player "you bought in for X" — their own stake, not the
// table's. The engine's BoughtIn is the sum across every human (the audit wants
// that), so a two-human table would quote both of them the pair's total if the
// view read it straight. It reads each seat's own staked row from storage instead.
func TestBoughtInIsPerPlayerNotTheTableTotal(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000) // reala
	fundUser(t, bobPlayer, 5000)

	call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 2, "buyin": 500}))
	tableID, _ := storage.TableOf(testPlayer)
	call(t, s, s.handleHoldemSit, as(t, s, "bob", "POST", "/api/games/holdem/sit",
		map[string]any{"table": tableID, "buyin": 500}))

	for _, who := range []string{testPlayer, bobPlayer} {
		v, err := s.table(who)
		if err != nil {
			t.Fatal(err)
		}
		if v.Holdem == nil {
			t.Fatalf("%s has no table", who)
		}
		if v.Holdem.BoughtIn != 500 {
			t.Errorf("%s is shown bought-in %d, want their own 500 — not the table's 1000",
				who, v.Holdem.BoughtIn)
		}
	}
}

// Leaving a shared table gives you your stack and hands your chair back to the
// house; the table stays open for the people still on it. Only the last human out
// closes the felt.
func TestHoldemLeavingSharedTableKeepsItOpen(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)
	fundUser(t, bobPlayer, 5000)

	call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 2, "buyin": 500}))
	tableID, _ := storage.TableOf(testPlayer)
	call(t, s, s.handleHoldemSit, as(t, s, "bob", "POST", "/api/games/holdem/sit",
		map[string]any{"table": tableID, "buyin": 500}))

	// Bob gets up. His 500 comes home and his chair goes back to a bot.
	if _, code := call(t, s, s.handleHoldemLeave, as(t, s, "bob", "POST", "/api/games/holdem/leave", nil)); code != 200 {
		t.Fatalf("bob leave = %d, want 200", code)
	}
	if got := chipsOf(t, bobPlayer); got != 5000 {
		t.Errorf("bob left with %d, want his 5000 back", got)
	}
	if _, _, err := storage.PlayerSeat(bobPlayer); err != storage.ErrNoLiveHand {
		t.Errorf("bob still holds a seat after leaving: %v", err)
	}
	if _, _, err := storage.LoadTable(tableID); err != nil {
		t.Errorf("the table closed under reala when bob left: %v", err)
	}

	// Reala is the last one out, so the felt closes behind them.
	if _, code := call(t, s, s.handleHoldemLeave, as(t, s, "reala", "POST", "/api/games/holdem/leave", nil)); code != 200 {
		t.Fatalf("reala leave = %d, want 200", code)
	}
	if _, _, err := storage.LoadTable(tableID); err != storage.ErrNoSuchTable {
		t.Errorf("an empty table survived the last human: %v", err)
	}
}

// A table everyone has walked away from is cashed out and closed by the reaper —
// the chips inside a walked-away poker session are not left in limbo.
func TestHoldemReaperCashesOutAbandonedTable(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}))
	tableID, _ := storage.TableOf(testPlayer)

	// The seat has been away, and last acted for itself longer ago than the idle
	// cutoff — the state the reaper is meant to find.
	tbl, seats, err := storage.LoadTable(tableID)
	if err != nil {
		t.Fatal(err)
	}
	seats[0].Away = true
	seats[0].LastSeen = time.Now().Unix() - int64(storage.SessionIdleAfter.Seconds()) - 60
	if err := storage.CommitTable(storage.TableCommit{Table: tbl, Seats: []storage.Seat{seats[0]}}); err != nil {
		t.Fatal(err)
	}

	s.reapAbandonedTables()

	if got := chipsOf(t, testPlayer); got != 5000 {
		t.Errorf("the reaper sent home %d, want the whole 5000 back (buy-in and all)", got)
	}
	if _, _, err := storage.LoadTable(tableID); err != storage.ErrNoSuchTable {
		t.Errorf("the reaper left the table standing: %v", err)
	}
	if _, err := storage.TableOf(testPlayer); err != storage.ErrNoLiveHand {
		t.Errorf("the reaper left the occupancy claim behind: %v", err)
	}
}

// A fresh table is not abandoned, and the reaper leaves it alone.
func TestHoldemReaperSparesALiveTable(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}))
	tableID, _ := storage.TableOf(testPlayer)

	s.reapAbandonedTables()

	if _, _, err := storage.LoadTable(tableID); err != nil {
		t.Errorf("the reaper closed a table someone is sitting at: %v", err)
	}
	if got := chipsOf(t, testPlayer); got != 4500 {
		t.Errorf("chips = %d — the reaper should not have moved a live table's money", got)
	}
}

// The lobby lists a table with a seat going spare, and drops it once it is full.
func TestHoldemLobbyListsJoinableTables(t *testing.T) {
	s := newCasino(t)
	fund(t, 5000)

	// A one-bot table: two seats, one human, one open.
	call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
		map[string]any{"tier": "low", "bots": 1, "buyin": 500}))

	tables, err := storage.LobbyTables(gameHoldem, 50)
	if err != nil {
		t.Fatal(err)
	}
	if len(tables) != 1 {
		t.Fatalf("lobby has %d tables, want 1", len(tables))
	}
	if tables[0].Humans != 1 || tables[0].Seats != 2 {
		t.Errorf("lobby table = %d/%d humans/seats, want 1/2", tables[0].Humans, tables[0].Seats)
	}
}
