package trivia

import (
	"math/rand/v2"
	"testing"
	"time"
)

func rng() *rand.Rand { return rand.New(rand.NewPCG(1, 2)) }

var epoch = time.Date(2026, 7, 14, 12, 0, 0, 0, time.UTC)

// bank builds n questions whose right answer is always "right", so a test can
// find it after the shuffle without caring where it landed.
func bank(n int) []Question {
	qs := make([]Question, n)
	for i := range qs {
		qs[i] = Question{
			Category: "General",
			Text:     "question?",
			Answers:  []string{"right", "wrong1", "wrong2", "wrong3"},
			Correct:  0,
		}
	}
	return qs
}

func tier(slug string) Tier {
	t, err := TierBySlug(slug)
	if err != nil {
		panic(err)
	}
	return t
}

func newGame(t *testing.T, bet int64, slug string) State {
	t.Helper()
	s, evs, err := New(bet, tier(slug), 0.05, bank(Rungs), epoch, rng())
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	if len(evs) != 1 || evs[0].Kind != "ask" {
		t.Fatalf("New should open with one ask, got %+v", evs)
	}
	if s.Multiple != 1 {
		t.Fatalf("a fresh ladder is worth the stake, got multiple %v", s.Multiple)
	}
	return s
}

// answerRight plays the live question correctly, after `took` on the clock.
func answerRight(t *testing.T, s State, took time.Duration) (State, []Event) {
	t.Helper()
	q := s.Live()
	next, evs, err := ApplyMove(s, Move{Choice: q.Correct}, s.AskedAt.Add(took))
	if err != nil {
		t.Fatalf("right answer refused: %v", err)
	}
	return next, evs
}

func TestNewShufflesButKeepsTheAnswer(t *testing.T) {
	s := newGame(t, 100, "medium")
	moved := 0
	for _, q := range s.Ladder {
		if q.Answers[q.Correct] != "right" {
			t.Fatalf("Correct points at %q, not the right answer", q.Answers[q.Correct])
		}
		if q.Correct != 0 {
			moved++
		}
	}
	// All twelve landing on index 0 would mean the shuffle isn't running, and the
	// game would be "always click the first one".
	if moved == 0 {
		t.Fatal("the right answer is first in every question — the shuffle did nothing")
	}
}

func TestShortBankIsRefused(t *testing.T) {
	if _, _, err := New(100, tier("easy"), 0.05, bank(Rungs-1), epoch, rng()); err != ErrShortLadder {
		t.Fatalf("a ladder with a missing rung should be refused, got %v", err)
	}
}

// The one that matters most: the number the felt quotes is the number the
// player is actually paid, at every rung, exactly as in hangman.
func TestTheQuoteIsThePayout(t *testing.T) {
	s := newGame(t, 200, "hard")
	for rung := 1; rung < Rungs; rung++ {
		s, _ = answerRight(t, s, 3*time.Second)

		quoted := s.Pays() // what the "take the money" button says it's worth

		banked, _, err := ApplyMove(s, Move{Walk: true}, s.AskedAt)
		if err != nil {
			t.Fatalf("rung %d: walk refused: %v", rung, err)
		}
		if banked.Payout != quoted {
			t.Fatalf("rung %d: the felt quoted %d and the house paid %d", rung, quoted, banked.Payout)
		}
		if banked.Phase != PhaseDone || banked.Outcome != OutcomeWalked {
			t.Fatalf("rung %d: walking should end the game, got %s/%s", rung, banked.Phase, banked.Outcome)
		}
	}
}

// Walking before answering anything would be a free look at the first question:
// stake, peek, walk, restake until the question is one you happen to know.
func TestYouCannotWalkOffTheFirstRung(t *testing.T) {
	s := newGame(t, 100, "easy")
	if _, _, err := ApplyMove(s, Move{Walk: true}, epoch); err != ErrNothingBanked {
		t.Fatalf("walking on rung 0 should be refused, got %v", err)
	}

	// One right answer, and now you may.
	s, _ = answerRight(t, s, time.Second)
	if _, _, err := ApplyMove(s, Move{Walk: true}, s.AskedAt); err != nil {
		t.Fatalf("walking after a right answer should be allowed, got %v", err)
	}
}

func TestAWrongAnswerLosesTheLot(t *testing.T) {
	s := newGame(t, 300, "medium")
	// Build a decent ladder first, so there is something real to lose.
	for i := 0; i < 4; i++ {
		s, _ = answerRight(t, s, time.Second)
	}
	if s.Pays() <= 300 {
		t.Fatalf("four right answers should be worth more than the stake, got %d", s.Pays())
	}

	q := s.Live()
	wrong := (q.Correct + 1) % len(q.Answers)
	out, evs, err := ApplyMove(s, Move{Choice: wrong}, s.AskedAt.Add(time.Second))
	if err != nil {
		t.Fatalf("a wrong answer is a legal move: %v", err)
	}
	if out.Outcome != OutcomeWrong || out.Payout != 0 {
		t.Fatalf("a wrong answer should pay nothing, got %s/%d", out.Outcome, out.Payout)
	}
	if out.Rake != 0 {
		t.Fatalf("a loss must never be charged a rake, got %d", out.Rake)
	}
	if out.Net() != -300 {
		t.Fatalf("a wrong answer costs the stake and nothing more, got %d", out.Net())
	}
	// The player is told which one it was.
	if evs[0].Kind != "wrong" || evs[0].Correct != q.Correct {
		t.Fatalf("a wrong answer should reveal the right one, got %+v", evs[0])
	}
}

// The clock is the whole anti-google mechanism: running out of it has to cost
// as much as being wrong, or leaving the tab open and looking it up wins.
func TestTheClockTakesEverything(t *testing.T) {
	s := newGame(t, 250, "hard")
	for i := 0; i < 3; i++ {
		s, _ = answerRight(t, s, time.Second)
	}
	banked := s.Pays()

	q := s.Live()
	late := s.AskedAt.Add(s.Tier.Clock() + time.Millisecond)
	out, evs, err := ApplyMove(s, Move{Choice: q.Correct}, late) // the *right* answer, too late
	if err != nil {
		t.Fatalf("a late answer is a legal move: %v", err)
	}
	if out.Outcome != OutcomeTimeout {
		t.Fatalf("answering past the limit should time out, got %s", out.Outcome)
	}
	if out.Payout != 0 {
		t.Fatalf("a timeout pays nothing — it was worth %d a moment ago, and paid %d", banked, out.Payout)
	}
	if evs[0].Kind != "timeout" {
		t.Fatalf("expected a timeout event, got %+v", evs[0])
	}

	// And answering on the final tick still counts.
	onTime := s.AskedAt.Add(s.Tier.Clock())
	if out, _, err = ApplyMove(s, Move{Choice: q.Correct}, onTime); err != nil {
		t.Fatalf("an answer on the buzzer is legal: %v", err)
	}
	if out.Rung != s.Rung+1 {
		t.Fatal("an answer on the final tick should still count")
	}
}

// Speed is the only thing separating a slow right answer from a fast one.
func TestFasterPaysMore(t *testing.T) {
	base := newGame(t, 1000, "hard")

	quick, _ := answerRight(t, base, time.Second)
	slow, _ := answerRight(t, base, 14*time.Second)

	if quick.Multiple <= slow.Multiple {
		t.Fatalf("a quick answer should be worth more: quick %v, slow %v", quick.Multiple, slow.Multiple)
	}
	if quick.Pays() <= slow.Pays() {
		t.Fatalf("a quick answer should pay more: quick %d, slow %d", quick.Pays(), slow.Pays())
	}

	// The ends of the scale are the tier's own numbers, and nothing is outside them.
	instant, _ := answerRight(t, base, 0)
	buzzer, _ := answerRight(t, base, base.Tier.Clock())
	if instant.Multiple != base.Tier.Fast {
		t.Fatalf("an instant answer is worth Fast (%v), got %v", base.Tier.Fast, instant.Multiple)
	}
	if buzzer.Multiple != base.Tier.Buzzer {
		t.Fatalf("an answer on the buzzer is worth Buzzer (%v), got %v", base.Tier.Buzzer, buzzer.Multiple)
	}
	if quick.Multiple > base.Tier.Fast || slow.Multiple < base.Tier.Buzzer {
		t.Fatal("a step escaped the tier's range")
	}
}

// Clearing the ladder ends the run and banks it, rather than leaving the player
// on a rung that doesn't exist.
func TestClearingTheLadderBanks(t *testing.T) {
	s := newGame(t, 100, "easy")
	for i := 0; i < Rungs; i++ {
		if s.Phase != PhasePlaying {
			t.Fatalf("the game ended early, on rung %d", i)
		}
		s, _ = answerRight(t, s, time.Second)
	}
	if s.Outcome != OutcomeCleared {
		t.Fatalf("twelve right answers should clear the ladder, got %s", s.Outcome)
	}
	if s.Rung != Rungs {
		t.Fatalf("expected to be on rung %d, got %d", Rungs, s.Rung)
	}
	if s.Payout != s.Pays() || s.Payout <= s.Bet {
		t.Fatalf("clearing should bank a win, got payout %d on a %d stake", s.Payout, s.Bet)
	}
	if _, _, err := ApplyMove(s, Move{Choice: 0}, s.AskedAt); err != ErrGameOver {
		t.Fatalf("a cleared ladder takes no more moves, got %v", err)
	}
}

// The rake comes out of winnings, never out of the stake.
func TestRakeOnlyBitesWinnings(t *testing.T) {
	s := newGame(t, 1000, "medium")
	s, _ = answerRight(t, s, 0) // instant: multiple is exactly Fast, so the sum is checkable by hand

	banked, _, err := ApplyMove(s, Move{Walk: true}, s.AskedAt)
	if err != nil {
		t.Fatalf("walk: %v", err)
	}

	total := int64(float64(1000) * s.Tier.Fast) // 1550
	profit := total - 1000                      // 550
	rake := int64(float64(profit) * 0.05)       // 27
	want := 1000 + profit - rake                // 1523

	if banked.Payout != want {
		t.Fatalf("payout should be stake + winnings - 5%% of winnings = %d, got %d", want, banked.Payout)
	}
	if banked.Rake != rake {
		t.Fatalf("rake should be %d, got %d", rake, banked.Rake)
	}
	if banked.Payout < banked.Bet {
		t.Fatal("a win handed back less than the stake")
	}
}

// A move must not scribble on the state it came from — a game has to replay.
func TestApplyMoveDoesNotMutateItsInput(t *testing.T) {
	s := newGame(t, 100, "easy")
	before := s.Live()

	next, _, err := ApplyMove(s, Move{Choice: before.Correct}, s.AskedAt.Add(time.Second))
	if err != nil {
		t.Fatalf("move: %v", err)
	}
	if s.Rung != 0 || s.Multiple != 1 || s.Phase != PhasePlaying {
		t.Fatalf("the original state moved underneath us: rung %d multiple %v", s.Rung, s.Multiple)
	}
	if next.Rung != 1 {
		t.Fatalf("the derived state should have climbed a rung, got %d", next.Rung)
	}
	// The same move replays to the same place.
	again, _, err := ApplyMove(s, Move{Choice: before.Correct}, s.AskedAt.Add(time.Second))
	if err != nil {
		t.Fatalf("replay: %v", err)
	}
	if again.Multiple != next.Multiple || again.Rung != next.Rung {
		t.Fatal("the same move from the same state landed somewhere else")
	}
}

func TestLeftCountsDown(t *testing.T) {
	s := newGame(t, 100, "hard") // 15s
	if got := s.Left(epoch); got != 15*time.Second {
		t.Fatalf("a fresh question has the whole clock, got %v", got)
	}
	if got := s.Left(epoch.Add(10 * time.Second)); got != 5*time.Second {
		t.Fatalf("expected 5s left, got %v", got)
	}
	// It floors at nought rather than going negative, so a browser can render it.
	if got := s.Left(epoch.Add(time.Hour)); got != 0 {
		t.Fatalf("the clock should stop at zero, got %v", got)
	}
}

func TestGarbageMovesAreRefused(t *testing.T) {
	s := newGame(t, 100, "easy")
	for _, choice := range []int{-1, 4, 99} {
		if _, _, err := ApplyMove(s, Move{Choice: choice}, s.AskedAt); err != ErrUnknownMove {
			t.Fatalf("choice %d should be refused, got %v", choice, err)
		}
	}
	if s.Phase != PhasePlaying {
		t.Fatal("a refused move should leave the game alone")
	}
}

// The clock has to beat the walk button, or it is not a deadline.
//
// If a dead clock could still be walked away from, the ladder would carry no
// risk at all: sit on every question for as long as you like, answer the ones
// you can look up, and walk off the ones you can't. The timeout has to be the
// first thing that happens to a move.
func TestWalkingOffADeadClockIsATimeout(t *testing.T) {
	s := newGame(t, 500, "hard")
	s, _ = answerRight(t, s, time.Second) // one rung banked, so a walk is otherwise legal

	late := s.AskedAt.Add(s.Tier.Clock() + time.Second)
	out, evs, err := ApplyMove(s, Move{Walk: true}, late)
	if err != nil {
		t.Fatalf("walking after the clock died should resolve, not error: %v", err)
	}
	if out.Outcome != OutcomeTimeout {
		t.Fatalf("a walk after the clock ran out is a timeout, got %q", out.Outcome)
	}
	if out.Payout != 0 {
		t.Fatalf("a timeout pays nothing, got %d", out.Payout)
	}
	if len(evs) == 0 || evs[0].Kind != "timeout" {
		t.Fatalf("expected the timeout event first, got %+v", evs)
	}

	// And the same walk, one tick inside the limit, still banks.
	intime := s.AskedAt.Add(s.Tier.Clock() - time.Millisecond)
	banked, _, err := ApplyMove(s, Move{Walk: true}, intime)
	if err != nil {
		t.Fatalf("walk with the clock still running: %v", err)
	}
	if banked.Outcome != OutcomeWalked || banked.Payout <= 0 {
		t.Fatalf("a walk inside the clock banks, got %q paying %d", banked.Outcome, banked.Payout)
	}
}
