package game

import (
	"testing"
	"time"

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

// Phase 2 FSM: road spine freeze during excursion, HP-based exits, no rest while fighting.

func TestFSM_ExcursionFreezesRoadProgress(t *testing.T) {
	graph := testGraph()
	hero := testHeroOnRoad(1, 500, 1000)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)

	hm.WaypointIndex = 0
	hm.WaypointFraction = 0.5
	from := hm.Road.Waypoints[0]
	to := hm.Road.Waypoints[1]
	hm.CurrentX = from.X + (to.X-from.X)*0.5
	hm.CurrentY = from.Y + (to.Y-from.Y)*0.5
	hm.LastMoveTick = now

	hm.beginExcursion(now)
	if hm.Excursion.RoadFreezeWaypoint != 0 || hm.Excursion.RoadFreezeFraction != 0.5 {
		t.Fatalf("unexpected freeze snapshot: wp=%d frac=%v", hm.Excursion.RoadFreezeWaypoint, hm.Excursion.RoadFreezeFraction)
	}

	later := now.Add(30 * time.Second)
	reached := hm.AdvanceTick(later, graph)
	if reached {
		t.Fatal("AdvanceTick should not reach town while excursion is active")
	}
	if hm.WaypointIndex != 0 || hm.WaypointFraction != 0.5 {
		t.Fatalf("waypoint progress should stay frozen during excursion, got idx=%d frac=%v", hm.WaypointIndex, hm.WaypointFraction)
	}
	ps := hm.PositionSyncPayload(later)
	if ps.WaypointIndex != 0 || ps.WaypointFraction != 0.5 {
		t.Fatalf("PositionSync should reflect frozen road PB, got idx=%d frac=%v", ps.WaypointIndex, ps.WaypointFraction)
	}
}

func TestFSM_NormalWalking_AdvanceTickMovesAlongRoad(t *testing.T) {
	graph := testGraph()
	hero := testHeroOnRoad(1, 500, 1000)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)

	hm.WaypointIndex = 0
	hm.WaypointFraction = 0.5
	from := hm.Road.Waypoints[0]
	to := hm.Road.Waypoints[1]
	hm.CurrentX = from.X + (to.X-from.X)*0.5
	hm.CurrentY = from.Y + (to.Y-from.Y)*0.5
	hm.LastMoveTick = now
	if hm.Excursion.Active() {
		t.Fatal("excursion should not be active")
	}

	later := now.Add(5 * time.Second)
	reached := hm.AdvanceTick(later, graph)
	if reached {
		t.Fatal("should not reach town from mid-segment in 5s")
	}
	if hm.WaypointIndex == 0 && hm.WaypointFraction == 0.5 {
		t.Fatal("expected road progress to advance without active excursion")
	}
}

func TestFSM_RoadsideRest_HPExit_ForcesReturnBeforeWildTimer(t *testing.T) {
	graph := testGraph()
	cfg := tuning.Get()
	maxHP := 1000
	hero := testHeroOnRoad(1, int(float64(maxHP)*cfg.RoadsideRestExitHp), maxHP)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.beginRoadsideRest(now)
	origRestUntil := hm.RestUntil

	// Skip "out" leg: test HP exit from wild (campfire) phase.
	hm.Excursion.Phase = model.ExcursionWild
	hm.LastMoveTick = now
	tick := now.Add(time.Second)
	ProcessSingleHeroMovementTick(hero.ID, hm, graph, tick, nil, nil, nil, nil, nil, nil, nil)

	if hm.Excursion.Phase != model.ExcursionReturn {
		t.Fatalf("expected Return phase after HP exit in Wild, got %s", hm.Excursion.Phase)
	}
	if !tick.Before(origRestUntil) {
		t.Fatal("HP exit should force return before RestUntil wild cap")
	}
}

func TestFSM_AdventureInlineRest_HPExit_ExcursionStillActive(t *testing.T) {
	graph := testGraph()
	cfg := tuning.Get()
	maxHP := 1000
	targetHP := int(float64(maxHP) * cfg.AdventureRestTargetHp)
	hero := testHeroOnRoad(1, targetHP, maxHP)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.beginExcursion(now)
	hm.Excursion.Phase = model.ExcursionWild
	hm.beginAdventureInlineRest(now)

	ProcessSingleHeroMovementTick(hero.ID, hm, graph, now.Add(time.Second), nil, nil, nil, nil, nil, nil, nil)

	if hm.State != model.StateWalking {
		t.Fatalf("expected back to walking after HP target, got %s", hm.State)
	}
	if !hm.Excursion.Active() {
		t.Fatal("excursion session should continue after adventure-inline HP exit")
	}
}

func TestFSM_AdventureReturnAfterVictoryWhenTimerElapsedDuringFight(t *testing.T) {
	graph := testGraph()
	hero := testHeroOnRoad(1, 500, 1000)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.beginExcursion(now)
	hm.Excursion.Phase = model.ExcursionWild
	hm.Excursion.AdventureEndsAt = now.Add(-time.Second)

	hm.StartFighting()
	victoryAt := now.Add(5 * time.Second)
	hm.ResumeWalking(victoryAt)
	hm.TryAdventureReturnAfterCombat(victoryAt)

	if hm.Excursion.Phase != model.ExcursionReturn {
		t.Fatalf("expected Return phase after victory with elapsed adventure timer, got %s", hm.Excursion.Phase)
	}
	if !hm.Excursion.AttractorSet {
		t.Fatal("return attractor should be set toward road")
	}
}

func TestFSM_AdventureReturnAfterVictoryWhenPendingFlagSet(t *testing.T) {
	graph := testGraph()
	hero := testHeroOnRoad(1, 500, 1000)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.beginExcursion(now)
	hm.Excursion.Phase = model.ExcursionWild
	hm.Excursion.AdventureEndsAt = now.Add(time.Hour)
	hm.Excursion.PendingReturnAfterCombat = true

	hm.TryAdventureReturnAfterCombat(now)

	if hm.Excursion.Phase != model.ExcursionReturn {
		t.Fatalf("expected Return phase when pending flag set, got %s", hm.Excursion.Phase)
	}
}

func TestFSM_ProcessTick_IgnoresLowHP_WhenFighting(t *testing.T) {
	graph := testGraph()
	cfg := tuning.Get()
	maxHP := 1000
	lowHP := int(float64(maxHP)*cfg.LowHpThreshold) - 1

	hero := testHeroOnRoad(1, lowHP, maxHP)
	hero.State = model.StateFighting
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)

	ProcessSingleHeroMovementTick(hero.ID, hm, graph, now, nil, nil, nil, nil, nil, nil, nil)

	if hm.State != model.StateFighting {
		t.Fatalf("expected StateFighting unchanged, got %s", hm.State)
	}
	if hm.State == model.StateResting {
		t.Fatal("must not enter rest while fighting")
	}
}

func TestFSM_AdminStartRoadsideRest_RejectsFighting(t *testing.T) {
	graph := testGraph()
	hero := testHeroOnRoad(1, 100, 1000)
	hero.State = model.StateFighting
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.State = model.StateFighting

	if hm.AdminStartRoadsideRest(now) {
		t.Fatal("AdminStartRoadsideRest must reject fighting hero")
	}
}

func TestFSM_AdminStartExcursion_RejectsFighting(t *testing.T) {
	graph := testGraph()
	hero := testHeroOnRoad(1, 500, 1000)
	hero.State = model.StateFighting
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.State = model.StateFighting

	if hm.AdminStartExcursion(now) {
		t.Fatal("AdminStartExcursion must reject fighting hero")
	}
}

func TestFSM_AdminStopExcursion_RejectsFighting(t *testing.T) {
	graph := testGraph()
	hero := testHeroOnRoad(1, 500, 1000)
	now := time.Now()
	hm := NewHeroMovement(hero, graph, now)
	hm.beginExcursion(now)
	hm.State = model.StateFighting
	hm.Hero.State = model.StateFighting

	if hm.AdminStopExcursion(now) {
		t.Fatal("AdminStopExcursion must reject fighting hero")
	}
}
