package mcp

import (
	"context"
	"encoding/json"
	"testing"

	"github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zzet/gortex/internal/analysis"
	"github.com/zzet/gortex/internal/contracts"
	"github.com/zzet/gortex/internal/graph"
)

func newArchitectureTestServer(t *testing.T) *Server {
	t.Helper()
	g := graph.New()
	// A small architecture: handler.go calls service.go calls repo.go.
	// Plus a Python file in a different language for the language mix.
	g.AddNode(&graph.Node{ID: "p/handler.go::Handle", Name: "Handle", Kind: graph.KindFunction, FilePath: "p/handler.go", Language: "go", StartLine: 5})
	g.AddNode(&graph.Node{ID: "p/service.go::Process", Name: "Process", Kind: graph.KindFunction, FilePath: "p/service.go", Language: "go"})
	g.AddNode(&graph.Node{ID: "p/repo.go::Fetch", Name: "Fetch", Kind: graph.KindFunction, FilePath: "p/repo.go", Language: "go"})
	g.AddNode(&graph.Node{ID: "p/script.py::main", Name: "main", Kind: graph.KindFunction, FilePath: "p/script.py", Language: "python"})

	g.AddEdge(&graph.Edge{From: "p/handler.go::Handle", To: "p/service.go::Process", Kind: graph.EdgeCalls})
	g.AddEdge(&graph.Edge{From: "p/service.go::Process", To: "p/repo.go::Fetch", Kind: graph.EdgeCalls})

	return &Server{
		graph:      g,
		session:    newSessionState(),
		tokenStats: &tokenStats{},
		symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
		sessions:   newSessionMap(),
		toolScopes: newScopeRegistry(),
	}
}

func callArchitectureHandler(t *testing.T, s *Server, args map[string]any) map[string]any {
	t.Helper()
	req := mcp.CallToolRequest{}
	req.Params.Arguments = args
	res, err := s.handleGetArchitecture(context.Background(), req)
	require.NoError(t, err)
	require.NotNil(t, res)
	require.False(t, res.IsError, "handler error: %+v", res.Content)
	tc, ok := res.Content[0].(mcp.TextContent)
	require.True(t, ok)
	var m map[string]any
	require.NoError(t, json.Unmarshal([]byte(tc.Text), &m))
	return m
}

func TestArchitecture_SummaryReportsLanguageMix(t *testing.T) {
	s := newArchitectureTestServer(t)
	out := callArchitectureHandler(t, s, map[string]any{})

	summary := out["summary"].(map[string]any)
	assert.Equal(t, "go", summary["primary_language"], "go wins on node count")
	languages, _ := summary["languages"].([]any)
	require.NotEmpty(t, languages)

	// Find python entry.
	pythonSeen := false
	for _, l := range languages {
		m := l.(map[string]any)
		if m["name"] == "python" {
			pythonSeen = true
		}
	}
	assert.True(t, pythonSeen, "python should appear in the language mix")

	assert.EqualValues(t, 4, summary["total_nodes"].(float64))
	assert.EqualValues(t, 2, summary["total_edges"].(float64))
}

func TestArchitecture_EntryPointsAreUncalledFunctions(t *testing.T) {
	s := newArchitectureTestServer(t)
	out := callArchitectureHandler(t, s, map[string]any{})

	entries, _ := out["entry_points"].([]any)
	require.NotEmpty(t, entries, "Handle has zero in-edges and one out-edge — it must surface")
	ids := map[string]bool{}
	for _, e := range entries {
		ids[e.(map[string]any)["id"].(string)] = true
	}
	assert.True(t, ids["p/handler.go::Handle"], "Handle is the obvious entry point")
	assert.False(t, ids["p/service.go::Process"], "Process has a caller, not an entry point")
}

func TestArchitecture_CommunitiesIncludedWhenAnalysisCached(t *testing.T) {
	s := newArchitectureTestServer(t)
	s.analysisMu.Lock()
	s.communities = &analysis.CommunityResult{
		Communities: []analysis.Community{
			{
				ID:    "c-handler",
				Label: "handler-service-repo",
				Size:  3,
				Files: []string{"p/handler.go", "p/service.go", "p/repo.go"},
				Members: []string{
					"p/handler.go::Handle", "p/service.go::Process", "p/repo.go::Fetch",
				},
			},
		},
		Modularity: 0.5,
	}
	s.analysisMu.Unlock()

	out := callArchitectureHandler(t, s, map[string]any{})
	communities := out["communities"].(map[string]any)
	assert.EqualValues(t, 1, communities["count"].(float64))
	top := communities["top"].([]any)
	require.Len(t, top, 1)
	assert.Equal(t, "c-handler", top[0].(map[string]any)["id"])
}

func TestArchitecture_CrossRepoRollup(t *testing.T) {
	s := newArchitectureTestServer(t)
	// Add a second repo's node and a cross-repo call edge.
	s.graph.AddNode(&graph.Node{ID: "repoB/lib.go::Util", Name: "Util", Kind: graph.KindFunction, FilePath: "repoB/lib.go", Language: "go", RepoPrefix: "repoB"})
	for n := range []string{"p/handler.go::Handle", "p/service.go::Process", "p/repo.go::Fetch", "p/script.py::main"} {
		_ = n
	}
	// Mark our existing handler node with a RepoPrefix so the
	// cross-repo edge has named endpoints.
	hn := s.graph.GetNode("p/handler.go::Handle")
	hn.RepoPrefix = "repoA"
	s.graph.AddEdge(&graph.Edge{
		From: "p/handler.go::Handle",
		To:   "repoB/lib.go::Util",
		Kind: graph.EdgeCrossRepoCalls,
	})

	out := callArchitectureHandler(t, s, map[string]any{})
	cross, _ := out["cross_repo"].([]any)
	require.Len(t, cross, 1)
	row := cross[0].(map[string]any)
	assert.Equal(t, "cross_repo_calls", row["kind"])
	assert.Equal(t, "repoA", row["from_repo"])
	assert.Equal(t, "repoB", row["to_repo"])
	assert.EqualValues(t, 1, row["count"].(float64))
}

func TestArchitecture_ContractsRollup(t *testing.T) {
	s := newArchitectureTestServer(t)
	reg := contracts.NewRegistry()
	reg.Add(contracts.Contract{
		ID: "http:GET:/api/users", Type: contracts.ContractType("http"), Role: contracts.RoleProvider,
		SymbolID: "p/handler.go::Handle", FilePath: "p/handler.go", RepoPrefix: "repoA",
	})
	reg.Add(contracts.Contract{
		ID: "http:GET:/api/users", Type: contracts.ContractType("http"), Role: contracts.RoleConsumer,
		SymbolID: "p/client.go::Call", FilePath: "p/client.go", RepoPrefix: "repoB",
	})
	s.contractRegistry = reg

	out := callArchitectureHandler(t, s, map[string]any{})
	contractsSection := out["contracts"].(map[string]any)
	assert.EqualValues(t, 2, contractsSection["total"].(float64))
	byType := contractsSection["by_type"].(map[string]any)
	assert.EqualValues(t, 2, byType["http"].(float64))
	byRole := contractsSection["by_role"].(map[string]any)
	assert.EqualValues(t, 1, byRole["provider"].(float64))
	assert.EqualValues(t, 1, byRole["consumer"].(float64))
}

func TestArchitecture_PathPrefixFilter(t *testing.T) {
	s := newArchitectureTestServer(t)
	// Add an extra file outside p/.
	s.graph.AddNode(&graph.Node{ID: "vendor/x.go::X", Name: "X", Kind: graph.KindFunction, FilePath: "vendor/x.go", Language: "go"})
	s.graph.AddEdge(&graph.Edge{From: "vendor/x.go::X", To: "p/repo.go::Fetch", Kind: graph.EdgeCalls})

	out := callArchitectureHandler(t, s, map[string]any{"path_prefix": "p/"})
	summary := out["summary"].(map[string]any)
	// vendor/x.go is excluded; only the 4 original p/ nodes.
	assert.EqualValues(t, 4, summary["total_nodes"].(float64))
}

func TestArchitecture_EmptyGraphIsSafe(t *testing.T) {
	s := &Server{
		graph:      graph.New(),
		session:    newSessionState(),
		tokenStats: &tokenStats{},
		symHistory: &symbolHistory{entries: make(map[string][]SymbolModification)},
		sessions:   newSessionMap(),
		toolScopes: newScopeRegistry(),
	}
	out := callArchitectureHandler(t, s, map[string]any{})

	// Every section should be present and empty/zero.
	for _, key := range []string{"summary", "communities", "hotspots", "entry_points", "processes", "cross_repo", "contracts"} {
		_, ok := out[key]
		assert.True(t, ok, "section %q must be present even on empty graph", key)
	}
}

func TestArchitecture_TopCommunitiesCap(t *testing.T) {
	s := newArchitectureTestServer(t)
	comms := []analysis.Community{}
	for i := range 10 {
		comms = append(comms, analysis.Community{
			ID:      "c" + string(rune('A'+i)),
			Label:   "c" + string(rune('A'+i)),
			Size:    10 - i,
			Members: []string{"p/handler.go::Handle"},
			Files:   []string{"p/handler.go"},
		})
	}
	s.analysisMu.Lock()
	s.communities = &analysis.CommunityResult{Communities: comms}
	s.analysisMu.Unlock()

	out := callArchitectureHandler(t, s, map[string]any{"top_communities": 3})
	top := out["communities"].(map[string]any)["top"].([]any)
	assert.Len(t, top, 3, "cap honored")
}
