package plugin

import (
	"strings"
	"testing"
	"time"
)

// ── Monster Data Tests ──────────────────────────────────────────────────────

func TestArenaTierData(t *testing.T) {
	if len(arenaTiers) != 5 {
		t.Fatalf("expected 5 arena tiers, got %d", len(arenaTiers))
	}

	for i, tier := range arenaTiers {
		if tier.Number != i+1 {
			t.Errorf("tier %d: Number = %d, want %d", i, tier.Number, i+1)
		}
		if tier.Name == "" {
			t.Errorf("tier %d: empty name", tier.Number)
		}
		if tier.MinLevel <= 0 {
			t.Errorf("tier %d: MinLevel %d <= 0", tier.Number, tier.MinLevel)
		}
		if tier.BasePayout <= 0 {
			t.Errorf("tier %d: BasePayout %d <= 0", tier.Number, tier.BasePayout)
		}
		if tier.SkillMultiplier <= 0 {
			t.Errorf("tier %d: SkillMultiplier %f <= 0", tier.Number, tier.SkillMultiplier)
		}
		if tier.CompletionBonus <= 0 {
			t.Errorf("tier %d: CompletionBonus %d <= 0", tier.Number, tier.CompletionBonus)
		}
		if tier.BattleXP <= 0 {
			t.Errorf("tier %d: BattleXP %d <= 0", tier.Number, tier.BattleXP)
		}

		for j, m := range tier.Monsters {
			if m.Name == "" {
				t.Errorf("tier %d round %d: empty monster name", tier.Number, j+1)
			}
			if m.Flavor == "" {
				t.Errorf("tier %d round %d: empty monster flavor", tier.Number, j+1)
			}
			if m.BaseLethality < 0.01 || m.BaseLethality > 0.99 {
				t.Errorf("tier %d round %d: BaseLethality %f out of [0.01, 0.99]", tier.Number, j+1, m.BaseLethality)
			}
			if m.ThreatLevel <= 0 {
				t.Errorf("tier %d round %d: ThreatLevel %d <= 0", tier.Number, j+1, m.ThreatLevel)
			}
		}
	}
}

func TestArenaGetTier(t *testing.T) {
	for i := 1; i <= 5; i++ {
		tier := arenaGetTier(i)
		if tier == nil {
			t.Errorf("arenaGetTier(%d) returned nil", i)
		}
	}
	if arenaGetTier(0) != nil {
		t.Error("arenaGetTier(0) should return nil")
	}
	if arenaGetTier(6) != nil {
		t.Error("arenaGetTier(6) should return nil")
	}
}

func TestArenaGetMonster(t *testing.T) {
	for tier := 1; tier <= 5; tier++ {
		for round := 1; round <= 4; round++ {
			m := arenaGetMonster(tier, round)
			if m == nil {
				t.Errorf("arenaGetMonster(%d, %d) returned nil", tier, round)
			}
		}
	}
	if arenaGetMonster(1, 0) != nil {
		t.Error("arenaGetMonster(1, 0) should return nil")
	}
	if arenaGetMonster(1, 5) != nil {
		t.Error("arenaGetMonster(1, 5) should return nil")
	}
}

func TestArenaMonsterLethality_Increasing(t *testing.T) {
	// Within each tier, lethality should increase per round
	for i, tier := range arenaTiers {
		for j := 1; j < 4; j++ {
			if tier.Monsters[j].BaseLethality < tier.Monsters[j-1].BaseLethality {
				t.Errorf("tier %d: round %d lethality (%f) < round %d (%f)",
					i+1, j+1, tier.Monsters[j].BaseLethality, j, tier.Monsters[j-1].BaseLethality)
			}
		}
	}
}

func TestArenaMinLevel_Increasing(t *testing.T) {
	for i := 1; i < 5; i++ {
		if arenaTiers[i].MinLevel <= arenaTiers[i-1].MinLevel {
			t.Errorf("tier %d MinLevel (%d) <= tier %d MinLevel (%d)",
				i+1, arenaTiers[i].MinLevel, i, arenaTiers[i-1].MinLevel)
		}
	}
}

// ── Reward Formula Tests ────────────────────────────────────────────────────

func TestArenaRoundReward(t *testing.T) {
	tier1 := arenaGetTier(1)

	// Round 1, no skill bonus
	r := arenaRoundReward(tier1, 1, 0)
	if r != 150 {
		t.Errorf("T1R1 skill=0: got %d, want 150", r)
	}

	// Round 4, no skill bonus
	r = arenaRoundReward(tier1, 4, 0)
	if r != 600 {
		t.Errorf("T1R4 skill=0: got %d, want 600", r)
	}

	// Round 1 with skill
	r = arenaRoundReward(tier1, 1, 10)
	// base=150, skill_bonus=floor(10*1.0)=10
	if r != 160 {
		t.Errorf("T1R1 skill=10: got %d, want 160", r)
	}

	// Tier 5 round 4, max skill
	tier5 := arenaGetTier(5)
	r = arenaRoundReward(tier5, 4, 50)
	// base=15000*4=60000, skill_bonus=floor(50*25.0)=1250
	if r != 61250 {
		t.Errorf("T5R4 skill=50: got %d, want 61250", r)
	}
}

func TestArenaRewardScaling(t *testing.T) {
	// Rewards should increase with round number
	tier := arenaGetTier(3)
	prev := int64(0)
	for round := 1; round <= 4; round++ {
		r := arenaRoundReward(tier, round, 20)
		if r <= prev {
			t.Errorf("T3R%d reward (%d) should exceed R%d (%d)", round, r, round-1, prev)
		}
		prev = r
	}
}

// ── Render Tests ────────────────────────────────────────────────────────────

func TestRenderArenaStreakEntry(t *testing.T) {
	// Level 5 unlocks T1+T2 only (post-L2 brackets: 1/4/8/13/18); T3-T5 locked.
	char := &DnDCharacter{Level: 5}
	tier := arenaGetTier(1)
	monster := arenaGetMonster(1, 1)

	// nil stats — no tiers cleared
	text := renderArenaStreakEntry(char, nil, tier, monster)
	if !strings.Contains(text, "THE ARENA") {
		t.Error("streak entry should contain header")
	}
	if !strings.Contains(text, "Tier 1") {
		t.Error("streak entry should list tier 1")
	}
	if !strings.Contains(text, "Tier 5") {
		t.Error("streak entry should list tier 5")
	}
	if !strings.Contains(text, "streak") {
		t.Error("streak entry should mention streak")
	}
	if !strings.Contains(text, "⬚") {
		t.Error("streak entry should show eligible-but-uncleared tiers")
	}
	if !strings.Contains(text, "🔒") {
		t.Error("streak entry should show locked tiers")
	}

	// with stats — tier 2 cleared
	stats := &ArenaPersonalStats{TotalRuns: 5, TotalDeaths: 1, TotalEarnings: 3000, HighestTier: 2}
	text = renderArenaStreakEntry(char, stats, tier, monster)
	if !strings.Contains(text, "✅") {
		t.Error("streak entry should show cleared tiers when stats provided")
	}
	if !strings.Contains(text, "Runs: 5") {
		t.Error("streak entry should show run summary when stats provided")
	}
}

func TestRenderArenaRoundStart(t *testing.T) {
	tier := arenaGetTier(2)
	monster := arenaGetMonster(2, 1)
	run := &ArenaRun{Earnings: 500}

	text := renderArenaRoundStart(tier, 1, monster, run)
	if !strings.Contains(text, "Tier 2") {
		t.Error("round start should show tier")
	}
	if !strings.Contains(text, monster.Name) {
		t.Error("round start should show monster name")
	}
	if !strings.Contains(text, "€500") {
		t.Error("round start should show earnings")
	}
	if !strings.Contains(text, "!arena fight") {
		t.Error("round start should show fight command")
	}
}

func TestRenderArenaDeath(t *testing.T) {
	tier := arenaGetTier(3)
	monster := arenaGetMonster(3, 2)

	text := renderArenaDeath(tier, 2, monster, 5000, "The Arena has collected its fee.")
	if !strings.Contains(text, "DEAD") {
		t.Error("death should say DEAD")
	}
	if !strings.Contains(text, "€5000") {
		t.Error("death should show forfeited earnings")
	}
	if !strings.Contains(text, "midnight UTC") {
		t.Error("death should mention lockout")
	}
}

func TestArenaStreakMultipliers(t *testing.T) {
	// Verify multiplier arrays are sane
	for tier := 1; tier <= 5; tier++ {
		if arenaStreakEuroMultiplier[tier] < 1.0 {
			t.Errorf("tier %d euro multiplier %f < 1.0", tier, arenaStreakEuroMultiplier[tier])
		}
	}
	for tiers := 1; tiers <= 5; tiers++ {
		if arenaStreakXPMultiplier[tiers] < 1.0 {
			t.Errorf("tiers won %d XP multiplier %f < 1.0", tiers, arenaStreakXPMultiplier[tiers])
		}
	}
	// Multipliers should be non-decreasing
	for i := 2; i <= 5; i++ {
		if arenaStreakEuroMultiplier[i] < arenaStreakEuroMultiplier[i-1] {
			t.Errorf("euro multiplier tier %d (%f) < tier %d (%f)", i, arenaStreakEuroMultiplier[i], i-1, arenaStreakEuroMultiplier[i-1])
		}
		if arenaStreakXPMultiplier[i] < arenaStreakXPMultiplier[i-1] {
			t.Errorf("XP multiplier %d tiers (%f) < %d tiers (%f)", i, arenaStreakXPMultiplier[i], i-1, arenaStreakXPMultiplier[i-1])
		}
	}

	// Verify exact values match spec
	wantEuro := map[int]float64{1: 1.0, 2: 1.5, 3: 2.0, 4: 2.75, 5: 4.0}
	for tier, want := range wantEuro {
		if arenaStreakEuroMultiplier[tier] != want {
			t.Errorf("euro multiplier tier %d: got %f, want %f", tier, arenaStreakEuroMultiplier[tier], want)
		}
	}
	wantXP := map[int]float64{1: 1.0, 2: 1.2, 3: 1.5, 4: 1.85, 5: 2.5}
	for tiers, want := range wantXP {
		if arenaStreakXPMultiplier[tiers] != want {
			t.Errorf("XP multiplier %d tiers: got %f, want %f", tiers, arenaStreakXPMultiplier[tiers], want)
		}
	}

	// Index 0 should be 0 (unused sentinel)
	if arenaStreakEuroMultiplier[0] != 0 {
		t.Errorf("euro multiplier index 0 should be 0, got %f", arenaStreakEuroMultiplier[0])
	}
	if arenaStreakXPMultiplier[0] != 0 {
		t.Errorf("XP multiplier index 0 should be 0, got %f", arenaStreakXPMultiplier[0])
	}
}

// ── Streak Reward Calculation Tests ────────────────────────────────────────

func TestArenaStreakPayout_SingleTier(t *testing.T) {
	// Bail after T1: multiplier = 1.0× on T1 earnings
	tier1 := arenaGetTier(1)
	var tierEarnings int64
	for round := 1; round <= 4; round++ {
		tierEarnings += arenaRoundReward(tier1, round, 0)
	}
	tierEarnings += tier1.CompletionBonus

	multiplied := int64(float64(tierEarnings) * arenaStreakEuroMultiplier[1])
	if multiplied != tierEarnings {
		t.Errorf("T1 payout with 1.0× multiplier should equal raw: got %d, want %d", multiplied, tierEarnings)
	}
}

func TestArenaStreakPayout_FullRun(t *testing.T) {
	// Full T1-T5 run: each tier's earnings multiplied independently
	var totalMultiplied int64
	var totalRaw int64

	for tierNum := 1; tierNum <= 5; tierNum++ {
		tier := arenaGetTier(tierNum)
		var tierEarnings int64
		for round := 1; round <= 4; round++ {
			tierEarnings += arenaRoundReward(tier, round, 0)
		}
		tierEarnings += tier.CompletionBonus
		totalRaw += tierEarnings
		totalMultiplied += int64(float64(tierEarnings) * arenaStreakEuroMultiplier[tierNum])
	}

	// Multiplied total should exceed raw total (multipliers > 1 for T2+)
	if totalMultiplied <= totalRaw {
		t.Errorf("multiplied total (%d) should exceed raw total (%d)", totalMultiplied, totalRaw)
	}

	// T5 multiplier is 4.0×, so multiplied should be significantly higher
	ratio := float64(totalMultiplied) / float64(totalRaw)
	if ratio < 1.5 {
		t.Errorf("overall multiplier ratio %f seems too low for streak system", ratio)
	}
}

func TestArenaStreakXPMultiplier_Application(t *testing.T) {
	// XP multiplier is applied to total session XP at payout
	rawXP := 500

	for tiersWon := 1; tiersWon <= 5; tiersWon++ {
		mult := arenaStreakXPMultiplier[tiersWon]
		totalXP := int(float64(rawXP) * mult)

		if tiersWon == 1 && totalXP != rawXP {
			t.Errorf("1 tier won: XP should be unmodified, got %d want %d", totalXP, rawXP)
		}
		if tiersWon > 1 && totalXP <= rawXP {
			t.Errorf("%d tiers won: XP %d should exceed raw %d", tiersWon, totalXP, rawXP)
		}
	}
}

func TestArenaStreakPayout_TierEarningsReset(t *testing.T) {
	// Simulate accumulation: tier earnings should reset per tier
	run := &ArenaRun{Earnings: 0, TierEarnings: 0}

	// Simulate T1 rounds
	for round := 1; round <= 4; round++ {
		tier := arenaGetTier(1)
		run.TierEarnings += arenaRoundReward(tier, round, 10)
	}
	run.TierEarnings += arenaGetTier(1).CompletionBonus

	// Apply T1 multiplier
	run.Earnings += int64(float64(run.TierEarnings) * arenaStreakEuroMultiplier[1])
	t1Total := run.Earnings
	run.TierEarnings = 0

	if t1Total <= 0 {
		t.Fatal("T1 earnings should be positive")
	}
	if run.TierEarnings != 0 {
		t.Error("TierEarnings should be 0 after reset")
	}

	// Simulate T2 rounds
	for round := 1; round <= 4; round++ {
		tier := arenaGetTier(2)
		run.TierEarnings += arenaRoundReward(tier, round, 10)
	}
	run.TierEarnings += arenaGetTier(2).CompletionBonus

	// Apply T2 multiplier (1.5×)
	run.Earnings += int64(float64(run.TierEarnings) * arenaStreakEuroMultiplier[2])
	run.TierEarnings = 0

	if run.Earnings <= t1Total {
		t.Errorf("session total after T2 (%d) should exceed T1 total (%d)", run.Earnings, t1Total)
	}
}

func TestArenaStreakPayout_DeathForfeitsAll(t *testing.T) {
	// On death, earnings, tier_earnings, and XP are all zeroed
	run := &ArenaRun{
		Earnings:      50000,
		TierEarnings:  3000,
		XPAccumulated: 200,
	}

	// Simulate death
	run.Earnings = 0
	run.TierEarnings = 0
	run.XPAccumulated = 0

	if run.Earnings != 0 || run.TierEarnings != 0 || run.XPAccumulated != 0 {
		t.Error("death should forfeit all session state")
	}
}

// ── Gladiator's Helm Tests ─────────────────────────────────────────────────

func TestGladiatorHelmDropRates(t *testing.T) {
	if gladiatorHelmDropT4 != 0.08 {
		t.Errorf("T4 drop rate: got %f, want 0.08", gladiatorHelmDropT4)
	}
	if gladiatorHelmDropT5 != 0.18 {
		t.Errorf("T5 drop rate: got %f, want 0.18", gladiatorHelmDropT5)
	}
	if gladiatorHelmDropT5 <= gladiatorHelmDropT4 {
		t.Error("T5 drop rate should exceed T4")
	}
}

func TestGladiatorHelmTier(t *testing.T) {
	if gladiatorHelmTier != 5 {
		t.Errorf("Gladiator's Helm tier: got %d, want 5", gladiatorHelmTier)
	}
}

// ── Arena Gear Tests ──────────────────────────────────────────────────────

func TestArenaGearByTier(t *testing.T) {
	for tier := 1; tier <= 5; tier++ {
		gear := arenaGearByTier(tier)
		if gear == nil {
			t.Errorf("arenaGearByTier(%d) returned nil", tier)
			continue
		}
		if gear.Tier != tier {
			t.Errorf("arenaGearByTier(%d).Tier = %d", tier, gear.Tier)
		}
		if gear.SetKey == "" {
			t.Errorf("tier %d: empty SetKey", tier)
		}
		if gear.HelmetName == "" {
			t.Errorf("tier %d: empty HelmetName", tier)
		}
		if gear.DropRate <= 0 || gear.DropRate >= 1 {
			t.Errorf("tier %d: DropRate %f out of (0, 1)", tier, gear.DropRate)
		}
	}

	// Out of bounds
	if arenaGearByTier(0) != nil {
		t.Error("arenaGearByTier(0) should return nil")
	}
	if arenaGearByTier(6) != nil {
		t.Error("arenaGearByTier(6) should return nil")
	}
}

func TestArenaGearDropRates_Decreasing(t *testing.T) {
	// Higher tiers should have lower (or equal) drop rates
	for i := 2; i <= 5; i++ {
		curr := arenaGearByTier(i)
		prev := arenaGearByTier(i - 1)
		if curr.DropRate > prev.DropRate {
			t.Errorf("tier %d drop rate (%f) > tier %d (%f)", i, curr.DropRate, i-1, prev.DropRate)
		}
	}
}

// ── Render Tests (New/Updated) ─────────────────────────────────────────────

func TestRenderArenaStreakEntry_ShowsMultipliers(t *testing.T) {
	char := &DnDCharacter{Level: 20}
	tier := arenaGetTier(1)
	monster := arenaGetMonster(1, 1)

	text := renderArenaStreakEntry(char, nil, tier, monster)
	// Should show streak multiplier for at least T1
	if !strings.Contains(text, "1.0×") {
		t.Error("streak entry should show T1 multiplier")
	}
	if !strings.Contains(text, "4.0×") {
		t.Error("streak entry should show T5 multiplier")
	}
	// Should show first monster
	if !strings.Contains(text, monster.Name) {
		t.Error("streak entry should show first monster name")
	}
	// Should show fight command
	if !strings.Contains(text, "!arena fight") {
		t.Error("streak entry should show fight command")
	}
	// Should show cancel option
	if !strings.Contains(text, "!arena cancel") {
		t.Error("streak entry should show cancel command")
	}
}

func TestRenderArenaStreakEntry_HighLevel_AllEligible(t *testing.T) {
	// T5 requires level 18 (post-L2 D&D scale); level 20 unlocks everything.
	char := &DnDCharacter{Level: 20}
	tier := arenaGetTier(1)
	monster := arenaGetMonster(1, 1)

	text := renderArenaStreakEntry(char, nil, tier, monster)
	// Level 20 (D&D cap) should make all tiers eligible (no 🔒)
	if strings.Contains(text, "🔒") {
		t.Error("level 20 should have all tiers eligible (no locked icons)")
	}
}

func TestRenderArenaRoundStart_WithTierEarnings(t *testing.T) {
	tier := arenaGetTier(3)
	monster := arenaGetMonster(3, 1)
	run := &ArenaRun{Earnings: 10000, TierEarnings: 2000}

	text := renderArenaRoundStart(tier, 1, monster, run)
	// Should show combined session total (Earnings + TierEarnings)
	if !strings.Contains(text, "€12000") {
		t.Error("round start should show combined session total (10000+2000)")
	}
}

func TestRenderArenaRoundStart_ZeroEarnings(t *testing.T) {
	tier := arenaGetTier(1)
	monster := arenaGetMonster(1, 1)
	run := &ArenaRun{Earnings: 0, TierEarnings: 0}

	text := renderArenaRoundStart(tier, 1, monster, run)
	// Should NOT show earnings line when zero
	if strings.Contains(text, "Session earnings") {
		t.Error("round start should not show earnings line when zero")
	}
}

func TestRenderArenaStatus_Active(t *testing.T) {
	run := &ArenaRun{Tier: 2, Round: 3, Status: "active", Earnings: 8000, TierEarnings: 1500, RoundsSurvived: 6}

	text := renderArenaStatus(run)
	if !strings.Contains(text, "Streak") {
		t.Error("status should say Arena Streak")
	}
	if !strings.Contains(text, "€9500") {
		t.Error("status should show combined session total (8000+1500)")
	}
	if !strings.Contains(text, "Round: 3/4") {
		t.Error("status should show round")
	}
	if !strings.Contains(text, "!arena fight") {
		t.Error("active status should show fight command")
	}
}

func TestRenderArenaStatus_Awaiting(t *testing.T) {
	run := &ArenaRun{Tier: 3, Status: "awaiting", Earnings: 20000, TierEarnings: 0, RoundsSurvived: 12}

	text := renderArenaStatus(run)
	if !strings.Contains(text, "advancing shortly") {
		t.Error("awaiting status should mention advancing")
	}
	if !strings.Contains(text, "!bail") {
		t.Error("awaiting status should show bail command")
	}
	if !strings.Contains(text, "€20000") {
		t.Error("awaiting status should show session total")
	}
}

func TestRenderArenaAlreadyInRun_Active(t *testing.T) {
	run := &ArenaRun{Tier: 2, Round: 3, Status: "active"}
	text := renderArenaAlreadyInRun(run)
	if !strings.Contains(text, "Tier 2") {
		t.Error("active run message should show tier")
	}
	if !strings.Contains(text, "!arena fight") {
		t.Error("active run message should show fight command")
	}
}

func TestRenderArenaAlreadyInRun_Awaiting(t *testing.T) {
	run := &ArenaRun{Tier: 3, Status: "awaiting", Earnings: 15000}
	text := renderArenaAlreadyInRun(run)
	if !strings.Contains(text, "Tier 3") {
		t.Error("awaiting run message should show tier")
	}
	if !strings.Contains(text, "!bail") {
		t.Error("awaiting run message should mention bail")
	}
	if !strings.Contains(text, "€15000") {
		t.Error("awaiting run message should show accumulated euros")
	}
}

func TestRenderArenaHelmetDrop_AllSets(t *testing.T) {
	for tier := 1; tier <= 5; tier++ {
		gear := arenaGearByTier(tier)
		text := renderArenaHelmetDrop(gear)
		if !strings.Contains(text, gear.HelmetName) {
			t.Errorf("tier %d: helmet drop should show helmet name", tier)
		}
		if !strings.Contains(text, "Set bonus") {
			t.Errorf("tier %d: helmet drop should show set bonus", tier)
		}
		if !strings.Contains(text, "Equipped automatically") {
			t.Errorf("tier %d: helmet drop should mention auto-equip", tier)
		}
	}
}

func TestRenderArenaDeathReprieve(t *testing.T) {
	nextWindow := time.Date(2026, 4, 15, 14, 30, 0, 0, time.UTC)
	text := renderArenaDeathReprieve("Alice", "Ratticus the Persistent", nextWindow)
	if !strings.Contains(text, "Alice") {
		t.Error("reprieve should mention player name")
	}
	if !strings.Contains(text, "Death's Reprieve") {
		t.Error("reprieve should mention the ability name")
	}
	if !strings.Contains(text, "2026-04-15") {
		t.Error("reprieve should show next window date")
	}
}

func TestRenderArenaPersonalStats_NoStats(t *testing.T) {
	text := renderArenaPersonalStats("TestPlayer", 0, 0, nil)
	if !strings.Contains(text, "No arena runs") {
		t.Error("empty stats should say no runs")
	}
}

func TestRenderArenaPersonalStats_WithStats(t *testing.T) {
	stats := &ArenaPersonalStats{
		TotalRuns: 11, TotalEarnings: 150000, TotalDeaths: 3,
		HighestTier: 5, Tier5Completions: 2,
	}
	text := renderArenaPersonalStats("Alice", 8, 3, stats)
	if !strings.Contains(text, "Alice") {
		t.Error("stats should show player name")
	}
	if !strings.Contains(text, "€150000") {
		t.Error("stats should show total earnings")
	}
	if !strings.Contains(text, "Tier 5 completions: 2") {
		t.Error("stats should show T5 completions")
	}
	if !strings.Contains(text, "8/3") {
		t.Error("stats should show W/L record")
	}
}

func TestRenderArenaLeaderboard_Empty(t *testing.T) {
	text := renderArenaLeaderboard("2026-Q3", nil)
	if !strings.Contains(text, "Nobody has entered") {
		t.Error("empty leaderboard should say nobody entered")
	}
}

func TestRenderArenaLeaderboard_WithEntries(t *testing.T) {
	entries := []ArenaLeaderboardEntry{
		{DisplayName: "Alice", TotalEarnings: 100000, HighestTier: 5, Tier5Completions: 1, TotalRuns: 5, TotalDeaths: 2},
		{DisplayName: "Bob", TotalEarnings: 50000, HighestTier: 3, TotalRuns: 10, TotalDeaths: 7},
	}
	text := renderArenaLeaderboard("2026-Q3", entries)
	if !strings.Contains(text, "Alice") {
		t.Error("leaderboard should contain Alice")
	}
	if !strings.Contains(text, "Bob") {
		t.Error("leaderboard should contain Bob")
	}
	if !strings.Contains(text, "T5×1") {
		t.Error("leaderboard should show T5 completions")
	}
}

func TestRenderArenaLevelGate(t *testing.T) {
	tier := arenaGetTier(3)
	text := renderArenaLevelGate(tier, 5)
	if !strings.Contains(text, "Level 8") {
		t.Error("gate message should show required level")
	}
	if !strings.Contains(text, "Level 5") {
		t.Error("gate message should show player level")
	}
}

func TestRenderArenaStatus(t *testing.T) {
	run := &ArenaRun{Tier: 3, Round: 2, Status: "active", Earnings: 5000, RoundsSurvived: 5}

	text := renderArenaStatus(run)
	if !strings.Contains(text, "Tier: 3") {
		t.Error("status should show tier")
	}
	if !strings.Contains(text, "Round: 2/4") {
		t.Error("status should show round")
	}
	if !strings.Contains(text, "€5000") {
		t.Error("status should show earnings")
	}
}

// ── Death Message Tests ─────────────────────────────────────────────────────

func TestArenaDeathMessages_NotEmpty(t *testing.T) {
	if len(arenaDeathMessages) == 0 {
		t.Error("arenaDeathMessages pool is empty")
	}
	if len(arenaMonsterDeathMessages) == 0 {
		t.Error("arenaMonsterDeathMessages pool is empty")
	}
}

func TestArenaPickDeathMessage(t *testing.T) {
	monster := arenaGetMonster(2, 3) // The Impersonator
	for i := 0; i < 20; i++ {
		msg := arenaPickDeathMessage(monster, 2, 3)
		if msg == "" {
			t.Error("death message should not be empty")
		}
	}
}

// ── Full Run Reward Calculation ─────────────────────────────────────────────

func TestArenaFullRunRewards_Tier1(t *testing.T) {
	// Calculate total earnings for a clean Tier 1 run (no skill)
	tier := arenaGetTier(1)
	var total int64
	for round := 1; round <= 4; round++ {
		total += arenaRoundReward(tier, round, 0)
	}
	total += tier.CompletionBonus

	// Round rewards: 150 + 300 + 450 + 600 = 1500; + 2500 bonus = 4000
	if total != 4000 {
		t.Errorf("clean T1 run (no skill): got €%d, want €4000", total)
	}
}

func TestArenaFullRunRewards_AllTiers(t *testing.T) {
	// A full T1-T5 run with skill=0 should be well over 100k
	var grandTotal int64
	for tier := 1; tier <= 5; tier++ {
		t := arenaGetTier(tier)
		for round := 1; round <= 4; round++ {
			grandTotal += arenaRoundReward(t, round, 0)
		}
		grandTotal += t.CompletionBonus
	}
	if grandTotal < 100000 {
		// Sanity check that the numbers aren't accidentally tiny
		t.Errorf("full T1-T5 run (no skill) = €%d, expected > €100,000", grandTotal)
	}
}

// ── Full Streak Simulation ────────────────────────────────────────────────

func TestArenaStreakSimulation_FullRun(t *testing.T) {
	// Simulate a complete T1-T5 streak: accumulate rewards as the real code does
	run := &ArenaRun{
		StartTier:     1,
		Tier:          1,
		Round:         1,
		Status:        "active",
		Earnings:      0,
		TierEarnings:  0,
		XPAccumulated: 0,
	}
	skillBonus := 20

	for tierNum := 1; tierNum <= 5; tierNum++ {
		tier := arenaGetTier(tierNum)
		run.Tier = tierNum

		for round := 1; round <= 4; round++ {
			run.Round = round
			reward := arenaRoundReward(tier, round, skillBonus)
			run.TierEarnings += reward
			run.XPAccumulated += tier.BattleXP
			run.RoundsSurvived++
		}

		// Tier complete — add completion bonus
		run.TierEarnings += tier.CompletionBonus

		// Apply streak multiplier
		multiplier := arenaStreakEuroMultiplier[tierNum]
		run.Earnings += int64(float64(run.TierEarnings) * multiplier)
		run.TierEarnings = 0 // reset for next tier
	}

	// Verify tier earnings reset
	if run.TierEarnings != 0 {
		t.Errorf("TierEarnings should be 0 after all tiers, got %d", run.TierEarnings)
	}

	// Verify earnings are positive and substantial
	if run.Earnings <= 0 {
		t.Fatal("session earnings should be positive")
	}

	// Calculate what raw (unmultiplied) total would be
	var rawTotal int64
	for tierNum := 1; tierNum <= 5; tierNum++ {
		tier := arenaGetTier(tierNum)
		for round := 1; round <= 4; round++ {
			rawTotal += arenaRoundReward(tier, round, skillBonus)
		}
		rawTotal += tier.CompletionBonus
	}

	// Multiplied total should exceed raw
	if run.Earnings <= rawTotal {
		t.Errorf("multiplied total (%d) should exceed raw total (%d)", run.Earnings, rawTotal)
	}

	// Verify XP accumulated across 20 rounds (4 rounds × 5 tiers)
	if run.RoundsSurvived != 20 {
		t.Errorf("rounds survived: got %d, want 20", run.RoundsSurvived)
	}
	if run.XPAccumulated <= 0 {
		t.Error("XP should be accumulated")
	}

	// Apply XP multiplier for 5 tiers won
	xpMult := arenaStreakXPMultiplier[5]
	totalXP := int(float64(run.XPAccumulated) * xpMult)
	if totalXP <= run.XPAccumulated {
		t.Errorf("multiplied XP (%d) should exceed raw XP (%d) with 5-tier multiplier", totalXP, run.XPAccumulated)
	}
	// 2.5× multiplier means totalXP should be 2.5× raw
	expectedXP := int(float64(run.XPAccumulated) * 2.5)
	if totalXP != expectedXP {
		t.Errorf("total XP: got %d, want %d (2.5× raw)", totalXP, expectedXP)
	}
}

func TestArenaStreakSimulation_BailAfterT3(t *testing.T) {
	// Simulate bail after clearing T3
	run := &ArenaRun{Earnings: 0, TierEarnings: 0, XPAccumulated: 0}

	for tierNum := 1; tierNum <= 3; tierNum++ {
		tier := arenaGetTier(tierNum)
		for round := 1; round <= 4; round++ {
			run.TierEarnings += arenaRoundReward(tier, round, 20)
			run.XPAccumulated += tier.BattleXP
		}
		run.TierEarnings += tier.CompletionBonus
		run.Earnings += int64(float64(run.TierEarnings) * arenaStreakEuroMultiplier[tierNum])
		run.TierEarnings = 0
	}

	// XP multiplier for 3 tiers = 1.5×
	totalXP := int(float64(run.XPAccumulated) * arenaStreakXPMultiplier[3])

	if totalXP <= run.XPAccumulated {
		t.Error("multiplied XP should exceed raw with 3-tier multiplier")
	}
	if run.Earnings <= 0 {
		t.Error("session earnings should be positive after 3 tiers")
	}
}

func TestArenaStreakSimulation_DeathMidTier(t *testing.T) {
	// Simulate death in T2R3 — should have accumulated T1 earnings + partial T2
	run := &ArenaRun{Earnings: 0, TierEarnings: 0, XPAccumulated: 0}

	// Complete T1
	tier1 := arenaGetTier(1)
	for round := 1; round <= 4; round++ {
		run.TierEarnings += arenaRoundReward(tier1, round, 10)
		run.XPAccumulated += tier1.BattleXP
	}
	run.TierEarnings += tier1.CompletionBonus
	run.Earnings += int64(float64(run.TierEarnings) * arenaStreakEuroMultiplier[1])
	t1Earnings := run.Earnings
	run.TierEarnings = 0

	// Partial T2 — 2 rounds
	tier2 := arenaGetTier(2)
	for round := 1; round <= 2; round++ {
		run.TierEarnings += arenaRoundReward(tier2, round, 10)
		run.XPAccumulated += tier2.BattleXP
	}

	// Death — forfeit everything
	lostTotal := run.Earnings + run.TierEarnings
	if lostTotal <= t1Earnings {
		t.Error("lost total should include partial T2 earnings")
	}

	run.Earnings = 0
	run.TierEarnings = 0
	run.XPAccumulated = 0

	if run.Earnings != 0 || run.TierEarnings != 0 || run.XPAccumulated != 0 {
		t.Error("all session state should be zero after death")
	}
}

func TestArenaRunFields_NewFieldsDefault(t *testing.T) {
	run := ArenaRun{}
	if run.TierEarnings != 0 {
		t.Errorf("default TierEarnings should be 0, got %d", run.TierEarnings)
	}
	if run.XPAccumulated != 0 {
		t.Errorf("default XPAccumulated should be 0, got %d", run.XPAccumulated)
	}
}

// ── Streak Euro Multiplier Math (Exact) ───────────────────────────────────

func TestArenaStreakEuroMultiplier_Exact(t *testing.T) {
	// T1 raw = 4000 (verified in TestArenaFullRunRewards_Tier1)
	// T1 multiplied = 4000 * 1.0 = 4000
	tier1 := arenaGetTier(1)
	var t1Raw int64
	for r := 1; r <= 4; r++ {
		t1Raw += arenaRoundReward(tier1, r, 0)
	}
	t1Raw += tier1.CompletionBonus

	t1Mult := int64(float64(t1Raw) * arenaStreakEuroMultiplier[1])
	if t1Mult != 4000 {
		t.Errorf("T1 multiplied (1.0×): got %d, want 4000", t1Mult)
	}

	// T2 raw with skill=0
	tier2 := arenaGetTier(2)
	var t2Raw int64
	for r := 1; r <= 4; r++ {
		t2Raw += arenaRoundReward(tier2, r, 0)
	}
	t2Raw += tier2.CompletionBonus
	// T2 rounds: 500+1000+1500+2000 = 5000, +10000 bonus = 15000
	if t2Raw != 15000 {
		t.Errorf("T2 raw: got %d, want 15000", t2Raw)
	}
	// T2 multiplied: 15000 * 1.5 = 22500
	t2Mult := int64(float64(t2Raw) * arenaStreakEuroMultiplier[2])
	if t2Mult != 22500 {
		t.Errorf("T2 multiplied (1.5×): got %d, want 22500", t2Mult)
	}
}

// ── Streak Entry Shows Brim & Battle Sponsorship ──────────────────────────

func TestArenaStreakEntry_Sponsorship(t *testing.T) {
	char := &DnDCharacter{Level: 1}
	tier := arenaGetTier(1)
	monster := arenaGetMonster(1, 1)

	text := renderArenaStreakEntry(char, nil, tier, monster)
	if !strings.Contains(text, "Brim & Battle") {
		t.Error("arena entry should include Brim & Battle sponsorship")
	}
}

// ── Help Text Tests ──────────────────────────────────────────────────────

func TestArenaHelpText_StreakContent(t *testing.T) {
	if !strings.Contains(arenaHelpText, "!bail") {
		t.Error("help text should mention !bail")
	}
	if !strings.Contains(arenaHelpText, "streak") {
		t.Error("help text should mention streak")
	}
	if strings.Contains(arenaHelpText, "!arena tier") {
		t.Error("help text should NOT mention !arena tier (removed)")
	}
	if strings.Contains(arenaHelpText, "!arena descend") {
		t.Error("help text should NOT mention !arena descend (removed)")
	}
	if strings.Contains(arenaHelpText, "!arena cashout") {
		t.Error("help text should NOT mention !arena cashout (removed)")
	}
}

// ── Gladiator's Helm Drop Rate Logic ──────────────────────────────────────

func TestGladiatorHelmDropRate_BelowT4(t *testing.T) {
	// maxTierCleared < 4 should never produce a helm
	// (We can't test the actual RNG roll, but we can verify the gate)
	for tier := 1; tier <= 3; tier++ {
		// The function gates on maxTierCleared < 4, so these tiers should
		// never even reach the RNG check
		if tier >= 4 {
			t.Error("test logic error")
		}
	}
}

func TestGladiatorHelmDropRate_Selection(t *testing.T) {
	// At T4, rate should be 0.08; at T5, rate should be 0.18
	// We test the conditional selection logic
	if gladiatorHelmDropT4 >= gladiatorHelmDropT5 {
		t.Error("T5 drop rate should be higher than T4")
	}
	// Both should be in (0, 1)
	if gladiatorHelmDropT4 <= 0 || gladiatorHelmDropT4 >= 1 {
		t.Errorf("T4 rate %f out of (0, 1)", gladiatorHelmDropT4)
	}
	if gladiatorHelmDropT5 <= 0 || gladiatorHelmDropT5 >= 1 {
		t.Errorf("T5 rate %f out of (0, 1)", gladiatorHelmDropT5)
	}
}
