package blackjack

import (
	"encoding/json"
	"testing"

	"pete/internal/games/cards"
)

// hand builds a hand from "A♠"-ish shorthand: rank letters/numbers only.
func hand(ranks ...cards.Rank) []cards.Card {
	h := make([]cards.Card, len(ranks))
	for i, r := range ranks {
		h[i] = cards.Card{Rank: r, Suit: cards.Spades}
	}
	return h
}

func TestHandValue(t *testing.T) {
	tests := []struct {
		name string
		hand []cards.Card
		want int
		soft bool
	}{
		{"two aces are 12, not 22", hand(cards.Ace, cards.Ace), 12, true},
		{"ace plus king is a soft 21", hand(cards.Ace, cards.King), 21, true},
		{"faces are all ten", hand(cards.Jack, cards.Queen), 20, false},
		{"ace demotes to save the hand", hand(cards.Ace, 9, 5), 15, false},
		{"three aces and an eight", hand(cards.Ace, cards.Ace, cards.Ace, 8), 21, true},
		{"soft 17 is an ace and a six", hand(cards.Ace, 6), 17, true},
		{"hard 17 has no ace", hand(cards.King, 7), 17, false},
		{"a bust stays busted", hand(cards.King, cards.Queen, 5), 25, false},
		{"empty hand", nil, 0, false},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got, soft := HandValue(tc.hand)
			if got != tc.want || soft != tc.soft {
				t.Fatalf("HandValue = (%d, soft=%v), want (%d, soft=%v)", got, soft, tc.want, tc.soft)
			}
		})
	}
}

func TestIsBlackjack_OnlyOnTwoCards(t *testing.T) {
	if !IsBlackjack(hand(cards.Ace, cards.King)) {
		t.Fatal("A+K is a natural")
	}
	// 21 built from three cards is not a natural and must not be paid 3:2.
	if IsBlackjack(hand(7, 7, 7)) {
		t.Fatal("7+7+7 is 21 but not a blackjack")
	}
	if IsBlackjack(hand(cards.Ace)) {
		t.Fatal("one card is not a blackjack")
	}
}

// settleWith forces a finished hand and reads back the money, bypassing the
// deal so the payout math can be checked case by case.
func settleWith(t *testing.T, r Rules, bet int64, player, dealer []cards.Card) State {
	t.Helper()
	s := State{Rules: r, Bet: bet, Hands: []Hand{{Cards: player, Bet: bet}}, Dealer: dealer}
	evs := []Event{}
	s.settle(&evs)
	if s.Phase != PhaseDone {
		t.Fatal("settle left the hand unfinished")
	}
	return s
}

func TestSettle_PayoutsAndRake(t *testing.T) {
	r := DefaultRules() // 3:2, 5% rake

	tests := []struct {
		name        string
		player      []cards.Card
		dealer      []cards.Card
		wantOutcome Outcome
		wantPayout  int64 // chips returned to the stack
		wantRake    int64
	}{
		{
			// 100 stake, 100 profit, 5 raked → 195 back, net +95.
			name:   "a plain win is raked on the profit only",
			player: hand(cards.King, 9), dealer: hand(cards.King, 8),
			wantOutcome: OutcomeWin, wantPayout: 195, wantRake: 5,
		},
		{
			// 3:2 on 100 is 150 profit, 7 raked (floor of 7.5) → 243 back.
			name:   "a natural pays 3:2 less rake",
			player: hand(cards.Ace, cards.King), dealer: hand(cards.King, 8),
			wantOutcome: OutcomeBlackjack, wantPayout: 243, wantRake: 7,
		},
		{
			name:   "a push returns the stake untouched — the house takes nothing",
			player: hand(cards.King, 9), dealer: hand(cards.Queen, 9),
			wantOutcome: OutcomePush, wantPayout: 100, wantRake: 0,
		},
		{
			name:   "two naturals push",
			player: hand(cards.Ace, cards.King), dealer: hand(cards.Ace, cards.Queen),
			wantOutcome: OutcomePush, wantPayout: 100, wantRake: 0,
		},
		{
			name:   "a loss pays nothing and is not charged a rake",
			player: hand(cards.King, 8), dealer: hand(cards.King, 9),
			wantOutcome: OutcomeLose, wantPayout: 0, wantRake: 0,
		},
		{
			name:   "a bust pays nothing even if the dealer would have busted too",
			player: hand(cards.King, 8, 9), dealer: hand(cards.King, 6, 9),
			wantOutcome: OutcomeBust, wantPayout: 0, wantRake: 0,
		},
		{
			name:   "dealer blackjack beats the player's twenty",
			player: hand(cards.King, cards.Queen), dealer: hand(cards.Ace, cards.Jack),
			wantOutcome: OutcomeLose, wantPayout: 0, wantRake: 0,
		},
		{
			name:   "dealer bust pays even money less rake",
			player: hand(cards.King, 5), dealer: hand(cards.King, 6, 9),
			wantOutcome: OutcomeDealerBust, wantPayout: 195, wantRake: 5,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			s := settleWith(t, r, 100, tc.player, tc.dealer)
			if s.Outcome != tc.wantOutcome {
				t.Errorf("outcome = %q, want %q", s.Outcome, tc.wantOutcome)
			}
			if s.Payout != tc.wantPayout {
				t.Errorf("payout = %d, want %d", s.Payout, tc.wantPayout)
			}
			if s.Rake != tc.wantRake {
				t.Errorf("rake = %d, want %d", s.Rake, tc.wantRake)
			}
			// The invariant the ledger depends on: every chip the player staked
			// either comes back, goes to the house as rake, or is lost to the table.
			if s.Payout < 0 || s.Rake < 0 {
				t.Errorf("negative chips: payout=%d rake=%d", s.Payout, s.Rake)
			}
		})
	}
}

func TestSettle_RakeNeverTouchesTheStake(t *testing.T) {
	// A 100% rake is absurd, but it must still never claw back a player's own
	// stake: the worst a rake can do is take all the winnings.
	r := Rules{Decks: 6, BlackjackPays: 1.5, RakePct: 1.0}
	s := settleWith(t, r, 100, hand(cards.King, 9), hand(cards.King, 8))
	if s.Payout != 100 {
		t.Fatalf("payout = %d, want the stake back (100)", s.Payout)
	}
	if s.Net() != 0 {
		t.Fatalf("net = %d, want 0", s.Net())
	}
}

func TestNew_DealsFourCardsAndAskThePlayer(t *testing.T) {
	rng := cards.NewRNG(1, 2)
	s, evs, err := New(50, DefaultRules(), rng)
	if err != nil {
		t.Fatal(err)
	}
	if len(s.Hands[0].Cards) != 2 || len(s.Dealer) != 2 {
		t.Fatalf("dealt %d/%d cards, want 2/2", len(s.Hands[0].Cards), len(s.Dealer))
	}
	if len(s.Deck) != 6*52-4 {
		t.Fatalf("shoe has %d cards left, want %d", len(s.Deck), 6*52-4)
	}
	if len(evs) == 0 || evs[0].Kind != "deal" {
		t.Fatal("no deal event")
	}
	// Unless somebody was dealt a natural, it's the player's move.
	if !IsBlackjack(s.Hands[0].Cards) && !IsBlackjack(s.Dealer) && s.Phase != PhasePlayer {
		t.Fatalf("phase = %q, want %q", s.Phase, PhasePlayer)
	}
}

func TestNew_RejectsNonPositiveBet(t *testing.T) {
	for _, bet := range []int64{0, -100} {
		if _, _, err := New(bet, DefaultRules(), cards.NewRNG(1, 2)); err == nil {
			t.Fatalf("bet %d was accepted", bet)
		}
	}
}

func TestNew_NaturalSettlesImmediately(t *testing.T) {
	// Search seeds for a deal that gives the player a natural, then assert the
	// hand is already over — a player holding blackjack is never asked to hit.
	for seed := uint64(1); seed < 200; seed++ {
		s, _, err := New(100, DefaultRules(), cards.NewRNG(seed, seed))
		if err != nil {
			t.Fatal(err)
		}
		if IsBlackjack(s.Hands[0].Cards) {
			if s.Phase != PhaseDone {
				t.Fatalf("seed %d: player has a natural but phase = %q", seed, s.Phase)
			}
			if _, _, err := ApplyMove(s, Hit); err != ErrHandOver {
				t.Fatalf("seed %d: hitting a settled natural gave %v, want ErrHandOver", seed, err)
			}
			return
		}
	}
	t.Skip("no natural dealt in 200 seeds")
}

func TestApplyMove_HitUntilBustSettles(t *testing.T) {
	s, _, err := New(100, DefaultRules(), cards.NewRNG(7, 7))
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase == PhaseDone {
		t.Skip("dealt a natural; not the hand under test")
	}
	for i := 0; i < 12 && s.Phase == PhasePlayer; i++ {
		s, _, err = ApplyMove(s, Hit)
		if err != nil {
			t.Fatal(err)
		}
	}
	if s.Phase != PhaseDone {
		t.Fatal("hitting a dozen times never ended the hand")
	}
	if v, _ := HandValue(s.Hands[0].Cards); v <= 21 {
		t.Fatalf("player stopped at %d without busting — the loop should have gone over", v)
	}
	if s.Outcome != OutcomeBust || s.Payout != 0 {
		t.Fatalf("outcome=%q payout=%d, want bust/0", s.Outcome, s.Payout)
	}
	// A busted player must not have made the dealer draw.
	if len(s.Dealer) != 2 {
		t.Fatalf("dealer drew %d cards against a busted player", len(s.Dealer)-2)
	}
}

func TestApplyMove_StandRunsTheDealerOut(t *testing.T) {
	s, _, err := New(100, DefaultRules(), cards.NewRNG(3, 9))
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase == PhaseDone {
		t.Skip("dealt a natural")
	}
	s, evs, err := ApplyMove(s, Stand)
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase != PhaseDone {
		t.Fatalf("phase = %q after stand, want done", s.Phase)
	}
	v, soft := HandValue(s.Dealer)
	if v < 17 {
		t.Fatalf("dealer stood on %d, must draw below 17", v)
	}
	if v == 17 && soft {
		t.Fatal("dealer stood on soft 17; the house rule says hit")
	}
	var reveal bool
	for _, e := range evs {
		if e.Kind == "reveal" {
			reveal = true
		}
	}
	if !reveal {
		t.Fatal("dealer played without a reveal event")
	}
}

func TestApplyMove_DoubleTakesOneCardThenStands(t *testing.T) {
	s, _, err := New(100, DefaultRules(), cards.NewRNG(11, 4))
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase == PhaseDone {
		t.Skip("dealt a natural")
	}
	if !s.CanDouble() {
		t.Fatal("double should be legal on the opening two cards")
	}
	s, _, err = ApplyMove(s, Double)
	if err != nil {
		t.Fatal(err)
	}
	if !s.Hands[0].Doubled || s.Bet != 200 {
		t.Fatalf("bet = %d doubled = %v, want 200/true", s.Bet, s.Hands[0].Doubled)
	}
	if len(s.Hands[0].Cards) != 3 {
		t.Fatalf("player has %d cards after a double, want exactly 3", len(s.Hands[0].Cards))
	}
	if s.Phase != PhaseDone {
		t.Fatal("a double must end the player's turn")
	}
}

func TestApplyMove_DoubleIsIllegalAfterHitting(t *testing.T) {
	s, _, err := New(100, DefaultRules(), cards.NewRNG(5, 5))
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase == PhaseDone {
		t.Skip("dealt a natural")
	}
	s, _, err = ApplyMove(s, Hit)
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase != PhasePlayer {
		t.Skip("busted on the hit; not the hand under test")
	}
	before := s.Bet
	after, _, err := ApplyMove(s, Double)
	if err != ErrCantDouble {
		t.Fatalf("double after a hit gave %v, want ErrCantDouble", err)
	}
	if after.Bet != before {
		t.Fatalf("a rejected double still moved the bet: %d -> %d", before, after.Bet)
	}
	if s.CanDouble() {
		t.Fatal("CanDouble says yes on a three-card hand")
	}
}

func TestApplyMove_RejectsGarbage(t *testing.T) {
	s, _, err := New(100, DefaultRules(), cards.NewRNG(2, 8))
	if err != nil {
		t.Fatal(err)
	}
	if _, _, err := ApplyMove(s, Move("surrender")); err != ErrUnknownMove {
		t.Fatalf("got %v, want ErrUnknownMove", err)
	}
}

// The engine's state has to survive a redeploy: no timers, no pointers, no
// unexported fields that JSON would quietly drop.
func TestState_RoundTripsThroughJSON(t *testing.T) {
	s, _, err := New(100, DefaultRules(), cards.NewRNG(13, 21))
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase == PhaseDone {
		t.Skip("dealt a natural")
	}
	blob, err := json.Marshal(s)
	if err != nil {
		t.Fatal(err)
	}
	var back State
	if err := json.Unmarshal(blob, &back); err != nil {
		t.Fatal(err)
	}

	// Play both forward identically; a state that survives the trip settles the same.
	live, _, err := ApplyMove(s, Stand)
	if err != nil {
		t.Fatal(err)
	}
	revived, _, err := ApplyMove(back, Stand)
	if err != nil {
		t.Fatal(err)
	}
	if live.Outcome != revived.Outcome || live.Payout != revived.Payout {
		t.Fatalf("revived hand settled differently: %q/%d vs %q/%d",
			revived.Outcome, revived.Payout, live.Outcome, live.Payout)
	}
}

// Same seed, same shoe — this is what lets a disputed hand be re-dealt.
func TestNew_IsReproducibleFromItsSeed(t *testing.T) {
	a, _, err := New(100, DefaultRules(), cards.NewRNG(42, 42))
	if err != nil {
		t.Fatal(err)
	}
	b, _, err := New(100, DefaultRules(), cards.NewRNG(42, 42))
	if err != nil {
		t.Fatal(err)
	}
	if cards.Hand(a.Hands[0].Cards) != cards.Hand(b.Hands[0].Cards) || cards.Hand(a.Dealer) != cards.Hand(b.Dealer) {
		t.Fatalf("same seed dealt different hands: %s/%s vs %s/%s",
			cards.Hand(a.Hands[0].Cards), cards.Hand(a.Dealer), cards.Hand(b.Hands[0].Cards), cards.Hand(b.Dealer))
	}
}

// A State handed to ApplyMove twice must produce two independent hands. If the
// engine let derived states share a backing array, the second deal would scribble
// over the first one's cards — and a player could watch a card change under them.
func TestApplyMove_DerivedStatesDoNotShareCards(t *testing.T) {
	s, _, err := New(100, DefaultRules(), cards.NewRNG(23, 5))
	if err != nil {
		t.Fatal(err)
	}
	if s.Phase == PhaseDone {
		t.Skip("dealt a natural")
	}
	before := cards.Hand(s.Hands[0].Cards)

	a, _, err := ApplyMove(s, Hit)
	if err != nil {
		t.Fatal(err)
	}
	aHand := cards.Hand(a.Hands[0].Cards)

	if _, _, err := ApplyMove(s, Hit); err != nil { // same start, applied again
		t.Fatal(err)
	}
	if got := cards.Hand(a.Hands[0].Cards); got != aHand {
		t.Fatalf("the first hand changed under us: %q became %q", aHand, got)
	}
	if got := cards.Hand(s.Hands[0].Cards); got != before {
		t.Fatalf("ApplyMove mutated the state it was given: %q became %q", before, got)
	}
}
