package compiler

import (
	"bytes"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"c65gm/internal/preproc"

	"go.starlark.net/lib/math"
	"go.starlark.net/starlark"
)

// --- Unit tests for mapStarlarkLine ---

func TestMapStarlarkLine_Regular(t *testing.T) {
	tests := []struct {
		starlarkLine  int
		numLines      int
		isLibrary     bool
		want          int
	}{
		{2, 5, false, 0},   // first script line
		{3, 5, false, 1},   // second script line
		{6, 5, false, 4},   // last script line
		{1, 5, false, -1},  // def _main(): line — out of range
		{7, 5, false, -1},  // _main() call — out of range
		{0, 5, false, -1},  // before start
		{2, 1, false, 0},   // single line script
		{3, 1, false, -1},  // single line, past it
	}

	for _, tt := range tests {
		got := mapStarlarkLine(tt.starlarkLine, tt.numLines, tt.isLibrary)
		if got != tt.want {
			t.Errorf("mapStarlarkLine(%d, %d, %v) = %d, want %d",
				tt.starlarkLine, tt.numLines, tt.isLibrary, got, tt.want)
		}
	}
}

func TestMapStarlarkLine_Library(t *testing.T) {
	tests := []struct {
		starlarkLine int
		numLines     int
		want         int
	}{
		{1, 5, 0},   // first script line
		{2, 5, 1},   // second script line
		{5, 5, 4},   // last script line
		{0, 5, -1},  // before start
		{6, 5, -1},  // past end
		{1, 1, 0},   // single line
		{2, 1, -1},  // single line, past it
	}

	for _, tt := range tests {
		got := mapStarlarkLine(tt.starlarkLine, tt.numLines, true)
		if got != tt.want {
			t.Errorf("mapStarlarkLine(%d, %d, true) = %d, want %d",
				tt.starlarkLine, tt.numLines, got, tt.want)
		}
	}
}

// --- Tests for starlarkPosition and starlarkErrorMsg ---

func TestStarlarkPosition_NoEvalError(t *testing.T) {
	got := starlarkPosition(fmt.Errorf("plain error"))
	if got != 0 {
		t.Errorf("starlarkPosition(plain error) = %d, want 0", got)
	}
}

func TestStarlarkPosition_WithCallStack(t *testing.T) {
	// Create an EvalError by executing Starlark code with a runtime error
	thread := &starlark.Thread{}
	predeclared := starlark.StringDict{"math": math.Module}
	_, err := starlark.ExecFile(thread, "test.star",
		`def f():
  x = 1 // 0
f()`,
		predeclared)
	if err == nil {
		t.Fatal("expected Starlark error")
	}

	pos := starlarkPosition(err)
	if pos <= 0 {
		t.Errorf("starlarkPosition() = %d, want > 0 (error: %v)", pos, err)
	}
}

func TestStarlarkErrorMsg_StripsCallStack(t *testing.T) {
	thread := &starlark.Thread{}
	predeclared := starlark.StringDict{"math": math.Module}
	_, err := starlark.ExecFile(thread, "test.star", `x = 1 // 0`, predeclared)
	if err == nil {
		t.Fatal("expected Starlark error")
	}

	msg := starlarkErrorMsg(err)
	if !strings.Contains(msg, "division by zero") {
		t.Errorf("starlarkErrorMsg() = %q, want division by zero", msg)
	}
	// Should NOT contain traceback prefixes
	if strings.Contains(msg, "Traceback") {
		t.Errorf("starlarkErrorMsg() should not contain Traceback, got %q", msg)
	}
}

func TestStarlarkErrorMsg_PlainError(t *testing.T) {
	msg := starlarkErrorMsg(fmt.Errorf("hello"))
	if msg != "hello" {
		t.Errorf("starlarkErrorMsg(plain) = %q, want 'hello'", msg)
	}
}

// --- Tests for printScriptErrorContext ---

func TestPrintScriptErrorContext_Basic(t *testing.T) {
	scriptLines := []preproc.Line{
		{Text: "  for i in range(3):", Filename: "test.c65", LineNo: 7, Kind: preproc.Script},
		{Text: "    print(i)", Filename: "test.c65", LineNo: 8, Kind: preproc.Script},
		{Text: "  end", Filename: "test.c65", LineNo: 9, Kind: preproc.Script},
	}

	// Capture stderr
	old := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	err := fmt.Errorf("test error")
	printScriptErrorContext(err, "test error", scriptLines, 1, "SCRIPT")

	w.Close()
	os.Stderr = old

	var buf bytes.Buffer
	buf.ReadFrom(r)
	output := buf.String()

	// Should contain SCRIPT marker
	if !strings.Contains(output, "SCRIPT") {
		t.Errorf("output should contain SCRIPT marker, got:\n%s", output)
	}
	// Should contain ENDSCRIPT
	if !strings.Contains(output, "ENDSCRIPT") {
		t.Errorf("output should contain ENDSCRIPT, got:\n%s", output)
	}
	// Should contain the error line
	if !strings.Contains(output, "print(i)") {
		t.Errorf("output should contain the error line, got:\n%s", output)
	}
	// Should contain Starlark error header
	if !strings.Contains(output, "Starlark error:") {
		t.Errorf("output should contain 'Starlark error:', got:\n%s", output)
	}
	// Should contain source marker
	if !strings.Contains(output, "-->") {
		t.Errorf("output should contain '-->', got:\n%s", output)
	}
}

func TestPrintScriptErrorContext_EdgeCases(t *testing.T) {
	tests := []struct {
		name       string
		lines      []preproc.Line
		errorIdx   int
		blockType  string
		wantMarker bool
	}{
		{
			name:       "empty lines",
			lines:      []preproc.Line{},
			errorIdx:   0,
			blockType:  "SCRIPT",
			wantMarker: false,
		},
		{
			name:       "invalid index",
			lines:      []preproc.Line{{Text: "x", Filename: "t", LineNo: 1}},
			errorIdx:   5,
			blockType:  "SCRIPT",
			wantMarker: false,
		},
		{
			name:       "first line",
			lines:      []preproc.Line{{Text: "x", Filename: "t", LineNo: 1}},
			errorIdx:   0,
			blockType:  "SCRIPT LIBRARY",
			wantMarker: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			old := os.Stderr
			r, w, _ := os.Pipe()
			os.Stderr = w

			printScriptErrorContext(fmt.Errorf("err"), "err", tt.lines, tt.errorIdx, tt.blockType)

			w.Close()
			os.Stderr = old

			var buf bytes.Buffer
			buf.ReadFrom(r)
			output := buf.String()

			if tt.wantMarker && !strings.Contains(output, tt.blockType) {
				t.Errorf("output should contain %q, got:\n%s", tt.blockType, output)
			}
			if !tt.wantMarker && !strings.Contains(output, "Starlark error") {
				// Should at minimum print the error
				t.Errorf("output should contain error even in edge case, got:\n%s", output)
			}
		})
	}
}

// --- Tests for executeScript error prefix ---

func TestExecuteScript_ErrorPrefix(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)

	old := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	scriptLines := []preproc.Line{
		{Text: "def f():\n  return 1 // 0\nf()", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
	}

	_, err := executeScript(scriptLines, ctx, false)

	w.Close()
	os.Stderr = old
	var buf bytes.Buffer
	buf.ReadFrom(r)

	if err == nil {
		t.Fatal("expected error")
	}

	if !strings.Contains(err.Error(), "Starlark error:") {
		t.Errorf("error should contain 'Starlark error:', got: %v", err)
	}
}

func TestExecuteScript_LibraryRuntimeError(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)

	// Define a library function that errors
	libLines := []preproc.Line{
		{Text: "def bad():\n  return 1 // 0", Filename: "test.c65", LineNo: 5, Kind: preproc.ScriptLibrary},
	}
	_, err := executeScript(libLines, ctx, true)
	if err != nil {
		t.Fatalf("library definition should succeed: %v", err)
	}

	// Capture stderr
	old := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	// Call the library function — should error
	callLines := []preproc.Line{
		{Text: "bad()", Filename: "test.c65", LineNo: 10, Kind: preproc.Script},
	}
	_, err = executeScript(callLines, ctx, false)

	w.Close()
	os.Stderr = old

	var buf bytes.Buffer
	buf.ReadFrom(r)
	stderr := buf.String()

	if err == nil {
		t.Fatal("expected error from library call")
	}

	if !strings.Contains(err.Error(), "Starlark error:") {
		t.Errorf("error should contain 'Starlark error:', got: %v", err)
	}

	// Should show bounded context for the calling SCRIPT block
	if !strings.Contains(stderr, "SCRIPT") {
		t.Errorf("stderr should contain SCRIPT marker, got:\n%s", stderr)
	}
	if !strings.Contains(stderr, "ENDSCRIPT") {
		t.Errorf("stderr should contain ENDSCRIPT, got:\n%s", stderr)
	}
}

func TestExecuteScript_DirectRuntimeError(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)

	// Capture stderr
	old := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	scriptLines := []preproc.Line{
		{Text: "  print(1 // 0)", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
	}
	_, err := executeScript(scriptLines, ctx, false)

	w.Close()
	os.Stderr = old

	var buf bytes.Buffer
	buf.ReadFrom(r)
	stderr := buf.String()

	if err == nil {
		t.Fatal("expected error")
	}

	if !strings.Contains(err.Error(), "Starlark error:") {
		t.Errorf("error should contain 'Starlark error:', got: %v", err)
	}

	// Should show the error source line in stderr context
	if !strings.Contains(stderr, "print(1 // 0)") {
		t.Errorf("stderr should contain the error line, got:\n%s", stderr)
	}
}

// --- Tests for ExecuteMacro error mapping ---

func TestExecuteMacro_ErrorDefinitionSite(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)

	// Register a macro with source provenance
	ctx.ScriptMacros["div_macro"] = &ScriptMacro{
		Name:       "div_macro",
		Params:     []string{"a", "b"},
		Body:       []string{"result = a // b"},
		SourceFile: "defs.c65",
		StartLine:  12,
	}

	// Capture stderr
	old := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	_, err := ExecuteMacro("div_macro", []string{"10", "0"}, ctx, 0)

	w.Close()
	os.Stderr = old

	var buf bytes.Buffer
	buf.ReadFrom(r)

	if err == nil {
		t.Fatal("expected error")
	}

	// Error should mention the definition site
	if !strings.Contains(err.Error(), "defs.c65:12") {
		t.Errorf("error should reference definition site 'defs.c65:12', got: %v", err)
	}
	if !strings.Contains(err.Error(), "Starlark error:") {
		t.Errorf("error should contain 'Starlark error:', got: %v", err)
	}
}

func TestExecuteMacro_ErrorFallbackNoSource(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)

	// Register a macro WITHOUT source provenance (backward compatibility)
	ctx.ScriptMacros["m"] = &ScriptMacro{
		Name:   "m",
		Params: []string{},
		Body:   []string{"1 // 0"},
	}

	_, err := ExecuteMacro("m", []string{}, ctx, 0)
	if err == nil {
		t.Fatal("expected error")
	}
	if !strings.Contains(err.Error(), "Starlark error:") {
		t.Errorf("error should contain 'Starlark error:', got: %v", err)
	}
}

// --- Tests for printStarlarkBacktrace ---

func TestPrintStarlarkBacktrace_NoFrames(t *testing.T) {
	old := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	printStarlarkBacktrace(fmt.Errorf("plain error"), 10, "test.c65")

	w.Close()
	os.Stderr = old

	var buf bytes.Buffer
	buf.ReadFrom(r)
	output := buf.String()

	if output != "" {
		t.Errorf("expected empty output for plain error, got: %q", output)
	}
}

func TestPrintScriptErrorFallback(t *testing.T) {
	old := os.Stderr
	r, w, _ := os.Pipe()
	os.Stderr = w

	printScriptErrorFallback(fmt.Errorf("test message"))

	w.Close()
	os.Stderr = old

	var buf bytes.Buffer
	buf.ReadFrom(r)
	output := buf.String()

	if !strings.Contains(output, "Starlark error:") {
		t.Errorf("output should contain 'Starlark error:', got: %q", output)
	}
	if !strings.Contains(output, "test message") {
		t.Errorf("output should contain the error message, got: %q", output)
	}
}

// --- Tests for validateScriptFilePath ---

func TestValidateScriptFilePath_Valid(t *testing.T) {
	projectRoot := "/home/user/project"

	tests := []struct {
		path string
		want string
	}{
		{"file.bin", filepath.Join(projectRoot, "file.bin")},
		{"subdir/file.bin", filepath.Join(projectRoot, "subdir/file.bin")},
		{"./file.bin", filepath.Join(projectRoot, "file.bin")},
	}

	for _, tt := range tests {
		got, err := validateScriptFilePath(projectRoot, tt.path)
		if err != nil {
			t.Errorf("validateScriptFilePath(%q, %q) unexpected error: %v", projectRoot, tt.path, err)
			continue
		}
		if got != tt.want {
			t.Errorf("validateScriptFilePath(%q, %q) = %q, want %q", projectRoot, tt.path, got, tt.want)
		}
	}
}

func TestValidateScriptFilePath_RejectsAbsolute(t *testing.T) {
	_, err := validateScriptFilePath("/home/user/project", "/etc/passwd")
	if err == nil {
		t.Error("expected error for absolute path")
	}
}

func TestValidateScriptFilePath_RejectsTraversal(t *testing.T) {
	projectRoot := "/home/user/project"
	tests := []string{
		"../secret.bin",
		"subdir/../../secret.bin",
	}

	for _, path := range tests {
		_, err := validateScriptFilePath(projectRoot, path)
		if err == nil {
			t.Errorf("expected error for traversal path: %s", path)
		}
	}
}

func TestValidateScriptFilePath_RejectsEmptyProjectRoot(t *testing.T) {
	_, err := validateScriptFilePath("", "file.bin")
	if err == nil {
		t.Error("expected error for empty project root")
	}
}

func TestValidateScriptFilePath_RejectsEmptyPath(t *testing.T) {
	_, err := validateScriptFilePath("/home/user/project", "")
	if err == nil {
		t.Error("expected error for empty path")
	}
}

func TestValidateScriptFilePath_RejectsSymlinkEscape(t *testing.T) {
	projectRoot := t.TempDir()

	// Symlink pointing outside the project root
	outsideTarget := filepath.Join(projectRoot, "..", "outside.txt")
	if err := os.WriteFile(outsideTarget, []byte("secret"), 0644); err != nil {
		t.Fatal(err)
	}

	symlinkPath := filepath.Join(projectRoot, "evil_link")
	if err := os.Symlink(outsideTarget, symlinkPath); err != nil {
		t.Fatal(err)
	}

	_, err := validateScriptFilePath(projectRoot, "evil_link")
	if err == nil {
		t.Error("expected error for symlink escaping project root, got nil")
	}
}

func TestValidateScriptFilePath_AllowsSymlinkWithinProject(t *testing.T) {
	projectRoot := t.TempDir()

	// Create a file inside the project
	targetPath := filepath.Join(projectRoot, "real_file.txt")
	if err := os.WriteFile(targetPath, []byte("data"), 0644); err != nil {
		t.Fatal(err)
	}

	// Create a symlink pointing inside the project
	symlinkPath := filepath.Join(projectRoot, "link_to_real.txt")
	if err := os.Symlink("real_file.txt", symlinkPath); err != nil {
		t.Fatal(err)
	}

	got, err := validateScriptFilePath(projectRoot, "link_to_real.txt")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != targetPath {
		t.Errorf("got %q, want %q", got, targetPath)
	}
}

// --- Tests for load_binary builtin ---

func TestExecuteScript_LoadBinary(t *testing.T) {
	tmpDir := t.TempDir()
	binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
	if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "data = load_binary(\"data.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
		{Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
		{Text: "print(\"last=\" + str(data[7]))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 3 {
		t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output)
	}
	if output[0] != "len=8" {
		t.Errorf("output[0] = %q, want \"len=8\"", output[0])
	}
	if output[1] != "first=0" {
		t.Errorf("output[1] = %q, want \"first=0\"", output[1])
	}
	if output[2] != "last=7" {
		t.Errorf("output[2] = %q, want \"last=7\"", output[2])
	}
}

func TestExecuteScript_LoadBinary_Offset(t *testing.T) {
	tmpDir := t.TempDir()
	binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05}
	if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	// Load with offset 2: should return [0x02, 0x03, 0x04, 0x05]
	scriptLines := []preproc.Line{
		{Text: "data = load_binary(\"data.bin\", 2)", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
		{Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 2 {
		t.Fatalf("expected at least 2 output lines, got %d: %v", len(output), output)
	}
	if output[0] != "len=4" {
		t.Errorf("output[0] = %q, want \"len=4\"", output[0])
	}
	if output[1] != "first=2" {
		t.Errorf("output[1] = %q, want \"first=2\"", output[1])
	}
}

func TestExecuteScript_LoadBinary_OffsetAndLength(t *testing.T) {
	tmpDir := t.TempDir()
	binData := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
	if err := os.WriteFile(filepath.Join(tmpDir, "data.bin"), binData, 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	// Load with offset 3, length 2: should return [0x03, 0x04]
	scriptLines := []preproc.Line{
		{Text: "data = load_binary(\"data.bin\", 3, 2)", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(data)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
		{Text: "print(\"first=\" + str(data[0]))", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
		{Text: "print(\"last=\" + str(data[1]))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 3 {
		t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output)
	}
	if output[0] != "len=2" {
		t.Errorf("output[0] = %q, want \"len=2\"", output[0])
	}
	if output[1] != "first=3" {
		t.Errorf("output[1] = %q, want \"first=3\"", output[1])
	}
	if output[2] != "last=4" {
		t.Errorf("output[2] = %q, want \"last=4\"", output[2])
	}
}

func TestExecuteScript_LoadBinary_NonexistentFile(t *testing.T) {
	tmpDir := t.TempDir()
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "load_binary(\"nonexistent.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
	}

	_, err := executeScript(scriptLines, ctx, false)
	if err == nil {
		t.Error("expected error for nonexistent file")
	}
	if err != nil && !strings.Contains(err.Error(), "Starlark error") {
		t.Errorf("error should contain 'Starlark error', got: %v", err)
	}
}

func TestExecuteScript_LoadBinary_TraversalRejected(t *testing.T) {
	tmpDir := t.TempDir()
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "load_binary(\"../outside.bin\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
	}

	_, err := executeScript(scriptLines, ctx, false)
	if err == nil {
		t.Error("expected error for path traversal")
	}
}

func TestExecuteScript_LoadBinary_AbsoluteRejected(t *testing.T) {
	tmpDir := t.TempDir()
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "load_binary(\"/etc/passwd\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
	}

	_, err := executeScript(scriptLines, ctx, false)
	if err == nil {
		t.Error("expected error for absolute path")
	}
}

// --- Tests for load_text builtin ---

func TestExecuteScript_LoadText(t *testing.T) {
	tmpDir := t.TempDir()
	textContent := "hello\nworld\nfoo"
	if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
		{Text: "print(\"line0=\" + lines[0])", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
		{Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
		{Text: "print(\"line2=\" + lines[2])", Filename: "test.c65", LineNo: 5, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 4 {
		t.Fatalf("expected at least 4 output lines, got %d: %v", len(output), output)
	}
	if output[0] != "len=3" {
		t.Errorf("output[0] = %q, want \"len=3\"", output[0])
	}
	if output[1] != "line0=hello" {
		t.Errorf("output[1] = %q, want \"line0=hello\"", output[1])
	}
	if output[2] != "line1=world" {
		t.Errorf("output[2] = %q, want \"line1=world\"", output[2])
	}
	if output[3] != "line2=foo" {
		t.Errorf("output[3] = %q, want \"line2=foo\"", output[3])
	}
}

func TestExecuteScript_LoadText_TrailingNewline(t *testing.T) {
	tmpDir := t.TempDir()
	textContent := "hello\nworld\n"
	if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 1 {
		t.Fatalf("expected at least 1 output line, got %d: %v", len(output), output)
	}
	if output[0] != "len=2" {
		t.Errorf("output[0] = %q, want \"len=2\"", output[0])
	}
}

func TestExecuteScript_LoadText_EmptyFile(t *testing.T) {
	tmpDir := t.TempDir()
	if err := os.WriteFile(filepath.Join(tmpDir, "empty.txt"), []byte{}, 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "lines = load_text(\"empty.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 1 {
		t.Fatalf("expected at least 1 output line, got %d: %v", len(output), output)
	}
	if output[0] != "len=0" {
		t.Errorf("output[0] = %q, want \"len=0\"", output[0])
	}
}

func TestExecuteScript_LoadText_CRLF(t *testing.T) {
	tmpDir := t.TempDir()
	textContent := "hello\r\nworld\r\n"
	if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	scriptLines := []preproc.Line{
		{Text: "lines = load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
		{Text: "print(\"line0=\" + lines[0])", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
		{Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 4, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 3 {
		t.Fatalf("expected at least 3 output lines, got %d: %v", len(output), output)
	}
	if output[0] != "len=2" {
		t.Errorf("output[0] = %q, want \"len=2\"", output[0])
	}
	if output[1] != "line0=hello" {
		t.Errorf("output[1] = %q, want \"line0=hello\"", output[1])
	}
	if output[2] != "line1=world" {
		t.Errorf("output[2] = %q, want \"line1=world\"", output[2])
	}
}

func TestExecuteScript_LoadText_LibraryFunction(t *testing.T) {
	tmpDir := t.TempDir()
	textContent := "foo\nbar\nbaz"
	if err := os.WriteFile(filepath.Join(tmpDir, "data.txt"), []byte(textContent), 0644); err != nil {
		t.Fatal(err)
	}

	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	ctx.ProjectRoot = tmpDir

	// Define a library function that uses load_text
	libLines := []preproc.Line{
		{Text: "def get_lines():\n  return load_text(\"data.txt\")", Filename: "test.c65", LineNo: 1, Kind: preproc.ScriptLibrary},
	}
	_, err := executeScript(libLines, ctx, true)
	if err != nil {
		t.Fatalf("library definition failed: %v", err)
	}

	// Use the library function from a regular script
	scriptLines := []preproc.Line{
		{Text: "lines = get_lines()", Filename: "test.c65", LineNo: 5, Kind: preproc.Script},
		{Text: "print(\"len=\" + str(len(lines)))", Filename: "test.c65", LineNo: 6, Kind: preproc.Script},
		{Text: "print(\"line1=\" + lines[1])", Filename: "test.c65", LineNo: 7, Kind: preproc.Script},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed: %v", err)
	}

	if len(output) < 2 {
		t.Fatalf("expected at least 2 output lines, got %d: %v", len(output), output)
	}
	if output[0] != "len=3" {
		t.Errorf("output[0] = %q, want \"len=3\"", output[0])
	}
	if output[1] != "line1=bar" {
		t.Errorf("output[1] = %q, want \"line1=bar\"", output[1])
	}
}

// --- Tests for _P_SCRIPT_MAX_STEPS pragma ---

func TestReadScriptMaxSteps_Default(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)
	// No pragma set — index 0 has empty set
	got := readScriptMaxSteps(ctx, 0)
	if got != 1000000 {
		t.Errorf("readScriptMaxSteps() with no pragma = %d, want 1000000", got)
	}
}

func TestReadScriptMaxSteps_CustomValue(t *testing.T) {
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5000000")
	ctx := NewCompilerContext(pragma)

	got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex())
	if got != 5000000 {
		t.Errorf("readScriptMaxSteps() = %d, want 5000000", got)
	}
}

func TestReadScriptMaxSteps_Zero(t *testing.T) {
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "0")
	ctx := NewCompilerContext(pragma)

	got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex())
	if got != 1000000 {
		t.Errorf("readScriptMaxSteps(0) = %d, want 1000000 (default)", got)
	}
}

func TestReadScriptMaxSteps_Invalid(t *testing.T) {
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "not-a-number")
	ctx := NewCompilerContext(pragma)

	got := readScriptMaxSteps(ctx, pragma.GetCurrentPragmaSetIndex())
	if got != 1000000 {
		t.Errorf("readScriptMaxSteps(invalid) = %d, want 1000000 (default)", got)
	}
}

func TestExecuteScript_MaxStepsPragma_LowLimit(t *testing.T) {
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") // very low limit
	ctx := NewCompilerContext(pragma)

	// A simple loop will exceed 5 steps
	scriptLines := []preproc.Line{
		{Text: "  for i in range(10):", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
		{Text: "    x = i", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
	}

	_, err := executeScript(scriptLines, ctx, false)
	if err == nil {
		t.Fatal("expected step-limit error, got nil")
	}
	if !strings.Contains(err.Error(), "Starlark error") {
		t.Errorf("error should contain 'Starlark error', got: %v", err)
	}
}

func TestExecuteScript_MaxStepsPragma_HighLimit(t *testing.T) {
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "500000") // generous limit
	ctx := NewCompilerContext(pragma)

	scriptLines := []preproc.Line{
		{Text: "  result = 0", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
		{Text: "  for i in range(100):", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
		{Text: "    result = result + i", Filename: "test.c65", LineNo: 3, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
		{Text: "  print(str(result))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex()},
	}

	output, err := executeScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("executeScript failed with generous limit: %v", err)
	}
	if len(output) < 1 || output[0] != "4950" {
		t.Errorf("expected output '4950', got %v", output)
	}
}

func TestExecuteMacro_MaxStepsPragma_LowLimit(t *testing.T) {
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5") // very low limit
	ctx := NewCompilerContext(pragma)

	// Register a simple macro
	ctx.ScriptMacros["loop_macro"] = &ScriptMacro{
		Name:       "loop_macro",
		Params:     []string{},
		Body:       []string{"for i in range(10):", "  x = i"},
		SourceFile: "test.c65",
		StartLine:  1,
	}

	_, err := ExecuteMacro("loop_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex())
	if err == nil {
		t.Fatal("expected step-limit error, got nil")
	}
	if !strings.Contains(err.Error(), "Starlark error") {
		t.Errorf("error should contain 'Starlark error', got: %v", err)
	}
}

// Test that two SCRIPT blocks in the same compilation get different step limits
// when a #PRAGMA _P_SCRIPT_MAX_STEPS change appears between them.
func TestExecuteScript_MaxStepsPragma_ChangesBetweenBlocks(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)

	// First block: default limit (index 0, no pragmas) — should succeed
	firstLines := []preproc.Line{
		{Text: "  result = 0", Filename: "test.c65", LineNo: 1, Kind: preproc.Script, PragmaSetIndex: 0},
		{Text: "  for i in range(10):", Filename: "test.c65", LineNo: 2, Kind: preproc.Script, PragmaSetIndex: 0},
		{Text: "    result = result + i", Filename: "test.c65", LineNo: 3, Kind: preproc.Script, PragmaSetIndex: 0},
		{Text: "  print(str(result))", Filename: "test.c65", LineNo: 4, Kind: preproc.Script, PragmaSetIndex: 0},
	}

	output, err := executeScript(firstLines, ctx, false)
	if err != nil {
		t.Fatalf("first script (default limit) should succeed: %v", err)
	}
	if len(output) < 1 || output[0] != "45" {
		t.Errorf("first script output = %v, want ['45']", output)
	}

	// Simulate #PRAGMA _P_SCRIPT_MAX_STEPS 5 between the blocks
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5")
	lowLimitIdx := pragma.GetCurrentPragmaSetIndex()

	// Second block: low limit (index from pragma) — should fail
	secondLines := []preproc.Line{
		{Text: "  for i in range(10):", Filename: "test.c65", LineNo: 10, Kind: preproc.Script, PragmaSetIndex: lowLimitIdx},
		{Text: "    x = i", Filename: "test.c65", LineNo: 11, Kind: preproc.Script, PragmaSetIndex: lowLimitIdx},
	}

	_, err = executeScript(secondLines, ctx, false)
	if err == nil {
		t.Fatal("second script (limit=5) should fail with step-limit error, got nil")
	}
	if !strings.Contains(err.Error(), "Starlark error") {
		t.Errorf("second script error should contain 'Starlark error', got: %v", err)
	}
}

// Test that two macro invocations in the same compilation get different step limits
// when a #PRAGMA _P_SCRIPT_MAX_STEPS change appears between them.
func TestExecuteMacro_MaxStepsPragma_ChangesBetweenBlocks(t *testing.T) {
	pragma := preproc.NewPragma()
	ctx := NewCompilerContext(pragma)

	ctx.ScriptMacros["loop_macro"] = &ScriptMacro{
		Name:       "loop_macro",
		Params:     []string{},
		Body:       []string{"result = 0", "for i in range(10):", "  result = result + i", "print(str(result))"},
		SourceFile: "test.c65",
		StartLine:  1,
	}

	// First invocation: default limit (first pragma set) — should succeed
	output, err := ExecuteMacro("loop_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex())
	if err != nil {
		t.Fatalf("first macro (default limit) should succeed: %v", err)
	}
	if len(output) < 1 || output[0] != "45" {
		t.Errorf("first macro output = %v, want ['45']", output)
	}

	// Simulate #PRAGMA _P_SCRIPT_MAX_STEPS 5 between the invocations
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "5")
	lowLimitIdx := pragma.GetCurrentPragmaSetIndex()

	// Second invocation: low limit — should fail
	_, err = ExecuteMacro("loop_macro", []string{}, ctx, lowLimitIdx)
	if err == nil {
		t.Fatal("second macro (limit=5) should fail with step-limit error, got nil")
	}
	if !strings.Contains(err.Error(), "Starlark error") {
		t.Errorf("second macro error should contain 'Starlark error', got: %v", err)
	}
}

func TestExecuteMacro_MaxStepsPragma_HighLimit(t *testing.T) {
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_SCRIPT_MAX_STEPS", "500000") // generous limit
	ctx := NewCompilerContext(pragma)

	ctx.ScriptMacros["sum_macro"] = &ScriptMacro{
		Name:       "sum_macro",
		Params:     []string{},
		Body:       []string{"result = 0", "for i in range(100):", "  result = result + i", "print(str(result))"},
		SourceFile: "test.c65",
		StartLine:  1,
	}

	output, err := ExecuteMacro("sum_macro", []string{}, ctx, pragma.GetCurrentPragmaSetIndex())
	if err != nil {
		t.Fatalf("ExecuteMacro failed with generous limit: %v", err)
	}
	if len(output) < 1 || output[0] != "4950" {
		t.Errorf("expected output '4950', got %v", output)
	}
}
