package game

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

	"github.com/denisovdennis/autohero/internal/model"
	"github.com/denisovdennis/autohero/internal/tuning"
)

func testGraphTownTour(t *testing.T) *RoadGraph {
	t.Helper()
	g := testGraph()
	g.Towns[1].Radius = 35
	npc := TownNPC{ID: 101, Name: "Merchant", NameKey: "npc.merchant.test", Type: "merchant", OffsetX: 2, OffsetY: 1}
	g.TownNPCs[1] = []TownNPC{npc}
	g.NPCByID[101] = npc
	return g
}

func heroInTown(id int64, townID int64) *model.Hero {
	return &model.Hero{
		ID: id, Level: 5, HP: 900, MaxHP: 1000,
		Attack: 50, Defense: 30, Speed: 1.0,
		Strength: 10, Constitution: 10, Agility: 10, Luck: 5,
		State:         model.StateInTown,
		CurrentTownID: &townID,
		PositionX:     1, PositionY: 1,
	}
}

func TestTownTour_WelcomeTimeoutReturnsToWander(t *testing.T) {
	graph := testGraphTownTour(t)
	hero := heroInTown(1, 1)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.CurrentTownID = 1
	hm.CurrentX = 1
	hm.CurrentY = 1
	hm.LastMoveTick = now
	hm.Excursion = model.ExcursionSession{
		Kind:              model.ExcursionKindTown,
		Phase:             model.ExcursionWild,
		TownTourPhase:     string(model.TownTourPhaseNpcWelcome),
		TownWelcomeUntil:  now.Add(-time.Second),
		TownTourNpcID:     101,
		TownTourStandX:    3,
		TownTourStandY:    2,
		AttractorSet:      true,
		AttractorX:        3,
		AttractorY:        2,
	}
	ProcessSingleHeroMovementTick(hero.ID, hm, graph, now.Add(50*time.Millisecond), nil, nil, nil, nil, nil, nil, nil)
	if hm.Excursion.TownTourPhase != string(model.TownTourPhaseWander) {
		t.Fatalf("expected wander after welcome timeout, got %q", hm.Excursion.TownTourPhase)
	}
}

func TestTownTour_DialogClosedFromWelcomeLeavesWelcome(t *testing.T) {
	graph := testGraphTownTour(t)
	hero := heroInTown(1, 1)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.CurrentTownID = 1
	hm.Excursion = model.ExcursionSession{
		Kind:          model.ExcursionKindTown,
		Phase:         model.ExcursionWild,
		TownTourPhase: string(model.TownTourPhaseNpcWelcome),
		TownWelcomeUntil: now.Add(time.Hour),
		TownTourNpcID: 101,
	}
	hm.townTourNPCDialogClosed(now, graph)
	if model.TownTourPhase(hm.Excursion.TownTourPhase) == model.TownTourPhaseNpcWelcome {
		t.Fatal("still in npc_welcome after dialog closed")
	}
	if !hm.Excursion.TownWelcomeUntil.IsZero() {
		t.Fatal("expected TownWelcomeUntil cleared")
	}
}

// TestTownTour_DefaultPNpc_AtLeastOneNpcOpportunity uses default tuning to estimate P(≥1 NPC attractor pick)
// over a synthetic town stay (retarget cadence vs tour length). Target from design: ≥ 0.6.
func TestTownTour_DefaultPNpc_AtLeastOneNpcOpportunity(t *testing.T) {
	cfg := tuning.DefaultValues()
	pNpc := cfg.TownTourNpcAttractorChance
	minRT := cfg.TownTourWanderRetargetMinMs
	maxRT := cfg.TownTourWanderRetargetMaxMs
	minStay := cfg.TownRestMinMs
	maxStay := cfg.TownRestMaxMs
	if minRT <= 0 || maxRT < minRT || minStay <= 0 || maxStay < minStay {
		t.Fatal("invalid default town tour / rest durations")
	}
	const trials = 8000
	rng := rand.New(rand.NewSource(42))
	hits := 0
	for i := 0; i < trials; i++ {
		stayMs := minStay + rng.Int63n(maxStay-minStay+1)
		anyNpc := false
		for elapsed := int64(0); elapsed < stayMs; {
			if rng.Float64() < pNpc {
				anyNpc = true
				break
			}
			step := minRT + rng.Int63n(maxRT-minRT+1)
			if step < 1 {
				step = 1
			}
			elapsed += step
		}
		if anyNpc {
			hits++
		}
	}
	rate := float64(hits) / float64(trials)
	if rate < 0.6 {
		t.Fatalf("Monte Carlo P(≥1 NPC retarget)=%.3f with defaults; want >= 0.6 (townTourNpcAttractorChance=%.3f)", rate, pNpc)
	}
}
