package plugin

import (
	"testing"
)

// ── Damage die parser ──────────────────────────────────────────────────────

func TestParseDamageDie(t *testing.T) {
	cases := []struct {
		s            string
		count, sides int
		ok           bool
	}{
		{"1d8", 1, 8, true},
		{"2d6", 2, 6, true},
		{"1D10", 1, 10, true},
		{"  1d12  ", 1, 12, true},
		{"d8", 0, 0, false},  // no count
		{"1d", 0, 0, false},  // no sides
		{"1d1", 0, 0, false}, // sides < 2
		{"banana", 0, 0, false},
		{"", 0, 0, false},
	}
	for _, c := range cases {
		count, sides, ok := parseDamageDie(c.s)
		if ok != c.ok || count != c.count || sides != c.sides {
			t.Errorf("parseDamageDie(%q) = (%d,%d,%v); want (%d,%d,%v)",
				c.s, count, sides, ok, c.count, c.sides, c.ok)
		}
	}
}

// ── Registry coverage ──────────────────────────────────────────────────────

func TestWeaponRegistryComplete(t *testing.T) {
	// Spot-check that key weapons from appendix §2 + §3 are present and
	// have the right damage dice. If any of these fail, the registry has
	// drifted from the appendix.
	cases := map[string]struct {
		count, sides int
		dmgType      string
	}{
		"wpn_dagger":         {1, 4, "piercing"},
		"wpn_longsword":      {1, 8, "slashing"},
		"wpn_greatsword":     {2, 6, "slashing"},
		"wpn_greataxe":       {1, 12, "slashing"},
		"wpn_maul":           {2, 6, "bludgeoning"},
		"wpn_quarterstaff":   {1, 6, "bludgeoning"},
		"wpn_shortbow":       {1, 6, "piercing"},
		"wpn_longbow":        {1, 8, "piercing"},
		"wpn_crossbow_heavy": {1, 10, "piercing"},
	}
	for id, want := range cases {
		w := weaponByID(id)
		if w == nil {
			t.Errorf("missing weapon %s", id)
			continue
		}
		if w.DamageCount != want.count || w.DamageSides != want.sides {
			t.Errorf("%s damage = %dd%d, want %dd%d", id, w.DamageCount, w.DamageSides, want.count, want.sides)
		}
		if w.DamageType != want.dmgType {
			t.Errorf("%s damage type = %s, want %s", id, w.DamageType, want.dmgType)
		}
	}
}

func TestVersatileWeaponsHaveLargerDie(t *testing.T) {
	for _, id := range []string{"wpn_quarterstaff", "wpn_spear", "wpn_battleaxe", "wpn_longsword", "wpn_warhammer", "wpn_trident"} {
		w := weaponByID(id)
		if w == nil {
			t.Errorf("missing %s", id)
			continue
		}
		if !w.HasProperty(PropVersatile) {
			t.Errorf("%s should be versatile", id)
		}
		if w.VersaSides <= w.DamageSides {
			t.Errorf("%s versa die (%dd%d) not larger than one-handed (%dd%d)",
				id, w.VersaCount, w.VersaSides, w.DamageCount, w.DamageSides)
		}
	}
}

func TestArmorRegistryAppendixValues(t *testing.T) {
	// Direct values from appendix §5.
	cases := map[string]struct {
		baseAC      int
		armorType   ArmorType
		maxDexBonus int
		strReq      int
		stealth     bool
	}{
		"arm_padded":      {11, ArmorTypeLight, -1, 0, true},
		"arm_leather":     {11, ArmorTypeLight, -1, 0, false},
		"arm_studded":     {12, ArmorTypeLight, -1, 0, false},
		"arm_chain_shirt": {13, ArmorTypeMedium, 2, 0, false},
		"arm_scale_mail":  {14, ArmorTypeMedium, 2, 0, true},
		"arm_breastplate": {14, ArmorTypeMedium, 2, 0, false},
		"arm_half_plate":  {15, ArmorTypeMedium, 2, 0, true},
		"arm_chain_mail":  {16, ArmorTypeHeavy, 0, 13, true},
		"arm_splint":      {17, ArmorTypeHeavy, 0, 15, true},
		"arm_plate":       {18, ArmorTypeHeavy, 0, 15, true},
	}
	for id, want := range cases {
		a := armorByID(id)
		if a == nil {
			t.Errorf("missing %s", id)
			continue
		}
		if a.BaseAC != want.baseAC || a.Type != want.armorType ||
			a.MaxDEXBonus != want.maxDexBonus || a.STRRequire != want.strReq ||
			a.StealthDisad != want.stealth {
			t.Errorf("%s mismatch: got base=%d type=%d dex=%d str=%d stealth=%v; want %+v",
				id, a.BaseAC, a.Type, a.MaxDEXBonus, a.STRRequire, a.StealthDisad, want)
		}
	}
}

// ── ComputeAC math ──────────────────────────────────────────────────────────

func TestComputeArmorAC_Unarmored(t *testing.T) {
	// Unarmored, DEX +3 → 13
	if got := computeArmorAC(nil, nil, 3); got != 13 {
		t.Errorf("unarmored DEX+3 AC = %d, want 13", got)
	}
	// Unarmored + shield, DEX +0 → 12
	if got := computeArmorAC(nil, armorByID("arm_shield"), 0); got != 12 {
		t.Errorf("unarmored + shield DEX+0 AC = %d, want 12", got)
	}
}

func TestComputeArmorAC_Light(t *testing.T) {
	leather := armorByID("arm_leather")
	// Leather (11) + DEX +4 → 15
	if got := computeArmorAC(leather, nil, 4); got != 15 {
		t.Errorf("leather DEX+4 AC = %d, want 15", got)
	}
	// Studded (12) + DEX +5 → 17 (light has no cap)
	if got := computeArmorAC(armorByID("arm_studded"), nil, 5); got != 17 {
		t.Errorf("studded DEX+5 AC = %d, want 17", got)
	}
}

func TestComputeArmorAC_MediumCapped(t *testing.T) {
	// Half plate (15) + DEX +4 → 17 (capped at +2)
	if got := computeArmorAC(armorByID("arm_half_plate"), nil, 4); got != 17 {
		t.Errorf("half plate DEX+4 AC = %d, want 17 (cap)", got)
	}
	// Chain shirt (13) + DEX +1 → 14 (under cap)
	if got := computeArmorAC(armorByID("arm_chain_shirt"), nil, 1); got != 14 {
		t.Errorf("chain shirt DEX+1 AC = %d, want 14", got)
	}
}

func TestComputeArmorAC_HeavyIgnoresDex(t *testing.T) {
	// Plate (18) + DEX +5 → 18 (no DEX)
	if got := computeArmorAC(armorByID("arm_plate"), nil, 5); got != 18 {
		t.Errorf("plate DEX+5 AC = %d, want 18 (no DEX)", got)
	}
	// Chain mail (16) + shield + DEX -1 → 18
	if got := computeArmorAC(armorByID("arm_chain_mail"), armorByID("arm_shield"), -1); got != 18 {
		t.Errorf("chain mail + shield AC = %d, want 18", got)
	}
}

func TestComputeArmorAC_MagicBonus(t *testing.T) {
	plate := *armorByID("arm_plate")
	plate.MagicBonus = 2
	// +2 plate (18 + 2) → 20
	if got := computeArmorAC(&plate, nil, 0); got != 20 {
		t.Errorf("+2 plate AC = %d, want 20", got)
	}
}

// ── Weapon damage rolls ─────────────────────────────────────────────────────

func TestRollWeaponDamage_Bounds(t *testing.T) {
	greatsword := weaponByID("wpn_greatsword") // 2d6
	for i := 0; i < 1000; i++ {
		total, dice := rollWeaponDamage(nil, greatsword, 3, false)
		if dice < 2 || dice > 12 {
			t.Fatalf("greatsword dice out of [2,12]: %d", dice)
		}
		if total != dice+3 {
			t.Fatalf("total %d != dice %d + mod 3", total, dice)
		}
	}
}

func TestRollWeaponDamage_VersatileTwoHanded(t *testing.T) {
	longsword := weaponByID("wpn_longsword") // 1d8 / 1d10
	// One-handed
	for i := 0; i < 100; i++ {
		_, dice := rollWeaponDamage(nil, longsword, 0, false)
		if dice < 1 || dice > 8 {
			t.Errorf("longsword 1H dice = %d, want [1,8]", dice)
		}
	}
	// Two-handed (versatile)
	for i := 0; i < 100; i++ {
		_, dice := rollWeaponDamage(nil, longsword, 0, true)
		if dice < 1 || dice > 10 {
			t.Errorf("longsword 2H dice = %d, want [1,10]", dice)
		}
	}
}

func TestRollWeaponDamage_FloorAt1(t *testing.T) {
	// Pathological: 1d4 with -10 mod always rolls 1+(-10) = negative, floored to 1.
	dagger := weaponByID("wpn_dagger")
	for i := 0; i < 100; i++ {
		total, _ := rollWeaponDamage(nil, dagger, -10, false)
		if total < 1 {
			t.Errorf("damage floor violated: %d", total)
		}
	}
}

func TestAvgWeaponDamage(t *testing.T) {
	// 1d8 + 0 mean = 4.5
	got := avgWeaponDamage(weaponByID("wpn_longsword"), 0)
	if got != 4.5 {
		t.Errorf("longsword avg = %v, want 4.5", got)
	}
	// 2d6 + 3 mean = 7 + 3 = 10
	got = avgWeaponDamage(weaponByID("wpn_greatsword"), 3)
	if got != 10.0 {
		t.Errorf("greatsword+3 avg = %v, want 10", got)
	}
}

// ── Class proficiency ──────────────────────────────────────────────────────

func TestClassWeaponProficiency(t *testing.T) {
	cases := []struct {
		class DnDClass
		wpnID string
		want  bool
	}{
		// Simple weapons: everyone proficient
		{ClassMage, "wpn_dagger", true},
		{ClassCleric, "wpn_mace", true},
		{ClassRogue, "wpn_quarterstaff", true},
		// Martial: Fighter/Ranger always
		{ClassFighter, "wpn_greatsword", true},
		{ClassRanger, "wpn_longbow", true},
		// Mage/Cleric: NOT proficient with martial
		{ClassMage, "wpn_longsword", false},
		{ClassCleric, "wpn_greatsword", false},
		// Rogue: restricted martial list
		{ClassRogue, "wpn_shortsword", true},
		{ClassRogue, "wpn_rapier", true},
		{ClassRogue, "wpn_longsword", true},
		{ClassRogue, "wpn_crossbow_hand", true},
		{ClassRogue, "wpn_greatsword", false},
		{ClassRogue, "wpn_warhammer", false},
	}
	for _, c := range cases {
		w := weaponByID(c.wpnID)
		if got := dndClassWeaponProficiency(c.class, w); got != c.want {
			t.Errorf("class=%s wpn=%s = %v, want %v", c.class, c.wpnID, got, c.want)
		}
	}
}

func TestClassArmorProficiency(t *testing.T) {
	cases := []struct {
		class   DnDClass
		armorID string
		want    bool
	}{
		{ClassFighter, "arm_plate", true},
		{ClassFighter, "arm_leather", true},
		{ClassFighter, "arm_shield", true},
		{ClassRanger, "arm_chain_shirt", true}, // medium
		{ClassRanger, "arm_plate", false},      // heavy
		{ClassCleric, "arm_chain_shirt", true},
		{ClassCleric, "arm_plate", false},
		{ClassCleric, "arm_shield", true},
		{ClassRogue, "arm_leather", true},
		{ClassRogue, "arm_chain_shirt", false}, // medium
		{ClassRogue, "arm_shield", false},
		{ClassMage, "arm_leather", false},
		{ClassMage, "arm_shield", false},
	}
	for _, c := range cases {
		a := armorByID(c.armorID)
		if got := dndClassArmorProficiency(c.class, a); got != c.want {
			t.Errorf("class=%s armor=%s = %v, want %v", c.class, c.armorID, got, c.want)
		}
	}
}

// ── Legacy synthesis ───────────────────────────────────────────────────────

func TestSynthesizeWeaponProfile_TierMapping(t *testing.T) {
	cases := []struct {
		name     string
		tier     int
		wantBase string
	}{
		{"Iron Club", 1, "wpn_club"},
		{"Wooden Dagger", 1, "wpn_dagger"},
		{"Steel Mace", 2, "wpn_mace"},
		{"Apprentice Staff", 2, "wpn_quarterstaff"},
		{"Iron Sword", 3, "wpn_longsword"},
		{"Hunter's Bow", 3, "wpn_shortbow"},
		{"Knight's Sword", 4, "wpn_longsword"},
		{"Greatsword", 5, "wpn_greatsword"},
		{"Battleaxe", 5, "wpn_greataxe"},
	}
	for _, c := range cases {
		eq := &AdvEquipment{Slot: SlotWeapon, Tier: c.tier, Name: c.name}
		w := synthesizeWeaponProfile(eq)
		if w == nil {
			t.Errorf("%s tier %d: synthesize returned nil", c.name, c.tier)
			continue
		}
		base := weaponByID(c.wantBase)
		if w.DamageCount != base.DamageCount || w.DamageSides != base.DamageSides {
			t.Errorf("%s tier %d: synthesized %dd%d, expected %dd%d (%s)",
				c.name, c.tier, w.DamageCount, w.DamageSides,
				base.DamageCount, base.DamageSides, c.wantBase)
		}
	}
}

func TestSynthesizeWeaponProfile_HighTierMagicBonus(t *testing.T) {
	eq := &AdvEquipment{Slot: SlotWeapon, Tier: 7, Name: "Legendary Sword"}
	w := synthesizeWeaponProfile(eq)
	if w == nil {
		t.Fatal("synthesize returned nil")
	}
	if w.MagicBonus != 2 { // tier 7 - 5 = +2
		t.Errorf("tier 7 magic bonus = %d, want 2", w.MagicBonus)
	}
}

func TestSynthesizeWeaponProfile_MasterworkPromotes(t *testing.T) {
	eq := &AdvEquipment{Slot: SlotWeapon, Tier: 2, Name: "Sword", Masterwork: true}
	w := synthesizeWeaponProfile(eq)
	if w.DamageSides < 6 {
		t.Errorf("masterwork promoted should be at least tier-5 base (1d8/2d6); got %dd%d",
			w.DamageCount, w.DamageSides)
	}
	if w.MagicBonus < 1 {
		t.Errorf("masterwork should grant +1 minimum, got +%d", w.MagicBonus)
	}
}

func TestSynthesizeWeaponProfile_NonWeaponSlotReturnsNil(t *testing.T) {
	eq := &AdvEquipment{Slot: SlotArmor, Tier: 3, Name: "Plate"}
	if got := synthesizeWeaponProfile(eq); got != nil {
		t.Errorf("non-weapon slot should return nil, got %+v", got)
	}
}

func TestSynthesizeArmorProfile_TierMapping(t *testing.T) {
	cases := []struct {
		tier       int
		wantBaseAC int
		wantType   ArmorType
	}{
		{1, 11, ArmorTypeLight},  // Padded
		{2, 11, ArmorTypeLight},  // Leather
		{3, 13, ArmorTypeMedium}, // Chain Shirt
		{4, 14, ArmorTypeMedium}, // Scale Mail
		{5, 15, ArmorTypeMedium}, // Half Plate
		{6, 18, ArmorTypeHeavy},  // Plate
	}
	for _, c := range cases {
		eq := &AdvEquipment{Slot: SlotArmor, Tier: c.tier, Name: "Armor"}
		a := synthesizeArmorProfile(eq)
		if a == nil {
			t.Errorf("tier %d: synth returned nil", c.tier)
			continue
		}
		if a.BaseAC != c.wantBaseAC || a.Type != c.wantType {
			t.Errorf("tier %d: got AC=%d type=%d, want AC=%d type=%d",
				c.tier, a.BaseAC, a.Type, c.wantBaseAC, c.wantType)
		}
	}
}

func TestSynthesizeShield_NameMatch(t *testing.T) {
	cases := []struct {
		slot EquipmentSlot
		name string
		want bool
	}{
		{SlotTool, "Iron Shield", true},
		{SlotTool, "Tower Shield", true},
		{SlotTool, "Pickaxe", false},
		{SlotTool, "", false},
		{SlotWeapon, "Shield", false}, // wrong slot
	}
	for _, c := range cases {
		eq := &AdvEquipment{Slot: c.slot, Name: c.name, Tier: 3}
		got := synthesizeShield(eq) != nil
		if got != c.want {
			t.Errorf("slot=%s name=%q: shield=%v, want %v", c.slot, c.name, got, c.want)
		}
	}
}

// ── pickWeaponAbilityMod ────────────────────────────────────────────────────

func TestPickWeaponAbilityMod(t *testing.T) {
	c := &DnDCharacter{STR: 16, DEX: 14, CON: 13, INT: 8, WIS: 10, CHA: 12}
	// STR mod = +3, DEX mod = +2.
	cases := []struct {
		wpnID string
		want  int
	}{
		{"wpn_greatsword", 3}, // melee non-finesse → STR
		{"wpn_longsword", 3},
		{"wpn_dagger", 3},  // finesse melee, but STR > DEX, picks STR
		{"wpn_longbow", 2}, // ranged → DEX
		{"wpn_shortbow", 2},
	}
	for _, tc := range cases {
		got := pickWeaponAbilityMod(weaponByID(tc.wpnID), c)
		if got != tc.want {
			t.Errorf("%s: ability mod = %d, want %d", tc.wpnID, got, tc.want)
		}
	}

	// Now with DEX > STR: finesse should pick DEX.
	c2 := &DnDCharacter{STR: 10, DEX: 18, CON: 14}
	if got := pickWeaponAbilityMod(weaponByID("wpn_dagger"), c2); got != 4 {
		t.Errorf("DEX-favored finesse: got %d, want 4 (DEX +4)", got)
	}
	if got := pickWeaponAbilityMod(weaponByID("wpn_greatsword"), c2); got != 0 {
		t.Errorf("non-finesse with DEX>STR: got %d, want 0 (STR +0)", got)
	}
}

// ── End-to-end: applyDnDEquipmentLayer ─────────────────────────────────────

func TestApplyDnDEquipmentLayer_FighterFullKit(t *testing.T) {
	c := &DnDCharacter{
		Race: RaceHuman, Class: ClassFighter, Level: 5,
		STR: 18, DEX: 14, CON: 16, INT: 10, WIS: 12, CHA: 8,
		ArmorClass: 10, // base
	}
	equip := map[EquipmentSlot]*AdvEquipment{
		SlotWeapon: {Slot: SlotWeapon, Tier: 5, Name: "Greatsword"},
		SlotArmor:  {Slot: SlotArmor, Tier: 6, Name: "Plate"},
	}
	stats := CombatStats{}
	applyDnDPlayerLayer(&stats, c)
	applyDnDEquipmentLayer(&stats, c, equip)

	if stats.Weapon == nil {
		t.Fatal("weapon not set")
	}
	if stats.Weapon.DamageCount != 1 || stats.Weapon.DamageSides != 12 {
		// Greatsword name + tier 5 → wpn_greataxe (1d12) per synth name match
		// (greatsword name routes through "axe" branch? No, "greatsword" doesn't
		// contain "axe". Let me check: tier 5 default else branch → wpn_greatsword (2d6).
		// Adjust test if synthesis differs.
		if !(stats.Weapon.DamageCount == 2 && stats.Weapon.DamageSides == 6) {
			t.Errorf("greatsword tier 5 weapon = %dd%d, expected 2d6 or 1d12",
				stats.Weapon.DamageCount, stats.Weapon.DamageSides)
		}
	}
	if !stats.WeaponProficient {
		t.Error("Fighter should be proficient with synthesized weapon")
	}
	// Plate (tier 6) → +1 plate, AC = 18+1 = 19; Phase 5-B player floor
	// adds +3, so combat-stat AC = 22.
	if stats.AC != 22 {
		t.Errorf("Fighter+plate+1 AC = %d, want 22 (19 base + phase5BACBonus)", stats.AC)
	}
	// Two-handed mode: greatsword has TwoHanded property (no shield).
	// Greatsword's properties include Heavy + TwoHanded — TwoHandedMode set.
	if !stats.TwoHandedMode {
		t.Error("expected TwoHandedMode for greatsword without shield")
	}
}

func TestApplyDnDEquipmentLayer_MageWithMartialWeapon(t *testing.T) {
	// Mage equips a longsword → not proficient, -4 attack penalty path.
	c := &DnDCharacter{
		Class: ClassMage, Level: 1, STR: 8, DEX: 14, CON: 12, INT: 16, WIS: 13, CHA: 10,
		ArmorClass: 10,
	}
	equip := map[EquipmentSlot]*AdvEquipment{
		SlotWeapon: {Slot: SlotWeapon, Tier: 3, Name: "Longsword"},
	}
	stats := CombatStats{}
	applyDnDPlayerLayer(&stats, c)
	applyDnDEquipmentLayer(&stats, c, equip)
	if stats.Weapon == nil {
		t.Fatal("weapon nil")
	}
	if stats.WeaponProficient {
		t.Error("Mage should NOT be proficient with longsword")
	}
}
