package plugin

import (
	"testing"
	"time"

	"maunium.net/go/mautrix/id"
)

// D7-b: applyAutoCamp drives maybeAutoCamp under the sim's synthetic
// clock. When simNow is past the nightCampWindow on an event-anchored
// expedition, the scheduler pitches a Night camp — current_day++,
// supplies burn, last_briefing_at stamped. The helper then advances
// simNow past minAutoCampDwell and breaks the camp so the next walk
// proceeds.
func TestSimRunner_ApplyAutoCamp_NightCampAdvancesDay(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@sim-applycamp-night:example")
	campTestCharacter(t, uid, 3)
	defer cleanupExpeditions(uid)

	run, err := startZoneRun(uid, ZoneGoblinWarrens, 3, nil)
	if err != nil {
		t.Fatalf("startZoneRun: %v", err)
	}
	exp, err := startExpedition(uid, ZoneGoblinWarrens, run.RunID,
		ExpeditionSupplies{Current: 20, Max: 20, DailyBurn: 2, HarshMod: 1})
	if err != nil {
		t.Fatal(err)
	}
	useEventAnchored(t, exp)

	sim := &SimRunner{P: &AdventurePlugin{}}
	// Park simNow past the night-camp window so decideAutopilotCamp
	// flags Night=true on a healthy character.
	simNow := exp.StartDate.Add(nightCampWindow + time.Hour)
	startDay := exp.CurrentDay
	startSU := exp.Supplies.Current
	before := simNow

	night, err := sim.applyAutoCamp(exp, &simNow)
	if err != nil {
		t.Fatalf("applyAutoCamp: %v", err)
	}
	if !night {
		t.Fatal("expected Night=true beyond nightCampWindow")
	}
	if simNow.Sub(before) < minAutoCampDwell {
		t.Errorf("simNow advanced %v, want >= %v", simNow.Sub(before), minAutoCampDwell)
	}

	got, _ := getExpedition(exp.ID)
	if got == nil {
		t.Fatal("expedition missing after night camp")
	}
	if got.CurrentDay != startDay+1 {
		t.Errorf("CurrentDay = %d, want %d", got.CurrentDay, startDay+1)
	}
	if got.Supplies.Current >= startSU {
		t.Errorf("Supplies.Current = %v, want < %v (burn should have landed)", got.Supplies.Current, startSU)
	}
	if got.Camp != nil && got.Camp.Active {
		t.Errorf("camp should have been broken after dwell, got %+v", got.Camp)
	}
}

// HP-low mid-day rest: under nightCampWindow with a hurt character,
// maybeAutoCamp pitches a Standard camp (cleared room → standard) but
// doesn't advance the day. HP gets restored by applyCampRest; DayTicks
// stays untouched.
func TestSimRunner_ApplyAutoCamp_HPLowMidDayRest(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@sim-applycamp-hp:example")
	campTestCharacter(t, uid, 3)
	c, _ := LoadDnDCharacter(uid)
	c.HPCurrent = 1 // far below autoCampHPPct (55%)
	_ = SaveDnDCharacter(c)
	defer cleanupExpeditions(uid)

	run, err := startZoneRun(uid, ZoneGoblinWarrens, 3, nil)
	if err != nil {
		t.Fatalf("startZoneRun: %v", err)
	}
	exp, err := startExpedition(uid, ZoneGoblinWarrens, run.RunID,
		ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
	if err != nil {
		t.Fatal(err)
	}
	useEventAnchored(t, exp)

	sim := &SimRunner{P: &AdventurePlugin{}}
	// simNow well inside the active-day window — no Night pitch.
	simNow := exp.StartDate.Add(2 * time.Hour)
	startDay := exp.CurrentDay
	startHP := c.HPCurrent

	night, err := sim.applyAutoCamp(exp, &simNow)
	if err != nil {
		t.Fatalf("applyAutoCamp: %v", err)
	}
	if night {
		t.Error("expected non-Night pitch within active-day window")
	}

	got, _ := getExpedition(exp.ID)
	if got == nil {
		t.Fatal("expedition missing")
	}
	if got.CurrentDay != startDay {
		t.Errorf("CurrentDay = %d, want %d (no day advance on HP rest)", got.CurrentDay, startDay)
	}
	c2, _ := LoadDnDCharacter(uid)
	if c2 == nil || c2.HPCurrent <= startHP {
		t.Errorf("HPCurrent = %v, want > %v (rest should heal)", c2.HPCurrent, startHP)
	}
	if got.Camp != nil && got.Camp.Active {
		t.Errorf("camp should have been broken after dwell, got %+v", got.Camp)
	}
}

// stopBossSafety: pitchBossSafetyCamp force-pitches a camp regardless
// of the regular HP threshold or the boss-room block in decideAutopilotCamp.
// applyAutoCampBossSafety wraps that and runs the same dwell/break dance.
func TestSimRunner_ApplyAutoCampBossSafety_PitchesAndHeals(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@sim-bosssafety:example")
	campTestCharacter(t, uid, 3)
	c, _ := LoadDnDCharacter(uid)
	// HP at 80% — above autoCampHPPct so maybeAutoCamp would NOT fire,
	// but the boss-safety path should still pitch.
	c.HPCurrent = (c.HPMax * 80) / 100
	if c.HPCurrent < 1 {
		c.HPCurrent = 1
	}
	_ = SaveDnDCharacter(c)
	defer cleanupExpeditions(uid)

	run, err := startZoneRun(uid, ZoneGoblinWarrens, 3, nil)
	if err != nil {
		t.Fatalf("startZoneRun: %v", err)
	}
	exp, err := startExpedition(uid, ZoneGoblinWarrens, run.RunID,
		ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1, HarshMod: 1})
	if err != nil {
		t.Fatal(err)
	}
	useEventAnchored(t, exp)

	sim := &SimRunner{P: &AdventurePlugin{}}
	simNow := exp.StartDate.Add(2 * time.Hour)
	startSU := exp.Supplies.Current
	startHP := c.HPCurrent
	before := simNow

	if _, err := sim.applyAutoCampBossSafety(exp, &simNow); err != nil {
		t.Fatalf("applyAutoCampBossSafety: %v", err)
	}
	if simNow.Sub(before) < minAutoCampDwell {
		t.Errorf("simNow advanced %v, want >= %v", simNow.Sub(before), minAutoCampDwell)
	}

	got, _ := getExpedition(exp.ID)
	if got == nil {
		t.Fatal("expedition missing after boss-safety pitch")
	}
	if got.Supplies.Current >= startSU {
		t.Errorf("Supplies.Current = %v, want < %v (camp cost should have debited)", got.Supplies.Current, startSU)
	}
	c2, _ := LoadDnDCharacter(uid)
	if c2 == nil || c2.HPCurrent <= startHP {
		t.Errorf("HPCurrent = %v, want > %v (rest should heal)", c2.HPCurrent, startHP)
	}
	if got.Camp != nil && got.Camp.Active {
		t.Errorf("camp should have been broken after dwell, got %+v", got.Camp)
	}
}

// No trigger → no pitch: healthy HP, fresh simNow, no region cleared.
// maybeAutoCamp should bail and applyAutoCamp returns (false, nil).
func TestSimRunner_ApplyAutoCamp_NoTriggerIsNoop(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@sim-applycamp-noop:example")
	campTestCharacter(t, uid, 3)
	defer cleanupExpeditions(uid)

	exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
		ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
	if err != nil {
		t.Fatal(err)
	}
	useEventAnchored(t, exp)

	sim := &SimRunner{P: &AdventurePlugin{}}
	simNow := exp.StartDate.Add(time.Hour)
	before := simNow

	night, err := sim.applyAutoCamp(exp, &simNow)
	if err != nil {
		t.Fatalf("applyAutoCamp: %v", err)
	}
	if night {
		t.Error("expected no Night pitch from no-op call")
	}
	if !simNow.Equal(before) {
		t.Errorf("simNow advanced %v on no-op call", simNow.Sub(before))
	}
}

// D7-a: SimRunner.TickDay must advance event-anchored expeditions. The
// production deliverBriefing branch reads wall-clock time for its safety-
// net check, so the sim path short-circuits to processNightCamp + a
// Standard rest. Without this fix, CurrentDay never increments under
// D2-b and supply-burn baselining is impossible.
func TestSimRunner_TickDay_EventAnchoredAdvancesDay(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@sim-tickday-evt:example")
	defer cleanupExpeditions(uid)

	exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
		ExpeditionSupplies{Current: 20, Max: 20, DailyBurn: 2, HarshMod: 1})
	if err != nil {
		t.Fatal(err)
	}
	useEventAnchored(t, exp)

	sim := &SimRunner{P: &AdventurePlugin{}}
	for i := 0; i < 3; i++ {
		fresh, _ := getExpedition(exp.ID)
		if fresh == nil {
			t.Fatalf("expedition vanished after %d ticks", i)
		}
		if err := sim.TickDay(fresh); err != nil {
			t.Fatalf("TickDay #%d: %v", i+1, err)
		}
	}

	got, _ := getExpedition(exp.ID)
	if got == nil {
		t.Fatal("expedition missing after ticks")
	}
	if got.CurrentDay != 4 {
		t.Errorf("CurrentDay = %d, want 4 after 3 ticks (started at 1)", got.CurrentDay)
	}
	if got.Supplies.Current >= 20 {
		t.Errorf("Supplies.Current = %v, want < 20 (burn should have landed)", got.Supplies.Current)
	}
	if got.LastBriefingAt == nil {
		t.Fatal("LastBriefingAt nil — drift stamp didn't fire")
	}
	// Each tick stamps last_briefing_at at the synthetic briefAt
	// (start_date + N days at 06:00:30). After 3 ticks the stamp should
	// be at least 2 days past start_date.
	if got.LastBriefingAt.Sub(exp.StartDate) < 48*time.Hour {
		t.Errorf("LastBriefingAt %v not advanced enough vs start %v",
			got.LastBriefingAt, exp.StartDate)
	}
}

// Low-SU branch: a tight supplies budget shouldn't crash the sim — the
// rest is skipped, but burn/drift still fire so the day advances. This
// mirrors prod where a stalled autopilot in a low-SU state has its
// rollover force-fired by the safety net without a rest.
func TestSimRunner_TickDay_EventAnchoredLowSupplies(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@sim-tickday-lowsu:example")
	defer cleanupExpeditions(uid)

	exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
		// 0.5 SU is below Rough camp cost (1 SU) but above zero, so the
		// rest branch is skipped while burn still proceeds.
		ExpeditionSupplies{Current: 0.5, Max: 20, DailyBurn: 0.2, HarshMod: 1})
	if err != nil {
		t.Fatal(err)
	}
	useEventAnchored(t, exp)
	// Pin supplies low after start (startExpedition may reset them).
	exp.Supplies.Current = 0.5
	if err := updateSupplies(exp.ID, exp.Supplies); err != nil {
		t.Fatal(err)
	}

	sim := &SimRunner{P: &AdventurePlugin{}}
	if err := sim.TickDay(exp); err != nil {
		t.Fatalf("TickDay: %v", err)
	}
	got, _ := getExpedition(exp.ID)
	if got == nil {
		// Forced extraction from starvation is acceptable — the day still
		// advanced before extraction, which is what matters for the sim.
		return
	}
	if got.CurrentDay < 2 {
		t.Errorf("CurrentDay = %d, want >= 2", got.CurrentDay)
	}
}

// D7-c: captureDaySnapshot must record the current expedition's day,
// supplies, and threat plus the character's live HP into res.DaySnapshots.
// Used by RunExpedition to trace state at every day rollover for
// long-expedition baselining; verified here in isolation since
// RunExpedition end-to-end isn't covered by package tests.
func TestSimRunner_CaptureDaySnapshot_PopulatesFields(t *testing.T) {
	setupZoneRunTestDB(t)
	uid := id.UserID("@sim-snap:example")
	campTestCharacter(t, uid, 3)
	defer cleanupExpeditions(uid)

	exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
		ExpeditionSupplies{Current: 12, Max: 20, DailyBurn: 2, HarshMod: 1})
	if err != nil {
		t.Fatal(err)
	}
	exp.CurrentDay = 3
	exp.ThreatLevel = 7
	if err := updateSupplies(exp.ID, exp.Supplies); err != nil {
		t.Fatal(err)
	}
	c, _ := LoadDnDCharacter(uid)

	sim := &SimRunner{P: &AdventurePlugin{}}
	res := &SimResult{Rooms: 9}
	sim.captureDaySnapshot(res, exp, uid)

	if len(res.DaySnapshots) != 1 {
		t.Fatalf("DaySnapshots len = %d, want 1", len(res.DaySnapshots))
	}
	snap := res.DaySnapshots[0]
	if snap.Day != 3 {
		t.Errorf("Day = %d, want 3", snap.Day)
	}
	if snap.Supplies != 12 {
		t.Errorf("Supplies = %v, want 12", snap.Supplies)
	}
	if snap.Threat != 7 {
		t.Errorf("Threat = %d, want 7", snap.Threat)
	}
	if snap.Rooms != 9 {
		t.Errorf("Rooms = %d, want 9", snap.Rooms)
	}
	if snap.HPCurrent != c.HPCurrent || snap.HPMax != c.HPMax {
		t.Errorf("HP = %d/%d, want %d/%d", snap.HPCurrent, snap.HPMax, c.HPCurrent, c.HPMax)
	}
}
