package storage

import (
	"errors"
	"testing"
)

// openTestTable stands up a table with a full ring of bots and returns it. Six
// seats, because that is hold'em's ring and the most seats any game here has.
func openTestTable(t *testing.T, id, game string) Table {
	t.Helper()
	tbl := Table{
		ID: id, Game: game, Tier: "1-2", State: []byte(`{}`),
		Seed1: 1, Seed2: 2, Phase: "betting", HandNo: 1,
	}
	seats := make([]Seat, 6)
	for i := range seats {
		seats[i] = Seat{Seat: i, Name: "bot"}
	}
	if err := OpenTable(tbl, seats); err != nil {
		t.Fatal(err)
	}
	return tbl
}

// reload reads a table back and fails the test if it is gone.
func reload(t *testing.T, id string) (Table, []Seat) {
	t.Helper()
	tbl, seats, err := LoadTable(id)
	if err != nil {
		t.Fatal(err)
	}
	return tbl, seats
}

func TestOpenTable_SeatsAreAllBots(t *testing.T) {
	setupTestDB(t)
	openTestTable(t, "t1", "holdem")

	_, seats := reload(t, "t1")
	if len(seats) != 6 {
		t.Fatalf("want 6 seats, got %d", len(seats))
	}
	for _, s := range seats {
		if !s.Bot() {
			t.Errorf("seat %d should be a bot", s.Seat)
		}
	}
}

func TestSitDown_MovesChipsOntoTheTable(t *testing.T) {
	setupTestDB(t)
	tbl := openTestTable(t, "t1", "holdem")
	fund(t, player, 5000)

	if err := SitDown(Sit{
		Table: tbl,
		Seat:  Seat{Seat: 2, MatrixUser: player, Name: "reala"},
		BuyIn: 1000,
	}); err != nil {
		t.Fatal(err)
	}

	// The chips are off the stack...
	if got := chipsOf(t, player); got != 4000 {
		t.Errorf("stack: want 4000, got %d", got)
	}
	// ...and onto the seat.
	_, seats := reload(t, "t1")
	seat := seats[2]
	if seat.MatrixUser != player || seat.Staked != 1000 {
		t.Errorf("seat 2: want reala staked 1000, got %q staked %d", seat.MatrixUser, seat.Staked)
	}
	// The occupancy claim points at the table.
	id, err := TableOf(player)
	if err != nil || id != "t1" {
		t.Errorf("TableOf: want t1, got %q err %v", id, err)
	}
}

func TestSitDown_CannotTakeATakenSeat(t *testing.T) {
	setupTestDB(t)
	tbl := openTestTable(t, "t1", "holdem")
	fund(t, player, 5000)
	fund(t, "@bob:parodia.dev", 5000)

	if err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 2, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil {
		t.Fatal(err)
	}
	tbl2, _ := reload(t, "t1")
	err := SitDown(Sit{Table: tbl2, Seat: Seat{Seat: 2, MatrixUser: "@bob:parodia.dev", Name: "bob"}, BuyIn: 1000})
	if !errors.Is(err, ErrSeatTaken) {
		t.Fatalf("want ErrSeatTaken, got %v", err)
	}
	// Bob's chips did not move.
	if got := chipsOf(t, "@bob:parodia.dev"); got != 5000 {
		t.Errorf("bob's stack should be untouched, got %d", got)
	}
}

func TestSitDown_CannotSitAtTwoTables(t *testing.T) {
	setupTestDB(t)
	tbl1 := openTestTable(t, "t1", "holdem")
	tbl2 := openTestTable(t, "t2", "holdem")
	fund(t, player, 5000)

	if err := SitDown(Sit{Table: tbl1, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil {
		t.Fatal(err)
	}
	err := SitDown(Sit{Table: tbl2, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000})
	if !errors.Is(err, ErrHandInProgress) {
		t.Fatalf("want ErrHandInProgress, got %v", err)
	}
	// The buy-in for the second table rolled back.
	if got := chipsOf(t, player); got != 4000 {
		t.Errorf("only the first buy-in should have left the stack, got %d", got)
	}
}

func TestSitDown_InsufficientChipsRollsBack(t *testing.T) {
	setupTestDB(t)
	tbl := openTestTable(t, "t1", "holdem")
	fund(t, player, 500)

	err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000})
	if !errors.Is(err, ErrInsufficientChips) {
		t.Fatalf("want ErrInsufficientChips, got %v", err)
	}
	if _, err := TableOf(player); !errors.Is(err, ErrNoLiveHand) {
		t.Errorf("no seat should have been claimed, got %v", err)
	}
	_, seats := reload(t, "t1")
	if seats[0].MatrixUser != "" {
		t.Errorf("seat should still be a bot")
	}
}

func TestLeaveTable_BringsChipsHome(t *testing.T) {
	setupTestDB(t)
	tbl := openTestTable(t, "t1", "holdem")
	fund(t, player, 5000)
	if err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil {
		t.Fatal(err)
	}

	tbl2, _ := reload(t, "t1")
	// Got up with 1,240 — up on the session.
	if err := LeaveTable(Leave{Table: tbl2, Seat: 0, MatrixUser: player, Bot: "bot", Amount: 1240}); err != nil {
		t.Fatal(err)
	}
	if got := chipsOf(t, player); got != 5240 {
		t.Errorf("want 5240 back on the stack, got %d", got)
	}
	if _, err := TableOf(player); !errors.Is(err, ErrNoLiveHand) {
		t.Errorf("seat claim should be gone, got %v", err)
	}
	_, seats := reload(t, "t1")
	if seats[0].MatrixUser != "" {
		t.Errorf("a bot should have taken the empty chair")
	}
}

func TestSaveTable_VersionGuardsTheWrite(t *testing.T) {
	setupTestDB(t)
	openTestTable(t, "t1", "holdem")

	a, _ := reload(t, "t1") // both read version 0
	b, _ := reload(t, "t1")

	a.State = []byte(`{"a":1}`)
	if err := CommitTable(TableCommit{Table: a}); err != nil {
		t.Fatalf("first write should win: %v", err)
	}
	b.State = []byte(`{"b":2}`)
	if err := CommitTable(TableCommit{Table: b}); !errors.Is(err, ErrStaleTable) {
		t.Fatalf("second write should be stale, got %v", err)
	}

	after, _ := reload(t, "t1")
	if string(after.State) != `{"a":1}` {
		t.Errorf("the winning write should stand, got %s", after.State)
	}
	if after.Version != 1 {
		t.Errorf("version should have bumped once, got %d", after.Version)
	}
}

func TestDueTables_OnlyExpiredClocks(t *testing.T) {
	setupTestDB(t)
	now := nowUnix()

	past := openTestTable(t, "past", "holdem")
	past.Deadline = now - 5
	if err := CommitTable(TableCommit{Table: past}); err != nil {
		t.Fatal(err)
	}
	future := openTestTable(t, "future", "holdem")
	future.Deadline = now + 60
	if err := CommitTable(TableCommit{Table: future}); err != nil {
		t.Fatal(err)
	}
	openTestTable(t, "noclock", "holdem") // deadline 0

	due, err := DueTables(now)
	if err != nil {
		t.Fatal(err)
	}
	if len(due) != 1 || due[0].ID != "past" {
		t.Fatalf("only the past-due table should show, got %+v", due)
	}
}

func TestChat_KeepsTheHandItWasSaidDuring(t *testing.T) {
	setupTestDB(t)
	openTestTable(t, "t1", "holdem") // hand_no 1

	if _, err := Say("t1", player, "reala", "nice hand"); err != nil {
		t.Fatal(err)
	}
	// The table moves to the next hand.
	tbl2, _ := reload(t, "t1")
	tbl2.HandNo = 2
	if err := CommitTable(TableCommit{Table: tbl2}); err != nil {
		t.Fatal(err)
	}
	if _, err := Say("t1", player, "reala", "and another"); err != nil {
		t.Fatal(err)
	}

	lines, err := Chat("t1", 50)
	if err != nil {
		t.Fatal(err)
	}
	if len(lines) != 2 {
		t.Fatalf("want 2 lines, got %d", len(lines))
	}
	if lines[0].Body != "nice hand" || lines[0].HandNo != 1 {
		t.Errorf("first line should be hand 1: %+v", lines[0])
	}
	if lines[1].HandNo != 2 {
		t.Errorf("second line should be hand 2: %+v", lines[1])
	}
}

func TestCloseTable_KeepsATableWithAHumanAtIt(t *testing.T) {
	setupTestDB(t)
	tbl := openTestTable(t, "t1", "holdem")
	fund(t, player, 5000)
	if err := SitDown(Sit{Table: tbl, Seat: Seat{Seat: 0, MatrixUser: player, Name: "reala"}, BuyIn: 1000}); err != nil {
		t.Fatal(err)
	}

	if err := CloseTable("t1"); err != nil {
		t.Fatal(err)
	}
	if _, _, err := LoadTable("t1"); err != nil {
		t.Errorf("a table with a human should survive close, got %v", err)
	}

	// Everyone leaves; now it goes.
	tbl2, _ := reload(t, "t1")
	if err := LeaveTable(Leave{Table: tbl2, Seat: 0, MatrixUser: player, Bot: "bot", Amount: 1000}); err != nil {
		t.Fatal(err)
	}
	if err := CloseTable("t1"); err != nil {
		t.Fatal(err)
	}
	if _, _, err := LoadTable("t1"); !errors.Is(err, ErrNoSuchTable) {
		t.Errorf("an all-bot table should close, got %v", err)
	}
}
