Step 7: Add integration tests for function definitions - define+call, multi-param, cross-function, nested, errors
diff --git a/integration_test.go b/integration_test.go
index 31ddd3f..b928d00 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -5,6 +5,7 @@
 	"strings"
 	"testing"
 
+	"matheval/ast"
 	"matheval/evaluator"
 	"matheval/lexer"
 	"matheval/parser"
@@ -43,6 +44,76 @@
 	return err
 }
 
+// evalLine is a helper that uses ParseLine + Evaluator for a multi-line session.
+// It processes lines sequentially on a shared evaluator and returns the result
+// of the last expression statement.
+func evalLines(t *testing.T, ev *evaluator.Evaluator, lines ...string) float64 {
+	t.Helper()
+	var lastResult float64
+	for _, line := range lines {
+		tokens, err := lexer.Tokenize(line)
+		if err != nil {
+			t.Fatalf("lexer error for %q: %v", line, err)
+		}
+		stmt, err := parser.ParseLine(tokens)
+		if err != nil {
+			t.Fatalf("parser error for %q: %v", line, err)
+		}
+		switch s := stmt.(type) {
+		case *ast.FuncDef:
+			if err := ev.Define(s); err != nil {
+				t.Fatalf("define error for %q: %v", line, err)
+			}
+		case *ast.ExprStmt:
+			result, err := ev.Eval(s.Expr, nil)
+			if err != nil {
+				t.Fatalf("eval error for %q: %v", line, err)
+			}
+			lastResult = result
+		}
+	}
+	return lastResult
+}
+
+// evalLinesErr processes lines and expects the last one to produce an error.
+func evalLinesErr(t *testing.T, ev *evaluator.Evaluator, lines ...string) error {
+	t.Helper()
+	for i, line := range lines {
+		tokens, err := lexer.Tokenize(line)
+		if err != nil {
+			if i == len(lines)-1 {
+				return err
+			}
+			t.Fatalf("lexer error for %q: %v", line, err)
+		}
+		stmt, err := parser.ParseLine(tokens)
+		if err != nil {
+			if i == len(lines)-1 {
+				return err
+			}
+			t.Fatalf("parser error for %q: %v", line, err)
+		}
+		switch s := stmt.(type) {
+		case *ast.FuncDef:
+			if err := ev.Define(s); err != nil {
+				if i == len(lines)-1 {
+					return err
+				}
+				t.Fatalf("define error for %q: %v", line, err)
+			}
+		case *ast.ExprStmt:
+			_, err := ev.Eval(s.Expr, nil)
+			if err != nil {
+				if i == len(lines)-1 {
+					return err
+				}
+				t.Fatalf("eval error for %q: %v", line, err)
+			}
+		}
+	}
+	return nil
+}
+
 func assertApprox(t *testing.T, input string, expected, got float64) {
 	t.Helper()
 	if math.Abs(expected-got) > 1e-9 {
@@ -259,3 +330,108 @@
 		t.Fatal("expected error for consecutive numbers without operator")
 	}
 }
+
+// --- Function definitions (full pipeline) ---
+
+func TestIntegration_DefineAndCallSingleParam(t *testing.T) {
+	ev := evaluator.New()
+	result := evalLines(t, ev, "f(x) = x + 1", "f(5)")
+	assertApprox(t, "f(5)", 6, result)
+}
+
+func TestIntegration_DefineAndCallMultiParam(t *testing.T) {
+	ev := evaluator.New()
+	result := evalLines(t, ev, "add(x, y) = x + y", "add(3, 4)")
+	assertApprox(t, "add(3, 4)", 7, result)
+}
+
+func TestIntegration_CrossFunctionCalls(t *testing.T) {
+	ev := evaluator.New()
+	result := evalLines(t, ev,
+		"double(x) = x * 2",
+		"quad(x) = double(double(x))",
+		"quad(3)",
+	)
+	assertApprox(t, "quad(3)", 12, result)
+}
+
+func TestIntegration_NestedFuncCallsInExpr(t *testing.T) {
+	ev := evaluator.New()
+	result := evalLines(t, ev,
+		"f(x) = x + 1",
+		"f(f(f(1)))",
+	)
+	// f(1)=2, f(2)=3, f(3)=4
+	assertApprox(t, "f(f(f(1)))", 4, result)
+}
+
+func TestIntegration_FuncCallInBinaryExpr(t *testing.T) {
+	ev := evaluator.New()
+	result := evalLines(t, ev,
+		"f(x) = x * 2",
+		"f(3) + f(4)",
+	)
+	// f(3)=6, f(4)=8, 6+8=14
+	assertApprox(t, "f(3)+f(4)", 14, result)
+}
+
+func TestIntegration_FuncWithExprBody(t *testing.T) {
+	ev := evaluator.New()
+	result := evalLines(t, ev,
+		"area(w, h) = w * h",
+		"area(3, 5) + 1",
+	)
+	assertApprox(t, "area(3,5)+1", 16, result)
+}
+
+func TestIntegration_FuncNoParams(t *testing.T) {
+	ev := evaluator.New()
+	result := evalLines(t, ev, "pi() = 3", "pi() + 1")
+	assertApprox(t, "pi()+1", 4, result)
+}
+
+// --- Function error cases (full pipeline) ---
+
+func TestIntegration_UndefinedFunction(t *testing.T) {
+	ev := evaluator.New()
+	err := evalLinesErr(t, ev, "f(1)")
+	if err == nil {
+		t.Fatal("expected error for undefined function")
+	}
+	if !strings.Contains(err.Error(), "undefined function") {
+		t.Errorf("expected 'undefined function' in error, got: %v", err)
+	}
+}
+
+func TestIntegration_WrongArgCount(t *testing.T) {
+	ev := evaluator.New()
+	err := evalLinesErr(t, ev, "f(x) = x", "f(1, 2)")
+	if err == nil {
+		t.Fatal("expected error for wrong argument count")
+	}
+	if !strings.Contains(err.Error(), "expects 1 arguments, got 2") {
+		t.Errorf("expected arg count error, got: %v", err)
+	}
+}
+
+func TestIntegration_FunctionRedefinition(t *testing.T) {
+	ev := evaluator.New()
+	err := evalLinesErr(t, ev, "f(x) = x", "f(x) = x + 1")
+	if err == nil {
+		t.Fatal("expected error for function redefinition")
+	}
+	if !strings.Contains(err.Error(), "already defined") {
+		t.Errorf("expected 'already defined' in error, got: %v", err)
+	}
+}
+
+func TestIntegration_UndefinedVariable(t *testing.T) {
+	ev := evaluator.New()
+	err := evalLinesErr(t, ev, "f(x) = x + y", "f(1)")
+	if err == nil {
+		t.Fatal("expected error for undefined variable")
+	}
+	if !strings.Contains(err.Error(), "undefined variable") {
+		t.Errorf("expected 'undefined variable' in error, got: %v", err)
+	}
+}