package mcp

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

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

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

// callSearchAST is the test harness: build a request with the given
// args, invoke the MCP handler, decode the JSON result. Any tool
// error fails the test.
func callSearchAST(t *testing.T, srv *Server, args map[string]any) map[string]any {
	t.Helper()
	req := mcplib.CallToolRequest{}
	req.Params.Name = "search_ast"
	req.Params.Arguments = args
	res, err := srv.handleSearchAST(context.Background(), req)
	if err != nil {
		t.Fatalf("handleSearchAST: %v", err)
	}
	if res.IsError {
		t.Fatalf("error result: %+v", res.Content)
	}
	textBlock := res.Content[0].(mcplib.TextContent)
	var out map[string]any
	if err := json.Unmarshal([]byte(textBlock.Text), &out); err != nil {
		t.Fatalf("json: %v\n%s", err, textBlock.Text)
	}
	return out
}

// writeTempGoFile drops fixture content into a temp dir and returns
// the absolute path. Tests register a KindFile node pointing at the
// returned path so the engine can find and parse it.
func writeTempGoFile(t *testing.T, name, src string) string {
	t.Helper()
	dir := t.TempDir()
	abs := filepath.Join(dir, name)
	if err := os.WriteFile(abs, []byte(src), 0o644); err != nil {
		t.Fatalf("write fixture: %v", err)
	}
	return abs
}

func TestSearchAST_RawPattern_GoPanic(t *testing.T) {
	srv, _ := setupTestServer(t)
	abs := writeTempGoFile(t, "lib.go", `package x

func F() {
	panic("boom")
}
`)
	srv.graph.AddNode(&graph.Node{
		ID: abs, Kind: graph.KindFile, Name: abs,
		FilePath: abs, Language: "go", StartLine: 1, EndLine: 5,
	})
	srv.graph.AddNode(&graph.Node{
		ID:        abs + "::F",
		Kind:      graph.KindFunction,
		Name:      "F",
		FilePath:  abs,
		StartLine: 3, EndLine: 5,
	})

	out := callSearchAST(t, srv, map[string]any{
		"pattern":  `((call_expression function: (identifier) @fn) @match (#eq? @fn "panic"))`,
		"language": "go",
	})
	if got, _ := out["total"].(float64); got != 1 {
		t.Fatalf("expected 1 match, got %v\n%v", got, out)
	}
	matches := out["matches"].([]any)
	first := matches[0].(map[string]any)
	if first["symbol_id"] != abs+"::F" {
		t.Errorf("expected enclosing symbol_id %q, got %q", abs+"::F", first["symbol_id"])
	}
	if first["symbol_name"] != "F" {
		t.Errorf("expected symbol_name F, got %v", first["symbol_name"])
	}
}

func TestSearchAST_BundledDetector_HardcodedSecret(t *testing.T) {
	srv, _ := setupTestServer(t)
	abs := writeTempGoFile(t, "creds.go", `package x

func F() {
	password := "hunter2hunter2hunter"
	emptyDefault := ""
	_ = password
	_ = emptyDefault
}
`)
	srv.graph.AddNode(&graph.Node{
		ID: abs, Kind: graph.KindFile, Name: abs,
		FilePath: abs, Language: "go", StartLine: 1, EndLine: 8,
	})

	out := callSearchAST(t, srv, map[string]any{
		"detector": "hardcoded-secret",
	})
	total, _ := out["total"].(float64)
	if total != 1 {
		t.Fatalf("expected 1 match, got %v\n%v", total, out)
	}
	first := out["matches"].([]any)[0].(map[string]any)
	if first["detector"] != "hardcoded-secret" {
		t.Errorf("expected detector enrichment, got %v", first["detector"])
	}
}

func TestSearchAST_PathPrefixFilter(t *testing.T) {
	srv, _ := setupTestServer(t)
	matched := writeTempGoFile(t, "match.go", `package x
func F() { panic("hit") }
`)
	excluded := writeTempGoFile(t, "skip.go", `package x
func G() { panic("skip") }
`)
	for _, p := range []string{matched, excluded} {
		srv.graph.AddNode(&graph.Node{
			ID: p, Kind: graph.KindFile, Name: p,
			FilePath: p, Language: "go", StartLine: 1, EndLine: 2,
		})
	}

	out := callSearchAST(t, srv, map[string]any{
		"pattern":     `((call_expression function: (identifier) @fn) @match (#eq? @fn "panic"))`,
		"language":    "go",
		"path_prefix": filepath.Dir(matched),
	})
	total, _ := out["total"].(float64)
	if total != 1 {
		t.Fatalf("expected 1 match (path_prefix scopes the file set), got %v", total)
	}
	first := out["matches"].([]any)[0].(map[string]any)
	if first["file"] != matched {
		t.Errorf("expected match in %q, got %q", matched, first["file"])
	}
}

func TestSearchAST_RejectsBadInputs(t *testing.T) {
	srv, _ := setupTestServer(t)

	// No pattern, no detector.
	{
		req := mcplib.CallToolRequest{}
		req.Params.Name = "search_ast"
		req.Params.Arguments = map[string]any{}
		res, err := srv.handleSearchAST(context.Background(), req)
		if err != nil {
			t.Fatalf("handleSearchAST: %v", err)
		}
		if !res.IsError {
			t.Fatal("expected error when neither pattern nor detector is set")
		}
	}
	// Pattern without language.
	{
		req := mcplib.CallToolRequest{}
		req.Params.Name = "search_ast"
		req.Params.Arguments = map[string]any{"pattern": "(identifier) @match"}
		res, err := srv.handleSearchAST(context.Background(), req)
		if err != nil {
			t.Fatalf("handleSearchAST: %v", err)
		}
		if !res.IsError {
			t.Fatal("expected error for pattern without language")
		}
	}
	// Both pattern and detector.
	{
		req := mcplib.CallToolRequest{}
		req.Params.Name = "search_ast"
		req.Params.Arguments = map[string]any{
			"pattern":  "(identifier) @match",
			"detector": "panic-in-library",
			"language": "go",
		}
		res, err := srv.handleSearchAST(context.Background(), req)
		if err != nil {
			t.Fatalf("handleSearchAST: %v", err)
		}
		if !res.IsError {
			t.Fatal("expected error for mutually-exclusive args")
		}
	}
	// Unknown detector.
	{
		req := mcplib.CallToolRequest{}
		req.Params.Name = "search_ast"
		req.Params.Arguments = map[string]any{"detector": "no-such-rule"}
		res, err := srv.handleSearchAST(context.Background(), req)
		if err != nil {
			t.Fatalf("handleSearchAST: %v", err)
		}
		if !res.IsError {
			t.Fatal("expected error for unknown detector")
		}
	}
}

func TestSearchAST_MinFanInOfEnclosingFunc(t *testing.T) {
	srv, _ := setupTestServer(t)
	abs := writeTempGoFile(t, "two.go", `package x

func Hot() { panic("a") }
func Cold() { panic("b") }
`)
	srv.graph.AddNode(&graph.Node{
		ID: abs, Kind: graph.KindFile, Name: abs,
		FilePath: abs, Language: "go", StartLine: 1, EndLine: 4,
	})
	hot := &graph.Node{ID: abs + "::Hot", Kind: graph.KindFunction, Name: "Hot",
		FilePath: abs, StartLine: 3, EndLine: 3}
	cold := &graph.Node{ID: abs + "::Cold", Kind: graph.KindFunction, Name: "Cold",
		FilePath: abs, StartLine: 4, EndLine: 4}
	srv.graph.AddNode(hot)
	srv.graph.AddNode(cold)
	// Three callers on Hot, zero on Cold.
	for i := 0; i < 3; i++ {
		caller := &graph.Node{
			ID:       abs + "::caller" + string(rune('A'+i)),
			Kind:     graph.KindFunction,
			Name:     "caller",
			FilePath: abs, StartLine: 10 + i, EndLine: 10 + i,
		}
		srv.graph.AddNode(caller)
		srv.graph.AddEdge(&graph.Edge{From: caller.ID, To: hot.ID, Kind: graph.EdgeCalls})
	}

	out := callSearchAST(t, srv, map[string]any{
		"detector":                       "panic-in-library",
		"min_fan_in_of_enclosing_func":   float64(2),
	})
	total, _ := out["total"].(float64)
	if total != 1 {
		t.Fatalf("expected only Hot's panic to survive min_fan_in=2 filter, got total=%v", total)
	}
	first := out["matches"].([]any)[0].(map[string]any)
	if first["symbol_name"] != "Hot" {
		t.Errorf("expected enclosing symbol Hot, got %v", first["symbol_name"])
	}
}
