package ingest

import (
	"context"
	"testing"

	sitter "github.com/smacker/go-tree-sitter"
	"github.com/smacker/go-tree-sitter/golang"
	"github.com/smacker/go-tree-sitter/python"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestSitterWalkerPython(t *testing.T) {
	code := []byte(`
def hello():
    print("world")

def add(a, b):
    return a + b
`)
	// Create parser
	lang := python.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)

	// Parse code
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	w := NewSitterWalker()

	// Query for function definitions
	// This query captures the function name identifier as @name
	query := `(function_definition name: (identifier) @name)`

	// Prepare root context
	root := SitterRoot{
		Node:   tree.RootNode(),
		Source: code,
		Lang:   lang, // Query needs language
	}

	matches, err := w.Query(root, query)
	require.NoError(t, err)

	assert.Len(t, matches, 2)

	// Verify captures
	assert.Equal(t, map[string]any{"name": "hello"}, matches[0].Values())
	assert.Equal(t, map[string]any{"name": "add"}, matches[1].Values())
}

func TestSitterWalkerGo(t *testing.T) {
	code := []byte(`
package main

func main() {
	println("hello")
}

func helper() {}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)

	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	w := NewSitterWalker()

	// Query for function declarations
	query := `(function_declaration name: (identifier) @name)`

	root := SitterRoot{
		Node:   tree.RootNode(),
		Source: code,
		Lang:   lang,
	}

	matches, err := w.Query(root, query)
	require.NoError(t, err)

	assert.Len(t, matches, 2)
	assert.Equal(t, map[string]any{"name": "main"}, matches[0].Values())
	assert.Equal(t, map[string]any{"name": "helper"}, matches[1].Values())
}

// parseSitterRoot is a test helper that parses Go source and returns a SitterRoot.
func parseSitterRoot(t *testing.T, code []byte) SitterRoot {
	t.Helper()
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)
	return SitterRoot{Node: tree.RootNode(), Source: code, Lang: lang}
}

func TestSitterWalkerGo_DollarRootSelector(t *testing.T) {
	code := []byte(`package main

func hello() {}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	matches, err := w.Query(root, "$")
	require.NoError(t, err)
	require.Len(t, matches, 1)

	// "$" returns empty values (literal names don't need captures)
	assert.Empty(t, matches[0].Values())

	// Context is non-nil and can be used for child queries
	ctx := matches[0].Context()
	require.NotNil(t, ctx)

	// Child query against the context should find the function
	childMatches, err := w.Query(ctx, `(function_declaration name: (identifier) @name) @scope`)
	require.NoError(t, err)
	require.Len(t, childMatches, 1)
	assert.Equal(t, "hello", childMatches[0].Values()["name"])
}

func TestSitterWalkerGo_PackageClause(t *testing.T) {
	code := []byte(`package mypackage

func Foo() {}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	matches, err := w.Query(root, `(source_file (package_clause (package_identifier) @pkg) @scope)`)
	require.NoError(t, err)
	require.Len(t, matches, 1)
	assert.Equal(t, "mypackage", matches[0].Values()["pkg"])

	// Scope context is non-nil (the package_clause node)
	ctx := matches[0].Context()
	assert.NotNil(t, ctx)
}

func TestSitterWalkerGo_Methods(t *testing.T) {
	code := []byte(`package foo

type MyStruct struct{}

func (m *MyStruct) PointerMethod() {}
func (m MyStruct) ValueMethod() {}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	// Query for methods with pointer receiver
	pointerQuery := `(method_declaration receiver: (parameter_list (parameter_declaration type: (pointer_type (type_identifier) @receiver))) name: (field_identifier) @name) @scope`
	matches, err := w.Query(root, pointerQuery)
	require.NoError(t, err)
	require.Len(t, matches, 1)
	assert.Equal(t, "MyStruct", matches[0].Values()["receiver"])
	assert.Equal(t, "PointerMethod", matches[0].Values()["name"])

	// Query for methods with value receiver
	valueQuery := `(method_declaration receiver: (parameter_list (parameter_declaration type: (type_identifier) @receiver)) name: (field_identifier) @name) @scope`
	matches, err = w.Query(root, valueQuery)
	require.NoError(t, err)
	require.Len(t, matches, 1)
	assert.Equal(t, "MyStruct", matches[0].Values()["receiver"])
	assert.Equal(t, "ValueMethod", matches[0].Values()["name"])
}

func TestSitterWalkerGo_Types(t *testing.T) {
	code := []byte(`package foo

type MyStruct struct {
	Name string
}

type MyInterface interface {
	DoSomething()
}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	query := `(type_declaration (type_spec name: (type_identifier) @name) @scope)`
	matches, err := w.Query(root, query)
	require.NoError(t, err)
	require.Len(t, matches, 2)
	assert.Equal(t, "MyStruct", matches[0].Values()["name"])
	assert.Equal(t, "MyInterface", matches[1].Values()["name"])

	// @scope on type_spec captures just the spec, not the whole declaration
	structSource := matches[0].Values()["scope"].(string)
	assert.Contains(t, structSource, "MyStruct struct")
	assert.NotContains(t, structSource, "MyInterface")
}

func TestSitterWalkerGo_GroupedTypes(t *testing.T) {
	code := []byte(`package foo

type (
	Grouped1 struct {
		X int
	}

	Grouped2 interface {
		Method()
	}
)
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	query := `(type_declaration (type_spec name: (type_identifier) @name) @scope)`
	matches, err := w.Query(root, query)
	require.NoError(t, err)
	require.Len(t, matches, 2)

	// Each match captures only its own type_spec, not the whole grouped block
	g1Source := matches[0].Values()["scope"].(string)
	assert.Contains(t, g1Source, "Grouped1 struct")
	assert.NotContains(t, g1Source, "Grouped2")

	g2Source := matches[1].Values()["scope"].(string)
	assert.Contains(t, g2Source, "Grouped2 interface")
	assert.NotContains(t, g2Source, "Grouped1")
}

func TestSitterWalkerGo_GroupedConstants(t *testing.T) {
	code := []byte(`package foo

const (
	A = "alpha"
	B = "beta"
)
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	query := `(const_spec name: (identifier) @name) @scope`
	matches, err := w.Query(root, query)
	require.NoError(t, err)
	require.Len(t, matches, 2)

	// Each const_spec captures only its own line, not the whole block
	aSource := matches[0].Values()["scope"].(string)
	assert.Contains(t, aSource, "A")
	assert.NotContains(t, aSource, "B")

	bSource := matches[1].Values()["scope"].(string)
	assert.Contains(t, bSource, "B")
	assert.NotContains(t, bSource, "A")
}

func TestSitterWalkerGo_ImportSpec(t *testing.T) {
	code := []byte(`package main

import "fmt"

import (
	"os"
	"strings"
)
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	query := `(import_spec path: (interpreted_string_literal) @path) @scope`
	matches, err := w.Query(root, query)
	require.NoError(t, err)
	require.Len(t, matches, 3)

	// Single import
	assert.Equal(t, `"fmt"`, matches[0].Values()["path"])
	assert.Contains(t, matches[0].Values()["scope"].(string), `"fmt"`)

	// Grouped imports
	assert.Equal(t, `"os"`, matches[1].Values()["path"])
	assert.Contains(t, matches[1].Values()["scope"].(string), `"os"`)

	assert.Equal(t, `"strings"`, matches[2].Values()["path"])
	assert.Contains(t, matches[2].Values()["scope"].(string), `"strings"`)

	// Each scope captures only its own import_spec
	assert.NotContains(t, matches[1].Values()["scope"].(string), "strings")
	assert.NotContains(t, matches[2].Values()["scope"].(string), "os")
}

func TestSitterWalkerGo_ConstantsAndVariables(t *testing.T) {
	code := []byte(`package foo

const SingleConst = 42

const (
	GroupedA = "a"
	GroupedB = "b"
)

var SingleVar = "hello"

var (
	GroupedVarX = 1
	GroupedVarY = 2
)
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	// Constants
	constQuery := `(const_spec name: (identifier) @name) @scope`
	matches, err := w.Query(root, constQuery)
	require.NoError(t, err)
	require.Len(t, matches, 3)
	assert.Equal(t, "SingleConst", matches[0].Values()["name"])
	assert.Equal(t, "GroupedA", matches[1].Values()["name"])
	assert.Equal(t, "GroupedB", matches[2].Values()["name"])

	// Variables
	varQuery := `(var_spec name: (identifier) @name) @scope`
	matches, err = w.Query(root, varQuery)
	require.NoError(t, err)
	require.Len(t, matches, 3)
	assert.Equal(t, "SingleVar", matches[0].Values()["name"])
	assert.Equal(t, "GroupedVarX", matches[1].Values()["name"])
	assert.Equal(t, "GroupedVarY", matches[2].Values()["name"])
}

// cobraSource is a minimal Go file with cobra.Command and flag patterns,
// used by the CLI schema query tests below.
const cobraSource = `package cmd

import "github.com/spf13/cobra"

var (
	schemaPath string
	dataPath   string
	writable   bool
)

func init() {
	rootCmd.Flags().StringVarP(&schemaPath, "schema", "s", "", "Path to topology schema")
	rootCmd.Flags().StringVarP(&dataPath, "data", "d", "", "Path to data source")
	rootCmd.Flags().BoolVarP(&writable, "writable", "w", false, "Enable write-back")
}

var rootCmd = &cobra.Command{
	Use:   "mache [mountpoint]",
	Short: "Mache: The Universal Semantic Overlay Engine",
	Args:  cobra.ExactArgs(1),
}
`

func TestSitterWalkerGo_CobraCommandQuery(t *testing.T) {
	root := parseSitterRoot(t, []byte(cobraSource))
	w := NewSitterWalker()

	query := `(var_spec
  name: (identifier) @cmd_var
  value: (expression_list
    (unary_expression
      operand: (composite_literal
        type: (qualified_type
          package: (package_identifier) @_pkg (#eq? @_pkg "cobra")
          name: (type_identifier) @_type (#eq? @_type "Command"))
        body: (literal_value) @scope))))`

	matches, err := w.Query(root, query)
	require.NoError(t, err)
	require.Len(t, matches, 1, "should find exactly one cobra.Command var")

	vals := matches[0].Values()
	assert.Equal(t, "rootCmd", vals["cmd_var"])
	assert.Equal(t, "cobra", vals["_pkg"])
	assert.Equal(t, "Command", vals["_type"])

	// Scope should contain the literal_value (struct body)
	scope, ok := vals["scope"].(string)
	require.True(t, ok)
	assert.Contains(t, scope, `"mache [mountpoint]"`)
	assert.Contains(t, scope, `"Mache: The Universal Semantic Overlay Engine"`)

	// Context should be non-nil so child queries can run against the scope
	assert.NotNil(t, matches[0].Context())
}

func TestSitterWalkerGo_CobraCommandFields(t *testing.T) {
	root := parseSitterRoot(t, []byte(cobraSource))
	w := NewSitterWalker()

	// First find the cobra.Command scope
	cmdQuery := `(var_spec
  name: (identifier) @cmd_var
  value: (expression_list
    (unary_expression
      operand: (composite_literal
        type: (qualified_type
          package: (package_identifier) @_pkg (#eq? @_pkg "cobra")
          name: (type_identifier) @_type (#eq? @_type "Command"))
        body: (literal_value) @scope))))`

	cmdMatches, err := w.Query(root, cmdQuery)
	require.NoError(t, err)
	require.Len(t, cmdMatches, 1)

	// Now query for string-valued keyed_elements within the scope
	fieldQuery := `(keyed_element
  (literal_element (identifier) @field_name)
  (literal_element (interpreted_string_literal) @value)) @scope`

	scopeCtx := cmdMatches[0].Context()
	require.NotNil(t, scopeCtx)

	fieldMatches, err := w.Query(scopeCtx, fieldQuery)
	require.NoError(t, err)
	require.Len(t, fieldMatches, 2, "should find Use and Short (Args is not a string)")

	assert.Equal(t, "Use", fieldMatches[0].Values()["field_name"])
	assert.Equal(t, `"mache [mountpoint]"`, fieldMatches[0].Values()["value"])

	assert.Equal(t, "Short", fieldMatches[1].Values()["field_name"])
	assert.Equal(t, `"Mache: The Universal Semantic Overlay Engine"`, fieldMatches[1].Values()["value"])
}

func TestSitterWalkerGo_FlagQuery(t *testing.T) {
	root := parseSitterRoot(t, []byte(cobraSource))
	w := NewSitterWalker()

	query := `(call_expression
  function: (selector_expression
    operand: (call_expression
      function: (selector_expression
        operand: (identifier) @receiver
        field: (field_identifier) @_flags_fn (#eq? @_flags_fn "Flags")))
    field: (field_identifier) @flag_method)
  arguments: (argument_list
    (_)
    (interpreted_string_literal) @flag_name
    (interpreted_string_literal) @flag_short
    (_) @flag_default
    (interpreted_string_literal) @flag_desc)) @scope`

	matches, err := w.Query(root, query)
	require.NoError(t, err)
	require.Len(t, matches, 3, "should find schema, data, writable flags")

	// Flag 1: schema (StringVarP)
	assert.Equal(t, "StringVarP", matches[0].Values()["flag_method"])
	assert.Equal(t, `"schema"`, matches[0].Values()["flag_name"])
	assert.Equal(t, `"s"`, matches[0].Values()["flag_short"])
	assert.Equal(t, `""`, matches[0].Values()["flag_default"])
	assert.Equal(t, `"Path to topology schema"`, matches[0].Values()["flag_desc"])

	// Flag 2: data (StringVarP)
	assert.Equal(t, "StringVarP", matches[1].Values()["flag_method"])
	assert.Equal(t, `"data"`, matches[1].Values()["flag_name"])
	assert.Equal(t, `"d"`, matches[1].Values()["flag_short"])

	// Flag 3: writable (BoolVarP) — non-string default
	assert.Equal(t, "BoolVarP", matches[2].Values()["flag_method"])
	assert.Equal(t, `"writable"`, matches[2].Values()["flag_name"])
	assert.Equal(t, `"w"`, matches[2].Values()["flag_short"])
	assert.Equal(t, "false", matches[2].Values()["flag_default"])
}

func TestRefQueryRegistry_DispatchesByLanguage(t *testing.T) {
	// Go has a registered ref query that captures call_expression
	// and adds function-value patterns (keyed_element, assignment).
	w := NewSitterWalker()
	code := []byte(`package main

func main() {
	foo()
	bar()
}

func foo() {}
func bar() {}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	calls, err := w.ExtractCalls(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)
	assert.Contains(t, calls, "foo")
	assert.Contains(t, calls, "bar")
}

// TestExtractCalls_GoFunctionValueRefs guards the keyed_element
// pattern in the Go ref query (mache-02r9). dead_code used to flag
// every cobra RunE callback because static call extraction only saw
// call_expression. The keyed_element capture lets the value
// identifier (the function reference) land in node_refs.
//
// Earlier versions of this query also matched assignment_statement
// and short_var_declaration RHS identifiers, but those over-collected
// every variable read on the RHS of normal assignments — see
// TestExtractCalls_AssignmentRHSDoesNotPolluteRefs below for the
// regression guard.
func TestExtractCalls_GoFunctionValueRefs(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package main

import "github.com/spf13/cobra"

func runServe() error    { return nil }

func setup() {
	// keyed_element value (cobra RunE pattern, mache-02r9 case 1).
	_ = &cobra.Command{
		Use:  "x",
		RunE: runServe,
	}
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	calls, err := w.ExtractCalls(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)
	assert.Contains(t, calls, "runServe", "keyed_element value identifier captured")

	// "RunE" is the keyed_element FIELD NAME — must NOT be captured
	// (we want the value identifier, not the field name).
	assert.NotContains(t, calls, "RunE",
		"struct field name must not leak into refs — only the value identifier should")
	assert.NotContains(t, calls, "Use",
		"struct field name must not leak into refs")
}

// TestExtractCalls_GoTypeReferences guards the type-reference patterns
// added under mache-9c615a. PR #373 bench surfaced that find_callers
// returned [] for type names like Topology despite 141 grep hits —
// the index only captured call_expression, not type usages. These
// patterns capture type references in parameters, fields, composite
// literals, type assertions, and var specs so find_callers(Topology)
// returns actual usage sites.
func TestExtractCalls_GoTypeReferences(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package pkg

type Topology struct {
	Name string
}

type Node interface {
	Get() string
}

// Topology as a parameter type
func consume(t Topology) {}

// Topology as a pointer parameter type
func consumePtr(t *Topology) {}

// Topology as a struct field type
type Wrapper struct {
	T Topology
	P *Topology
}

// Topology in a composite literal
var instance = Topology{Name: "x"}

// Topology in a var spec
var declared Topology

// Topology in a pointer var spec
var ptr *Topology

// Node in a type assertion
func assertNode(x any) Node {
	return x.(Node)
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	calls, err := w.ExtractCalls(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)

	// Topology must appear; the patterns capture each of:
	//   - parameter_declaration (consume, consumePtr)
	//   - field_declaration (Wrapper.T, Wrapper.P)
	//   - composite_literal (instance)
	//   - var_spec (declared, ptr)
	// Multiple occurrences are expected; we only assert presence.
	assert.Contains(t, calls, "Topology",
		"Topology must appear in node_refs — used as parameter, field, composite literal, and var type")

	// Node must appear from the type assertion + interface return type
	assert.Contains(t, calls, "Node",
		"Node must appear in node_refs — used in a type_assertion_expression")
}

// TestExtractCalls_GoQualifiedTypeReferences covers the qualified-type
// half of mache-9c615a. Patterns like (qualified_type name: ...) close
// the gap for cross-package type uses — api.Topology, graph.Node, etc.
// — which dominate real codebases. Without these patterns the bare-only
// half captures only same-package uses (a small fraction of the total).
func TestExtractCalls_GoQualifiedTypeReferences(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package consumer

import "example/api"

// api.Topology as a parameter type
func consume(t api.Topology) {}

// *api.Topology as a pointer parameter type
func consumePtr(t *api.Topology) {}

// api.Topology as a struct field
type Wrapper struct {
	T api.Topology
	P *api.Topology
}

// api.Topology in a composite literal
var instance = api.Topology{}

// api.Topology in a var spec
var declared api.Topology

// api.Topology in a pointer var spec
var ptr *api.Topology

// api.Node in a type assertion
func assertNode(x any) {
	_ = x.(api.Node)
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	calls, err := w.ExtractCalls(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)

	// The token is the bare type name — matches how (selector_expression
	// field: (field_identifier) @call) captures qualified CALLS by name
	// only (without the package qualifier). Consistent extraction shape.
	assert.Contains(t, calls, "Topology",
		"Topology must appear via qualified_type — used as api.Topology in parameter, field, composite literal, and var spec")
	assert.Contains(t, calls, "Node",
		"Node must appear via qualified_type — used as api.Node in type_assertion_expression")

	// "api" (the package name) MUST NOT appear as a token — only the
	// type name is the ref, matching the call-expression pattern's
	// behavior of capturing the symbol but not the package qualifier.
	assert.NotContains(t, calls, "api",
		"package qualifier (api) must not leak into node_refs — only the type name is the ref")
}

// TestExtractFileLevelRefs_GoTopLevelCobraVar guards the file-level
// ref extraction path (mache-02r9). Per-scope ExtractCalls only sees
// call_expressions within a matched function body — top-level
// `var serveCmd = &cobra.Command{ RunE: runServe }` is outside any
// function_declaration, so runServe never lands in node_refs and
// gets flagged as dead by find_smells.
//
// File-level extraction runs the same keyed_element query against
// the source_file root, so the top-level keyed_element value
// identifier is captured.
func TestExtractFileLevelRefs_GoTopLevelCobraVar(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package cmd

import "github.com/spf13/cobra"

func runServe() error { return nil }

var serveCmd = &cobra.Command{
	Use:  "serve",
	RunE: runServe,
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	refs, err := w.ExtractFileLevelRefs(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)
	assert.Contains(t, refs, "runServe",
		"top-level keyed_element value identifier must be captured by file-level extraction")
	assert.NotContains(t, refs, "RunE",
		"struct field name (RunE) must not leak into refs — only the value identifier")
}

// TestExtractFileLevelRefs_GoCallsInsideTopLevelClosure asserts the
// closure-body half of mache-02r9. The Go file-level query also
// includes call_expression patterns so calls inside top-level
// func_literal values (cobra RunE: func() { cliExit(...) }) are
// captured. Per-scope ExtractCalls would never see them because
// the func_literal lives outside any function_declaration.
func TestExtractFileLevelRefs_GoCallsInsideTopLevelClosure(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package cmd

import "github.com/spf13/cobra"

func cliExit(code int, err error) error { return err }

var initCmd = &cobra.Command{
	Use: "init",
	RunE: func(cmd *cobra.Command, args []string) error {
		return cliExit(4, nil)
	},
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	refs, err := w.ExtractFileLevelRefs(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)
	assert.Contains(t, refs, "cliExit",
		"call_expression inside a top-level func_literal value must be captured")
}

// TestExtractFileLevelRefs_GoFunctionValueAsArgument asserts the
// argument-list capture. Test runners that take a factory function
// as a parameter (e.g. RunGraphSuite(t, hotSwapFactory)) pass a bare
// identifier — captured as part of the file-level argument_list
// pattern, which lands the factory in node_refs under the sentinel
// caller_id and prevents dead_code from flagging it.
func TestExtractFileLevelRefs_GoFunctionValueAsArgument(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package graph

import "testing"

func hotSwapFactory(t *testing.T) Graph { return nil }

type Graph interface{}

func RunGraphSuite(t *testing.T, factory func(*testing.T) Graph) {}

func TestHotSwap_GraphSuite(t *testing.T) {
	RunGraphSuite(t, hotSwapFactory)
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	refs, err := w.ExtractFileLevelRefs(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)
	assert.Contains(t, refs, "hotSwapFactory",
		"function-value passed as a call argument must be captured")
}

// TestExtractFileLevelRefs_NoQueryReturnsNil asserts that languages
// without a registered file-level query get nil-no-error from
// ExtractFileLevelRefs (caller treats that as 'skip').
func TestExtractFileLevelRefs_NoQueryReturnsNil(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`def main():
    print("hi")
`)
	lang := python.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	refs, err := w.ExtractFileLevelRefs(tree.RootNode(), code, lang, "python")
	require.NoError(t, err)
	assert.Nil(t, refs, "no file-level query registered for python; should return nil cleanly")
}

// TestExtractCalls_AssignmentRHSDoesNotPolluteRefs guards against
// over-broad capture: variable reads on the RHS of `=` or `:=` must
// NOT be added to node_refs. An earlier iteration of the Go ref
// query matched assignment_statement / short_var_declaration RHS
// identifiers (intended for `factories[k] = goFactory` shapes), but
// those captured every `filtered := findings[:0]` too, polluting
// fan_out_skew with non-callee identifiers.
func TestExtractCalls_AssignmentRHSDoesNotPolluteRefs(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package p

func handler(input []int, threshold int) []int {
	filtered := input[:0]
	for _, v := range input {
		if v > threshold {
			filtered = append(filtered, v)
		}
	}
	return filtered
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	calls, err := w.ExtractCalls(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)
	// `append` is a real call_expression and SHOULD be captured.
	assert.Contains(t, calls, "append")
	// `input`, `filtered`, `threshold`, `v` are local variables read
	// in expression positions — NOT calls. They must not appear.
	for _, varName := range []string{"input", "filtered", "threshold", "v"} {
		assert.NotContains(t, calls, varName,
			"local variable %q must not appear in node_refs", varName)
	}
}

func TestRefQueryRegistry_PythonUsesRegistered(t *testing.T) {
	// Python uses registered query (call, not call_expression)
	w := NewSitterWalker()
	code := []byte(`
def main():
    print("hello")
    helper()
`)
	lang := python.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	calls, err := w.ExtractCalls(tree.RootNode(), code, lang, "python")
	require.NoError(t, err)
	assert.Contains(t, calls, "print")
	assert.Contains(t, calls, "helper")
}

func TestContextQueryRegistry_GoReturnsContext(t *testing.T) {
	w := NewSitterWalker()
	code := []byte(`package main

import "fmt"

const MaxSize = 100

var globalVar = "hello"

func main() {
	fmt.Println(globalVar)
}
`)
	lang := golang.GetLanguage()
	parser := sitter.NewParser()
	parser.SetLanguage(lang)
	tree, err := parser.ParseCtx(context.Background(), nil, code)
	require.NoError(t, err)

	ctx, err := w.ExtractContext(tree.RootNode(), code, lang, "go")
	require.NoError(t, err)
	require.NotNil(t, ctx)
	assert.Contains(t, string(ctx), `import "fmt"`)
	assert.Contains(t, string(ctx), "MaxSize")
	assert.Contains(t, string(ctx), "globalVar")
}

func TestContextQueryRegistry_UnsupportedReturnsNil(t *testing.T) {
	w := NewSitterWalker()
	// YAML has no context query registered
	lang := python.GetLanguage()
	ctx, err := w.ExtractContext(nil, nil, lang, "yaml")
	require.NoError(t, err)
	assert.Nil(t, ctx)
}

func TestExtractDocComments_WithComments(t *testing.T) {
	code := []byte(`package main

// Hello is a greeting function.
// It prints "hello world".
func Hello() {
	println("hello world")
}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	matches, err := w.Query(root, `(function_declaration name: (identifier) @name) @scope`)
	require.NoError(t, err)
	require.Len(t, matches, 1)

	docText, startByte, endByte, hasScope := extractDocComments(matches[0])
	require.True(t, hasScope)
	assert.Contains(t, docText, "// Hello is a greeting function.")
	assert.Contains(t, docText, "// It prints \"hello world\".")
	// startByte should point to the first comment, before the scope node
	assert.Less(t, startByte, endByte)
}

func TestExtractDocComments_NoComments(t *testing.T) {
	code := []byte(`package main

func Hello() {}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	matches, err := w.Query(root, `(function_declaration name: (identifier) @name) @scope`)
	require.NoError(t, err)
	require.Len(t, matches, 1)

	docText, _, _, hasScope := extractDocComments(matches[0])
	require.True(t, hasScope)
	assert.Empty(t, docText)
}

func TestExtractDocComments_NonAdjacentComment(t *testing.T) {
	code := []byte(`package main

// This comment is far away.


func Hello() {}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	matches, err := w.Query(root, `(function_declaration name: (identifier) @name) @scope`)
	require.NoError(t, err)
	require.Len(t, matches, 1)

	docText, _, _, hasScope := extractDocComments(matches[0])
	require.True(t, hasScope)
	// Gap > 2 bytes (multiple blank lines), so comment is not captured
	assert.Empty(t, docText)
}

func TestExtractDocComments_MultipleFunctions(t *testing.T) {
	code := []byte(`package main

// DocA describes FuncA.
func FuncA() {}

// DocB describes FuncB.
func FuncB() {}
`)
	root := parseSitterRoot(t, code)
	w := NewSitterWalker()

	matches, err := w.Query(root, `(function_declaration name: (identifier) @name) @scope`)
	require.NoError(t, err)
	require.Len(t, matches, 2)

	docA, _, _, _ := extractDocComments(matches[0])
	assert.Contains(t, docA, "DocA")
	assert.NotContains(t, docA, "DocB")

	docB, _, _, _ := extractDocComments(matches[1])
	assert.Contains(t, docB, "DocB")
	assert.NotContains(t, docB, "DocA")
}

func TestSitterWalkerGo_PredicateFiltering(t *testing.T) {
	root := parseSitterRoot(t, []byte(cobraSource))
	w := NewSitterWalker()

	// Query for ANY var_spec with qualified_type — without predicates, this
	// would match any package.Type composite literal.
	queryNoPredicate := `(var_spec
  name: (identifier) @cmd_var
  value: (expression_list
    (unary_expression
      operand: (composite_literal
        type: (qualified_type
          package: (package_identifier) @pkg
          name: (type_identifier) @typ)
        body: (literal_value) @scope))))`

	matches, err := w.Query(root, queryNoPredicate)
	require.NoError(t, err)
	require.Len(t, matches, 1, "only one qualified-type var in test source")

	// Now with a WRONG predicate — should filter out the match
	queryWrongPredicate := `(var_spec
  name: (identifier) @cmd_var
  value: (expression_list
    (unary_expression
      operand: (composite_literal
        type: (qualified_type
          package: (package_identifier) @_pkg (#eq? @_pkg "notcobra")
          name: (type_identifier) @_type (#eq? @_type "Command"))
        body: (literal_value) @scope))))`

	matches, err = w.Query(root, queryWrongPredicate)
	require.NoError(t, err)
	assert.Len(t, matches, 0, "wrong predicate should filter out the match")
}
