package plugin

import (
	"strings"
	"testing"
)

func TestBuildLinearGraph_BasicShape(t *testing.T) {
	seq := []ZoneNodeKind{
		NodeKindEntry,
		NodeKindExploration,
		NodeKindElite,
		NodeKindBoss,
	}
	g := BuildLinearGraph("test_zone", seq)
	if err := validateZoneGraph(g); err != nil {
		t.Fatalf("linear graph should validate: %v", err)
	}
	if len(g.Nodes) != 4 {
		t.Fatalf("want 4 nodes, got %d", len(g.Nodes))
	}
	if g.Entry == "" || g.Boss == "" {
		t.Fatalf("entry/boss not set: entry=%q boss=%q", g.Entry, g.Boss)
	}
	if !reachable(g, g.Entry, g.Boss) {
		t.Fatalf("boss unreachable from entry")
	}
}

func TestBuildLinearGraph_Empty(t *testing.T) {
	g := BuildLinearGraph("empty", nil)
	if err := validateZoneGraph(g); err == nil {
		t.Fatalf("empty graph should fail validation")
	}
}

func TestValidateZoneGraph_OrphanNode(t *testing.T) {
	nodes := []ZoneNode{
		{NodeID: "e", Kind: NodeKindEntry, IsEntry: true},
		{NodeID: "b", Kind: NodeKindBoss, IsBoss: true},
		{NodeID: "orphan", Kind: NodeKindExploration},
	}
	edges := []ZoneEdge{{From: "e", To: "b"}}
	g := ZoneGraph{
		ZoneID: "z",
		Nodes:  map[string]ZoneNode{},
		Edges:  map[string][]ZoneEdge{},
	}
	for _, n := range nodes {
		g.Nodes[n.NodeID] = n
		if n.IsEntry {
			g.Entry = n.NodeID
		}
		if n.IsBoss {
			g.Boss = n.NodeID
		}
	}
	for _, e := range edges {
		g.Edges[e.From] = append(g.Edges[e.From], e)
	}
	err := validateZoneGraph(g)
	if err == nil || !strings.Contains(err.Error(), "orphan") {
		t.Fatalf("expected orphan error, got %v", err)
	}
}

func TestValidateZoneGraph_BossUnreachable(t *testing.T) {
	// b has an incoming self-loop (so it's not orphaned) but is not
	// reachable from entry — exercises the BFS reachability check.
	g := ZoneGraph{
		ZoneID: "z",
		Nodes: map[string]ZoneNode{
			"e": {NodeID: "e", Kind: NodeKindEntry, IsEntry: true},
			"b": {NodeID: "b", Kind: NodeKindBoss, IsBoss: true,
				Content: ZoneNodeContent{AllowSelfLoop: true}},
		},
		Edges: map[string][]ZoneEdge{
			"b": {{From: "b", To: "b"}},
		},
		Entry: "e",
		Boss:  "b",
	}
	err := validateZoneGraph(g)
	if err == nil || !strings.Contains(err.Error(), "not reachable") {
		t.Fatalf("expected unreachable error, got %v", err)
	}
}

func TestValidateZoneGraph_TwoEntries(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("expected panic on two entries")
		}
	}()
	BuildGraph("z",
		[]ZoneNode{
			{NodeID: "e1", Kind: NodeKindEntry, IsEntry: true},
			{NodeID: "e2", Kind: NodeKindEntry, IsEntry: true},
			{NodeID: "b", Kind: NodeKindBoss, IsBoss: true},
		},
		[]ZoneEdge{
			{From: "e1", To: "b"},
			{From: "e2", To: "b"},
		},
	)
}

func TestValidateZoneGraph_SelfLoopRejected(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("expected panic on self-loop without opt-in")
		}
	}()
	BuildGraph("z",
		[]ZoneNode{
			{NodeID: "e", Kind: NodeKindEntry, IsEntry: true},
			{NodeID: "b", Kind: NodeKindBoss, IsBoss: true},
		},
		[]ZoneEdge{
			{From: "e", To: "e"},
			{From: "e", To: "b"},
		},
	)
}

func TestValidateZoneGraph_SelfLoopAllowed(t *testing.T) {
	g := BuildGraph("z",
		[]ZoneNode{
			{NodeID: "e", Kind: NodeKindEntry, IsEntry: true,
				Content: ZoneNodeContent{AllowSelfLoop: true}},
			{NodeID: "b", Kind: NodeKindBoss, IsBoss: true},
		},
		[]ZoneEdge{
			{From: "e", To: "e"},
			{From: "e", To: "b"},
		},
	)
	if err := validateZoneGraph(g); err != nil {
		t.Fatalf("self-loop with opt-in should validate: %v", err)
	}
}

func TestBuildGraph_BranchedShape(t *testing.T) {
	g := BuildGraph("crypt",
		[]ZoneNode{
			{NodeID: "entry", Kind: NodeKindEntry, IsEntry: true},
			{NodeID: "fork", Kind: NodeKindFork},
			{NodeID: "left", Kind: NodeKindElite},
			{NodeID: "right", Kind: NodeKindExploration},
			{NodeID: "merge", Kind: NodeKindMerge},
			{NodeID: "boss", Kind: NodeKindBoss, IsBoss: true},
		},
		[]ZoneEdge{
			{From: "entry", To: "fork"},
			{From: "fork", To: "left", Weight: 1},
			{From: "fork", To: "right", Weight: 2,
				Lock: LockPerception, LockData: map[string]any{"dc": 12}},
			{From: "left", To: "merge"},
			{From: "right", To: "merge"},
			{From: "merge", To: "boss"},
		},
	)
	outs := g.outgoingEdges("fork")
	if len(outs) != 2 {
		t.Fatalf("want 2 fork edges, got %d", len(outs))
	}
	if outs[0].To != "left" {
		t.Fatalf("weight ordering broken: %v", outs)
	}
}

func TestCompileLegacyZoneGraph_AllRegistered(t *testing.T) {
	for _, z := range allZones() {
		g, ok := loadZoneGraph(z.ID)
		if !ok {
			t.Errorf("loadZoneGraph(%q): not ok", z.ID)
			continue
		}
		if err := validateZoneGraph(g); err != nil {
			t.Errorf("zone %q: legacy graph invalid: %v", z.ID, err)
		}
		if g.Entry == "" || g.Boss == "" {
			t.Errorf("zone %q: missing entry/boss", z.ID)
		}
		// Boss reachability is enforced by validate, but assert again for
		// clarity.
		if !reachable(g, g.Entry, g.Boss) {
			t.Errorf("zone %q: boss unreachable from entry", z.ID)
		}
	}
}

// TestZoneGraphs_NoSoftLockedFork guards the invariant that every fork
// node offers at least one traversable (LockNone) exit. A fork whose
// every edge is skill-locked strands any player who fails all the checks
// — fork rolls are deterministic with no retry. D8-f part 2 found
// feywild fork1 violating this (it stranded ~60% of runs). Catch any
// future author who locks every exit of a fork.
func TestZoneGraphs_NoSoftLockedFork(t *testing.T) {
	for _, z := range allZones() {
		g, ok := loadZoneGraph(z.ID)
		if !ok {
			continue
		}
		for nodeID, node := range g.Nodes {
			if node.Kind != NodeKindFork {
				continue
			}
			outs := g.outgoingEdges(nodeID)
			if len(outs) == 0 {
				continue
			}
			free := false
			for _, e := range outs {
				if e.Lock == LockNone || e.Lock == "" {
					free = true
					break
				}
			}
			if !free {
				t.Errorf("zone %q fork %q: every exit is locked — soft-lock (a player failing all checks is stranded; add a LockNone fallback)", z.ID, nodeID)
			}
		}
	}
}

func TestDeriveLegacyNodeID_StableShape(t *testing.T) {
	got := deriveLegacyNodeID("crypt_valdris", 0)
	want := "crypt_valdris.r1"
	if got != want {
		t.Fatalf("want %q, got %q", want, got)
	}
	got = deriveLegacyNodeID("crypt_valdris", 4)
	want = "crypt_valdris.r5"
	if got != want {
		t.Fatalf("want %q, got %q", want, got)
	}
}

func TestRegisterZoneGraph_DuplicateRejected(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("expected panic on duplicate registration")
		}
		// scrub registry side-effect from this test
		delete(zoneGraphRegistry, "dup_test")
	}()
	g := BuildGraph("dup_test",
		[]ZoneNode{
			{NodeID: "e", Kind: NodeKindEntry, IsEntry: true},
			{NodeID: "b", Kind: NodeKindBoss, IsBoss: true},
		},
		[]ZoneEdge{{From: "e", To: "b"}},
	)
	registerZoneGraph(g)
	registerZoneGraph(g)
}
