package analysis

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zzet/gortex/internal/graph"
)

func buildTestGraph() *graph.Graph {
	g := graph.New()

	// auth module
	g.AddNode(&graph.Node{ID: "auth.go", Kind: graph.KindFile, Name: "auth.go", FilePath: "pkg/auth/auth.go", Language: "go"})
	g.AddNode(&graph.Node{ID: "auth.go::ValidateToken", Kind: graph.KindFunction, Name: "ValidateToken", FilePath: "pkg/auth/auth.go", Language: "go", StartLine: 10, EndLine: 30})
	g.AddNode(&graph.Node{ID: "auth.go::ParseClaims", Kind: graph.KindFunction, Name: "ParseClaims", FilePath: "pkg/auth/auth.go", Language: "go", StartLine: 32, EndLine: 50})

	// handler module
	g.AddNode(&graph.Node{ID: "handler.go", Kind: graph.KindFile, Name: "handler.go", FilePath: "pkg/api/handler.go", Language: "go"})
	g.AddNode(&graph.Node{ID: "handler.go::HandleLogin", Kind: graph.KindFunction, Name: "HandleLogin", FilePath: "pkg/api/handler.go", Language: "go", StartLine: 5, EndLine: 40})
	g.AddNode(&graph.Node{ID: "handler.go::HandleLogout", Kind: graph.KindFunction, Name: "HandleLogout", FilePath: "pkg/api/handler.go", Language: "go", StartLine: 42, EndLine: 60})

	// main
	g.AddNode(&graph.Node{ID: "main.go", Kind: graph.KindFile, Name: "main.go", FilePath: "cmd/main.go", Language: "go"})
	g.AddNode(&graph.Node{ID: "main.go::main", Kind: graph.KindFunction, Name: "main", FilePath: "cmd/main.go", Language: "go", StartLine: 1, EndLine: 20})

	// db module
	g.AddNode(&graph.Node{ID: "db.go::QueryUser", Kind: graph.KindFunction, Name: "QueryUser", FilePath: "pkg/db/db.go", Language: "go", StartLine: 5, EndLine: 20})

	// Edges
	g.AddEdge(&graph.Edge{From: "main.go::main", To: "handler.go::HandleLogin", Kind: graph.EdgeCalls})
	g.AddEdge(&graph.Edge{From: "main.go::main", To: "handler.go::HandleLogout", Kind: graph.EdgeCalls})
	g.AddEdge(&graph.Edge{From: "handler.go::HandleLogin", To: "auth.go::ValidateToken", Kind: graph.EdgeCalls})
	g.AddEdge(&graph.Edge{From: "handler.go::HandleLogin", To: "db.go::QueryUser", Kind: graph.EdgeCalls})
	g.AddEdge(&graph.Edge{From: "auth.go::ValidateToken", To: "auth.go::ParseClaims", Kind: graph.EdgeCalls})
	g.AddEdge(&graph.Edge{From: "handler.go::HandleLogout", To: "auth.go::ValidateToken", Kind: graph.EdgeCalls})

	return g
}

func TestDetectCommunities(t *testing.T) {
	g := buildTestGraph()
	result := DetectCommunities(g)

	require.NotNil(t, result)
	// With this small graph, we should get at least 1 community
	assert.GreaterOrEqual(t, len(result.Communities), 1)
	assert.NotEmpty(t, result.NodeToComm)

	// Check that communities have members
	for _, c := range result.Communities {
		assert.NotEmpty(t, c.ID)
		assert.NotEmpty(t, c.Members)
		assert.Greater(t, c.Size, 1)
		assert.GreaterOrEqual(t, c.Cohesion, 0.0)
		assert.LessOrEqual(t, c.Cohesion, 1.0)
	}
}

func TestDiscoverProcesses(t *testing.T) {
	g := buildTestGraph()
	result := DiscoverProcesses(g)

	require.NotNil(t, result)
	// main and HandleLogin should be discovered as entry points
	assert.GreaterOrEqual(t, len(result.Processes), 1)

	// Check process structure
	for _, p := range result.Processes {
		assert.NotEmpty(t, p.ID)
		assert.NotEmpty(t, p.Name)
		assert.NotEmpty(t, p.EntryPoint)
		assert.GreaterOrEqual(t, p.StepCount, 2)
		assert.NotEmpty(t, p.Files)
	}

	// main should trace through handler → auth → parseclaims
	found := false
	for _, p := range result.Processes {
		if p.EntryPoint == "main.go::main" {
			found = true
			assert.GreaterOrEqual(t, p.StepCount, 3)
			break
		}
	}
	assert.True(t, found, "main should be discovered as a process entry point")
}

func TestAnalyzeImpact(t *testing.T) {
	g := buildTestGraph()
	communities := DetectCommunities(g)
	processes := DiscoverProcesses(g)

	result := AnalyzeImpact(g, []string{"auth.go::ValidateToken"}, communities, processes)

	require.NotNil(t, result)
	assert.NotEmpty(t, result.Risk)
	assert.NotEmpty(t, result.Summary)

	// ValidateToken is called by HandleLogin and HandleLogout (depth 1)
	d1 := result.ByDepth[1]
	assert.GreaterOrEqual(t, len(d1), 1, "should have at least 1 direct dependent")

	// At depth 2, main should appear
	d2 := result.ByDepth[2]
	assert.GreaterOrEqual(t, len(d2), 0)

	assert.Greater(t, result.TotalAffected, 0)
}

// TestAnalyzeImpact_DropsHeuristicNoiseAtTransitiveDepths pins the
// regression: a leaf symbol used to surface as "90 transitively
// affected" because a heuristic / text-matched edge at d=1 fanned
// out through high-confidence d=2 edges into hundreds of unrelated
// downstream methods. Now d=2 and d=3 drop entries whose
// representative edge has confidence == 0 + label "INFERRED"
// (resolution edges without type info — the noise class). d=1
// stays untouched.
func TestAnalyzeImpact_DropsHeuristicNoiseAtTransitiveDepths(t *testing.T) {
	g := graph.New()
	target := &graph.Node{ID: "p/target.go::Target", Name: "Target", Kind: graph.KindFunction, FilePath: "p/target.go"}
	caller := &graph.Node{ID: "p/caller.go::Caller", Name: "Caller", Kind: graph.KindFunction, FilePath: "p/caller.go"}
	intermediate := &graph.Node{ID: "p/middle.go::Middle", Name: "Middle", Kind: graph.KindFunction, FilePath: "p/middle.go"}
	heuristic := &graph.Node{ID: "p/noise.go::Noise", Name: "Noise", Kind: graph.KindFunction, FilePath: "p/noise.go"}
	g.AddNode(target)
	g.AddNode(caller)
	g.AddNode(intermediate)
	g.AddNode(heuristic)
	// Direct caller: real call, high-conf.
	g.AddEdge(&graph.Edge{From: caller.ID, To: target.ID, Kind: graph.EdgeCalls, Confidence: 0.95})
	// d=2 caller arriving via a CONFIDENT edge.
	g.AddEdge(&graph.Edge{From: intermediate.ID, To: caller.ID, Kind: graph.EdgeCalls, Confidence: 0.95})
	// d=2 NOISE entry: heuristic edge with no type info — exactly
	// the shape the user reported as flooding the response.
	g.AddEdge(&graph.Edge{From: heuristic.ID, To: caller.ID, Kind: graph.EdgeCalls, Confidence: 0})

	result := AnalyzeImpact(g, []string{target.ID}, nil, nil)
	require.NotNil(t, result)
	require.GreaterOrEqual(t, len(result.ByDepth[1]), 1, "direct caller must survive")
	for _, e := range result.ByDepth[2] {
		require.False(t, e.EdgeConfidence == 0 && e.ConfidenceLabel == "INFERRED",
			"d=2 must not include heuristic / text-matched edges (got %s via confidence=%v label=%s)",
			e.ID, e.EdgeConfidence, e.ConfidenceLabel)
	}
}

func TestAnalyzeImpact_RiskLevels(t *testing.T) {
	assert.Equal(t, RiskLow, assessRisk(0, 0, 0))
	assert.Equal(t, RiskLow, assessRisk(1, 1, 0))
	assert.Equal(t, RiskMedium, assessRisk(2, 3, 0))
	assert.Equal(t, RiskHigh, assessRisk(5, 5, 0))
	assert.Equal(t, RiskCritical, assessRisk(10, 10, 0))
}

func TestScoreEntryPoint(t *testing.T) {
	main := &graph.Node{Name: "main", Language: "go", Kind: graph.KindFunction}
	handler := &graph.Node{Name: "HandleLogin", Language: "go", Kind: graph.KindFunction}
	getter := &graph.Node{Name: "getName", Language: "go", Kind: graph.KindFunction}

	// main with 3 callees, 0 callers should score high
	mainScore := scoreEntryPoint(main, 3, 0)
	handlerScore := scoreEntryPoint(handler, 2, 1)
	getterScore := scoreEntryPoint(getter, 1, 5)

	assert.Greater(t, mainScore, handlerScore)
	assert.Greater(t, handlerScore, getterScore)
}
