package assembler

import (
	"testing"
	"time"

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

func TestNormalizeGraphResolvesIDsDedupesAndDropsDanglingEdges(t *testing.T) {
	graph := schema.KnowledgeGraph{
		Version: "1.0.0",
		Kind:    schema.KindCodebase,
		Project: project("old"),
		Nodes: []schema.GraphNode{
			{ID: "raw-app", Type: "file", Name: "app.go", FilePath: "./src/app.go", Summary: "app", Tags: []string{}, Complexity: "low"},
			{ID: "raw-app-duplicate", Type: "file", Name: "app.go", FilePath: "src/app.go", Summary: "app duplicate", Tags: []string{}, Complexity: "simple"},
			{ID: "test-fn", Type: "function", Name: "TestRun", FilePath: "src/app_test.go", Summary: "test", Tags: []string{"test"}, Complexity: "simple"},
		},
		Edges: []schema.GraphEdge{
			{Source: "raw-app", Target: "missing", Type: "calls", Direction: "forward", Weight: 0.5},
			{Source: "raw-app", Target: "test-fn", Type: "tested_by", Direction: "forward", Weight: 0.5},
			{Source: "raw-app", Target: "test-fn", Type: "tested_by", Direction: "forward", Weight: 0.8},
			{Source: "raw-app", Target: "test-fn", Type: "tested_by", Direction: "backward", Weight: 0.5},
		},
	}

	normalized := NormalizeGraph(graph)
	requireIDs(t, normalized.Nodes, []string{"file:src/app.go", "function:src/app_test.go:TestRun"})
	require.Equal(t, []schema.GraphEdge{{Source: "file:src/app.go", Target: "function:src/app_test.go:TestRun", Type: "tested_by", Direction: "forward", Weight: 0.5}}, normalized.Edges)
}

func TestNormalizeGraphSynthesizesTestedByConventionEdgesAndTagsProduction(t *testing.T) {
	graph := schema.KnowledgeGraph{
		Version: "1.0.0",
		Kind:    schema.KindCodebase,
		Project: project("old"),
		Nodes: []schema.GraphNode{
			node("file:src/foo.go", "file", "foo.go", "src/foo.go"),
			node("file:src/foo_test.go", "file", "foo_test.go", "src/foo_test.go"),
			node("function:src/foo.go:Run", "function", "Run", "src/foo.go"),
			node("function:src/foo_test.go:TestRun", "function", "TestRun", "src/foo_test.go"),
			node("function:src/foo_test.go:TestHelper", "function", "TestHelper", "src/foo_test.go"),
			node("file:src/bar.go", "file", "bar.go", "src/bar.go"),
		},
		Edges: []schema.GraphEdge{
			edge("file:src/foo.go", "function:src/foo.go:Run", "contains"),
			edge("file:src/foo_test.go", "function:src/foo_test.go:TestRun", "contains"),
			edge("file:src/foo_test.go", "function:src/foo_test.go:TestHelper", "contains"),
		},
	}

	normalized := NormalizeGraph(graph)
	requireEdge(t, normalized.Edges, "file:src/foo.go", "file:src/foo_test.go", "tested_by")
	requireEdge(t, normalized.Edges, "function:src/foo.go:Run", "function:src/foo_test.go:TestRun", "tested_by")
	requireNoEdge(t, normalized.Edges, "function:src/foo_test.go:TestHelper", "function:src/foo_test.go:TestRun", "tested_by")
	requireNoEdge(t, normalized.Edges, "file:src/bar.go", "file:src/foo_test.go", "tested_by")
	requireNodeTag(t, normalized.Nodes, "file:src/foo.go", "tested")
	requireNodeTag(t, normalized.Nodes, "function:src/foo.go:Run", "tested")
	requireNodeWithoutTag(t, normalized.Nodes, "file:src/foo_test.go", "tested")
}

func TestMergeGraphUpdatePrunesChangedAndDeletedFilePathsBeforeMergingFreshGraph(t *testing.T) {
	existing := schema.KnowledgeGraph{
		Version: "1.0.0",
		Kind:    schema.KindCodebase,
		Project: project("old"),
		Nodes: []schema.GraphNode{
			node("file:src/app.go", "file", "app.go", "src/app.go"),
			node("function:src/app.go:Run", "function", "Run", "src/app.go"),
			node("service:src/app.go:api", "service", "api", "src/app.go"),
			node("file:src/keep.go", "file", "keep.go", "src/keep.go"),
			node("function:src/keep.go:Keep", "function", "Keep", "src/keep.go"),
			node("file:src/deleted.go", "file", "deleted.go", "src/deleted.go"),
		},
		Edges: []schema.GraphEdge{
			edge("file:src/app.go", "function:src/app.go:Run", "contains"),
			edge("file:src/app.go", "service:src/app.go:api", "contains"),
			edge("file:src/keep.go", "function:src/keep.go:Keep", "contains"),
			edge("file:src/keep.go", "file:src/app.go", "imports"),
			edge("file:src/deleted.go", "file:src/keep.go", "imports"),
		},
		Layers: []schema.Layer{{ID: "layer", Name: "Layer", Description: "Layer", NodeIDs: []string{"file:src/app.go", "file:src/keep.go", "missing"}}},
		Tour:   []schema.TourStep{{Order: 1, Title: "Tour", Description: "Tour", NodeIDs: []string{"file:src/app.go", "file:src/keep.go"}}},
	}
	fresh := schema.KnowledgeGraph{
		Version: "1.0.0",
		Kind:    schema.KindCodebase,
		Project: project("new"),
		Nodes: []schema.GraphNode{
			node("file:src/app.go", "file", "app.go", "src/app.go"),
			node("function:src/app.go:RunNew", "function", "RunNew", "src/app.go"),
		},
		Edges: []schema.GraphEdge{
			edge("file:src/app.go", "function:src/app.go:RunNew", "contains"),
			edge("file:src/app.go", "function:src/app.go:RunNew", "contains"),
		},
	}

	merged := MergeGraphUpdate(existing, fresh, []string{"src/app.go", "src/deleted.go"}, "new", time.Date(2026, 7, 2, 5, 0, 0, 0, time.UTC))
	require.Equal(t, "new", merged.Project.GitCommitHash)
	require.Equal(t, "2026-07-02T05:00:00Z", merged.Project.AnalyzedAt)
	requireIDs(t, merged.Nodes, []string{
		"file:src/app.go",
		"file:src/keep.go",
		"function:src/app.go:RunNew",
		"function:src/keep.go:Keep",
	})
	require.Equal(t, []schema.GraphEdge{
		edge("file:src/app.go", "function:src/app.go:RunNew", "contains"),
		edge("file:src/keep.go", "function:src/keep.go:Keep", "contains"),
	}, merged.Edges)
	for _, edge := range merged.Edges {
		require.NotContains(t, edge.Source, "deleted")
		require.NotContains(t, edge.Target, "deleted")
		require.NotEqual(t, "function:src/app.go:Run", edge.Source)
		require.NotEqual(t, "function:src/app.go:Run", edge.Target)
		require.NotEqual(t, "service:src/app.go:api", edge.Source)
		require.NotEqual(t, "service:src/app.go:api", edge.Target)
	}
	require.Equal(t, []string{"file:src/app.go", "file:src/keep.go"}, merged.Layers[0].NodeIDs)
	require.Equal(t, []string{"file:src/app.go", "file:src/keep.go"}, merged.Tour[0].NodeIDs)
}

func project(commit string) schema.ProjectMeta {
	return schema.ProjectMeta{Name: "tiny", Languages: []string{"go"}, Frameworks: []string{}, Description: "tiny", AnalyzedAt: "2026-07-02T00:00:00Z", GitCommitHash: commit}
}

func node(id, typ, name, path string) schema.GraphNode {
	return schema.GraphNode{ID: id, Type: typ, Name: name, FilePath: path, Summary: name, Tags: []string{}, Complexity: "simple"}
}

func edge(source, target, typ string) schema.GraphEdge {
	return schema.GraphEdge{Source: source, Target: target, Type: typ, Direction: "forward", Weight: 0.5}
}

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, typ string) {
	t.Helper()
	for _, edge := range edges {
		if edge.Source == source && edge.Target == target && edge.Type == typ {
			return
		}
	}
	t.Fatalf("missing edge %s -> %s (%s) in %#v", source, target, typ, edges)
}

func requireNoEdge(t *testing.T, edges []schema.GraphEdge, source, target, typ string) {
	t.Helper()
	for _, edge := range edges {
		require.False(t, edge.Source == source && edge.Target == target && edge.Type == typ, "unexpected edge %s -> %s (%s)", source, target, typ)
	}
}

func requireNodeTag(t *testing.T, nodes []schema.GraphNode, id, tag string) {
	t.Helper()
	for _, node := range nodes {
		if node.ID == id {
			require.Contains(t, node.Tags, tag)
			return
		}
	}
	t.Fatalf("missing node %s", id)
}

func requireNodeWithoutTag(t *testing.T, nodes []schema.GraphNode, id, tag string) {
	t.Helper()
	for _, node := range nodes {
		if node.ID == id {
			require.NotContains(t, node.Tags, tag)
			return
		}
	}
	t.Fatalf("missing node %s", id)
}
