package plugin

import "testing"

// Phase 11 D1a — zone registry tests. Validates that:
//   1. Tier 1 zones are registered.
//   2. All enemy/boss BestiaryID references resolve.
//   3. Level gating (zonesForLevel) respects the 2-tier-above ceiling.
//   4. Loot tables either have explicit drop chances in [0,1] or
//      UniqueAlways set.

func TestZoneRegistry_Tier1Present(t *testing.T) {
	if _, ok := getZone(ZoneGoblinWarrens); !ok {
		t.Fatal("Goblin Warrens not registered")
	}
	if _, ok := getZone(ZoneCryptValdris); !ok {
		t.Fatal("Crypt of Valdris not registered")
	}
	t1 := zonesByTier(ZoneTierBeginner)
	if len(t1) != 2 {
		t.Fatalf("expected 2 Tier 1 zones, got %d", len(t1))
	}
}

func TestZoneRegistry_Tier2Present(t *testing.T) {
	if _, ok := getZone(ZoneForestShadows); !ok {
		t.Fatal("Forest of Shadows not registered")
	}
	if _, ok := getZone(ZoneSunkenTemple); !ok {
		t.Fatal("Sunken Temple not registered")
	}
	t2 := zonesByTier(ZoneTierApprentice)
	if len(t2) != 2 {
		t.Fatalf("expected 2 Tier 2 zones, got %d", len(t2))
	}
}

func TestZoneRegistry_Tier3Present(t *testing.T) {
	if _, ok := getZone(ZoneManorBlackspire); !ok {
		t.Fatal("Haunted Manor of Blackspire not registered")
	}
	if _, ok := getZone(ZoneUnderforge); !ok {
		t.Fatal("The Underforge not registered")
	}
	t3 := zonesByTier(ZoneTierJourneyman)
	if len(t3) != 2 {
		t.Fatalf("expected 2 Tier 3 zones, got %d", len(t3))
	}
}

func TestZoneRegistry_Tier4Present(t *testing.T) {
	if _, ok := getZone(ZoneUnderdark); !ok {
		t.Fatal("The Underdark not registered")
	}
	if _, ok := getZone(ZoneFeywildCrossing); !ok {
		t.Fatal("Feywild Crossing not registered")
	}
	t4 := zonesByTier(ZoneTierVeteran)
	if len(t4) != 2 {
		t.Fatalf("expected 2 Tier 4 zones, got %d", len(t4))
	}
}

func TestZoneRegistry_Tier5Present(t *testing.T) {
	if _, ok := getZone(ZoneDragonsLair); !ok {
		t.Fatal("Dragon's Lair not registered")
	}
	if _, ok := getZone(ZoneAbyssPortal); !ok {
		t.Fatal("The Abyss Portal not registered")
	}
	t5 := zonesByTier(ZoneTierLegendary)
	if len(t5) != 2 {
		t.Fatalf("expected 2 Tier 5 zones, got %d", len(t5))
	}
}

func TestZoneRegistry_BestiaryRefsResolve(t *testing.T) {
	for _, z := range allZones() {
		for _, e := range z.Enemies {
			if _, ok := dndBestiary[e.BestiaryID]; !ok {
				t.Errorf("zone %s enemy %s missing from bestiary", z.ID, e.BestiaryID)
			}
		}
		if _, ok := dndBestiary[z.Boss.BestiaryID]; !ok {
			t.Errorf("zone %s boss %s missing from bestiary", z.ID, z.Boss.BestiaryID)
		}
	}
}

func TestZoneRegistry_BossCRMatchesZoneTier(t *testing.T) {
	// Sanity: boss CR should be at least at the high end of tier
	// CR range. Tier 1 = CR 1/8–1 enemies, but boss is CR 3–5.
	for _, z := range allZones() {
		if z.Boss.CR < 1 {
			t.Errorf("zone %s boss CR %.1f too low", z.ID, z.Boss.CR)
		}
	}
}

func TestZonesForLevel_TierGate(t *testing.T) {
	// L1 player (tier 1): max tier visible = 1+2 = 3. With T1+T2+T3
	// registered (D1a + D3a + D4a), L1 sees all six.
	got := zonesForLevel(1)
	if len(got) != 6 {
		t.Fatalf("L1 should see 6 zones (2 T1 + 2 T2 + 2 T3), got %d", len(got))
	}
	// L20 player should also see all zones (no upper cutoff).
	got = zonesForLevel(20)
	if len(got) < 6 {
		t.Fatalf("L20 should see all zones, got %d", len(got))
	}
}

func TestZoneRegistry_LootDropChances(t *testing.T) {
	for _, z := range allZones() {
		for _, l := range z.Loot {
			if l.UniqueAlways {
				continue
			}
			if l.DropChance <= 0 || l.DropChance > 1 {
				t.Errorf("zone %s loot %s drop chance %.2f out of range",
					z.ID, l.ItemID, l.DropChance)
			}
		}
	}
}

func TestZoneRegistry_RoomCountSane(t *testing.T) {
	// Long-expedition plan §2 widens the bands per tier: T1 12–14 up to
	// T5 ~36–44. The guard floor stays at 5 (no zone should ever drop
	// below that) and the ceiling tracks the T6 post-game target of 44–52.
	for _, z := range allZones() {
		if z.MinRooms < 5 || z.MaxRooms > 52 || z.MinRooms > z.MaxRooms {
			t.Errorf("zone %s rooms %d-%d outside design (5-52, min<=max)",
				z.ID, z.MinRooms, z.MaxRooms)
		}
	}
}

func TestZoneRegistry_ElitesFlagged(t *testing.T) {
	// Each Tier 1 zone roster should contain at least one elite (per
	// design doc each zone lists ELITE entries).
	for _, z := range allZones() {
		anyElite := false
		for _, e := range z.Enemies {
			if e.IsElite {
				anyElite = true
				break
			}
		}
		if !anyElite {
			t.Errorf("zone %s has no elite enemy", z.ID)
		}
	}
}

func TestZoneRegistry_LevelRangeMatchesTier(t *testing.T) {
	// Tier→expected level range per design doc §2.
	expected := map[ZoneTier][2]int{
		ZoneTierBeginner:   {1, 3},
		ZoneTierApprentice: {3, 5},
		ZoneTierJourneyman: {5, 8},
		ZoneTierVeteran:    {8, 12},
		ZoneTierLegendary:  {12, 20},
		ZoneTierMythic:     {18, 20}, // post-game: gated by unlock, not level band
	}
	for _, z := range allZones() {
		want, ok := expected[z.Tier]
		if !ok {
			t.Errorf("zone %s has unknown tier %d", z.ID, z.Tier)
			continue
		}
		if z.LevelMin < want[0] || z.LevelMax > want[1] {
			t.Errorf("zone %s level %d-%d outside tier %d range %d-%d",
				z.ID, z.LevelMin, z.LevelMax, z.Tier, want[0], want[1])
		}
	}
}
