package plugin

import (
	"io"
	"os"
	"path/filepath"
	"testing"

	"gogobee/internal/db"
	"maunium.net/go/mautrix/id"
)

func setupAbilitiesTestDB(t *testing.T) {
	t.Helper()
	src := "/home/reala-misaki/git/gogobee/data/gogobee.db"
	if _, err := os.Stat(src); err != nil {
		t.Skip("prod db not present")
	}
	dir := t.TempDir()
	dst := filepath.Join(dir, "gogobee.db")
	in, _ := os.Open(src)
	defer in.Close()
	out, _ := os.Create(dst)
	defer out.Close()
	io.Copy(out, in)

	db.Close()
	if err := db.Init(dir); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(db.Close)
}

func TestActiveAbilityCatalogue(t *testing.T) {
	required := map[string]DnDClass{
		"second_wind":   ClassFighter,
		"magic_missile": ClassMage,
		"healing_word":  ClassCleric,
	}
	for id, class := range required {
		ab, ok := dndActiveAbilities[id]
		if !ok {
			t.Errorf("missing active ability: %s", id)
			continue
		}
		if ab.Class != class {
			t.Errorf("%s class = %s, want %s", id, ab.Class, class)
		}
		if ab.Apply == nil {
			t.Errorf("%s has nil Apply func", id)
		}
	}
}

func TestParseAbility(t *testing.T) {
	cases := []struct {
		in string
		id string
		ok bool
	}{
		{"second_wind", "second_wind", true},
		{"second wind", "second_wind", true},
		{"second-wind", "second_wind", true},
		{"Second Wind", "second_wind", true},
		{"magic_missile", "magic_missile", true},
		{"frostbolt", "", false},
	}
	for _, c := range cases {
		ab, ok := parseAbility(c.in)
		if ok != c.ok {
			t.Errorf("parseAbility(%q) ok = %v, want %v", c.in, ok, c.ok)
		}
		if ok && ab.ID != c.id {
			t.Errorf("parseAbility(%q) id = %q, want %q", c.in, ab.ID, c.id)
		}
	}
}

func TestClassResourceMax(t *testing.T) {
	cases := []struct {
		class DnDClass
		typ   string
		max   int
	}{
		{ClassFighter, "stamina", 3},
		{ClassRogue, "focus", 2},
		{ClassMage, "spell_slot", 1},
		{ClassCleric, "favor", 3},
		{ClassRanger, "focus", 2},
	}
	for _, c := range cases {
		typ, max := classResourceMax(c.class)
		if typ != c.typ || max != c.max {
			t.Errorf("%s: got (%s, %d), want (%s, %d)", c.class, typ, max, c.typ, c.max)
		}
	}
}

func TestInitResources_Idempotent(t *testing.T) {
	setupAbilitiesTestDB(t)
	uid := id.UserID("@init_res:example")

	if err := initResources(uid, ClassFighter); err != nil {
		t.Fatal(err)
	}
	cur, max, _ := getResource(uid, "stamina")
	if cur != 3 || max != 3 {
		t.Errorf("post-init: cur=%d max=%d, want 3/3", cur, max)
	}

	// Second call should not bump.
	if err := initResources(uid, ClassFighter); err != nil {
		t.Fatal(err)
	}
	cur2, _, _ := getResource(uid, "stamina")
	if cur2 != cur {
		t.Errorf("init not idempotent: cur went %d → %d", cur, cur2)
	}
}

func TestSpendAndRefreshResource(t *testing.T) {
	setupAbilitiesTestDB(t)
	uid := id.UserID("@spend_test:example")
	if err := initResources(uid, ClassFighter); err != nil {
		t.Fatal(err)
	}

	for i := 0; i < 3; i++ {
		ok, err := spendResource(uid, "stamina", 1)
		if err != nil || !ok {
			t.Fatalf("spend %d failed: ok=%v err=%v", i, ok, err)
		}
	}
	// 4th spend should fail (drained).
	ok, _ := spendResource(uid, "stamina", 1)
	if ok {
		t.Error("4th spend succeeded; should drain at 0")
	}

	if err := refreshAllResources(uid); err != nil {
		t.Fatal(err)
	}
	cur, max, _ := getResource(uid, "stamina")
	if cur != max || cur != 3 {
		t.Errorf("post-refresh: cur=%d max=%d, want 3/3", cur, max)
	}
}

func TestArmAbilityForFight_FighterSecondWind(t *testing.T) {
	setupAbilitiesTestDB(t)
	uid := id.UserID("@arm_fighter:example")
	c := &DnDCharacter{
		UserID: uid, Race: RaceHuman, Class: ClassFighter, Level: 5,
		STR: 16, DEX: 12, CON: 14, INT: 8, WIS: 10, CHA: 12,
		ArmedAbility: "second_wind",
	}
	c.HPMax = computeMaxHP(c.Class, abilityModifier(c.CON), c.Level)
	c.HPCurrent = c.HPMax
	c.ArmorClass = computeAC(c.Class, abilityModifier(c.DEX))
	if err := SaveDnDCharacter(c); err != nil {
		t.Fatal(err)
	}

	mods := CombatModifiers{}
	name, fired := armAbilityForFight(c, &mods)
	if !fired {
		t.Fatal("ability did not fire")
	}
	if name != "Second Wind" {
		t.Errorf("name = %q, want Second Wind", name)
	}
	wantHeal := 5 + c.Level // 5 + 5 = 10
	if mods.HealItem != wantHeal {
		t.Errorf("HealItem = %d, want %d", mods.HealItem, wantHeal)
	}

	// Armed flag cleared, persisted.
	got, _ := LoadDnDCharacter(uid)
	if got.ArmedAbility != "" {
		t.Errorf("ArmedAbility not cleared: %q", got.ArmedAbility)
	}
}

func TestArmAbilityForFight_MageMagicMissile(t *testing.T) {
	setupAbilitiesTestDB(t)
	uid := id.UserID("@arm_mage:example")
	c := &DnDCharacter{
		UserID: uid, Class: ClassMage, Race: RaceHuman, Level: 1,
		INT: 14, ArmedAbility: "magic_missile", HPMax: 10, HPCurrent: 10,
	}
	if err := SaveDnDCharacter(c); err != nil {
		t.Fatal(err)
	}
	mods := CombatModifiers{}
	_, fired := armAbilityForFight(c, &mods)
	if !fired {
		t.Fatal("magic missile did not fire")
	}
	wantDmg := 9 + abilityModifier(c.INT) // 9 + 2 = 11
	if mods.FlatDmgStart != wantDmg {
		t.Errorf("FlatDmgStart = %d, want %d", mods.FlatDmgStart, wantDmg)
	}
}

func TestArmAbilityForFight_NoArm(t *testing.T) {
	c := &DnDCharacter{Class: ClassFighter, ArmedAbility: ""}
	mods := CombatModifiers{}
	_, fired := armAbilityForFight(c, &mods)
	if fired {
		t.Error("fired with no armed ability")
	}
}

// TestArmCommand_FullFlow integrates !arm via the handler: spends resource,
// sets armed_ability, surfaces in combat, clears.
func TestArmCommand_FullFlow(t *testing.T) {
	setupAbilitiesTestDB(t)
	uid := id.UserID("@arm_flow:example")

	if err := createAdvCharacter(uid, "arm_flow"); err != nil {
		t.Fatal(err)
	}
	c := &DnDCharacter{
		UserID: uid, Race: RaceHuman, Class: ClassFighter, Level: 3,
		STR: 16, DEX: 13, CON: 14, INT: 8, WIS: 10, CHA: 12,
	}
	c.HPMax = computeMaxHP(c.Class, abilityModifier(c.CON), c.Level)
	c.HPCurrent = c.HPMax
	c.ArmorClass = computeAC(c.Class, abilityModifier(c.DEX))
	if err := SaveDnDCharacter(c); err != nil {
		t.Fatal(err)
	}
	if err := initResources(uid, c.Class); err != nil {
		t.Fatal(err)
	}

	p := &AdventurePlugin{}
	if err := p.handleDnDArmCmd(MessageContext{Sender: uid}, "second_wind"); err != nil {
		t.Fatal(err)
	}

	got, _ := LoadDnDCharacter(uid)
	if got.ArmedAbility != "second_wind" {
		t.Errorf("ArmedAbility = %q, want second_wind", got.ArmedAbility)
	}
	cur, max, _ := getResource(uid, "stamina")
	if cur != 2 || max != 3 {
		t.Errorf("stamina = %d/%d, want 2/3", cur, max)
	}

	// Trying to arm again with one already armed should fail (without
	// changing state).
	if err := p.handleDnDArmCmd(MessageContext{Sender: uid}, "second_wind"); err != nil {
		t.Fatal(err)
	}
	got2, _ := LoadDnDCharacter(uid)
	if got2.ArmedAbility != "second_wind" {
		t.Errorf("double-arm changed state: %q", got2.ArmedAbility)
	}
	cur2, _, _ := getResource(uid, "stamina")
	if cur2 != 2 {
		t.Errorf("double-arm spent extra resource: cur=%d", cur2)
	}
}
