package query

import (
	"testing"

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

// buildTestGraph creates a small realistic graph:
//
//	main.go::main -> calls -> pkg/server.go::Start
//	pkg/server.go::Start -> calls -> pkg/db.go::Connect
//	pkg/db.go::Connect -> calls -> pkg/db.go::Ping
//	pkg/server.go -> imports -> pkg/db.go
//	pkg/db.go::DBImpl -> implements -> pkg/db.go::DB (interface)
func buildTestGraph() *graph.Graph {
	g := graph.New()

	nodes := []*graph.Node{
		{ID: "main.go", Kind: graph.KindFile, Name: "main.go", FilePath: "main.go", Language: "go"},
		{ID: "main.go::main", Kind: graph.KindFunction, Name: "main", FilePath: "main.go", Language: "go",
			StartLine: 5, EndLine: 10, Meta: map[string]any{"signature": "func main()"}},
		{ID: "pkg/server.go", Kind: graph.KindFile, Name: "server.go", FilePath: "pkg/server.go", Language: "go"},
		{ID: "pkg/server.go::Start", Kind: graph.KindFunction, Name: "Start", FilePath: "pkg/server.go", Language: "go",
			StartLine: 10, EndLine: 20},
		{ID: "pkg/db.go", Kind: graph.KindFile, Name: "db.go", FilePath: "pkg/db.go", Language: "go"},
		{ID: "pkg/db.go::Connect", Kind: graph.KindFunction, Name: "Connect", FilePath: "pkg/db.go", Language: "go",
			StartLine: 5, EndLine: 15},
		{ID: "pkg/db.go::Ping", Kind: graph.KindFunction, Name: "Ping", FilePath: "pkg/db.go", Language: "go",
			StartLine: 17, EndLine: 22},
		{ID: "pkg/db.go::DB", Kind: graph.KindInterface, Name: "DB", FilePath: "pkg/db.go", Language: "go",
			StartLine: 1, EndLine: 4},
		{ID: "pkg/db.go::DBImpl", Kind: graph.KindType, Name: "DBImpl", FilePath: "pkg/db.go", Language: "go",
			StartLine: 24, EndLine: 30},
	}
	for _, n := range nodes {
		g.AddNode(n)
	}

	edges := []*graph.Edge{
		{From: "main.go::main", To: "pkg/server.go::Start", Kind: graph.EdgeCalls, FilePath: "main.go", Line: 7},
		{From: "pkg/server.go::Start", To: "pkg/db.go::Connect", Kind: graph.EdgeCalls, FilePath: "pkg/server.go", Line: 12},
		{From: "pkg/db.go::Connect", To: "pkg/db.go::Ping", Kind: graph.EdgeCalls, FilePath: "pkg/db.go", Line: 10},
		{From: "pkg/server.go", To: "pkg/db.go", Kind: graph.EdgeImports, FilePath: "pkg/server.go", Line: 3},
		{From: "pkg/db.go::DBImpl", To: "pkg/db.go::DB", Kind: graph.EdgeImplements, FilePath: "pkg/db.go", Line: 24},
		{From: "main.go::main", To: "pkg/db.go::DBImpl", Kind: graph.EdgeReferences, FilePath: "main.go", Line: 8},
	}
	for _, e := range edges {
		g.AddEdge(e)
	}

	return g
}

func TestGetSymbol(t *testing.T) {
	e := NewEngine(buildTestGraph())
	n := e.GetSymbol("pkg/db.go::Connect")
	require.NotNil(t, n)
	assert.Equal(t, "Connect", n.Name)

	assert.Nil(t, e.GetSymbol("nonexistent"))
}

func TestFindSymbols(t *testing.T) {
	e := NewEngine(buildTestGraph())

	results := e.FindSymbols("Connect")
	require.Len(t, results, 1)

	results = e.FindSymbols("Connect", graph.KindFunction)
	require.Len(t, results, 1)

	results = e.FindSymbols("Connect", graph.KindType)
	assert.Empty(t, results)
}

func TestGetFileSymbols(t *testing.T) {
	e := NewEngine(buildTestGraph())
	sg := e.GetFileSymbols("pkg/db.go")
	assert.GreaterOrEqual(t, len(sg.Nodes), 4) // file + Connect + Ping + DB + DBImpl
}

func TestGetDependencies(t *testing.T) {
	e := NewEngine(buildTestGraph())

	// main calls Start (depth 1).
	sg := e.GetDependencies("main.go::main", QueryOptions{Depth: 1, Limit: 50, Detail: "full"})
	assert.GreaterOrEqual(t, len(sg.Nodes), 2) // main + Start

	// Depth 2 should also reach Connect.
	sg = e.GetDependencies("main.go::main", QueryOptions{Depth: 2, Limit: 50, Detail: "full"})
	ids := nodeIDs(sg.Nodes)
	assert.Contains(t, ids, "pkg/server.go::Start")
	assert.Contains(t, ids, "pkg/db.go::Connect")
}

func TestGetDependents(t *testing.T) {
	e := NewEngine(buildTestGraph())

	// Who depends on Connect?
	sg := e.GetDependents("pkg/db.go::Connect", QueryOptions{Depth: 2, Limit: 50})
	ids := nodeIDs(sg.Nodes)
	assert.Contains(t, ids, "pkg/server.go::Start")
}

func TestGetCallChain(t *testing.T) {
	e := NewEngine(buildTestGraph())

	sg := e.GetCallChain("main.go::main", QueryOptions{Depth: 3, Limit: 50})
	ids := nodeIDs(sg.Nodes)
	assert.Contains(t, ids, "pkg/server.go::Start")
	assert.Contains(t, ids, "pkg/db.go::Connect")
	assert.Contains(t, ids, "pkg/db.go::Ping")
}

func TestGetCallers(t *testing.T) {
	e := NewEngine(buildTestGraph())

	sg := e.GetCallers("pkg/db.go::Ping", QueryOptions{Depth: 3, Limit: 50})
	ids := nodeIDs(sg.Nodes)
	assert.Contains(t, ids, "pkg/db.go::Connect")
}

// Regression: methods wired in by reference (e.g. HTTP handler registration
// like `mux.HandleFunc("/p", h.foo)`) carry an EdgeReferences edge from the
// registration site to the method — they're never reached by a direct
// EdgeCalls. Before the fix GetCallers ignored EdgeReferences and these
// methods looked dead in the graph.
func TestGetCallers_IncludesMethodValueReferences(t *testing.T) {
	g := graph.New()
	g.AddNode(&graph.Node{ID: "routes.go::Register", Kind: graph.KindFunction, Name: "Register", FilePath: "routes.go", Language: "go"})
	g.AddNode(&graph.Node{
		ID: "handler.go::Handler.HandleHealth", Kind: graph.KindMethod, Name: "HandleHealth",
		FilePath: "handler.go", Language: "go", Meta: map[string]any{"receiver": "Handler"},
	})
	// `mux.HandleFunc("/health", h.HandleHealth)` after the resolver:
	g.AddEdge(&graph.Edge{
		From: "routes.go::Register", To: "handler.go::Handler.HandleHealth",
		Kind: graph.EdgeReferences, FilePath: "routes.go", Line: 12,
	})

	e := NewEngine(g)
	sg := e.GetCallers("handler.go::Handler.HandleHealth", QueryOptions{Depth: 1, Limit: 50})
	ids := nodeIDs(sg.Nodes)
	assert.Contains(t, ids, "routes.go::Register",
		"method-value registration site should surface as a caller via EdgeReferences")
}

func TestFindImplementations(t *testing.T) {
	e := NewEngine(buildTestGraph())

	impls := e.FindImplementations("pkg/db.go::DB")
	require.Len(t, impls, 1)
	assert.Equal(t, "DBImpl", impls[0].Name)
}

func TestFindUsages(t *testing.T) {
	e := NewEngine(buildTestGraph())

	sg := e.FindUsages("pkg/db.go::DBImpl")
	ids := nodeIDs(sg.Nodes)
	assert.Contains(t, ids, "main.go::main") // references DBImpl
}

func TestGetCluster(t *testing.T) {
	e := NewEngine(buildTestGraph())

	sg := e.GetCluster("pkg/server.go::Start", QueryOptions{Depth: 1, Limit: 50})
	assert.GreaterOrEqual(t, len(sg.Nodes), 3) // Start + main + Connect
}

func TestTruncation(t *testing.T) {
	e := NewEngine(buildTestGraph())

	sg := e.GetCallChain("main.go::main", QueryOptions{Depth: 10, Limit: 2})
	assert.True(t, sg.Truncated)
	assert.LessOrEqual(t, len(sg.Nodes), 2)
}

func TestCycleHandling(t *testing.T) {
	g := graph.New()
	g.AddNode(&graph.Node{ID: "a", Kind: graph.KindFunction, Name: "a", FilePath: "a.go", Language: "go"})
	g.AddNode(&graph.Node{ID: "b", Kind: graph.KindFunction, Name: "b", FilePath: "b.go", Language: "go"})
	g.AddEdge(&graph.Edge{From: "a", To: "b", Kind: graph.EdgeCalls, FilePath: "a.go", Line: 1})
	g.AddEdge(&graph.Edge{From: "b", To: "a", Kind: graph.EdgeCalls, FilePath: "b.go", Line: 1})

	e := NewEngine(g)
	sg := e.GetCallChain("a", QueryOptions{Depth: 10, Limit: 50})
	// Should terminate without infinite loop.
	assert.LessOrEqual(t, len(sg.Nodes), 2)
}

func TestStats(t *testing.T) {
	e := NewEngine(buildTestGraph())
	s := e.Stats()
	assert.Equal(t, 9, s.TotalNodes)
	assert.Equal(t, 6, s.TotalEdges)
	assert.Equal(t, 9, s.ByLanguage["go"])
}

func TestBriefModeStripsMeta(t *testing.T) {
	e := NewEngine(buildTestGraph())
	sg := e.GetDependencies("main.go::main", QueryOptions{Depth: 1, Limit: 50, Detail: "brief"})
	for _, n := range sg.Nodes {
		assert.Nil(t, n.Meta)
	}
}

// buildMultiRepoTestGraph creates a graph with nodes from two repos for testing repo-scoped queries.
func buildMultiRepoTestGraph() *graph.Graph {
	g := graph.New()

	nodes := []*graph.Node{
		{ID: "repoA/main.go::main", Kind: graph.KindFunction, Name: "main", FilePath: "repoA/main.go", Language: "go", RepoPrefix: "repoA", StartLine: 1, EndLine: 10},
		{ID: "repoA/pkg/util.go::Helper", Kind: graph.KindFunction, Name: "Helper", FilePath: "repoA/pkg/util.go", Language: "go", RepoPrefix: "repoA", StartLine: 1, EndLine: 5},
		{ID: "repoB/lib.go::Helper", Kind: graph.KindFunction, Name: "Helper", FilePath: "repoB/lib.go", Language: "go", RepoPrefix: "repoB", StartLine: 1, EndLine: 8},
		{ID: "repoB/lib.go::Process", Kind: graph.KindFunction, Name: "Process", FilePath: "repoB/lib.go", Language: "go", RepoPrefix: "repoB", StartLine: 10, EndLine: 20},
	}
	for _, n := range nodes {
		g.AddNode(n)
	}

	edges := []*graph.Edge{
		{From: "repoA/main.go::main", To: "repoA/pkg/util.go::Helper", Kind: graph.EdgeCalls, FilePath: "repoA/main.go", Line: 5},
		{From: "repoA/main.go::main", To: "repoB/lib.go::Process", Kind: graph.EdgeCalls, FilePath: "repoA/main.go", Line: 7, CrossRepo: true},
		{From: "repoB/lib.go::Process", To: "repoB/lib.go::Helper", Kind: graph.EdgeCalls, FilePath: "repoB/lib.go", Line: 12},
	}
	for _, e := range edges {
		g.AddEdge(e)
	}

	return g
}

func TestSearchSymbolsInRepo(t *testing.T) {
	g := buildMultiRepoTestGraph()
	e := NewEngine(g)

	// Search for "Helper" scoped to repoA — should only return repoA's Helper.
	results := e.SearchSymbolsInRepo("Helper", "repoA", 10)
	require.Len(t, results, 1)
	assert.Equal(t, "repoA/pkg/util.go::Helper", results[0].ID)

	// Search for "Helper" scoped to repoB — should only return repoB's Helper.
	results = e.SearchSymbolsInRepo("Helper", "repoB", 10)
	require.Len(t, results, 1)
	assert.Equal(t, "repoB/lib.go::Helper", results[0].ID)

	// Search for "Process" scoped to repoA — should return nothing.
	results = e.SearchSymbolsInRepo("Process", "repoA", 10)
	assert.Empty(t, results)

	// Search for a non-existent repo — should return nothing.
	results = e.SearchSymbolsInRepo("Helper", "repoC", 10)
	assert.Empty(t, results)
}

func TestSearchSymbolsInRepo_Limit(t *testing.T) {
	g := buildMultiRepoTestGraph()
	e := NewEngine(g)

	// With limit=1, should return at most 1 result.
	results := e.SearchSymbolsInRepo("Helper", "repoA", 1)
	assert.Len(t, results, 1)
}

func TestGetFileSymbolsInRepo(t *testing.T) {
	g := buildMultiRepoTestGraph()
	e := NewEngine(g)

	// Get symbols for repoB/lib.go scoped to repoB.
	sg := e.GetFileSymbolsInRepo("repoB/lib.go", "repoB")
	assert.Len(t, sg.Nodes, 2) // Helper and Process
	ids := nodeIDs(sg.Nodes)
	assert.Contains(t, ids, "repoB/lib.go::Helper")
	assert.Contains(t, ids, "repoB/lib.go::Process")

	// Get symbols for repoB/lib.go scoped to repoA — should return nothing.
	sg = e.GetFileSymbolsInRepo("repoB/lib.go", "repoA")
	assert.Empty(t, sg.Nodes)

	// Get symbols for a non-existent file — should return empty.
	sg = e.GetFileSymbolsInRepo("nonexistent.go", "repoA")
	assert.Empty(t, sg.Nodes)
}

func TestGetFileSymbolsInRepo_Edges(t *testing.T) {
	g := buildMultiRepoTestGraph()
	e := NewEngine(g)

	// Edges should be included when at least one endpoint is in the filtered set.
	sg := e.GetFileSymbolsInRepo("repoB/lib.go", "repoB")
	assert.Greater(t, len(sg.Edges), 0)
}

func nodeIDs(nodes []*graph.Node) []string {
	ids := make([]string, len(nodes))
	for i, n := range nodes {
		ids[i] = n.ID
	}
	return ids
}
