package storage

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

const player = "@reala:parodia.dev"

// fund runs a buy-in all the way through the happy path, so a test that needs
// chips on the table can just say so.
func fund(t *testing.T, user string, amount int64) {
	t.Helper()
	e, err := RequestBuyIn(user, amount)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	if _, err := SettleEscrow(e.GUID, true, "", 5000); err != nil {
		t.Fatal(err)
	}
}

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

func TestBuyIn_ChipsOnlyExistOnceGogobeeConfirms(t *testing.T) {
	setupTestDB(t)

	e, err := RequestBuyIn(player, 500)
	if err != nil {
		t.Fatal(err)
	}
	if e.State != EscrowRequested {
		t.Fatalf("state = %q, want %q", e.State, EscrowRequested)
	}

	// Requested is not funded. Nothing is spendable yet — gogobee hasn't taken
	// the euros, so creating chips here would mint money out of nothing.
	st, err := Chips(player)
	if err != nil {
		t.Fatal(err)
	}
	if st.Chips != 0 {
		t.Fatalf("chips = %d before settlement, want 0", st.Chips)
	}
	if st.Pending != 500 {
		t.Fatalf("pending = %d, want 500", st.Pending)
	}

	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	if _, err := SettleEscrow(e.GUID, true, "", 4500); err != nil {
		t.Fatal(err)
	}

	st, err = Chips(player)
	if err != nil {
		t.Fatal(err)
	}
	if st.Chips != 500 {
		t.Fatalf("chips = %d after funding, want 500", st.Chips)
	}
	if st.Pending != 0 {
		t.Fatalf("pending = %d after funding, want 0", st.Pending)
	}
	if st.EuroBalance != 4500 {
		t.Fatalf("advisory euro balance = %v, want 4500", st.EuroBalance)
	}
}

func TestBuyIn_RejectedCreatesNoChips(t *testing.T) {
	setupTestDB(t)

	e, err := RequestBuyIn(player, 500)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	got, err := SettleEscrow(e.GUID, false, "insufficient_funds", 12)
	if err != nil {
		t.Fatal(err)
	}
	if got.State != EscrowRejected {
		t.Fatalf("state = %q, want %q", got.State, EscrowRejected)
	}
	if c := chipsOf(t, player); c != 0 {
		t.Fatalf("chips = %d after a rejected buy-in, want 0", c)
	}
}

// The push queue retries. A verdict that lands twice must only move chips once.
func TestSettle_IsIdempotent(t *testing.T) {
	setupTestDB(t)

	e, err := RequestBuyIn(player, 500)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	for i := 0; i < 3; i++ {
		if _, err := SettleEscrow(e.GUID, true, "", 4500); err != nil {
			t.Fatalf("settle %d: %v", i, err)
		}
	}
	if c := chipsOf(t, player); c != 500 {
		t.Fatalf("chips = %d after three identical pushes, want 500", c)
	}
}

// A late, contradictory push must not overturn a settled row — otherwise a
// delayed "rejected" could confiscate chips the player already won hands with.
func TestSettle_TerminalStateWins(t *testing.T) {
	setupTestDB(t)

	e, err := RequestBuyIn(player, 500)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	if _, err := SettleEscrow(e.GUID, true, "", 4500); err != nil {
		t.Fatal(err)
	}
	got, err := SettleEscrow(e.GUID, false, "insufficient_funds", 0)
	if err != nil {
		t.Fatal(err)
	}
	if got.State != EscrowFunded {
		t.Fatalf("state = %q, want the original %q", got.State, EscrowFunded)
	}
	if c := chipsOf(t, player); c != 500 {
		t.Fatalf("chips = %d, want 500 — a late rejection took funded chips", c)
	}
}

func TestCashOut_ChipsLeaveImmediately(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 1000)

	e, err := RequestCashOut(player, 400)
	if err != nil {
		t.Fatal(err)
	}
	// The chips are gone the moment the row opens. If they lingered until gogobee
	// confirmed, the player could bet them while the euros were also in flight —
	// the same euro on both sides of the border.
	if c := chipsOf(t, player); c != 600 {
		t.Fatalf("chips = %d immediately after cash-out, want 600", c)
	}

	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	got, err := SettleEscrow(e.GUID, true, "", 4400)
	if err != nil {
		t.Fatal(err)
	}
	if got.State != EscrowSettled {
		t.Fatalf("state = %q, want %q", got.State, EscrowSettled)
	}
	if c := chipsOf(t, player); c != 600 {
		t.Fatalf("chips = %d after settlement, want 600", c)
	}
}

func TestCashOut_FailedCreditGivesTheChipsBack(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 1000)

	e, err := RequestCashOut(player, 400)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	// gogobee couldn't pay. The chips were already destroyed here, so they have to
	// come back — the player's money cannot simply evaporate at the border.
	if _, err := SettleEscrow(e.GUID, false, "ledger_error", 0); err != nil {
		t.Fatal(err)
	}
	if c := chipsOf(t, player); c != 1000 {
		t.Fatalf("chips = %d after a failed cash-out, want the full 1000 back", c)
	}
}

func TestCashOut_CannotExceedTheStack(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 100)

	if _, err := RequestCashOut(player, 500); !errors.Is(err, ErrInsufficientChips) {
		t.Fatalf("err = %v, want ErrInsufficientChips", err)
	}
	if c := chipsOf(t, player); c != 100 {
		t.Fatalf("chips = %d after a refused cash-out, want 100", c)
	}
}

func TestBuyIn_TableCapCountsChipsAndInFlightBuyIns(t *testing.T) {
	setupTestDB(t)

	if _, err := RequestBuyIn(player, MaxChipsOnTable+1); !errors.Is(err, ErrOverTableCap) {
		t.Fatalf("err = %v, want ErrOverTableCap", err)
	}

	fund(t, player, MaxChipsOnTable-1000)

	// A second buy-in that fits is fine.
	if _, err := RequestBuyIn(player, 1000); err != nil {
		t.Fatal(err)
	}
	// A third, while the second is still in flight, must not clear the cap by
	// racing — pending buy-ins count against it.
	if _, err := RequestBuyIn(player, 1); !errors.Is(err, ErrOverTableCap) {
		t.Fatalf("err = %v, want ErrOverTableCap — in-flight buy-ins must count", err)
	}
}

func TestRequest_RejectsNonPositiveAmounts(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 100)

	for _, amount := range []int64{0, -50} {
		if _, err := RequestBuyIn(player, amount); !errors.Is(err, ErrBadAmount) {
			t.Errorf("buy-in %d: err = %v, want ErrBadAmount", amount, err)
		}
		if _, err := RequestCashOut(player, amount); !errors.Is(err, ErrBadAmount) {
			t.Errorf("cash-out %d: err = %v, want ErrBadAmount", amount, err)
		}
	}
	if c := chipsOf(t, player); c != 100 {
		t.Fatalf("chips = %d, want 100", c)
	}
}

func TestStakeAndAward(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 500)

	if err := Stake(player, 100); err != nil {
		t.Fatal(err)
	}
	if c := chipsOf(t, player); c != 400 {
		t.Fatalf("chips = %d after staking 100, want 400", c)
	}

	// A win pays the stake back plus the profit, net of rake.
	if err := Award(player, 195); err != nil {
		t.Fatal(err)
	}
	if c := chipsOf(t, player); c != 595 {
		t.Fatalf("chips = %d after a 195 payout, want 595", c)
	}

	// You cannot bet chips you don't have.
	if err := Stake(player, 10_000); !errors.Is(err, ErrInsufficientChips) {
		t.Fatalf("err = %v, want ErrInsufficientChips", err)
	}
	if c := chipsOf(t, player); c != 595 {
		t.Fatalf("chips = %d after a refused stake, want 595", c)
	}
}

func TestStake_UnknownPlayerHasNothingToBet(t *testing.T) {
	setupTestDB(t)
	if err := Stake("@stranger:parodia.dev", 10); !errors.Is(err, ErrInsufficientChips) {
		t.Fatalf("err = %v, want ErrInsufficientChips", err)
	}
}

func TestPendingEscrow_OffersUnclaimedAndAbandonedRows(t *testing.T) {
	setupTestDB(t)

	fresh, err := RequestBuyIn(player, 100)
	if err != nil {
		t.Fatal(err)
	}
	abandoned, err := RequestBuyIn("@other:parodia.dev", 200)
	if err != nil {
		t.Fatal(err)
	}

	pending, err := PendingEscrow(10)
	if err != nil {
		t.Fatal(err)
	}
	if len(pending) != 2 {
		t.Fatalf("%d pending rows, want 2", len(pending))
	}

	// Claim both: neither should be offered again while gogobee is working on them.
	for _, e := range []Escrow{fresh, abandoned} {
		if _, err := ClaimEscrow(e.GUID); err != nil {
			t.Fatal(err)
		}
	}
	pending, err = PendingEscrow(10)
	if err != nil {
		t.Fatal(err)
	}
	if len(pending) != 0 {
		t.Fatalf("%d rows offered while claimed, want 0", len(pending))
	}

	// Now pretend gogobee died holding one of them. A claim that never came back
	// must be re-offered, or the player's money sits in limbo forever.
	stale := nowUnix() - int64(EscrowStaleAfter.Seconds()) - 1
	if _, err := Get().Exec(`UPDATE game_escrow SET claimed_at = ? WHERE guid = ?`, stale, abandoned.GUID); err != nil {
		t.Fatal(err)
	}
	pending, err = PendingEscrow(10)
	if err != nil {
		t.Fatal(err)
	}
	if len(pending) != 1 || pending[0].GUID != abandoned.GUID {
		t.Fatalf("stale claim was not re-offered: got %d rows", len(pending))
	}
}

func TestClaim_CannotReopenAFinishedRow(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 300)

	e, err := RequestCashOut(player, 300)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	if _, err := SettleEscrow(e.GUID, true, "", 5000); err != nil {
		t.Fatal(err)
	}

	// Re-claiming a settled cash-out must not walk it back to claimed, or gogobee
	// would pay the same euros out twice.
	got, err := ClaimEscrow(e.GUID)
	if err != nil {
		t.Fatal(err)
	}
	if got.State != EscrowSettled {
		t.Fatalf("state = %q after re-claiming a settled row, want %q", got.State, EscrowSettled)
	}
}

func TestEscrow_UnknownGUID(t *testing.T) {
	setupTestDB(t)
	if _, err := EscrowByGUID("nope"); !errors.Is(err, ErrNoSuchEscrow) {
		t.Fatalf("err = %v, want ErrNoSuchEscrow", err)
	}
	if _, err := SettleEscrow("nope", true, "", 0); !errors.Is(err, ErrNoSuchEscrow) {
		t.Fatalf("err = %v, want ErrNoSuchEscrow", err)
	}
}

func TestReaper_CashesOutTheWalkedAway(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 700)

	// Still playing: the reaper must leave them alone.
	Touch(player)
	n, err := ReapIdleSessions(SessionIdleAfter)
	if err != nil {
		t.Fatal(err)
	}
	if n != 0 || chipsOf(t, player) != 700 {
		t.Fatalf("reaped %d active players (chips now %d)", n, chipsOf(t, player))
	}

	// Now they've been gone an hour.
	old := nowUnix() - int64((2 * time.Hour).Seconds())
	if _, err := Get().Exec(`UPDATE game_chips SET last_played = ? WHERE matrix_user = ?`, old, player); err != nil {
		t.Fatal(err)
	}
	n, err = ReapIdleSessions(SessionIdleAfter)
	if err != nil {
		t.Fatal(err)
	}
	if n != 1 {
		t.Fatalf("reaped %d, want 1", n)
	}
	if c := chipsOf(t, player); c != 0 {
		t.Fatalf("chips = %d after the reaper ran, want 0 — they should be euros now", c)
	}

	// And it must be a real cash-out, waiting for gogobee to pay it out.
	pending, err := PendingEscrow(10)
	if err != nil {
		t.Fatal(err)
	}
	if len(pending) != 1 || pending[0].Kind != KindCashOut || pending[0].Amount != 700 {
		t.Fatalf("reaper did not queue a 700 cash-out: %+v", pending)
	}

	// Running again finds nothing left to reap.
	if n, err := ReapIdleSessions(SessionIdleAfter); err != nil || n != 0 {
		t.Fatalf("second sweep reaped %d (err=%v), want 0", n, err)
	}
}

func TestRecordHand_AndHouseTake(t *testing.T) {
	setupTestDB(t)

	hands := []Hand{
		{MatrixUser: player, Game: "blackjack", Bet: 100, Payout: 195, Rake: 5, Outcome: "win", Seed1: 42, Seed2: 7},
		{MatrixUser: player, Game: "blackjack", Bet: 100, Payout: 0, Rake: 0, Outcome: "bust", Seed1: 43, Seed2: 8},
		{MatrixUser: player, Game: "blackjack", Bet: 200, Payout: 486, Rake: 14, Outcome: "blackjack", Seed1: 44, Seed2: 9},
	}
	for _, h := range hands {
		if err := RecordHand(h); err != nil {
			t.Fatal(err)
		}
	}

	take, err := HouseTake(0)
	if err != nil {
		t.Fatal(err)
	}
	if take != 19 {
		t.Fatalf("house take = %d, want 19", take)
	}

	// The seeds have to survive the round trip, or a disputed hand can't be re-dealt.
	var s1, s2 int64
	if err := Get().QueryRow(
		`SELECT seed1, seed2 FROM game_hands WHERE outcome = 'blackjack'`,
	).Scan(&s1, &s2); err != nil {
		t.Fatal(err)
	}
	if s1 != 44 || s2 != 9 {
		t.Fatalf("seeds came back as (%d, %d), want (44, 9)", s1, s2)
	}
}

// The invariant, end to end: every euro that entered the casino is either a chip
// on the table or a euro on its way home. None are minted, none evaporate.
func TestBorder_ChipsAreConserved(t *testing.T) {
	setupTestDB(t)
	fund(t, player, 1000)

	// Play a losing hand and a winning one.
	if err := Stake(player, 100); err != nil {
		t.Fatal(err)
	} // 900
	if err := Stake(player, 100); err != nil {
		t.Fatal(err)
	} // 800
	if err := Award(player, 195); err != nil {
		t.Fatal(err)
	} // 995 — one loss, one win less rake

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

	e, err := RequestCashOut(player, 995)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := ClaimEscrow(e.GUID); err != nil {
		t.Fatal(err)
	}
	if _, err := SettleEscrow(e.GUID, true, "", 4995); err != nil {
		t.Fatal(err)
	}

	if c := chipsOf(t, player); c != 0 {
		t.Fatalf("chips = %d after cashing out everything, want 0", c)
	}
	// 1000 in, 995 out, 5 lost to the table and the rake. Nothing left stranded.
	st, err := Chips(player)
	if err != nil {
		t.Fatal(err)
	}
	if st.Pending != 0 {
		t.Fatalf("pending = %d, want 0", st.Pending)
	}
}
