package storage

import (
	"errors"
	"testing"
	"time"
)

// The settle used to be four autocommit statements in the web layer — save the
// state, award the payout, write the audit row, clear the felt — sequenced so
// that a crash between any two of them cost the player as little as possible.
//
// These are the tests for the thing that replaced it. They are less about the
// happy path (the game tests already cover that: a hand pays what it says it
// pays) and more about the two properties the old shape did not have, and which
// a shared table with a pot in it cannot do without.

func liveBlob(game string) LiveHand {
	return LiveHand{Game: game, State: []byte(`{"phase":"player"}`), Seed1: 7, Seed2: 9}
}

// A settled hand moves the money, writes the audit row, and leaves the felt
// empty — and it does all three or none of them.
//
// The old code logged and carried on if the clear failed. That is the double-pay:
// a settled hand still sitting in game_live_hands is a hand that settles again on
// the next request, and pays again with it.
func TestSettleMovesTheMoneyAndTheFeltTogether(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 1000)

	if err := Stake(player, 200); err != nil {
		t.Fatal(err)
	}
	if err := CommitHand(player, Commit{
		Live: liveBlob("blackjack"), Fresh: true, Stake: 200,
		Done: true, Payout: 390,
		Audit: Hand{MatrixUser: player, Game: "blackjack", Bet: 200, Payout: 390, Rake: 10, Outcome: "won"},
	}); err != nil {
		t.Fatal(err)
	}

	// Paid: 1000 - 200 staked + 390 back.
	if c := chipsOf(t, player); c != 1190 {
		t.Fatalf("chips = %d after a 390 payout on a 200 stake, want 1190", c)
	}
	// The felt is empty, which is what stops it settling a second time.
	if _, err := LoadLiveHand(player); !errors.Is(err, ErrNoLiveHand) {
		t.Fatalf("live hand after settle: err = %v, want ErrNoLiveHand", err)
	}
	// And the house took its cut, once.
	take, err := HouseTake(0)
	if err != nil {
		t.Fatal(err)
	}
	if take != 10 {
		t.Fatalf("house take = %d, want 10", take)
	}
}

// A seat that is already taken refuses the game *and hands the stake back*, in
// the same transaction that refused it.
//
// This was two statements: the save came back ErrHandInProgress, and then a
// separate Award put the chips back. A crash in between took a player's stake for
// a game that never existed anywhere — no felt, no audit row, no way to find it.
func TestARefusedSeatGivesTheStakeBackInTheSameBreath(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 1000)

	// A game is already in progress.
	if err := Stake(player, 200); err != nil {
		t.Fatal(err)
	}
	if err := CommitHand(player, Commit{Live: liveBlob("uno"), Fresh: true, Stake: 200}); err != nil {
		t.Fatal(err)
	}
	if c := chipsOf(t, player); c != 800 {
		t.Fatalf("chips = %d with 200 staked on a live game, want 800", c)
	}

	// A second game is dealt on top of it: the stake leaves, as it must, in the
	// same statement that checks it's there.
	if err := Stake(player, 300); err != nil {
		t.Fatal(err)
	}
	err := CommitHand(player, Commit{
		Live: liveBlob("blackjack"), Fresh: true, Stake: 300,
		Done: true, Payout: 600, // a natural, which would settle the instant it's dealt
		Audit: Hand{MatrixUser: player, Game: "blackjack", Bet: 300, Payout: 600},
	})
	if !errors.Is(err, ErrHandInProgress) {
		t.Fatalf("err = %v, want ErrHandInProgress", err)
	}

	// The 300 is back, and the natural was *not* paid — a game that was never
	// seated must not settle.
	if c := chipsOf(t, player); c != 800 {
		t.Fatalf("chips = %d after a refused deal, want 800 (the 300 refunded, the 600 never paid)", c)
	}
	// And the game already in progress is untouched. This is the bit that matters:
	// a natural dealt on top of a live game used to be able to settle, clear the
	// felt, and take the other game's stake down with it.
	live, err := LoadLiveHand(player)
	if err != nil {
		t.Fatalf("the live game is gone: %v", err)
	}
	if live.Game != "uno" {
		t.Fatalf("live game = %q, want the uno game still sitting there", live.Game)
	}
	// Nothing was recorded, because nothing finished.
	if take, err := HouseTake(0); err != nil || take != 0 {
		t.Fatalf("house take = %d (err %v), want 0 — no hand finished", take, err)
	}
}

// The deadlock canary.
//
// SQLite runs at MaxOpenConns(1), so the connection is a global mutex. A bare
// Get().Exec inside an open transaction waits for the one connection that the
// transaction is holding, and waits forever — it is not an error, it is a hung
// process, and because the news app shares the pool it hangs that too.
//
// So: if anybody ever reaches for Award or RecordHand (rather than award/
// recordHand) from inside CommitHand, this test stops returning. It does not
// fail with a nice message; it hangs, and the timeout is the message. That is
// exactly the failure mode in production, which is the point of catching it here.
func TestTheSettleDoesNotDeadlockAgainstItsOwnConnection(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 1000)
	if err := Stake(player, 100); err != nil {
		t.Fatal(err)
	}

	done := make(chan error, 1)
	go func() {
		done <- CommitHand(player, Commit{
			Live: liveBlob("hangman"), Fresh: true, Stake: 100,
			Done: true, Payout: 234,
			Audit: Hand{MatrixUser: player, Game: "hangman", Bet: 100, Payout: 234, Rake: 12, Outcome: "won"},
		})
	}()

	select {
	case err := <-done:
		if err != nil {
			t.Fatal(err)
		}
	case <-time.After(5 * time.Second):
		t.Fatal("CommitHand did not return: something inside the transaction is waiting " +
			"for the connection the transaction is holding. Look for a Get().Exec that " +
			"should be a tx.Exec.")
	}

	if c := chipsOf(t, player); c != 1134 {
		t.Fatalf("chips = %d, want 1134", c)
	}
}

// Being paid moves the idle clock, so the reaper leaves a player who is
// mid-session alone. Touch used to be a separate statement after the settle; it
// is inside it now, and this is what would notice if it got dropped on the way.
func TestSettlingKeepsTheReaperAway(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 1000)

	// Backdate the session well past the reaper's patience.
	if _, err := Get().Exec(
		`UPDATE game_chips SET last_played = ? WHERE matrix_user = ?`,
		nowUnix()-int64((2 * SessionIdleAfter).Seconds()), player,
	); err != nil {
		t.Fatal(err)
	}

	if err := Stake(player, 100); err != nil {
		t.Fatal(err)
	}
	if err := CommitHand(player, Commit{
		Live: liveBlob("trivia"), Fresh: true, Stake: 100,
		Done: true, Payout: 150,
		Audit: Hand{MatrixUser: player, Game: "trivia", Bet: 100, Payout: 150, Outcome: "won"},
	}); err != nil {
		t.Fatal(err)
	}

	stacks, _, err := IdleStacks(SessionIdleAfter)
	if err != nil {
		t.Fatal(err)
	}
	if len(stacks) != 0 {
		t.Fatalf("the reaper found %d idle stacks; a player who just settled a hand is not idle", len(stacks))
	}
}
