package languages

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/zzet/gortex/internal/graph"
)

func TestRbExtractor_Class(t *testing.T) {
	src := []byte(`class UserService
  def initialize(db)
    @db = db
  end

  def find_user(id)
    @db.query(id)
  end
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("service.rb", src)
	require.NoError(t, err)

	types := nodesOfKind(result.Nodes, graph.KindType)
	require.Len(t, types, 1)
	assert.Equal(t, "UserService", types[0].Name)

	methods := nodesOfKind(result.Nodes, graph.KindMethod)
	assert.Len(t, methods, 2)

	memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf)
	assert.Len(t, memberEdges, 2)
}

func TestRbExtractor_TopLevelMethod(t *testing.T) {
	src := []byte(`def greet(name)
  puts "Hello #{name}"
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("app.rb", src)
	require.NoError(t, err)

	funcs := nodesOfKind(result.Nodes, graph.KindFunction)
	require.Len(t, funcs, 1)
	assert.Equal(t, "greet", funcs[0].Name)
}

func TestRbExtractor_Require(t *testing.T) {
	src := []byte(`require "json"
require_relative "helper"
`)
	e := NewRubyExtractor()
	result, err := e.Extract("app.rb", src)
	require.NoError(t, err)

	imports := edgesOfKind(result.Edges, graph.EdgeImports)
	assert.GreaterOrEqual(t, len(imports), 1)
}

func TestRbExtractor_Module(t *testing.T) {
	src := []byte(`module Authentication
  def self.verify(token)
    true
  end
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("auth.rb", src)
	require.NoError(t, err)

	pkgs := nodesOfKind(result.Nodes, graph.KindPackage)
	require.Len(t, pkgs, 1)
	assert.Equal(t, "Authentication", pkgs[0].Name)
}

func TestRbExtractor_Constants(t *testing.T) {
	src := []byte(`MAX_RETRIES = 3
DEFAULT_HOST = "localhost"
`)
	e := NewRubyExtractor()
	result, err := e.Extract("config.rb", src)
	require.NoError(t, err)

	vars := nodesOfKind(result.Nodes, graph.KindVariable)
	require.Len(t, vars, 2)

	names := []string{vars[0].Name, vars[1].Name}
	assert.Contains(t, names, "MAX_RETRIES")
	assert.Contains(t, names, "DEFAULT_HOST")
}

func TestRbExtractor_CallSites(t *testing.T) {
	src := []byte(`class Greeter
  def greet(name)
    puts("Hello")
    logger.info("greeting")
  end
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("greeter.rb", src)
	require.NoError(t, err)

	calls := edgesOfKind(result.Edges, graph.EdgeCalls)
	assert.GreaterOrEqual(t, len(calls), 1)
}

func TestRbExtractor_NoTopLevelMethodInClass(t *testing.T) {
	// Methods inside a class should NOT appear as top-level functions.
	src := []byte(`class Foo
  def bar
  end
end

def baz
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("test.rb", src)
	require.NoError(t, err)

	funcs := nodesOfKind(result.Nodes, graph.KindFunction)
	require.Len(t, funcs, 1)
	assert.Equal(t, "baz", funcs[0].Name)

	methods := nodesOfKind(result.Nodes, graph.KindMethod)
	require.Len(t, methods, 1)
	assert.Equal(t, "bar", methods[0].Name)
}

func TestRubyExtractor_RailsBeforeAction(t *testing.T) {
	// before_action :name binds a callback method to the class's
	// action methods. With only: filters, only the listed actions
	// fire the callback. This test covers the default (all actions
	// bound) plus an only: list.
	src := []byte(`
class UsersController
  before_action :authenticate
  before_action :load_user, only: [:show, :update]

  def index; end
  def show; @user end
  def update; @user end

  private

  def authenticate; end
  def load_user; end
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("users_controller.rb", src)
	require.NoError(t, err)

	var authEdges, loadEdges int
	actionsForAuth := map[string]bool{}
	actionsForLoad := map[string]bool{}
	for _, ed := range edgesOfKind(result.Edges, graph.EdgeCalls) {
		if ed.Meta == nil {
			continue
		}
		cb, _ := ed.Meta["rails_callback"].(string)
		if cb == "authenticate" {
			authEdges++
			actionsForAuth[ed.From] = true
		}
		if cb == "load_user" {
			loadEdges++
			actionsForLoad[ed.From] = true
		}
	}
	// authenticate guards every action (no only:/except:).
	assert.Equal(t, 3, authEdges, "authenticate should bind to every action")
	// load_user has only: [:show, :update].
	assert.Equal(t, 2, loadEdges, "load_user should bind only to :show, :update")
	assert.Contains(t, actionsForLoad, "users_controller.rb::UsersController.show")
	assert.Contains(t, actionsForLoad, "users_controller.rb::UsersController.update")
	assert.NotContains(t, actionsForLoad, "users_controller.rb::UsersController.index")
}

func TestRubyExtractor_SingletonMethod(t *testing.T) {
	// `def self.x` (singleton_method) must be extracted alongside
	// regular instance methods — previously the extractor missed them.
	src := []byte(`
class User
  def instance; 1 end
  def self.class_method; 2 end
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("user.rb", src)
	require.NoError(t, err)
	names := make(map[string]bool)
	for _, n := range result.Nodes {
		if n.Kind == graph.KindMethod {
			names[n.Name] = true
		}
	}
	assert.True(t, names["instance"])
	assert.True(t, names["class_method"])
}

func TestRubyExtractor_DocAndVisibility(t *testing.T) {
	src := []byte(`# Greeter handles welcomes.
class Greeter
  # Says hi to the user.
  def hello
  end

  def silent
  end
end
`)
	e := NewRubyExtractor()
	result, err := e.Extract("greeter.rb", src)
	require.NoError(t, err)

	byID := map[string]*graph.Node{}
	for _, n := range result.Nodes {
		byID[n.ID] = n
	}

	greeter := byID["greeter.rb::Greeter"]
	require.NotNil(t, greeter)
	if greeter.Meta["visibility"] != "public" {
		t.Fatalf("Greeter.vis = %q", greeter.Meta["visibility"])
	}
	if greeter.Meta["doc"] != "Greeter handles welcomes." {
		t.Fatalf("Greeter.doc = %q", greeter.Meta["doc"])
	}

	hello := byID["greeter.rb::Greeter.hello"]
	require.NotNil(t, hello)
	if hello.Meta["doc"] != "Says hi to the user." {
		t.Fatalf("hello.doc = %q", hello.Meta["doc"])
	}
	if hello.Meta["visibility"] != "public" {
		t.Fatalf("hello.vis = %q", hello.Meta["visibility"])
	}
}
