package mcp

import (
	"context"
	"fmt"
	"sort"
	"strings"

	mcp "github.com/mark3labs/mcp-go/mcp"

	"github.com/zzet/gortex/internal/graph"
)

// ---------------------------------------------------------------------------
// analyze kind=tests_as_edges
// ---------------------------------------------------------------------------
//
// A first-class view over the EdgeTests layer — the test → code edges
// the indexer stamps for every call from a test function to a
// non-test symbol. get_untested_symbols answers the negative ("what
// has no tests"); this answers the positive: which symbols ARE
// exercised, and by which tests.
//
// group_by=symbol (default) lists each tested symbol with the tests
// covering it; group_by=test inverts it — each test with the symbols
// it exercises. Both carry a summary of the edge layer's size.

type testEdgeRef struct {
	ID   string `json:"id"`
	Name string `json:"name"`
}

type testEdgeRow struct {
	ID      string        `json:"id"`
	Name    string        `json:"name"`
	Kind    string        `json:"kind"`
	File    string        `json:"file"`
	Line    int           `json:"line"`
	Related []testEdgeRef `json:"related"`
	Count   int           `json:"count"`
}

func (s *Server) handleAnalyzeTestsAsEdges(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
	args := req.GetArguments()
	pathPrefix := strings.TrimSpace(stringArg(args, "path_prefix"))
	limit := intArg(args, "limit", 200)
	groupBy := strings.ToLower(strings.TrimSpace(stringArg(args, "group_by")))
	if groupBy == "" {
		groupBy = "symbol"
	}
	if groupBy != "symbol" && groupBy != "test" {
		return mcp.NewToolResultError("analyze tests_as_edges: group_by must be 'symbol' or 'test'"), nil
	}

	// Collect the EdgeTests layer. From = test function, To = the
	// non-test symbol it exercises.
	testsBySymbol := make(map[string][]string)
	symbolsByTest := make(map[string][]string)
	edgeCount := 0
	for _, e := range s.graph.AllEdges() {
		if e.Kind != graph.EdgeTests {
			continue
		}
		edgeCount++
		testsBySymbol[e.To] = append(testsBySymbol[e.To], e.From)
		symbolsByTest[e.From] = append(symbolsByTest[e.From], e.To)
	}

	primary := testsBySymbol
	if groupBy == "test" {
		primary = symbolsByTest
	}

	rows := make([]testEdgeRow, 0, len(primary))
	for id, relatedIDs := range primary {
		n := s.graph.GetNode(id)
		if n == nil {
			continue
		}
		if pathPrefix != "" && !strings.HasPrefix(n.FilePath, pathPrefix) {
			continue
		}
		related := make([]testEdgeRef, 0, len(relatedIDs))
		seen := make(map[string]bool, len(relatedIDs))
		for _, rid := range relatedIDs {
			if seen[rid] {
				continue
			}
			seen[rid] = true
			name := rid
			if rn := s.graph.GetNode(rid); rn != nil {
				name = rn.Name
			}
			related = append(related, testEdgeRef{ID: rid, Name: name})
		}
		sort.Slice(related, func(i, j int) bool { return related[i].ID < related[j].ID })
		rows = append(rows, testEdgeRow{
			ID:      n.ID,
			Name:    n.Name,
			Kind:    string(n.Kind),
			File:    n.FilePath,
			Line:    n.StartLine,
			Related: related,
			Count:   len(related),
		})
	}

	// Most-covered first — the symbols with the deepest test backing,
	// or the tests exercising the most code.
	sort.Slice(rows, func(i, j int) bool {
		if rows[i].Count != rows[j].Count {
			return rows[i].Count > rows[j].Count
		}
		return rows[i].ID < rows[j].ID
	})
	total := len(rows)
	truncated := false
	if limit > 0 && len(rows) > limit {
		rows = rows[:limit]
		truncated = true
	}

	relatedKey := "tests"
	if groupBy == "test" {
		relatedKey = "covers"
	}
	// Re-key the generic "related" field to the group-appropriate name
	// in the JSON payload via a thin projection.
	projected := make([]map[string]any, 0, len(rows))
	for _, r := range rows {
		projected = append(projected, map[string]any{
			"id":         r.ID,
			"name":       r.Name,
			"kind":       r.Kind,
			"file":       r.File,
			"line":       r.Line,
			relatedKey:   r.Related,
			"edge_count": r.Count,
		})
	}

	if isCompact(req) {
		var b strings.Builder
		for _, r := range rows {
			fmt.Fprintf(&b, "%-3d %s:%d  %s\n", r.Count, r.File, r.Line, r.ID)
		}
		if len(rows) == 0 {
			b.WriteString("no test edges in scope\n")
		}
		return mcp.NewToolResultText(b.String()), nil
	}

	resp := map[string]any{
		"group_by": groupBy,
		"rows":     projected,
		"total":    total,
		"summary": map[string]any{
			"test_edges":     edgeCount,
			"tested_symbols": len(testsBySymbol),
			"test_functions": len(symbolsByTest),
		},
		"truncated": truncated,
	}
	if truncated {
		resp["limit"] = limit
	}
	return s.respondJSONOrTOON(ctx, req, resp)
}
