package mcp

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

	mcplib "github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/zzet/gortex/internal/config"
	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/indexer"
	"github.com/zzet/gortex/internal/parser"
	"github.com/zzet/gortex/internal/parser/languages"
	"github.com/zzet/gortex/internal/query"
)

// dataflowTestServer indexes a single Go file with a known
// source-mid-sink shape so we can exercise the MCP tools end-to-end
// without depending on the larger fixture used by setupTestServer.
func dataflowTestServer(t *testing.T) *Server {
	t.Helper()
	dir := t.TempDir()
	src := `package main

func Source(seed string) string {
	return seed
}

func Mid(s string) string {
	v := s
	return v
}

func Sink(payload string) {}

func Driver(input string) {
	a := Source(input)
	b := Mid(a)
	Sink(b)
}
`
	require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644))
	g := graph.New()
	reg := parser.NewRegistry()
	languages.RegisterAll(reg)
	cfg := config.Default()
	idx := indexer.New(g, reg, cfg.Index, zap.NewNop())
	_, err := idx.Index(dir)
	require.NoError(t, err)
	idx.ResolveAll()
	eng := query.NewEngine(g)
	srv := NewServer(eng, g, idx, nil, zap.NewNop(), nil)
	return srv
}

// findFunctionID walks the graph for a function/method node by
// name and returns its ID (the prefix is repo-dependent in our
// indexer, so we discover it via the live graph).
func findFunctionID(t *testing.T, srv *Server, name string) string {
	t.Helper()
	for _, n := range srv.graph.AllNodes() {
		if n.Kind != graph.KindFunction && n.Kind != graph.KindMethod {
			continue
		}
		if n.Name == name {
			return n.ID
		}
	}
	t.Fatalf("function %q not found", name)
	return ""
}

func TestHandleFlowBetween_DirectPath(t *testing.T) {
	srv := dataflowTestServer(t)
	driverID := findFunctionID(t, srv, "Driver")

	req := mcplib.CallToolRequest{}
	req.Params.Arguments = map[string]any{
		"source_id": driverID + "#param:input",
		"sink_id":   findFunctionID(t, srv, "Sink") + "#param:payload",
		"max_depth": float64(10),
	}
	res, err := srv.handleFlowBetween(t.Context(), req)
	require.NoError(t, err)
	require.False(t, res.IsError, "tool errored: %v", res)

	var payload struct {
		Total int `json:"total"`
		Paths []struct {
			IDs   []string `json:"ids"`
			Edges []struct {
				Kind string `json:"kind"`
			} `json:"edges"`
		} `json:"paths"`
	}
	text := res.Content[0].(mcplib.TextContent).Text
	require.NoError(t, json.Unmarshal([]byte(text), &payload))
	require.Greater(t, payload.Total, 0, "expected at least one path; got: %s", text)
	// Path must traverse Source / Mid → Sink param.
	first := payload.Paths[0]
	if !contains(first.IDs, "Source") {
		t.Logf("path does not reference Source by short name (this is fine: IDs are full): %v", first.IDs)
	}
}

func TestHandleFlowBetween_MissingSource(t *testing.T) {
	srv := dataflowTestServer(t)
	req := mcplib.CallToolRequest{}
	req.Params.Arguments = map[string]any{
		"sink_id": "anything",
	}
	res, err := srv.handleFlowBetween(t.Context(), req)
	require.NoError(t, err)
	require.True(t, res.IsError, "expected error result for missing source_id")
}

func TestHandleFlowBetween_GCXFormat(t *testing.T) {
	srv := dataflowTestServer(t)
	driverID := findFunctionID(t, srv, "Driver")
	req := mcplib.CallToolRequest{}
	req.Params.Name = "flow_between"
	req.Params.Arguments = map[string]any{
		"source_id": driverID + "#param:input",
		"sink_id":   findFunctionID(t, srv, "Sink") + "#param:payload",
		"format":    "gcx",
	}
	res, err := srv.handleFlowBetween(t.Context(), req)
	require.NoError(t, err)
	require.False(t, res.IsError)
	text := res.Content[0].(mcplib.TextContent).Text
	require.Contains(t, text, "flow_between.summary")
	require.Contains(t, text, "flow_between.paths")
}

func TestHandleTaintPaths_PatternMatch(t *testing.T) {
	srv := dataflowTestServer(t)
	req := mcplib.CallToolRequest{}
	req.Params.Arguments = map[string]any{
		"source_pattern": "name:Source",
		"sink_pattern":   "name:Sink",
		"max_depth":      float64(10),
	}
	res, err := srv.handleTaintPaths(t.Context(), req)
	require.NoError(t, err)
	require.False(t, res.IsError, "errored: %v", res)
	text := res.Content[0].(mcplib.TextContent).Text
	require.Contains(t, text, "findings")
	var payload struct {
		Total    int              `json:"total"`
		Findings []map[string]any `json:"findings"`
	}
	require.NoError(t, json.Unmarshal([]byte(text), &payload))
	require.Greater(t, payload.Total, 0, "expected at least one finding; got %s", text)
}

func TestHandleTaintPaths_EmptyPattern(t *testing.T) {
	srv := dataflowTestServer(t)
	req := mcplib.CallToolRequest{}
	req.Params.Arguments = map[string]any{
		"source_pattern": "",
		"sink_pattern":   "name:Sink",
	}
	res, err := srv.handleTaintPaths(t.Context(), req)
	require.NoError(t, err)
	require.True(t, res.IsError, "expected error for empty source_pattern")
}

// TestHandleTaintPaths_E2EMultiFile builds a more realistic
// multi-file scenario: `os/Getenv`-shaped source returning a
// secret, threaded through a couple of helpers, and ultimately
// passed to a sink simulating `db.Query`. Verifies that
// flow_between → returns_to → arg_of compose into a path the
// agent can recover via `name:` patterns.
func TestHandleTaintPaths_E2EMultiFile(t *testing.T) {
	dir := t.TempDir()
	require.NoError(t, os.WriteFile(filepath.Join(dir, "secret.go"), []byte(`package main

func Getenv(key string) string {
	return key
}
`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "format.go"), []byte(`package main

func Format(secret string) string {
	out := secret
	return out
}
`), 0o644))
	require.NoError(t, os.WriteFile(filepath.Join(dir, "db.go"), []byte(`package main

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

func Driver(name string) {
	secret := Getenv(name)
	sql := Format(secret)
	DBQuery(sql)
}
`), 0o644))

	g := graph.New()
	reg := parser.NewRegistry()
	languages.RegisterAll(reg)
	cfg := config.Default()
	idx := indexer.New(g, reg, cfg.Index, zap.NewNop())
	_, err := idx.Index(dir)
	require.NoError(t, err)
	idx.ResolveAll()
	eng := query.NewEngine(g)
	srv := NewServer(eng, g, idx, nil, zap.NewNop(), nil)

	req := mcplib.CallToolRequest{}
	req.Params.Arguments = map[string]any{
		"source_pattern": "name:Getenv",
		"sink_pattern":   "name:DBQuery",
		"max_depth":      float64(20),
	}
	res, err := srv.handleTaintPaths(t.Context(), req)
	require.NoError(t, err)
	require.False(t, res.IsError, "tool errored: %v", res)
	text := res.Content[0].(mcplib.TextContent).Text

	var payload struct {
		Total    int `json:"total"`
		Findings []struct {
			Source map[string]any `json:"source"`
			Sink   map[string]any `json:"sink"`
			Paths  []struct {
				IDs []string `json:"ids"`
			} `json:"paths"`
		} `json:"findings"`
	}
	require.NoError(t, json.Unmarshal([]byte(text), &payload))
	require.Greater(t, payload.Total, 0, "expected E2E taint finding; got %s", text)
	bestPath := payload.Findings[0].Paths[0]
	if len(bestPath.IDs) < 3 {
		t.Errorf("expected multi-hop path; got %v", bestPath.IDs)
	}
	// Path must touch a Format-related node — confirms the BFS
	// crossed the intermediate function.
	found := false
	for _, id := range bestPath.IDs {
		if strings.Contains(id, "Format") {
			found = true
			break
		}
	}
	if !found {
		t.Errorf("expected Format on E2E path; got %v", bestPath.IDs)
	}
}

func TestHandleTaintPaths_GCXFormat(t *testing.T) {
	srv := dataflowTestServer(t)
	req := mcplib.CallToolRequest{}
	req.Params.Name = "taint_paths"
	req.Params.Arguments = map[string]any{
		"source_pattern": "name:Source",
		"sink_pattern":   "name:Sink",
		"format":         "gcx",
	}
	res, err := srv.handleTaintPaths(t.Context(), req)
	require.NoError(t, err)
	require.False(t, res.IsError)
	text := res.Content[0].(mcplib.TextContent).Text
	require.Contains(t, text, "taint_paths.summary")
	require.Contains(t, text, "taint_paths.findings")
}

func contains(haystack []string, needle string) bool {
	for _, s := range haystack {
		if strings.Contains(s, needle) {
			return true
		}
	}
	return false
}
