package indexer

import (
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"

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

// indexAll indexes a single-file Go fixture and runs the global
// resolve + dataflow materialisation pass. Returns the graph for
// assertions.
func indexAll(t *testing.T, src string) *graph.Graph {
	t.Helper()
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644))

	g := graph.New()
	idx := newTestIndexer(g)
	_, err := idx.Index(dir)
	require.NoError(t, err)
	idx.ResolveAll()
	return g
}

// findEdges returns all edges matching the predicate.
func findEdges(g *graph.Graph, kind graph.EdgeKind, match func(*graph.Edge) bool) []*graph.Edge {
	var out []*graph.Edge
	for _, e := range g.AllEdges() {
		if e.Kind != kind {
			continue
		}
		if match(e) {
			out = append(out, e)
		}
	}
	return out
}

// TestMaterializeDataflowParams_ArgOfLifted verifies the post-pass
// rewrites EdgeArgOf targets from the function node to the actual
// param node at the recorded position.
func TestMaterializeDataflowParams_ArgOfLifted(t *testing.T) {
	src := `package main

func Sink(payload string) {}

func Driver(input string) {
	Sink(input)
}
`
	g := indexAll(t, src)

	// Look up the resolved Driver function ID — we only know the
	// suffix, the prefix is whatever IndexDirectory uses.
	driverID := findFuncID(t, g, "Driver")
	sinkID := findFuncID(t, g, "Sink")

	args := findEdges(g, graph.EdgeArgOf, func(e *graph.Edge) bool {
		return e.From == driverID+"#param:input"
	})
	if len(args) == 0 {
		t.Fatalf("no arg_of edges from Driver#param:input; have edges:\n%s", dumpAllEdges(g))
	}
	// The post-pass should have lifted To from sinkID itself to
	// sinkID#param:payload.
	want := sinkID + "#param:payload"
	for _, e := range args {
		if e.To == want {
			return
		}
	}
	t.Fatalf("arg_of not lifted to %q; got: %s", want, dumpEdges(args))
}

// TestMaterializeDataflowParams_ReturnsToLifted verifies the post-pass
// rewrites EdgeReturnsTo From from the placeholder caller ID to
// the resolved callee.
func TestMaterializeDataflowParams_ReturnsToLifted(t *testing.T) {
	src := `package main

func Source() string { return "hi" }

func Driver() {
	v := Source()
	_ = v
}
`
	g := indexAll(t, src)

	driverID := findFuncID(t, g, "Driver")
	sourceID := findFuncID(t, g, "Source")

	rets := findEdges(g, graph.EdgeReturnsTo, func(e *graph.Edge) bool {
		return strings.HasPrefix(e.To, driverID+"#local:v@")
	})
	if len(rets) == 0 {
		t.Fatalf("no returns_to edges; have edges:\n%s", dumpAllEdges(g))
	}
	for _, e := range rets {
		if e.From == sourceID {
			return
		}
	}
	t.Fatalf("returns_to.From not lifted to %q; got: %s", sourceID, dumpEdges(rets))
}

// TestMaterializeDataflowParams_CrossFileBinding asserts dataflow
// edges connect across files: `Driver` (in driver.go) calls
// `Sink` (in sink.go) and the post-pass binds the arg_of edge to
// Sink's param node, even though neither side has the other's
// AST visible at extraction time.
func TestMaterializeDataflowParams_CrossFileBinding(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "sink.go"), []byte(`package main

func Sink(payload string) {}
`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "driver.go"), []byte(`package main

func Driver(input string) {
	Sink(input)
}
`), 0o644))

	g := graph.New()
	idx := newTestIndexer(g)
	_, err := idx.Index(dir)
	require.NoError(t, err)
	idx.ResolveAll()

	driverID := findFuncID(t, g, "Driver")
	sinkID := findFuncID(t, g, "Sink")
	want := sinkID + "#param:payload"

	args := findEdges(g, graph.EdgeArgOf, func(e *graph.Edge) bool {
		return e.From == driverID+"#param:input" && e.To == want
	})
	if len(args) == 0 {
		t.Fatalf("missing cross-file arg_of(Driver#param:input → %s); have:\n%s", want, dumpAllEdges(g))
	}
}

// TestMaterializeDataflowParams_NoStrandedPlaceholders asserts the
// post-pass leaves no stranded placeholders for resolvable callees.
func TestMaterializeDataflowParams_NoStrandedPlaceholders(t *testing.T) {
	src := `package main

func A(x int) int { return x }
func B(y int) int { return y }

func Driver(z int) int {
	return A(B(z))
}
`
	g := indexAll(t, src)

	// All arg_of edges that reference resolved callees should have
	// been lifted to a param-node target.
	driverID := findFuncID(t, g, "Driver")
	args := findEdges(g, graph.EdgeArgOf, func(e *graph.Edge) bool {
		return e.From == driverID+"#param:z" || e.From == ""
	})
	for _, e := range args {
		if !strings.Contains(e.To, "#param:") {
			t.Errorf("expected arg_of.To to contain #param:; got %q", e.To)
		}
	}
}

func findFuncID(t *testing.T, g *graph.Graph, name string) string {
	t.Helper()
	candidates := g.FindNodesByName(name)
	for _, n := range candidates {
		if n.Kind == graph.KindFunction || n.Kind == graph.KindMethod {
			return n.ID
		}
	}
	t.Fatalf("function %q not found", name)
	return ""
}

func dumpAllEdges(g *graph.Graph) string {
	var b strings.Builder
	for _, e := range g.AllEdges() {
		b.WriteString(string(e.Kind))
		b.WriteString(" ")
		b.WriteString(e.From)
		b.WriteString(" -> ")
		b.WriteString(e.To)
		b.WriteString("\n")
	}
	return b.String()
}

func dumpEdges(edges []*graph.Edge) string {
	var b strings.Builder
	for i, e := range edges {
		if i > 0 {
			b.WriteString(" | ")
		}
		b.WriteString(string(e.Kind))
		b.WriteString(" ")
		b.WriteString(e.From)
		b.WriteString(" -> ")
		b.WriteString(e.To)
	}
	return b.String()
}
