package graph

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// buildClassifyGraph wires three functions in one file, each with a
// distinct edge profile, so ClassifyZeroEdge can be exercised against
// every classification:
//
//   - Used      — the file defines it AND a caller calls it.
//   - Unused    — the file defines it, it makes an outgoing call, but
//     nothing calls it back. Indexed, dead.
//   - Orphaned  — added with no edges at all, mimicking a symbol the
//     extractor never processed.
func buildClassifyGraph() *Graph {
	g := New()
	g.AddNode(&Node{ID: "svc.go", Kind: KindFile, Name: "svc.go", FilePath: "svc.go"})
	g.AddNode(&Node{ID: "svc.go::Caller", Kind: KindFunction, Name: "Caller", FilePath: "svc.go"})
	g.AddNode(&Node{ID: "svc.go::Used", Kind: KindFunction, Name: "Used", FilePath: "svc.go"})
	g.AddNode(&Node{ID: "svc.go::Unused", Kind: KindFunction, Name: "Unused", FilePath: "svc.go"})
	g.AddNode(&Node{ID: "svc.go::Helper", Kind: KindFunction, Name: "Helper", FilePath: "svc.go"})

	// The file defines every function except the orphan.
	g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Caller", Kind: EdgeDefines})
	g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Used", Kind: EdgeDefines})
	g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Unused", Kind: EdgeDefines})
	g.AddEdge(&Edge{From: "svc.go", To: "svc.go::Helper", Kind: EdgeDefines})

	// Used has an incoming call edge.
	g.AddEdge(&Edge{From: "svc.go::Caller", To: "svc.go::Used", Kind: EdgeCalls})
	// Unused has only an outgoing call — no incoming usage edge.
	g.AddEdge(&Edge{From: "svc.go::Unused", To: "svc.go::Helper", Kind: EdgeCalls})
	return g
}

func TestClassifyZeroEdge(t *testing.T) {
	g := buildClassifyGraph()
	// A method node with only its member_of structural edge — indexed,
	// but nothing references it.
	g.AddNode(&Node{ID: "svc.go::T", Kind: KindType, Name: "T", FilePath: "svc.go"})
	g.AddNode(&Node{ID: "svc.go::T.Method", Kind: KindMethod, Name: "Method", FilePath: "svc.go"})
	g.AddEdge(&Edge{From: "svc.go::T.Method", To: "svc.go::T", Kind: EdgeMemberOf})
	// An orphan: in the graph, but carrying no edges of any kind.
	g.AddNode(&Node{ID: "svc.go::Orphan", Kind: KindFunction, Name: "Orphan", FilePath: "svc.go"})

	tests := []struct {
		name     string
		symbolID string
		want     ZeroEdgeClass
	}{
		{
			name:     "incoming call edge yields none",
			symbolID: "svc.go::Used",
			want:     ZeroEdgeNone,
		},
		{
			name:     "only structural defines plus outgoing call yields likely_unused",
			symbolID: "svc.go::Unused",
			want:     ZeroEdgeLikelyUnused,
		},
		{
			name:     "method with only member_of yields likely_unused",
			symbolID: "svc.go::T.Method",
			want:     ZeroEdgeLikelyUnused,
		},
		{
			name:     "zero edges of any kind yields possible_extraction_gap",
			symbolID: "svc.go::Orphan",
			want:     ZeroEdgePossibleExtractionGap,
		},
		{
			name:     "unknown symbol id yields possible_extraction_gap",
			symbolID: "svc.go::DoesNotExist",
			want:     ZeroEdgePossibleExtractionGap,
		},
		{
			name:     "empty symbol id yields possible_extraction_gap",
			symbolID: "",
			want:     ZeroEdgePossibleExtractionGap,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := ClassifyZeroEdge(g, tt.symbolID)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestClassifyZeroEdge_NilGraph(t *testing.T) {
	assert.Equal(t, ZeroEdgePossibleExtractionGap, ClassifyZeroEdge(nil, "svc.go::Used"))
}

func TestCaveatForZeroEdge(t *testing.T) {
	g := buildClassifyGraph()
	g.AddNode(&Node{ID: "svc.go::Orphan", Kind: KindFunction, Name: "Orphan", FilePath: "svc.go"})

	// A used symbol carries no caveat — nil is returned so callers can
	// attach it unconditionally.
	assert.Nil(t, CaveatForZeroEdge(g, "svc.go::Used"))

	// Likely-unused: classification plus a non-empty message.
	unused := CaveatForZeroEdge(g, "svc.go::Unused")
	require.NotNil(t, unused)
	assert.Equal(t, ZeroEdgeLikelyUnused, unused.Class)
	assert.NotEmpty(t, unused.Message)

	// Extraction gap: classification plus a non-empty message.
	gap := CaveatForZeroEdge(g, "svc.go::Orphan")
	require.NotNil(t, gap)
	assert.Equal(t, ZeroEdgePossibleExtractionGap, gap.Class)
	assert.NotEmpty(t, gap.Message)

	// The two caveat messages must differ — they describe opposite
	// situations and an agent has to be able to tell them apart.
	assert.NotEqual(t, unused.Message, gap.Message)
}
