package sim

import (
	"testing"

	"github.com/drjones/quantum-arcade/pkg/fixed"
)

func TestCrashPointNeverBelowOne(t *testing.T) {
	for i := 0; i < 20000; i++ {
		var seed [32]byte
		seed[0], seed[1] = byte(i), byte(i>>8)
		if cp := CrashPoint(seed); cp < 1<<32 {
			t.Fatalf("seed %d: crash point %v below 1.0", i, cp)
		}
	}
}

func TestCrashPointIsDeterministic(t *testing.T) {
	var seed [32]byte
	copy(seed[:], "repeatable")
	first := CrashPoint(seed)
	for i := 0; i < 100; i++ {
		if got := CrashPoint(seed); got != first {
			t.Fatalf("run %d: %v != %v", i, got, first)
		}
	}
}

// With a 2% house edge, a player cashing out at exactly 2.00x should win
// slightly under half the time. This pins the payout distribution.
func TestHouseEdgeAtTwoX(t *testing.T) {
	const n = 200000
	target := int64(2) << 32
	wins := 0
	for i := 0; i < n; i++ {
		var seed [32]byte
		seed[0], seed[1], seed[2] = byte(i), byte(i>>8), byte(i>>16)
		if int64(CrashPoint(seed)) >= target {
			wins++
		}
	}
	pct := float64(wins) * 100 / n
	if pct < 48.5 || pct > 51.0 {
		t.Fatalf("win rate at 2.00x = %.2f%%, want ~49.5%%", pct)
	}
}

// The expected return at any cash-out target should be about 98%.
func TestExpectedReturnMatchesEdge(t *testing.T) {
	const n = 200000
	for _, targetX := range []int64{2, 3, 5} {
		target := targetX << 32
		var returned float64
		for i := 0; i < n; i++ {
			var seed [32]byte
			seed[0], seed[1], seed[2], seed[3] = byte(i), byte(i>>8), byte(i>>16), byte(targetX)
			if int64(CrashPoint(seed)) >= target {
				returned += float64(targetX)
			}
		}
		rtp := returned * 100 / n
		if rtp < 97.0 || rtp > 101.0 {
			t.Fatalf("RTP at %dx = %.2f%%, want ~99%%", targetX, rtp)
		}
	}
}

func TestMultiplierStartsAtOne(t *testing.T) {
	if got := MultiplierAt(0); got != 1<<32 {
		t.Fatalf("MultiplierAt(0) = %v, want 1.0", got)
	}
}

func TestMultiplierIsMonotonic(t *testing.T) {
	prev := MultiplierAt(0)
	for tick := 1; tick < 5000; tick++ {
		cur := MultiplierAt(tick)
		if cur < prev {
			t.Fatalf("tick %d: multiplier decreased %v -> %v", tick, prev, cur)
		}
		prev = cur
	}
}

func TestTicksToMultiplierRoundTrips(t *testing.T) {
	for _, m := range []int64{2, 5, 10} {
		target := fixed.FromInt(m)
		tick := TicksToMultiplier(target)
		if MultiplierAt(tick) < target {
			t.Fatalf("tick %d does not reach %dx", tick, m)
		}
		if tick > 0 && MultiplierAt(tick-1) >= target {
			t.Fatalf("tick %d is not the first to reach %dx", tick, m)
		}
	}
}

// No round may outlast the ceiling, however extreme the crash point.
func TestRoundLengthIsBounded(t *testing.T) {
	if got := MultiplierAt(RoundTicks); got != MaxMultiplier() {
		t.Fatalf("curve past the ceiling = %v, want %v", got, MaxMultiplier())
	}
	// Even the most extreme crash point settles within the ceiling.
	if tick := TicksToMultiplier(MaxMultiplier()); tick > RoundTicks {
		t.Fatalf("extreme crash point needs %d ticks, ceiling is %d", tick, RoundTicks)
	}
}

// Timings that matter for how the game feels.
func TestCurveTimings(t *testing.T) {
	for _, c := range []struct {
		multiplier int64
		maxSeconds float64
	}{
		{2, 20}, // the common case should arrive quickly
		{10, 45},
		{100, 56},
	} {
		tick := TicksToMultiplier(fixed.FromInt(c.multiplier))
		secs := float64(tick) / TickHz
		if secs > c.maxSeconds {
			t.Errorf("%dx takes %.1fs, want under %.0fs", c.multiplier, secs, c.maxSeconds)
		}
	}
}

// The crash point must never be negative or below 1.0, at any seed. An
// unsigned quotient exceeding int64 previously wrapped negative here.
func TestCrashPointNeverOverflows(t *testing.T) {
	// Drive the derivation across seeds chosen to produce very small u, which
	// is where the quotient is largest.
	for i := 0; i < 200000; i++ {
		var seed [32]byte
		for j := 0; j < 32; j++ {
			seed[j] = byte(i >> (8 * (j % 4)))
		}
		cp := CrashPoint(seed)
		if cp < fixed.One {
			t.Fatalf("seed %d produced crash point %v, below 1.0", i, cp)
		}
		if cp > MaxMultiplier() {
			t.Fatalf("seed %d produced crash point %v, above the ceiling %v",
				i, cp, MaxMultiplier())
		}
	}
}

// The payout a single round can demand must be bounded, so settlement can
// always be covered.
func TestMaximumPayoutIsBounded(t *testing.T) {
	max := MaxMultiplier()
	if max <= 0 {
		t.Fatalf("ceiling is not positive: %v", max)
	}
	// A 1000-sat stake at the ceiling must stay well inside int64.
	const stakeMsat = int64(1_000_000)
	payout := stakeMsat * int64(max) / int64(fixed.One)
	if payout <= 0 {
		t.Fatalf("payout at the ceiling overflowed: %d", payout)
	}
	if payout > 1<<62 {
		t.Fatalf("payout at the ceiling is %d, unreasonably large", payout)
	}
}
