package compiler

import (
	"fmt"
	"strings"
	"testing"

	"c65gm/internal/preproc"
	"c65gm/internal/utils"
)

// TestBreakCommand is a simple command implementation for testing
type TestBreakCommand struct {
	line preproc.Line
}

func (c *TestBreakCommand) WillHandle(line preproc.Line) bool {
	params, err := utils.ParseParams(line.Text)
	if err != nil || len(params) == 0 {
		return false
	}
	return strings.ToUpper(params[0]) == "BREAK"
}

func (c *TestBreakCommand) Interpret(line preproc.Line, ctx *CompilerContext) error {
	c.line = line

	params, err := utils.ParseParams(line.Text)
	if err != nil {
		return err
	}

	if len(params) != 1 {
		return fmt.Errorf("BREAK does not expect parameters")
	}

	return nil
}

func (c *TestBreakCommand) Generate(ctx *CompilerContext) ([]string, error) {
	// BREAK jumps to end of WHILE loop
	label, err := ctx.LoopEndStack.Peek()
	if err != nil {
		return nil, fmt.Errorf("BREAK outside of WHILE loop")
	}

	return []string{
		fmt.Sprintf("  jmp %s_end", label),
	}, nil
}

func (c *TestBreakCommand) GetClass() CmdClass { return CmdLinear }
func (c *TestBreakCommand) GetName() string     { return "BREAK" }

func TestCompilerArchitecture(t *testing.T) {
	// Create pragma
	pragma := preproc.NewPragma()

	// Create compiler
	comp := NewCompiler(pragma)

	// Register BREAK command
	comp.Registry().Register(&TestBreakCommand{})

	// Create test input - BREAK inside a simulated WHILE
	lines := []preproc.Line{
		{
			Text:           "BREAK",
			Filename:       "test.c65",
			LineNo:         1,
			Kind:           preproc.Source,
			PragmaSetIndex: 0,
		},
	}

	// Manually push a WHILE label so BREAK has something to reference
	comp.Context().LoopEndStack.Push()

	// Compile
	output, err := comp.Compile(lines)

	// Should fail because BREAK needs proper WHILE context
	// But this tests the basic flow: WillHandle -> Interpret -> Generate
	if err != nil {
		t.Logf("Expected controlled error: %v", err)
	}

	// Check we got some output structure
	if len(output) == 0 {
		t.Logf("Got output lines: %d", len(output))
	}

	t.Logf("Output:\n%s", strings.Join(output, "\n"))
}

func TestCommandRegistry(t *testing.T) {
	registry := NewCommandRegistry()

	breakCmd := &TestBreakCommand{}
	registry.Register(breakCmd)

	line := preproc.Line{
		Text:     "BREAK",
		Filename: "test.c65",
		LineNo:   1,
		Kind:     preproc.Source,
	}

	cmd, found := registry.FindHandler(line)
	if !found {
		t.Fatal("Expected to find BREAK handler")
	}

	if cmd != breakCmd {
		t.Fatal("Expected to get same command instance")
	}
}

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

	// Test that all resources are initialized
	if ctx.SymbolTable == nil {
		t.Error("SymbolTable not initialized")
	}
	if ctx.FunctionHandler == nil {
		t.Error("FunctionHandler not initialized")
	}
	if ctx.ConstStrHandler == nil {
		t.Error("ConstStrHandler not initialized")
	}
	if ctx.LoopEndStack == nil {
		t.Error("LoopEndStack not initialized")
	}
	if ctx.IfStack == nil {
		t.Error("IfStack not initialized")
	}
	if ctx.GeneralStack == nil {
		t.Error("GeneralStack not initialized")
	}
	if ctx.Pragma == nil {
		t.Error("Pragma not initialized")
	}
	if ctx.ScriptLibraryGlobals == nil {
		t.Error("ScriptLibraryGlobals not initialized")
	}
	if ctx.ScriptMacros == nil {
		t.Error("ScriptMacros not initialized")
	}

	// Test CurrentScope
	scope := ctx.CurrentScope()
	if scope != nil {
		t.Errorf("Expected nil scope in global context, got %v", scope)
	}
}

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

	scriptLines := []string{
		"for i in range(3):",
		"  print('  nop')",
	}

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

	if len(output) != 3 {
		t.Fatalf("expected 3 output lines, got %d: %v", len(output), output)
	}

	for i, line := range output {
		if line != "  nop" {
			t.Errorf("line %d: expected '  nop', got %q", i, line)
		}
	}
}

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

	scriptLines := []string{
		"x = 1 + 1",
	}

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

	if len(output) != 0 {
		t.Errorf("expected 0 output lines, got %d: %v", len(output), output)
	}
}

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

	// Define a function in library mode
	libraryLines := []string{
		"def emit_nops(count):",
		"  for i in range(count):",
		"    print('  nop')",
	}

	_, err := testExecuteScript(libraryLines, ctx, true)
	if err != nil {
		t.Fatalf("library executeScript failed: %v", err)
	}

	// Verify function is in globals
	if _, ok := ctx.ScriptLibraryGlobals["emit_nops"]; !ok {
		t.Fatal("expected emit_nops to be defined in ScriptLibraryGlobals")
	}
}

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

	// First: define function in library
	libraryLines := []string{
		"def emit_nops(count):",
		"  for i in range(count):",
		"    print('  nop')",
	}

	_, err := testExecuteScript(libraryLines, ctx, true)
	if err != nil {
		t.Fatalf("library executeScript failed: %v", err)
	}

	// Second: call function from regular script
	scriptLines := []string{
		"emit_nops(2)",
	}

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

	if len(output) != 2 {
		t.Fatalf("expected 2 output lines, got %d: %v", len(output), output)
	}

	for i, line := range output {
		if line != "  nop" {
			t.Errorf("line %d: expected '  nop', got %q", i, line)
		}
	}
}

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

	// First library: define func_a
	lib1 := []string{
		"def func_a():",
		"  print('  ; from a')",
	}
	_, err := testExecuteScript(lib1, ctx, true)
	if err != nil {
		t.Fatalf("lib1 failed: %v", err)
	}

	// Second library: define func_b (should still have func_a)
	lib2 := []string{
		"def func_b():",
		"  print('  ; from b')",
	}
	_, err = testExecuteScript(lib2, ctx, true)
	if err != nil {
		t.Fatalf("lib2 failed: %v", err)
	}

	// Both functions should be available
	if _, ok := ctx.ScriptLibraryGlobals["func_a"]; !ok {
		t.Error("func_a missing after second library")
	}
	if _, ok := ctx.ScriptLibraryGlobals["func_b"]; !ok {
		t.Error("func_b missing after second library")
	}

	// Call both from a script
	scriptLines := []string{
		"func_a()",
		"func_b()",
	}
	output, err := testExecuteScript(scriptLines, ctx, false)
	if err != nil {
		t.Fatalf("script failed: %v", err)
	}

	if len(output) != 2 {
		t.Fatalf("expected 2 lines, got %d: %v", len(output), output)
	}
	if output[0] != "  ; from a" {
		t.Errorf("expected '  ; from a', got %q", output[0])
	}
	if output[1] != "  ; from b" {
		t.Errorf("expected '  ; from b', got %q", output[1])
	}
}

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

	// Define function in regular script (not library)
	scriptLines := []string{
		"def local_func():",
		"  print('hello')",
		"local_func()",
	}

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

	if len(output) != 1 || output[0] != "hello" {
		t.Errorf("unexpected output: %v", output)
	}

	// Function should NOT be in globals (it was in regular script)
	if _, ok := ctx.ScriptLibraryGlobals["local_func"]; ok {
		t.Error("local_func should not persist from regular script")
	}
}

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

	// Register a macro
	ctx.ScriptMacros["test_macro"] = &ScriptMacro{
		Name:   "test_macro",
		Params: []string{"count"},
		Body: []string{
			"for i in range(count):",
			"  print('  nop')",
		},
	}

	// Execute macro
	output, err := ExecuteMacro("test_macro", []string{"3"}, ctx, 0)
	if err != nil {
		t.Fatalf("ExecuteMacro failed: %v", err)
	}

	if len(output) != 3 {
		t.Fatalf("expected 3 output lines, got %d: %v", len(output), output)
	}

	for i, line := range output {
		if line != "  nop" {
			t.Errorf("line %d: expected '  nop', got %q", i, line)
		}
	}
}

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

	// Define library function
	lib := []string{
		"def emit_nop():",
		"  print('  nop')",
	}
	_, err := testExecuteScript(lib, ctx, true)
	if err != nil {
		t.Fatalf("library failed: %v", err)
	}

	// Register a macro that uses the library function
	ctx.ScriptMacros["nop_macro"] = &ScriptMacro{
		Name:   "nop_macro",
		Params: []string{},
		Body: []string{
			"emit_nop()",
		},
	}

	// Execute macro
	output, err := ExecuteMacro("nop_macro", []string{}, ctx, 0)
	if err != nil {
		t.Fatalf("ExecuteMacro failed: %v", err)
	}

	if len(output) != 1 || output[0] != "  nop" {
		t.Errorf("unexpected output: %v", output)
	}
}

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

	// Register a macro with string parameter (label)
	ctx.ScriptMacros["jump_to"] = &ScriptMacro{
		Name:   "jump_to",
		Params: []string{"label"},
		Body: []string{
			"print('  jmp %s' % label)",
		},
	}

	// Execute with identifier (should be passed as string)
	output, err := ExecuteMacro("jump_to", []string{"my_label"}, ctx, 0)
	if err != nil {
		t.Fatalf("ExecuteMacro failed: %v", err)
	}

	if len(output) != 1 || output[0] != "  jmp my_label" {
		t.Errorf("unexpected output: %v", output)
	}
}

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

	// Add a local variable in function scope "testfunc"
	ctx.SymbolTable.AddVar("myvar", "testfunc", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

	// Enter the function scope by declaring the function
	_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC testfunc"))
	if err != nil {
		t.Fatalf("HandleFuncDecl failed: %v", err)
	}

	// Register a macro that uses |%s| pattern - the ACTUAL use case
	// The macro parameter 'varname' receives "myvar", then |%s| % varname
	// produces |myvar| in the output, which should then be expanded
	ctx.ScriptMacros["load_var"] = &ScriptMacro{
		Name:   "load_var",
		Params: []string{"varname"},
		Body: []string{
			"print('  lda |%s|' % varname)",
		},
	}

	// Execute macro with "myvar" as argument - should expand |myvar| to testfunc_myvar
	output, err := ExecuteMacro("load_var", []string{"myvar"}, ctx, 0)
	if err != nil {
		t.Fatalf("ExecuteMacro failed: %v", err)
	}

	if len(output) != 1 {
		t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
	}

	expected := "  lda testfunc_myvar"
	if output[0] != expected {
		t.Errorf("expected %q, got %q", expected, output[0])
	}
}

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

	// Add local variables in function scope
	ctx.SymbolTable.AddVar("color_index", "myfunc", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
	ctx.SymbolTable.AddVar("row_color", "myfunc", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

	// Add a global table (no function scope)
	ctx.SymbolTable.AddVar("scroll_color_table", "", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

	// Enter the function scope
	_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC myfunc"))
	if err != nil {
		t.Fatalf("HandleFuncDecl failed: %v", err)
	}

	// Register a macro using |%s| pattern - matches actual usage:
	// SCRIPT MACRO table_lookup(table, index, dest)
	//     print("  ldx |%s|" % index)
	//     print("  lda %s,x" % table)
	//     print("  sta |%s|" % dest)
	// ENDSCRIPT
	ctx.ScriptMacros["table_lookup"] = &ScriptMacro{
		Name:   "table_lookup",
		Params: []string{"table", "index", "dest"},
		Body: []string{
			"print('  ldx |%s|' % index)",
			"print('  lda %s,x' % table)",
			"print('  sta |%s|' % dest)",
		},
	}

	// Execute macro with actual variable names as arguments
	output, err := ExecuteMacro("table_lookup", []string{"scroll_color_table", "color_index", "row_color"}, ctx, 0)
	if err != nil {
		t.Fatalf("ExecuteMacro failed: %v", err)
	}

	expectedLines := []string{
		"  ldx myfunc_color_index",    // local var expanded with scope
		"  lda scroll_color_table,x",  // global var (no pipes) stays as-is
		"  sta myfunc_row_color",      // local var expanded with scope
	}

	if len(output) != len(expectedLines) {
		t.Fatalf("expected %d output lines, got %d: %v", len(expectedLines), len(output), output)
	}

	for i, expected := range expectedLines {
		if output[i] != expected {
			t.Errorf("line %d: expected %q, got %q", i, expected, output[i])
		}
	}
}

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

	// Add a local variable in function scope
	ctx.SymbolTable.AddVar("counter", "loopfunc", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

	// Enter the function scope
	_, err := ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC loopfunc"))
	if err != nil {
		t.Fatalf("HandleFuncDecl failed: %v", err)
	}

	// Script that uses |varname| syntax
	scriptLines := []string{
		"print('  inc |counter|')",
	}

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

	if len(output) != 1 {
		t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
	}

	expected := "  inc loopfunc_counter"
	if output[0] != expected {
		t.Errorf("expected %q, got %q", expected, output[0])
	}
}

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

	// Add a global variable
	ctx.SymbolTable.AddVar("global_counter", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

	// Library script that uses |varname| syntax at global scope
	// Should expand to the global name (unchanged since it's already global)
	libraryLines := []string{
		"def inc_global():",
		"  print('  inc |global_counter|')",
	}

	_, err := testExecuteScript(libraryLines, ctx, true)
	if err != nil {
		t.Fatalf("library script failed: %v", err)
	}

	// Now call the library function from a regular script
	scriptLines := []string{
		"inc_global()",
	}

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

	if len(output) != 1 {
		t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
	}

	// Global variable should stay as-is (no function prefix)
	expected := "  inc global_counter"
	if output[0] != expected {
		t.Errorf("expected %q, got %q", expected, output[0])
	}
}

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

	// Add a local variable in a function scope
	ctx.SymbolTable.AddVar("local_var", "caller", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

	// Library is defined at GLOBAL scope (not inside any function)
	// So |varname| expansion happens at global scope during library definition
	libraryLines := []string{
		"def use_local():",
		"  print('  lda |local_var|')", // This expands at library definition time
	}

	// Library defined at global scope - |local_var| won't find caller's local
	_, err := testExecuteScript(libraryLines, ctx, true)
	if err != nil {
		t.Fatalf("library script failed: %v", err)
	}

	// Now enter the function scope and call the library function
	_, err = ctx.FunctionHandler.HandleFuncDecl(makeLine("FUNC caller"))
	if err != nil {
		t.Fatalf("HandleFuncDecl failed: %v", err)
	}

	scriptLines := []string{
		"use_local()",
	}

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

	if len(output) != 1 {
		t.Fatalf("expected 1 output line, got %d: %v", len(output), output)
	}

	// Variable expansion happened at library definition time (global scope),
	// so local_var was NOT found and stays as literal "local_var"
	expected := "  lda local_var"
	if output[0] != expected {
		t.Errorf("expected %q, got %q (library expands |vars| at definition time, not call time)", expected, output[0])
	}
}

func TestParseMacroInvocation(t *testing.T) {
	tests := []struct {
		input    string
		wantName string
		wantArgs []string
		wantErr  bool
	}{
		{"@delay(10)", "delay", []string{"10"}, false},
		{"@nops(5)", "nops", []string{"5"}, false},
		{"@setup(80, handler)", "setup", []string{"80", "handler"}, false},
		{"@empty()", "empty", []string{}, false},
		{"@expr(10+5)", "expr", []string{"10+5"}, false},
		{"missing_at()", "", nil, true},
		{"@no_parens", "", nil, true},
	}

	for _, tt := range tests {
		name, args, err := ParseMacroInvocation(tt.input)
		if tt.wantErr {
			if err == nil {
				t.Errorf("ParseMacroInvocation(%q): expected error, got none", tt.input)
			}
			continue
		}
		if err != nil {
			t.Errorf("ParseMacroInvocation(%q): unexpected error: %v", tt.input, err)
			continue
		}
		if name != tt.wantName {
			t.Errorf("ParseMacroInvocation(%q): name = %q, want %q", tt.input, name, tt.wantName)
		}
		if len(args) != len(tt.wantArgs) {
			t.Errorf("ParseMacroInvocation(%q): args = %v, want %v", tt.input, args, tt.wantArgs)
		}
	}
}

func TestFindAsmCommentStart(t *testing.T) {
	tests := []struct {
		input string
		want  int
	}{
		// Basic cases
		{"lda #$00", -1},                          // no comment
		{"; comment", 0},                          // comment at start
		{"lda #$00 ; comment", 9},                 // comment after code
		{"  lda #$00 ; comment", 11},              // with leading whitespace

		// Semicolon in double-quoted string
		{`!text "hello; world"`, -1},             // no comment, ; inside string
		{`!text "hello; world" ; comment`, 21},   // comment after string with ;
		{`!text "a;b;c"`, -1},                    // multiple ; in string

		// Semicolon in single-quoted string
		{`!byte ';'`, -1},                        // ; as character literal
		{`!byte ';' ; comment`, 10},              // comment after ; char literal

		// Escape sequences in strings
		{`!text "hello\"world"`, -1},             // escaped quote, no comment
		{`!text "hello\"world" ; comment`, 21},   // comment after string with escaped quote
		{`!text "path\\file"`, -1},               // escaped backslash
		{`!text "a\\;b"`, -1},                    // escaped backslash before ;
		{`!text "a\;b"`, -1},                     // escaped ; in string (stays in string)

		// Mixed quotes
		{`!text "it's"`, -1},                     // single quote inside double
		{`!byte '"'`, -1},                        // double quote as char literal
		{`!text "say \"hi\""`, -1},               // escaped quotes in string

		// Edge cases
		{"", -1},                                  // empty line
		{`""`, -1},                               // empty string
		{`"" ; comment`, 3},                      // empty string then comment
		{`!text "unterminated`, -1},              // unterminated string (no ; found)
	}

	for _, tt := range tests {
		got := findAsmCommentStart(tt.input)
		if got != tt.want {
			t.Errorf("findAsmCommentStart(%q) = %d, want %d", tt.input, got, tt.want)
		}
	}
}

func TestAsmBlock_VariableExpansion_IgnoresComments(t *testing.T) {
	pragma := preproc.NewPragma()
	comp := NewCompiler(pragma)

	// Add a variable to the symbol table so expansion can work
	comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

	tests := []struct {
		name     string
		input    string
		expected string
	}{
		{
			name:     "variable in code expands",
			input:    "  lda |myvar|",
			expected: "  lda myvar",
		},
		{
			name:     "variable in comment stays unexpanded",
			input:    "; use |myvar| here",
			expected: "; use |myvar| here",
		},
		{
			name:     "variable in code, different in comment",
			input:    "  lda |myvar| ; load |myvar|",
			expected: "  lda myvar ; load |myvar|",
		},
		{
			name:     "only comment with variable",
			input:    "  nop ; |myvar|",
			expected: "  nop ; |myvar|",
		},
		{
			name:     "variable in string not affected",
			input:    `  !text "|myvar|"`,
			expected: `  !text "|myvar|"`,
		},
		{
			name:     "variable after string with semicolon",
			input:    `  !text "a;b" ; |myvar|`,
			expected: `  !text "a;b" ; |myvar|`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create fresh compiler for each test
			comp := NewCompiler(pragma)
			comp.Context().SymbolTable.AddVar("myvar", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

			lines := []preproc.Line{
				{
					Text:     tt.input,
					Filename: "test.asm",
					LineNo:   1,
					Kind:     preproc.Assembler,
				},
				{
					// Empty source line to close the ASM block
					Text:     "",
					Filename: "test.asm",
					LineNo:   2,
					Kind:     preproc.Source,
				},
			}

			output, err := comp.Compile(lines)
			if err != nil {
				t.Fatalf("Compile failed: %v", err)
			}

			// Find the ASM output line (between ; ASM and ; ENDASM markers)
			var resultLine string
			inAsmBlock := false
			for _, line := range output {
				if line == "; ASM" {
					inAsmBlock = true
					continue
				}
				if line == "; ENDASM" {
					break
				}
				if inAsmBlock {
					resultLine = line
					break
				}
			}

			if resultLine != tt.expected {
				t.Errorf("got %q, want %q\nfull output: %v", resultLine, tt.expected, output)
			}
		})
	}
}

func TestFindPipeOutsideStrings(t *testing.T) {
	tests := []struct {
		input     string
		startFrom int
		want      int
	}{
		// Basic cases
		{"lda |var|", 0, 4},
		{"lda |var|", 5, 8},
		{"no pipes", 0, -1},

		// Pipe in string should be skipped
		{`"a|b"`, 0, -1},
		{`"a|b" |var|`, 0, 6},
		{`'|' |x|`, 0, 4},

		// Escape sequences
		{`"a\"|b"`, 0, -1},            // escaped quote, pipe still in string
		{`"a\\"|b|`, 0, 5},            // escaped backslash, pipe outside
		{`"a\|b"`, 0, -1},             // escaped pipe stays in string

		// Start from different positions
		{"|a| |b|", 0, 0},
		{"|a| |b|", 1, 2},
		{"|a| |b|", 3, 4},
	}

	for _, tt := range tests {
		got := findPipeOutsideStrings(tt.input, tt.startFrom)
		if got != tt.want {
			t.Errorf("findPipeOutsideStrings(%q, %d) = %d, want %d", tt.input, tt.startFrom, got, tt.want)
		}
	}
}

func TestAsmBlock_MacroExpansion_IgnoresComments(t *testing.T) {
	pragma := preproc.NewPragma()
	comp := NewCompiler(pragma)

	// Register a test macro
	comp.Context().ScriptMacros["delay"] = &ScriptMacro{
		Name:   "delay",
		Params: []string{"cycles"},
		Body: []string{
			"for i in range(cycles):",
			"  print('  nop')",
		},
	}

	tests := []struct {
		name           string
		input          string
		expectExpanded bool
		expectedLines  int // number of output lines (excluding comment wrappers)
	}{
		{
			name:           "macro in code expands",
			input:          "  |@delay(3)|",
			expectExpanded: true,
			expectedLines:  3, // 3 nops
		},
		{
			name:           "macro in comment does not expand",
			input:          "; |@delay(3)|",
			expectExpanded: false,
			expectedLines:  1, // just the original line
		},
		{
			name:           "macro after semicolon comment does not expand",
			input:          "  nop ; |@delay(3)|",
			expectExpanded: false,
			expectedLines:  1, // just the original line with nop
		},
		{
			name:           "macro in string does not expand",
			input:          `  !text "|@delay(3)|"`,
			expectExpanded: false,
			expectedLines:  1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create fresh compiler for each test to avoid state issues
			comp := NewCompiler(pragma)
			comp.Context().ScriptMacros["delay"] = &ScriptMacro{
				Name:   "delay",
				Params: []string{"cycles"},
				Body: []string{
					"for i in range(cycles):",
					"  print('  nop')",
				},
			}

			lines := []preproc.Line{
				{
					Text:     tt.input,
					Filename: "test.asm",
					LineNo:   1,
					Kind:     preproc.Assembler,
				},
				{
					// Empty source line to close the ASM block
					Text:     "",
					Filename: "test.asm",
					LineNo:   2,
					Kind:     preproc.Source,
				},
			}

			output, err := comp.Compile(lines)
			if err != nil {
				t.Fatalf("Compile failed: %v", err)
			}

			// Count nop lines to determine if macro expanded
			nopCount := 0
			for _, line := range output {
				if strings.TrimSpace(line) == "nop" {
					nopCount++
				}
			}

			if tt.expectExpanded {
				if nopCount != tt.expectedLines {
					t.Errorf("expected %d nop lines (macro expanded), got %d\nfull output: %v",
						tt.expectedLines, nopCount, output)
				}
			} else {
				if nopCount > 0 {
					t.Errorf("expected no nop lines (macro should not expand in comment), got %d\nfull output: %v",
						nopCount, output)
				}
			}
		})
	}
}

func TestAsmAfterVarsPragma(t *testing.T) {
	tests := []struct {
		name           string
		pragmaEnabled  bool
		asmLines       []string
		expectDeferred bool
	}{
		{
			name:           "pragma enabled with 1",
			pragmaEnabled:  true,
			asmLines:       []string{"  lda #$00", "  sta $d020"},
			expectDeferred: true,
		},
		{
			name:           "pragma disabled with 0",
			pragmaEnabled:  false,
			asmLines:       []string{"  lda #$01", "  sta $d021"},
			expectDeferred: false,
		},
		{
			name:           "pragma not set",
			pragmaEnabled:  false,
			asmLines:       []string{"  nop", "  rts"},
			expectDeferred: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Create pragma and optionally enable _P_ASM_AFTER_VARS
			pragma := preproc.NewPragma()
			if tt.pragmaEnabled {
				pragma.AddPragma("_P_ASM_AFTER_VARS", "1")
			} else if tt.name == "pragma disabled with 0" {
				pragma.AddPragma("_P_ASM_AFTER_VARS", "0")
			}

			comp := NewCompiler(pragma)

			// Build test lines - simulate preprocessor output
			// The preprocessor strips ASM/ENDASM and marks lines as Assembler
			lines := []preproc.Line{}
			
			// Add ASM lines (simulating what preprocessor produces)
			for i, asmLine := range tt.asmLines {
				lines = append(lines, preproc.Line{
					Text:           asmLine,
					Filename:       "test.c65",
					LineNo:         1 + i,
					Kind:           preproc.Assembler,
					PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
				})
			}

			// Add an empty source line to trigger kind transition and close ASM block
			lines = append(lines, preproc.Line{
				Text:           "",
				Filename:       "test.c65",
				LineNo:         1 + len(tt.asmLines),
				Kind:           preproc.Source,
				PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
			})

			output, err := comp.Compile(lines)
			if err != nil {
				t.Fatalf("Compile failed: %v", err)
			}

			// Check for deferred comment in main code section
			foundDeferredComment := false
			foundDeferredBlock := false
			inDeferredSection := false

			for _, line := range output {
				if line == "; ASM block deferred to end of source" {
					foundDeferredComment = true
				}
				if line == "; Deferred ASM blocks (after variables)" {
					inDeferredSection = true
				}
				if inDeferredSection && strings.Contains(line, "; ASM Block from test.c65, Line 1") {
					foundDeferredBlock = true
				}
			}

			if tt.expectDeferred {
				if !foundDeferredComment {
					t.Errorf("expected '; ASM block deferred to end of source' comment in main code")
				}
				if !foundDeferredBlock {
					t.Errorf("expected deferred ASM block with source location")
				}
			} else {
				if foundDeferredComment {
					t.Errorf("unexpected '; ASM block deferred to end of source' comment when pragma disabled")
				}
				if foundDeferredBlock {
					t.Errorf("unexpected deferred ASM block when pragma disabled")
				}
			}

			// Verify ASM lines appear somewhere in output
			asmFound := false
			for _, asmLine := range tt.asmLines {
				for _, outputLine := range output {
					if strings.TrimSpace(outputLine) == strings.TrimSpace(asmLine) {
						asmFound = true
						break
					}
				}
				if asmFound {
					break
				}
			}
			if !asmFound {
				t.Errorf("ASM lines not found in output")
			}
		})
	}
}

func TestUnterminatedAsmBlock(t *testing.T) {
	tests := []struct {
		name     string
		source   []preproc.Line
		wantErr  bool
		errMsg   string
	}{
		{
			name: "unterminated ASM block",
			source: []preproc.Line{
				{
					Text: "    lda #$00",
					Kind: preproc.Assembler,
				},
				{
					Text: "    sta $d020",
					Kind: preproc.Assembler,
				},
			},
			wantErr: true,
			errMsg: "Unclosed ASM block.",
		},
		{
			name: "properly terminated ASM block",
			source: []preproc.Line{
				{
					Text: "    lda #$00",
					Kind: preproc.Assembler,
				},
				{
					Text: "    sta $d020",
					Kind: preproc.Assembler,
				},
				{
					Text: "",
					Kind: preproc.Source,
				},
			},
			wantErr: false,
		},
		{
			name: "ASM block with pragma but no ENDASM",
			source: []preproc.Line{
				{
					Text: "    lda #$00",
					Kind: preproc.Assembler,
					PragmaSetIndex: 1, // Simulate pragma active
				},
				{
					Text: "    sta $d020",
					Kind: preproc.Assembler,
					PragmaSetIndex: 1,
				},
			},
			wantErr: true,
			errMsg: "Unclosed ASM block.",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pragma := preproc.NewPragma()
			// Set up pragma for test case 3
			if tt.name == "ASM block with pragma but no ENDASM" {
				pragma.AddPragma("_P_ASM_AFTER_VARS", "1")
			}
			
			compiler := NewCompiler(pragma)
			_, err := compiler.Compile(tt.source)

			if tt.wantErr {
				if err == nil {
					t.Errorf("TestUnterminatedAsmBlock(%s): expected error but got none", tt.name)
				} else if !strings.Contains(err.Error(), tt.errMsg) {
					t.Errorf("TestUnterminatedAsmBlock(%s): expected error containing %q, got %q", tt.name, tt.errMsg, err.Error())
				}
			} else {
				if err != nil {
					t.Errorf("TestUnterminatedAsmBlock(%s): unexpected error: %v", tt.name, err)
				}
			}
		})
	}
}

func TestAsmAfterVarsWithVariables(t *testing.T) {
	// Test that deferred ASM blocks appear in the deferred section
	pragma := preproc.NewPragma()
	pragma.AddPragma("_P_ASM_AFTER_VARS", "1")
	
	comp := NewCompiler(pragma)

	// Simple test with just an ASM block
	lines := []preproc.Line{
		// ASM line (simulating preprocessor output)
		{
			Text:           "  data: !8 1,2,3,4",
			Filename:       "test.c65",
			LineNo:         1,
			Kind:           preproc.Assembler,
			PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
		},
		// Empty source line to close ASM block
		{
			Text:           "",
			Filename:       "test.c65",
			LineNo:         2,
			Kind:           preproc.Source,
			PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
		},
	}

	output, err := comp.Compile(lines)
	if err != nil {
		t.Fatalf("Compile failed: %v", err)
	}

	// Check that deferred section exists and contains our ASM
	foundDeferredSection := false
	foundDeferredAsm := false
	inDeferredSection := false

	for _, line := range output {
		if line == "; Deferred ASM blocks (after variables)" {
			foundDeferredSection = true
			inDeferredSection = true
		}
		if inDeferredSection && strings.Contains(line, "data: !8 1,2,3,4") {
			foundDeferredAsm = true
		}
	}

	if !foundDeferredSection {
		t.Errorf("expected '; Deferred ASM blocks (after variables)' section")
	}
	if !foundDeferredAsm {
		t.Errorf("expected ASM block in deferred section")
	}
}

// testExecuteScript is a test helper that wraps executeScript with convenient types
func testExecuteScript(scriptLines []string, ctx *CompilerContext, isLibrary bool) ([]string, error) {
	lines := make([]preproc.Line, len(scriptLines))
	for i, text := range scriptLines {
		lines[i] = preproc.Line{
			Text:     text,
			Filename: "test.c65",
			LineNo:   i + 1,
		}
	}
	return executeScript(lines, ctx, isLibrary)
}

func TestCompile_MultipleScriptBlocks_Success(t *testing.T) {
	// Two consecutive SCRIPT blocks should execute independently
	pragma := preproc.NewPragma()
	comp := NewCompiler(pragma)

	lines := []preproc.Line{
		// Block 1: print("hello")
		{Text: "print('hello')", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
		// ENDSCRIPT boundary (empty Source)
		{Text: "", Filename: "test.c65", LineNo: 3, Kind: preproc.Source},
		// Block 2: print("world")
		{Text: "print('world')", Filename: "test.c65", LineNo: 5, Kind: preproc.Script},
		// ENDSCRIPT boundary (empty Source)
		{Text: "", Filename: "test.c65", LineNo: 6, Kind: preproc.Source},
	}

	output, err := comp.Compile(lines)
	if err != nil {
		t.Fatalf("Compile failed: %v", err)
	}

	// Output should contain both "hello" and "world" in that order
	foundHello := false
	foundWorld := false
	helloBeforeWorld := false
	for _, line := range output {
		if strings.Contains(line, "hello") && !foundHello {
			foundHello = true
		}
		if foundHello && strings.Contains(line, "world") && !foundWorld {
			foundWorld = true
			helloBeforeWorld = true
		}
	}
	if !foundHello {
		t.Error("expected 'hello' from block 1 in output")
	}
	if !foundWorld {
		t.Error("expected 'world' from block 2 in output")
	}
	if !helloBeforeWorld {
		t.Error("expected 'hello' before 'world' in output")
	}
}

func TestCompile_MultipleScriptBlocks_ErrorInBlock2(t *testing.T) {
	// Error in second SCRIPT block should not be confused with block 1
	pragma := preproc.NewPragma()
	comp := NewCompiler(pragma)

	lines := []preproc.Line{
		// Block 1: no error
		{Text: "x = 1", Filename: "test.c65", LineNo: 2, Kind: preproc.Script},
		{Text: "print(x)", Filename: "test.c65", LineNo: 3, Kind: preproc.Script},
		// ENDSCRIPT boundary
		{Text: "", Filename: "test.c65", LineNo: 4, Kind: preproc.Source},
		// Block 2: error at line 8 (division by zero)
		{Text: "y = 2", Filename: "test.c65", LineNo: 6, Kind: preproc.Script},
		{Text: "z = y + 1", Filename: "test.c65", LineNo: 7, Kind: preproc.Script},
		{Text: "1 / 0", Filename: "test.c65", LineNo: 8, Kind: preproc.Script},
		// ENDSCRIPT boundary
		{Text: "", Filename: "test.c65", LineNo: 9, Kind: preproc.Source},
	}

	_, err := comp.Compile(lines)
	if err == nil {
		t.Fatal("expected error from block 2, got none")
	}

	errMsg := err.Error()
	if !strings.Contains(errMsg, "Starlark error") {
		t.Errorf("expected Starlark error, got: %s", errMsg)
	}
	// Verify error references the correct source line in block 2 (line 8 = division by zero)
	if !strings.Contains(errMsg, ":8:") {
		t.Errorf("error should reference source line 8 in block 2, got: %s", errMsg)
	}
}

