package plugin

import (
	"strings"
	"testing"
)

// ── Recipe registry coverage ─────────────────────────────────────────────────

// TestRecipeRegistry_IngredientsExistInResources — every ingredient name
// referenced by a §8.2 recipe must match a Name in the zone-resource
// registry, otherwise !craft can never find the inventory items.
func TestRecipeRegistry_IngredientsExistInResources(t *testing.T) {
	known := map[string]bool{}
	for _, list := range zoneResources {
		for _, r := range list {
			known[r.Name] = true
		}
	}
	for _, r := range thomCraftRecipes {
		// AlwaysKnown pity recipes consume zone-loot crafting anchors, not
		// gather-resource materials — validated separately below.
		if r.AlwaysKnown {
			continue
		}
		for ing := range r.Ingredients {
			if !known[ing] {
				t.Errorf("recipe %s references unknown ingredient %q", r.ID, ing)
			}
		}
	}
}

// TestPityRecipes_AnchorsAndOutputsResolve — the T6 signature-item pity
// recipes consume a zone-loot UniqueAlways crafting anchor and emit a real
// registry magic item. Validate both ends: every ingredient name matches a
// UniqueAlways zone-loot drop's inventory name, and every OutputSkillSource
// points at a magicItemRegistry ID.
func TestPityRecipes_AnchorsAndOutputsResolve(t *testing.T) {
	anchorNames := map[string]bool{}
	for _, z := range allZones() {
		for _, e := range z.Loot {
			if e.UniqueAlways {
				anchorNames[titleCaseUnderscored(e.ItemID)] = true
			}
		}
	}
	pity := 0
	for _, r := range thomCraftRecipes {
		if !r.AlwaysKnown {
			continue
		}
		pity++
		for ing := range r.Ingredients {
			if !anchorNames[ing] {
				t.Errorf("pity recipe %s ingredient %q is not a zone-loot anchor name", r.ID, ing)
			}
		}
		if r.OutputType != "magic_item" {
			t.Errorf("pity recipe %s OutputType = %q, want magic_item", r.ID, r.OutputType)
		}
		id, ok := strings.CutPrefix(r.OutputSkillSource, "magic_item:")
		if !ok {
			t.Errorf("pity recipe %s OutputSkillSource %q missing magic_item: prefix", r.ID, r.OutputSkillSource)
			continue
		}
		if _, ok := magicItemRegistry[id]; !ok {
			t.Errorf("pity recipe %s output %q not in magicItemRegistry", r.ID, id)
		}
	}
	if pity != 5 {
		t.Errorf("expected 5 T6 pity recipes, found %d", pity)
	}
}

// TestRecipeRegistry_OutputValuesMatchTier — outputs should land in §8.1
// rarity brackets given their tier (rare 100–300, very rare 500–1200,
// epic 500–1200 in §8.1 — recipes here lean tier 3–4, so 200..1200 is the
// reasonable band).
func TestRecipeRegistry_OutputValuesInBand(t *testing.T) {
	for _, r := range thomCraftRecipes {
		// Pity recipes emit Legendary magic items priced well above the §8.1
		// crafting band by design — excluded from this invariant.
		if r.AlwaysKnown {
			continue
		}
		if r.OutputValue < 200 || r.OutputValue > 1200 {
			t.Errorf("recipe %s output value %d outside band [200,1200] (tier %d)",
				r.ID, r.OutputValue, r.OutputTier)
		}
	}
}

// TestRecipeRegistry_AllExemplarsPresent — the four implementable §8.2
// exemplars must be in the registry. Drow Poison Blade is intentionally
// deferred (needs "any blade weapon" matching, lands in R6).
func TestRecipeRegistry_AllExemplarsPresent(t *testing.T) {
	want := []string{
		"potion_fire_resist",
		"mithral_dagger",
		"undead_ward_charm",
		"shadow_step_cloak",
	}
	have := map[string]bool{}
	for _, r := range thomCraftRecipes {
		have[r.ID] = true
	}
	for _, id := range want {
		if !have[id] {
			t.Errorf("missing exemplar recipe: %s", id)
		}
	}
}

// ── findRecipeByName ─────────────────────────────────────────────────────────

func TestFindRecipeByName_FuzzyMatch(t *testing.T) {
	cases := []struct {
		query string
		want  string
		ok    bool
	}{
		{"mithral", "mithral_dagger", true},
		{"shadow step", "shadow_step_cloak", true},
		{"Potion of Fire", "potion_fire_resist", true},
		{"nonexistent", "", false},
		{"", "", false},
	}
	for _, c := range cases {
		got, ok := findRecipeByName(c.query)
		if ok != c.ok {
			t.Errorf("query %q: ok=%v want %v", c.query, ok, c.ok)
			continue
		}
		if ok && got.ID != c.want {
			t.Errorf("query %q: got %s want %s", c.query, got.ID, c.want)
		}
	}
}

// ── pickIngredients ──────────────────────────────────────────────────────────

func TestPickIngredients_ExactCover(t *testing.T) {
	items := []AdvItem{
		{ID: 1, Name: "Salamander Oil", Type: "material"},
		{ID: 2, Name: "Fire Essence", Type: "material"},
		{ID: 3, Name: "Fire Essence", Type: "material"},
		{ID: 4, Name: "Scrap Iron", Type: "material"},
	}
	need := map[string]int{"Salamander Oil": 1, "Fire Essence": 2}
	consume, missing := pickIngredients(items, need)
	if len(missing) != 0 {
		t.Errorf("missing should be empty, got %v", missing)
	}
	if len(consume) != 3 {
		t.Errorf("consume len = %d want 3", len(consume))
	}
}

func TestPickIngredients_Short(t *testing.T) {
	items := []AdvItem{
		{ID: 1, Name: "Fire Essence", Type: "material"},
	}
	need := map[string]int{"Fire Essence": 2, "Salamander Oil": 1}
	consume, missing := pickIngredients(items, need)
	if missing["Fire Essence"] != 1 {
		t.Errorf("missing Fire Essence = %d want 1", missing["Fire Essence"])
	}
	if missing["Salamander Oil"] != 1 {
		t.Errorf("missing Salamander Oil = %d want 1", missing["Salamander Oil"])
	}
	// We still consumed the one match optimistically; caller checks missing.
	if len(consume) != 1 {
		t.Errorf("consume len = %d want 1", len(consume))
	}
}

// ── filterSellable ───────────────────────────────────────────────────────────

func TestFilterSellable_ExcludesEquipment(t *testing.T) {
	items := []AdvItem{
		{Name: "Fire Essence", Type: "material"},
		{Name: "Shadow Trout", Type: "fish"},
		{Name: "Cursed Trinket", Type: "item"},
		{Name: "Mithril Sword", Type: "MasterworkGear"},
		{Name: "Arena Helm", Type: "ArenaGear"},
		{Name: "Healing Potion", Type: "consumable"},
		{Name: "Lucky Card", Type: "card"},
	}
	out := filterSellable(items)
	if len(out) != 3 {
		t.Errorf("filterSellable len = %d want 3 (material/fish/item only)", len(out))
	}
	for _, it := range out {
		switch it.Type {
		case "material", "fish", "item":
		default:
			t.Errorf("unexpected type in sellable: %s", it.Type)
		}
	}
}

// ── findInventoryMatch ──────────────────────────────────────────────────────

func TestFindInventoryMatch_FuzzyAndMiss(t *testing.T) {
	items := []AdvItem{
		{ID: 1, Name: "Salamander Oil"},
		{ID: 2, Name: "Fire Essence"},
	}
	if m := findInventoryMatch(items, "salamander"); m == nil || m.ID != 1 {
		t.Errorf("want match on Salamander Oil")
	}
	if m := findInventoryMatch(items, "FIRE"); m == nil || m.ID != 2 {
		t.Errorf("case-insensitive match should hit Fire Essence")
	}
	if m := findInventoryMatch(items, "mithral"); m != nil {
		t.Errorf("expected no match for 'mithral'")
	}
}

// ── inventoryByName ─────────────────────────────────────────────────────────

func TestInventoryByName_Counts(t *testing.T) {
	items := []AdvItem{
		{Name: "Fire Essence"}, {Name: "Fire Essence"}, {Name: "Fire Essence"},
		{Name: "Salamander Oil"},
	}
	got := inventoryByName(items)
	if got["Fire Essence"] != 3 || got["Salamander Oil"] != 1 {
		t.Errorf("counts off: %v", got)
	}
}

// ── Recipe book rendering ───────────────────────────────────────────────────

// TestRenderRecipeBook_HidesUndiscovered — discovery state must hide
// recipe details until they're known.
func TestRenderRecipeBook_HidesUndiscovered(t *testing.T) {
	known := map[string]bool{}
	out := renderRecipeBookForTest(known, nil)
	for _, r := range thomCraftRecipes {
		if strings.Contains(out, r.Name) {
			t.Errorf("undiscovered recipe %s leaked into book", r.Name)
		}
	}
	if !strings.Contains(out, "undiscovered") {
		t.Errorf("expected hidden-recipe count in output:\n%s", out)
	}

	known = map[string]bool{"mithral_dagger": true}
	out = renderRecipeBookForTest(known, nil)
	if !strings.Contains(out, "Mithral Dagger") {
		t.Errorf("known recipe Mithral Dagger missing:\n%s", out)
	}
	if strings.Contains(out, "Potion of Fire Resistance") {
		t.Errorf("undiscovered recipe should be hidden")
	}
}

// renderRecipeBookForTest is a DB-free version of renderRecipeBook for
// unit tests. It bypasses loadAdvInventory by accepting items directly.
func renderRecipeBookForTest(known map[string]bool, items []AdvItem) string {
	var sb strings.Builder
	sb.WriteString("📜 **Thom Krooke — Recipe Book**\n\n")
	have := inventoryByName(items)
	knownCount := 0
	for _, r := range thomCraftRecipes {
		if !known[r.ID] {
			continue
		}
		knownCount++
		sb.WriteString(r.Name + "\n")
		var parts []string
		for ing, need := range r.Ingredients {
			parts = append(parts, ing)
			_ = need
			_ = have
		}
		sb.WriteString(strings.Join(parts, " + ") + "\n")
	}
	hidden := len(thomCraftRecipes) - knownCount
	if hidden > 0 {
		sb.WriteString("undiscovered\n")
	}
	return sb.String()
}
