package plugin

import "testing"

func TestAbyssPortalGraph_Registered(t *testing.T) {
	g, ok := zoneGraphRegistry[ZoneAbyssPortal]
	if !ok {
		t.Fatal("zoneAbyssPortalGraph not registered")
	}
	// Long-expedition D1-e widened this zone from 13 → 51 nodes so the
	// longest entry→boss walk lands in the T5 [36,44] traversal band.
	if len(g.Nodes) != 51 {
		t.Errorf("nodes = %d, want 51", len(g.Nodes))
	}
}

func TestAbyssPortalGraph_LongestPathInBand(t *testing.T) {
	g := zoneAbyssPortalGraph()
	got := graphLongestPath(g)
	if got < 36 || got > 44 {
		t.Errorf("longest path = %d, want in T5 band [36,44]", got)
	}
}

// TestAbyssPortalGraph_AllNodesHaveRegion confirms D1-e backfilled the
// missing RegionID authoring per dnd_expedition_region.go: every node
// carries a non-empty RegionID matching the registry.
func TestAbyssPortalGraph_AllNodesHaveRegion(t *testing.T) {
	g := zoneAbyssPortalGraph()
	validRegions := map[string]bool{
		"abyss_outer_rift":     true,
		"abyss_demon_assembly": true,
		"abyss_wardens_post":   true,
		"abyss_the_tear":       true,
	}
	for id, n := range g.Nodes {
		if n.RegionID == "" {
			t.Errorf("node %s has empty RegionID — D1-e requires region authoring on every node", id)
		}
		if !validRegions[n.RegionID] {
			t.Errorf("node %s RegionID = %q, not in dnd_expedition_region.go registry", id, n.RegionID)
		}
	}
}

// TestAbyssPortalGraph_AllFourRegionsRepresented confirms each authored
// region has at least one node.
func TestAbyssPortalGraph_AllFourRegionsRepresented(t *testing.T) {
	g := zoneAbyssPortalGraph()
	regions := map[string]int{}
	for _, n := range g.Nodes {
		regions[n.RegionID]++
	}
	for _, r := range []string{
		"abyss_outer_rift",
		"abyss_demon_assembly",
		"abyss_wardens_post",
		"abyss_the_tear",
	} {
		if regions[r] == 0 {
			t.Errorf("region %q has no nodes — multi-region invariant broken", r)
		}
	}
}

// TestAbyssPortalGraph_ThreeSequentialForks confirms the design shape:
// three fork nodes in series, each binary or ternary.
func TestAbyssPortalGraph_ThreeSequentialForks(t *testing.T) {
	g := zoneAbyssPortalGraph()
	wants := map[string]int{
		"abyss_portal.fork1": 2,
		"abyss_portal.fork2": 2,
		"abyss_portal.fork3": 3,
	}
	for id, want := range wants {
		if got := len(g.outgoingEdges(id)); got != want {
			t.Errorf("%s outgoing = %d, want %d", id, got, want)
		}
	}
}

func TestAbyssPortalGraph_AllCapstoneLeavesReachBoss(t *testing.T) {
	g := zoneAbyssPortalGraph()
	for _, leaf := range []string{
		"abyss_portal.direct_assault",
		"abyss_portal.usurper_throne",
		"abyss_portal.reality_seam",
	} {
		if !reachable(g, leaf, "abyss_portal.boss") {
			t.Errorf("%s unreachable to boss", leaf)
		}
	}
}

// TestAbyssPortalGraph_FullStatRosterCoverage confirms the
// project-wide claim: by G8h, all six abilities (STR/DEX/CON/INT/WIS/
// CHA) appear as authored stat-check locks across shipping zones.
// CON is the missing one prior to this zone — locked here on
// mind_corridor.
func TestAbyssPortalGraph_FullStatRosterCoverage(t *testing.T) {
	g := zoneAbyssPortalGraph()
	conSeen := false
	for _, outs := range g.Edges {
		for _, e := range outs {
			if e.Lock == LockStatCheck && lockDataString(e.LockData, "stat") == "CON" {
				conSeen = true
			}
		}
	}
	if !conSeen {
		t.Error("expected at least one CON stat-check edge — completes ability roster by G8h")
	}
}

func TestAbyssPortalGraph_RealitySeamHighestBias(t *testing.T) {
	g := zoneAbyssPortalGraph()
	seam := g.Nodes["abyss_portal.reality_seam"]
	if seam.Content.LootBias < 3.0 {
		t.Errorf("reality_seam LootBias = %v, want >= 3.0 (Abyss capstone)", seam.Content.LootBias)
	}
}

// TestAbyssPortalGraph_D10Anchors verifies the D10 anchor-variety pass.
// The Abyss shipped with no Trap node and a single fork2 Elite, so D10
// adds two branch Traps (Hush Corridor, Seam Threshold) and a main-path
// region-guardian Elite (Warden's Hall). Counts: 2 traps, 2 elites.
func TestAbyssPortalGraph_D10Anchors(t *testing.T) {
	g := zoneAbyssPortalGraph()
	var trapCount, eliteCount int
	for _, n := range g.Nodes {
		switch n.Kind {
		case NodeKindTrap:
			trapCount++
		case NodeKindElite:
			eliteCount++
		}
	}
	if trapCount != 2 {
		t.Errorf("trap nodes = %d, want 2 (D10 hush_corridor + seam_threshold)", trapCount)
	}
	if eliteCount != 2 {
		t.Errorf("elite nodes = %d, want 2 (vrock_aerie + D10 wardens_hall)", eliteCount)
	}
	if g.Nodes["abyss_portal.wardens_hall"].Kind != NodeKindElite {
		t.Error("D10: wardens_hall (R3 region-guardian) should be an Elite")
	}
	for _, id := range []string{"abyss_portal.hush_corridor", "abyss_portal.seam_threshold"} {
		if g.Nodes[id].Kind != NodeKindTrap {
			t.Errorf("D10: %s should be a Trap", id)
		}
	}
}
