package schema

import (
	"encoding/json"
	"testing"

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

func TestTypeCounts(t *testing.T) {
	require.Len(t, NodeTypes, 21)
	require.Len(t, EdgeTypes, 35)
	require.Contains(t, NodeTypes, "source")
	require.Contains(t, EdgeTypes, "authored_by")
}

func TestMetadataRejectsUnknownLLMMode(t *testing.T) {
	meta := AnalysisMeta{LastAnalyzedAt: "now", GitCommitHash: "abc", Version: "1.0.0", LLMMode: "tour"}
	require.EqualError(t, ValidateAnalysisMeta(meta), "llmMode must be auto or off")

	store := FingerprintStore{Version: "1.0.0", GitCommitHash: "abc", GeneratedAt: "now", LLMMode: "tour", Files: map[string]FileFingerprint{}}
	require.EqualError(t, ValidateFingerprintStore(store), "llmMode must be auto or off")
}

func TestValidateGraphAutoFixAliasesAndDropsBrokenReferences(t *testing.T) {
	graph := minimalGraph()
	graph.Nodes = []GraphNode{
		{
			ID:         "old-file",
			Type:       "doc",
			Name:       "README",
			FilePath:   "README.md",
			Complexity: "low",
		},
	}
	graph.Edges = []GraphEdge{
		{Source: "old-file", Target: "old-file", Type: "uses", Direction: "to", Weight: 2},
		{Source: "old-file", Target: "missing", Type: "calls", Direction: "forward", Weight: 0.5},
	}
	graph.Layers = []Layer{{ID: "layer", Name: "Layer", Description: "Layer", NodeIDs: []string{"old-file", "missing"}}}
	graph.Tour = []TourStep{{Order: 1, Title: "Tour", Description: "Tour", NodeIDs: []string{"old-file", "missing"}}}

	result := ValidateGraph(graph)
	require.True(t, result.Success)
	require.NotNil(t, result.Data)
	require.Len(t, result.Data.Nodes, 1)
	node := result.Data.Nodes[0]
	require.Equal(t, "document", node.Type)
	require.Equal(t, "document:README.md", node.ID)
	require.Equal(t, "simple", node.Complexity)
	require.Equal(t, []string{}, node.Tags)
	require.Equal(t, "README", node.Summary)
	require.Len(t, result.Data.Edges, 1)
	require.Equal(t, "document:README.md", result.Data.Edges[0].Source)
	require.Equal(t, "document:README.md", result.Data.Edges[0].Target)
	require.Equal(t, "depends_on", result.Data.Edges[0].Type)
	require.Equal(t, "forward", result.Data.Edges[0].Direction)
	require.Equal(t, 1.0, result.Data.Edges[0].Weight)
	require.Equal(t, []string{"document:README.md"}, result.Data.Layers[0].NodeIDs)
	require.Equal(t, []string{"document:README.md"}, result.Data.Tour[0].NodeIDs)
	requireIssue(t, result.Issues, IssueAutoCorrected, "missing-field")
	requireIssue(t, result.Issues, IssueDropped, "invalid-reference")
}

func TestValidateGraphFatalWhenNoValidNodes(t *testing.T) {
	graph := minimalGraph()
	graph.Nodes = []GraphNode{{ID: "bad", Type: "nonsense", Name: "Bad", Summary: "Bad", Tags: []string{}, Complexity: "moderate"}}

	result := ValidateGraph(graph)
	require.False(t, result.Success)
	require.Equal(t, "No valid nodes found in knowledge graph", result.Fatal)
}

func TestValidateGraphJSONAutoFixesMissingEdgeFields(t *testing.T) {
	input := []byte(`{
		"version":"1.0.0",
		"project":{"name":"p","languages":[],"frameworks":[],"description":"p","analyzedAt":"now","gitCommitHash":"abc"},
		"nodes":[{"id":"file:src/main.go","type":"file","name":"main.go","filePath":"src/main.go","summary":"Main","tags":[],"complexity":"moderate"}],
		"edges":[{"source":"file:src/main.go","target":"file:src/main.go","type":"imports"}],
		"layers":[],
		"tour":[]
	}`)
	result := ValidateGraphJSON(input)
	require.True(t, result.Success)
	require.Equal(t, "forward", result.Data.Edges[0].Direction)
	require.Equal(t, 0.5, result.Data.Edges[0].Weight)
}

func TestValidateGraphJSONDropsInvalidObjectWeight(t *testing.T) {
	input := []byte(`{
		"version":"1.0.0",
		"project":{"name":"p","languages":[],"frameworks":[],"description":"p","analyzedAt":"now","gitCommitHash":"abc"},
		"nodes":[{"id":"file:src/main.go","type":"file","name":"main.go","filePath":"src/main.go","summary":"Main","tags":[],"complexity":"moderate"}],
		"edges":[{"source":"file:src/main.go","target":"file:src/main.go","type":"imports","direction":"forward","weight":{"bad":true}}],
		"layers":[],
		"tour":[]
	}`)
	result := ValidateGraphJSON(input)
	require.True(t, result.Success)
	require.Empty(t, result.Data.Edges)
	requireIssue(t, result.Issues, IssueDropped, "invalid-edge")
}

func TestValidateGraphJSONUsesRawTolerantPathBeforeStrongTyping(t *testing.T) {
	input := []byte(`{
		"version":"1.0.0",
		"project":{"name":"p","languages":[],"frameworks":[],"description":"p","analyzedAt":"now","gitCommitHash":"abc"},
		"nodes":[
			{"id":"raw-file","type":123,"name":"Raw File","filePath":"src/raw.go","summary":42,"tags":"not-array","complexity":"medium"},
			{"id":"bad-line","type":"file","name":"Bad Line","filePath":"src/bad.go","summary":"Bad","tags":[],"complexity":"moderate","lineRange":"bad"}
		],
		"edges":[
			{"source":"raw-file","target":"raw-file","type":"uses","direction":123,"weight":"0.75"},
			{"source":"raw-file","target":"missing","type":"calls","direction":"forward","weight":0.5}
		],
		"layers":[{"id":"l","name":"Layer","description":"Layer","nodeIds":["raw-file","missing"]}],
		"tour":[{"order":1,"title":"Tour","description":"Tour","nodeIds":["raw-file","missing"]}]
	}`)

	result := ValidateGraphJSON(input)
	require.True(t, result.Success, result.Fatal)
	require.NotNil(t, result.Data)
	require.Len(t, result.Data.Nodes, 1)
	node := result.Data.Nodes[0]
	require.Equal(t, "file", node.Type)
	require.Equal(t, "file:src/raw.go", node.ID)
	require.Equal(t, "Raw File", node.Summary)
	require.Equal(t, []string{}, node.Tags)
	require.Equal(t, "moderate", node.Complexity)
	require.Len(t, result.Data.Edges, 1)
	require.Equal(t, "file:src/raw.go", result.Data.Edges[0].Source)
	require.Equal(t, "file:src/raw.go", result.Data.Edges[0].Target)
	require.Equal(t, "depends_on", result.Data.Edges[0].Type)
	require.Equal(t, "forward", result.Data.Edges[0].Direction)
	require.Equal(t, 0.75, result.Data.Edges[0].Weight)
	requireIssue(t, result.Issues, IssueAutoCorrected, "missing-field")
	requireIssue(t, result.Issues, IssueDropped, "invalid-node")
	requireIssue(t, result.Issues, IssueDropped, "invalid-reference")
}

func TestValidateGraphPreservesOnlyPassthroughFields(t *testing.T) {
	input := []byte(`{
		"version":"1.0.0",
		"project":{"name":"p","languages":[],"frameworks":[],"description":"p","analyzedAt":"now","gitCommitHash":"abc","unknownProject":true},
		"nodes":[{"id":"file:src/main.go","type":"file","name":"main.go","filePath":"src/main.go","summary":"Main","tags":[],"complexity":"moderate","xNode":42,"domainMeta":{"entryType":"cli","xDomain":true},"knowledgeMeta":{"category":"docs","xKnowledge":"yes"}}],
		"edges":[{"source":"file:src/main.go","target":"file:src/main.go","type":"imports","direction":"forward","weight":0.5,"xEdge":true}],
		"layers":[{"id":"l","name":"L","description":"L","nodeIds":["file:src/main.go"],"xLayer":true}],
		"tour":[{"order":1,"title":"T","description":"T","nodeIds":["file:src/main.go"],"xTour":true}]
	}`)

	result := ValidateGraphJSON(input)
	require.True(t, result.Success)
	data, err := json.Marshal(result.Data)
	require.NoError(t, err)
	var raw map[string]any
	require.NoError(t, json.Unmarshal(data, &raw))
	nodes := raw["nodes"].([]any)
	node := nodes[0].(map[string]any)
	require.Equal(t, float64(42), node["xNode"])
	require.Equal(t, true, node["domainMeta"].(map[string]any)["xDomain"])
	require.Equal(t, "yes", node["knowledgeMeta"].(map[string]any)["xKnowledge"])
	require.NotContains(t, raw["project"].(map[string]any), "unknownProject")
	require.NotContains(t, raw["edges"].([]any)[0].(map[string]any), "xEdge")
	require.NotContains(t, raw["layers"].([]any)[0].(map[string]any), "xLayer")
	require.NotContains(t, raw["tour"].([]any)[0].(map[string]any), "xTour")
}

func TestNormalizeNodeIDPreservesCallableNames(t *testing.T) {
	graph := minimalGraph()
	graph.Nodes = []GraphNode{{ID: "legacy", Type: "function", Name: "processData", FilePath: "src/service.ts", Summary: "Process", Tags: []string{}, Complexity: "moderate"}}
	result := ValidateGraph(graph)
	require.True(t, result.Success)
	require.Equal(t, "function:src/service.ts:processData", result.Data.Nodes[0].ID)
}

func TestContainedNonCodeChildIDSkipsLayerRequirement(t *testing.T) {
	graph := minimalGraph()
	graph.Nodes = []GraphNode{
		{ID: "file:deploy/app.yaml", Type: "resource", Name: "app.yaml", FilePath: "deploy/app.yaml", Summary: "K8s file", Tags: []string{"yaml"}, Complexity: "simple"},
		{ID: "service:deploy/app.yaml:old", Type: "service", Name: "api", FilePath: "deploy/app.yaml", Summary: "K8s service", Tags: []string{"kubernetes"}, Complexity: "simple"},
	}
	graph.Edges = []GraphEdge{{Source: "file:deploy/app.yaml", Target: "service:deploy/app.yaml:old", Type: "contains", Direction: "forward", Weight: 1}}
	graph.Layers = []Layer{{ID: "layer:infra", Name: "Infra", Description: "Infra", NodeIDs: []string{"file:deploy/app.yaml"}}}

	result := ValidateGraph(graph)
	require.True(t, result.Success, result.Fatal)
	require.Equal(t, "service:deploy/app.yaml:api", result.Data.Nodes[1].ID)
	requireNoIssue(t, result.Issues, "file-node-not-in-any-layer")
	require.Equal(t, []string{"resource:deploy/app.yaml"}, result.Data.Layers[0].NodeIDs)
}

func TestValidateGraphLayerCoverageAndExclusivity(t *testing.T) {
	graph := minimalGraph()
	graph.Nodes = []GraphNode{
		{ID: "file:src/main.go", Type: "file", Name: "main.go", FilePath: "src/main.go", Summary: "Main", Tags: []string{"go"}, Complexity: "moderate"},
		{ID: "config:config.yaml", Type: "config", Name: "config.yaml", FilePath: "config.yaml", Summary: "Config", Tags: []string{"config"}, Complexity: "simple"},
		{ID: "function:src/main.go:Run", Type: "function", Name: "Run", FilePath: "src/main.go", Summary: "Run", Tags: []string{"go"}, Complexity: "simple"},
	}
	graph.Edges = []GraphEdge{{Source: "file:src/main.go", Target: "function:src/main.go:Run", Type: "contains", Direction: "forward", Weight: 1}}
	graph.Layers = []Layer{
		{ID: "layer:a", Name: "A", Description: "A", NodeIDs: []string{"file:src/main.go", "function:src/main.go:Run"}},
		{ID: "layer:b", Name: "B", Description: "B", NodeIDs: []string{"file:src/main.go"}},
	}

	result := ValidateGraph(graph)
	require.True(t, result.Success, result.Fatal)
	requireIssue(t, result.Issues, IssueError, "node-in-multiple-layers")
	requireIssue(t, result.Issues, IssueError, "file-node-not-in-any-layer")
	requireNoIssue(t, result.Issues, "function-node-not-in-any-layer")
}

func TestValidateGraphReportsOrphanNodeAsWarning(t *testing.T) {
	graph := minimalGraph()
	graph.Nodes = append(graph.Nodes, GraphNode{
		ID:         "file:src/orphan.go",
		Type:       "file",
		Name:       "orphan.go",
		FilePath:   "src/orphan.go",
		Summary:    "Orphan",
		Tags:       []string{"go"},
		Complexity: "simple",
	})
	graph.Layers = []Layer{{ID: "layer:core", Name: "Core", Description: "Core", NodeIDs: []string{"file:src/main.go", "file:src/orphan.go"}}}

	result := ValidateGraph(graph)
	require.True(t, result.Success, result.Fatal)
	requireIssue(t, result.Issues, IssueWarning, "orphan-node")
	requireNoIssue(t, result.Issues, "summary-degradation")
}

func TestValidateDiffOverlayContract(t *testing.T) {
	require.NoError(t, ValidateDiffOverlay(DiffOverlay{ChangedNodeIDs: []string{"a"}, AffectedNodeIDs: []string{"b"}}))
	require.Error(t, ValidateDiffOverlay(DiffOverlay{AffectedNodeIDs: []string{"b"}}))
	require.Error(t, ValidateDiffOverlay(DiffOverlay{ChangedNodeIDs: []string{"a"}}))
}

func minimalGraph() KnowledgeGraph {
	return KnowledgeGraph{
		Version: "1.0.0",
		Kind:    KindCodebase,
		Project: ProjectMeta{
			Name:          "project",
			Languages:     []string{},
			Frameworks:    []string{},
			Description:   "project",
			AnalyzedAt:    "2026-07-02T00:00:00Z",
			GitCommitHash: "abc123",
		},
		Nodes:  []GraphNode{{ID: "file:src/main.go", Type: "file", Name: "main.go", FilePath: "src/main.go", Summary: "Main", Tags: []string{}, Complexity: "moderate"}},
		Edges:  []GraphEdge{},
		Layers: []Layer{},
		Tour:   []TourStep{},
	}
}

func requireIssue(t *testing.T, issues []GraphIssue, level IssueLevel, category string) {
	t.Helper()
	for _, issue := range issues {
		if issue.Level == level && issue.Category == category {
			return
		}
	}
	t.Fatalf("missing issue level=%s category=%s in %#v", level, category, issues)
}

func requireNoIssue(t *testing.T, issues []GraphIssue, category string) {
	t.Helper()
	for _, issue := range issues {
		if issue.Category == category {
			t.Fatalf("unexpected issue category=%s in %#v", category, issues)
		}
	}
}
