package builder

import (
	"testing"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/extractor"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/importgraph"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/scanner"
	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
)

func TestBuildCreatesDeterministicGraphSkeletonWithoutDanglingEdges(t *testing.T) {
	files := []scanner.FileInfo{
		{Path: "cmd/app/main.go", Language: "go", Category: scanner.CategoryCode, Lines: 20},
		{Path: "internal/util/util.go", Language: "go", Category: scanner.CategoryCode, Lines: 12},
	}
	analyses := []extractor.StructuralAnalysis{
		{FilePath: "cmd/app/main.go", Language: "go", HasStructuralAnalysis: true, Functions: []extractor.Function{{Name: "Run", Exported: true, LineRange: [2]int{5, 12}}}, Imports: []extractor.Import{{Path: "example.com/acme/internal/util"}}, Calls: []extractor.Call{{Caller: "Run", Callee: "Helper", Line: 8}}},
		{FilePath: "internal/util/util.go", Language: "go", HasStructuralAnalysis: true, Types: []extractor.Type{{Name: "Tool", Kind: "struct", Exported: true, LineRange: [2]int{3, 6}}}, Functions: []extractor.Function{{Name: "Helper", Exported: true, LineRange: [2]int{8, 10}}}},
	}
	imports := importgraph.Build(files, analyses)
	project := schema.ProjectMeta{Name: "tiny", Languages: []string{"go"}, Frameworks: []string{}, Description: "tiny", AnalyzedAt: "2026-07-02T00:00:00Z", GitCommitHash: "abc"}
	first := Build(files, analyses, imports, Options{Project: project})
	second := Build(files, analyses, imports, Options{Project: project})
	require.Equal(t, first, second)
	requireIDs(t, first.Nodes, []string{
		"class:internal/util/util.go:Tool",
		"file:cmd/app/main.go",
		"file:internal/util/util.go",
		"function:cmd/app/main.go:Run",
		"function:internal/util/util.go:Helper",
	})
	requireEdge(t, first.Edges, "file:cmd/app/main.go", "function:cmd/app/main.go:Run", "contains")
	requireEdge(t, first.Edges, "file:internal/util/util.go", "class:internal/util/util.go:Tool", "contains")
	requireEdge(t, first.Edges, "file:cmd/app/main.go", "file:internal/util/util.go", "imports")
	requireEdge(t, first.Edges, "function:cmd/app/main.go:Run", "function:internal/util/util.go:Helper", "calls")
	nodeIDs := map[string]struct{}{}
	for _, node := range first.Nodes {
		nodeIDs[node.ID] = struct{}{}
	}
	for _, edge := range first.Edges {
		_, ok := nodeIDs[edge.Source]
		require.True(t, ok, edge.Source)
		_, ok = nodeIDs[edge.Target]
		require.True(t, ok, edge.Target)
	}
}

func TestBuildAddsNonCodeSubNodesWithoutLayeringThem(t *testing.T) {
	files := []scanner.FileInfo{
		{Path: "deploy/k8s.yaml", Language: "yaml", Category: scanner.CategoryInfra, Lines: 12},
	}
	analyses := []extractor.StructuralAnalysis{
		{
			FilePath: "deploy/k8s.yaml",
			Language: "yaml",
			SubNodes: []extractor.SubNode{
				{Type: "service", Name: "api", Summary: "Kubernetes Service api", Tags: []string{"kubernetes", "service"}, LineRange: [2]int{1, 4}},
				{Type: "resource", Name: "api-config", Summary: "Kubernetes ConfigMap api-config", Tags: []string{"kubernetes", "configmap"}, LineRange: [2]int{6, 10}},
			},
		},
	}
	imports := importgraph.Build(files, analyses)
	project := schema.ProjectMeta{Name: "tiny", Languages: []string{"yaml"}, Frameworks: []string{}, Description: "tiny", AnalyzedAt: "2026-07-02T00:00:00Z", GitCommitHash: "abc"}

	graph := Build(files, analyses, imports, Options{Project: project})

	requireIDs(t, graph.Nodes, []string{
		"resource:deploy/k8s.yaml",
		"resource:deploy/k8s.yaml:api-config",
		"service:deploy/k8s.yaml:api",
	})
	requireEdge(t, graph.Edges, "resource:deploy/k8s.yaml", "service:deploy/k8s.yaml:api", "contains")
	requireEdge(t, graph.Edges, "resource:deploy/k8s.yaml", "resource:deploy/k8s.yaml:api-config", "contains")
	require.True(t, schema.IsFileLevelNode(findNode(t, graph.Nodes, "service:deploy/k8s.yaml:api")), "file-level node types stay unchanged; layer generation and validation exclude contained child nodes via contains edges")
	require.True(t, schema.IsFileLevelNode(findNode(t, graph.Nodes, "resource:deploy/k8s.yaml")))
}

func TestBuildTreatsBuildScriptsAsPipelines(t *testing.T) {
	files := []scanner.FileInfo{{Path: "Makefile", Language: "makefile", Category: scanner.CategoryScript, Lines: 20}}
	graph := Build(files, nil, importgraph.Graph{}, Options{Project: schema.ProjectMeta{Name: "tiny", Languages: []string{}, Frameworks: []string{}}})
	requireIDs(t, graph.Nodes, []string{"pipeline:Makefile"})
}

func TestDeterministicSymbolSummariesRemainGroundedWithoutLLM(t *testing.T) {
	require.Equal(t, "Internal function helper.", deterministicFunctionSummary(extractor.Function{Name: "helper"}))
	require.Equal(t, "Exported method Run on Server.", deterministicFunctionSummary(extractor.Function{Name: "Run", Receiver: "Server", Exported: true}))
	require.Equal(t, "Internal struct state.", deterministicTypeSummary(extractor.Type{Name: "state", Kind: "struct"}))
	require.Equal(t, "Exported interface Store.", deterministicTypeSummary(extractor.Type{Name: "Store", Kind: "interface", Exported: true}))
}

func requireIDs(t *testing.T, nodes []schema.GraphNode, expected []string) {
	t.Helper()
	actual := make([]string, 0, len(nodes))
	for _, node := range nodes {
		actual = append(actual, node.ID)
	}
	require.Equal(t, expected, actual)
}

func requireEdge(t *testing.T, edges []schema.GraphEdge, source, target, edgeType string) {
	t.Helper()
	for _, edge := range edges {
		if edge.Source == source && edge.Target == target && edge.Type == edgeType {
			return
		}
	}
	t.Fatalf("missing edge %s -> %s type %s in %#v", source, target, edgeType, edges)
}

func findNode(t *testing.T, nodes []schema.GraphNode, id string) schema.GraphNode {
	t.Helper()
	for _, node := range nodes {
		if node.ID == id {
			return node
		}
	}
	t.Fatalf("missing node %s in %#v", id, nodes)
	return schema.GraphNode{}
}
