package plugin

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

// Pure-logic tests for Phase 12 E7 (ambient ticker). The full
// deliverAmbient path mutates the prod DB; those gates already live
// behind setupAuditTestDB in other expedition tests. The behavior tested
// here — eligibility, weighted pick, quiet-window math, DM render —
// covers the resolution layer in isolation.

func TestInAmbientQuietWindow(t *testing.T) {
	day := func(h, m int) time.Time {
		return time.Date(2026, 5, 14, h, m, 0, 0, time.UTC)
	}
	cases := []struct {
		name string
		t    time.Time
		want bool
	}{
		{"on briefing", day(6, 0), true},
		{"30 min before briefing", day(5, 30), true},
		{"59 min before briefing", day(5, 1), true},
		{"61 min before briefing", day(4, 59), false},
		{"mid-afternoon", day(14, 17), false},
		{"on recap", day(21, 0), true},
		{"30 min after recap", day(21, 30), true},
		{"61 min after recap", day(22, 1), false},
		{"deep night", day(3, 0), false},
	}
	for _, c := range cases {
		if got := inAmbientQuietWindow(c.t); got != c.want {
			t.Errorf("%s: inAmbientQuietWindow(%v) = %v, want %v", c.name, c.t, got, c.want)
		}
	}
}

func TestPickAmbientEvent_AlwaysReturnsEligible(t *testing.T) {
	// Brand-new expedition: no camp, no threat. Only "always" events
	// should ever come back. We sample many times with different seeds
	// to brute-force the weighted pick.
	e := &Expedition{Supplies: ExpeditionSupplies{Current: 5}}
	disallowed := map[string]bool{
		"camp_visitor":    true,
		"faction_whisper": true,
	}
	for i := 0; i < 200; i++ {
		rng := rand.New(rand.NewPCG(uint64(i), uint64(i*7+1)))
		ev := pickAmbientEvent(e, rng, "")
		if disallowed[ev.Kind] {
			t.Fatalf("seed %d picked ineligible event %q", i, ev.Kind)
		}
	}
}

func TestPickAmbientEvent_CampVisitorRequiresCamp(t *testing.T) {
	e := &Expedition{
		Supplies: ExpeditionSupplies{Current: 5},
		Camp:     &CampState{Active: true},
	}
	// With camp active, camp_visitor should appear at least once in 500 spins.
	sawVisitor := false
	for i := 0; i < 500; i++ {
		rng := rand.New(rand.NewPCG(uint64(i+1), uint64(i*13+2)))
		if pickAmbientEvent(e, rng, "").Kind == "camp_visitor" {
			sawVisitor = true
			break
		}
	}
	if !sawVisitor {
		t.Errorf("expected camp_visitor to be reachable when camped")
	}
}

func TestPickAmbientEvent_PackRatNeedsSupplies(t *testing.T) {
	// Starving expedition: pack_rat should never fire — there's nothing
	// for the rat to take, and a "−0.0 SU" DM would be silly.
	e := &Expedition{Supplies: ExpeditionSupplies{Current: 0}}
	for i := 0; i < 200; i++ {
		rng := rand.New(rand.NewPCG(uint64(i+3), uint64(i*5+7)))
		if pickAmbientEvent(e, rng, "").Kind == "pack_rat" {
			t.Fatalf("seed %d: pack_rat fired with no supplies", i)
		}
	}
}

func TestPickAmbientEvent_WhisperNeedsThreat(t *testing.T) {
	low := &Expedition{Supplies: ExpeditionSupplies{Current: 5}, ThreatLevel: 10}
	for i := 0; i < 200; i++ {
		rng := rand.New(rand.NewPCG(uint64(i+5), uint64(i*11+3)))
		if pickAmbientEvent(low, rng, "").Kind == "faction_whisper" {
			t.Fatalf("seed %d: whisper fired below threshold", i)
		}
	}
	hot := &Expedition{Supplies: ExpeditionSupplies{Current: 5}, ThreatLevel: 60}
	saw := false
	for i := 0; i < 500; i++ {
		rng := rand.New(rand.NewPCG(uint64(i+9), uint64(i*17+5)))
		if pickAmbientEvent(hot, rng, "").Kind == "faction_whisper" {
			saw = true
			break
		}
	}
	if !saw {
		t.Errorf("expected faction_whisper to be reachable at threat 60")
	}
}

func TestPickAmbientEvent_SiegeSuppressesWhisper(t *testing.T) {
	// In siege mode the faction has stopped whispering and started
	// kicking the door. The ambient whisper would undersell the moment.
	e := &Expedition{
		Supplies:    ExpeditionSupplies{Current: 5},
		ThreatLevel: 100,
		SiegeMode:   true,
	}
	for i := 0; i < 200; i++ {
		rng := rand.New(rand.NewPCG(uint64(i+13), uint64(i*19+11)))
		if pickAmbientEvent(e, rng, "").Kind == "faction_whisper" {
			t.Fatalf("seed %d: whisper fired during siege", i)
		}
	}
}

func TestPickAmbientEvent_AntiRepeatRerollsOnce(t *testing.T) {
	// When the first roll lands on avoidKind, we re-roll once. We can't
	// guarantee the second roll diverges (the same Kind can win twice
	// legitimately), but across many seeds the same-Kind repeat rate
	// must drop materially vs the no-avoid baseline.
	e := &Expedition{Supplies: ExpeditionSupplies{Current: 5}}
	const trials = 2000
	baseline := 0 // Kind == "pack_rat" with no avoid
	guarded := 0  // Kind == "pack_rat" with avoidKind == "pack_rat"
	for i := 0; i < trials; i++ {
		seedA := uint64(i*2 + 1)
		seedB := uint64(i*2 + 2)
		if pickAmbientEvent(e, rand.New(rand.NewPCG(seedA, seedB)), "").Kind == "pack_rat" {
			baseline++
		}
		if pickAmbientEvent(e, rand.New(rand.NewPCG(seedA, seedB)), "pack_rat").Kind == "pack_rat" {
			guarded++
		}
	}
	if guarded >= baseline {
		t.Fatalf("anti-repeat did not reduce pack_rat hits: baseline=%d guarded=%d", baseline, guarded)
	}
	// Sanity: the guarded count should be small (≈ baseline²/total), not
	// just one less. Allow generous slack but flag obvious regressions.
	if guarded*3 > baseline {
		t.Errorf("anti-repeat reduction weaker than expected: baseline=%d guarded=%d", baseline, guarded)
	}
}

func TestRenderAmbientDM_NoFooterPureFlavor(t *testing.T) {
	e := &Expedition{CurrentDay: 4}
	ev := ambientEventMonologue()
	body := renderAmbientDM(e, ev, "A pebble blinks.", "")
	if !strings.Contains(body, "Day 4") {
		t.Errorf("missing day header: %q", body)
	}
	if !strings.Contains(body, "A pebble blinks.") {
		t.Errorf("missing flavor line: %q", body)
	}
	if strings.Contains(body, "_") && strings.Count(body, "_") >= 2 {
		t.Errorf("unexpected italic footer in pure-flavor DM: %q", body)
	}
}

func TestRenderAmbientDM_WithFooter(t *testing.T) {
	e := &Expedition{CurrentDay: 7}
	body := renderAmbientDM(e, ambientEventMonologue(), "A door slams.", "Threat +1")
	if !strings.Contains(body, "Threat +1") {
		t.Errorf("missing footer: %q", body)
	}
}
