package preproc

import (
	"strings"
	"testing"
)

func TestPreProcess_BasicDefine(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE FOO = 42",
			"LDA #FOO",
			"STA $$D020",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	if lines[0].Text != "LDA #42" {
		t.Errorf("expected 'LDA #42', got %q", lines[0].Text)
	}

	if lines[0].Kind != Source {
		t.Errorf("expected Kind=Source, got %v", lines[0].Kind)
	}
}

func TestPreProcess_DefineExpansion(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE BASE = $$$$D000",
			"#DEFINE OFFSET = 32",
			"#DEFINE ADDR = BASE+OFFSET",
			"STA ADDR",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	if lines[0].Text != "STA $D000+32" {
		t.Errorf("expected 'STA $D000+32', got %q", lines[0].Text)
	}
}

func TestPreProcess_IncludeGuard(t *testing.T) {
	files := map[string][]string{
		"lib.c65": {
			"#IFNDEF __LIB",
			"#DEFINE __LIB = 1",
			"LABEL lib_init",
			"#IFEND",
		},
		"main.c65": {
			"#INCLUDE lib.c65",
			"#INCLUDE lib.c65",
			"GOTO main",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("main.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	if lines[0].Text != "LABEL lib_init" {
		t.Errorf("expected 'LABEL lib_init', got %q", lines[0].Text)
	}
	if lines[1].Text != "GOTO main" {
		t.Errorf("expected 'GOTO main', got %q", lines[1].Text)
	}
}

func TestPreProcess_ASMBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE FOO = 42",
			"ASM",
			"  lda #FOO",
			"  sta $d020",
			"ENDASM",
			"LDA #FOO",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// ASM and ENDASM markers are stripped, so only 2 asm lines + 1 source line
	if len(lines) != 3 {
		t.Fatalf("expected 3 lines, got %d", len(lines))
	}

	// ASM content should NOT be processed
	if lines[0].Text != "  lda #FOO" {
		t.Errorf("expected '  lda #FOO', got %q", lines[0].Text)
	}
	if lines[0].Kind != Assembler {
		t.Errorf("expected Kind=Assembler, got %v", lines[0].Kind)
	}

	if lines[1].Text != "  sta $d020" {
		t.Errorf("expected '  sta $d020', got %q", lines[1].Text)
	}
	if lines[1].Kind != Assembler {
		t.Errorf("expected Kind=Assembler, got %v", lines[1].Kind)
	}

	// After ENDASM, defines work again
	if lines[2].Text != "LDA #42" {
		t.Errorf("expected 'LDA #42', got %q", lines[2].Text)
	}
	if lines[2].Kind != Source {
		t.Errorf("expected Kind=Source, got %v", lines[2].Kind)
	}
}

func TestPreProcess_ScriptBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE VAR = 100",
			"SCRIPT",
			"  x = VAR + 1",
			"  print(x)",
			"ENDSCRIPT",
			"LDA #VAR",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// SCRIPT and ENDSCRIPT markers are stripped, but ENDSCRIPT emits
	// an empty Source boundary marker
	if len(lines) != 4 {
		t.Fatalf("expected 4 lines, got %d", len(lines))
	}

	// Script content should NOT be processed
	if lines[0].Text != "  x = VAR + 1" {
		t.Errorf("expected '  x = VAR + 1', got %q", lines[0].Text)
	}
	if lines[0].Kind != Script {
		t.Errorf("expected Kind=Script, got %v", lines[0].Kind)
	}

	if lines[1].Text != "  print(x)" {
		t.Errorf("expected '  print(x)', got %q", lines[1].Text)
	}
	if lines[1].Kind != Script {
		t.Errorf("expected Kind=Script, got %v", lines[1].Kind)
	}

	// Line 2 is the ENDSCRIPT boundary marker (empty Source line)
	if lines[2].Kind != Source {
		t.Errorf("line 2: expected Kind=Source (ENDSCRIPT boundary), got %v", lines[2].Kind)
	}
	if lines[2].Text != "" {
		t.Errorf("line 2: expected empty text, got %q", lines[2].Text)
	}

	// After ENDSCRIPT, defines work again
	if lines[3].Text != "LDA #100" {
		t.Errorf("expected 'LDA #100', got %q", lines[3].Text)
	}
	if lines[3].Kind != Source {
		t.Errorf("expected Kind=Source, got %v", lines[3].Kind)
	}
}

func TestPreProcess_CommentStripping(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"LDA #42 // load accumulator",
			"STA $D020 // border color",
			"NOP // comment only",
			"JMP $0810",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	if len(lines) != 4 {
		t.Fatalf("expected 4 lines, got %d", len(lines))
	}

	if lines[0].Text != "LDA #42" {
		t.Errorf("expected 'LDA #42', got %q", lines[0].Text)
	}
	if lines[0].RawText != "LDA #42 // load accumulator" {
		t.Errorf("expected RawText to preserve comment, got %q", lines[0].RawText)
	}

	if lines[1].Text != "STA $D020" {
		t.Errorf("expected 'STA $D020', got %q", lines[1].Text)
	}

	if lines[2].Text != "NOP" {
		t.Errorf("expected 'NOP', got %q", lines[2].Text)
	}

	if lines[3].Text != "JMP $0810" {
		t.Errorf("expected 'JMP $0810', got %q", lines[3].Text)
	}
}

func TestPreProcess_CommentInASMBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"ASM",
			"  lda #42 // this comment stays",
			"  sta $d020 // this too",
			"ENDASM",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	// Comments should be preserved in ASM blocks
	if lines[0].Text != "  lda #42 // this comment stays" {
		t.Errorf("expected comment preserved, got %q", lines[0].Text)
	}
	if lines[1].Text != "  sta $d020 // this too" {
		t.Errorf("expected comment preserved, got %q", lines[1].Text)
	}
}

func TestPreProcess_CommentInScriptBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"SCRIPT",
			"  x = 1 // script comment",
			"  y = 2 // another one",
			"ENDSCRIPT",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// 2 script lines + 1 ENDSCRIPT boundary marker + 0 trailing source = 3 lines
	if len(lines) != 3 {
		t.Fatalf("expected 3 lines, got %d", len(lines))
	}

	// Comments should be preserved in Script blocks
	if lines[0].Text != "  x = 1 // script comment" {
		t.Errorf("expected comment preserved, got %q", lines[0].Text)
	}
	if lines[1].Text != "  y = 2 // another one" {
		t.Errorf("expected comment preserved, got %q", lines[1].Text)
	}

	// Line 2 is ENDSCRIPT boundary marker
	if lines[2].Kind != Source || lines[2].Text != "" {
		t.Errorf("line 2: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[2].Kind, lines[2].Text)
	}
}

func TestPreProcess_RawTextPreservation(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE FOO = 42",
			"LDA #FOO // comment here",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	// RawText should be original
	if lines[0].RawText != "LDA #FOO // comment here" {
		t.Errorf("expected RawText 'LDA #FOO // comment here', got %q", lines[0].RawText)
	}

	// Text should be processed
	if lines[0].Text != "LDA #42" {
		t.Errorf("expected Text 'LDA #42', got %q", lines[0].Text)
	}
}

func TestPreProcess_MismatchedBlockTerminators(t *testing.T) {
	tests := []struct {
		name  string
		lines []string
	}{
		{
			name: "ASM ended with ENDSCRIPT",
			lines: []string{
				"ASM",
				"  lda #42",
				"ENDSCRIPT",
				"NOP",
			},
		},
		{
			name: "SCRIPT ended with ENDASM",
			lines: []string{
				"SCRIPT",
				"  x = 1",
				"ENDASM",
				"NOP",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			files := map[string][]string{
				"test.c65": tt.lines,
			}
			reader := NewMockFileReader(files)
			lines, _, err := PreProcess("test.c65", reader)
			if err != nil {
				t.Fatalf("PreProcess failed: %v", err)
			}

			// Wrong terminator won't close the block
			// All lines including the wrong terminator should be in the block
			if len(lines) < 2 {
				t.Errorf("expected at least 2 lines, got %d", len(lines))
			}

			// The mismatched terminator should be treated as block content
			// and NOP should still be in the block too
			for _, line := range lines {
				if line.Kind == Source {
					t.Errorf("found Source line when all should be in block: %q", line.Text)
				}
			}
		})
	}
}

func TestPreProcess_NestedConditionals(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE OUTER",
			"#IFDEF OUTER",
			"  LINE1",
			"  #IFDEF INNER",
			"    LINE2",
			"  #IFEND",
			"  LINE3",
			"#IFEND",
			"ALWAYS",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	expected := []string{"LINE1", "LINE3", "ALWAYS"}
	for i, exp := range expected {
		if !strings.Contains(lines[i].Text, exp) {
			t.Errorf("line %d: expected %q, got %q", i, exp, lines[i].Text)
		}
	}
}

func TestPreProcess_ConditionalBlocking(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#IFNDEF UNDEFINED",
			"  VISIBLE",
			"  #DEFINE FOO = 1",
			"#IFEND",
			"#IFDEF UNDEFINED",
			"  INVISIBLE",
			"  #DEFINE BAR = 2",
			"#IFEND",
			"LDA #FOO",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	// FOO should be defined, BAR should not
	if lines[1].Text != "LDA #1" {
		t.Errorf("expected 'LDA #1', got %q", lines[1].Text)
	}
}

func TestPreProcess_NestedIncludes(t *testing.T) {
	files := map[string][]string{
		"a.c65": {
			"#INCLUDE b.c65",
			"LINE_A",
		},
		"b.c65": {
			"#INCLUDE c.c65",
			"LINE_B",
		},
		"c.c65": {
			"LINE_C",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("a.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

	expected := []string{"LINE_C", "LINE_B", "LINE_A"}
	for i, exp := range expected {
		if lines[i].Text != exp {
			t.Errorf("line %d: expected %q, got %q", i, exp, lines[i].Text)
		}
	}
}

func TestPreProcess_Filename(t *testing.T) {
	files := map[string][]string{
		"main.c65": {
			"#INCLUDE lib.c65",
			"MAIN_LINE",
		},
		"lib.c65": {
			"LIB_LINE",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("main.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	if lines[0].Filename != "lib.c65" {
		t.Errorf("expected filename 'lib.c65', got %q", lines[0].Filename)
	}
	if lines[1].Filename != "main.c65" {
		t.Errorf("expected filename 'main.c65', got %q", lines[1].Filename)
	}
}

func TestPreProcess_LineNumbers(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"LINE1",
			"LINE2",
			"#DEFINE FOO = 1",
			"LINE4",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	expectedLines := []int{1, 2, 4}
	for i, exp := range expectedLines {
		if lines[i].LineNo != exp {
			t.Errorf("line %d: expected line number %d, got %d", i, exp, lines[i].LineNo)
		}
	}
}

func TestPreProcess_Undef(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE FOO = 1",
			"LDA #FOO",
			"#UNDEF FOO",
			"LDA #FOO",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	if lines[0].Text != "LDA #1" {
		t.Errorf("expected 'LDA #1', got %q", lines[0].Text)
	}
	if lines[1].Text != "LDA #FOO" {
		t.Errorf("expected 'LDA #FOO' (unexpanded), got %q", lines[1].Text)
	}
}

func TestPreProcess_PragmaWithDefines(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE SETTING = 1",
			"#PRAGMA USE_FEATURE SETTING",
			"CODE",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

func TestPreProcess_EmptyFile(t *testing.T) {
	files := map[string][]string{
		"test.c65": {},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

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

func TestPreProcess_MissingInclude(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#INCLUDE notfound.c65",
		},
	}
	reader := NewMockFileReader(files)
	_, _, err := PreProcess("test.c65", reader)
	if err == nil {
		t.Error("expected error for missing include")
	}
}

func TestPreProcess_HaltDirective(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"LINE1",
			"#HALT",
			"LINE2",
		},
	}
	reader := NewMockFileReader(files)
	_, _, err := PreProcess("test.c65", reader)
	if err == nil {
		t.Error("expected HaltError")
	}
	if _, ok := err.(HaltError); !ok {
		t.Errorf("expected HaltError, got %T", err)
	}
}

func TestPreProcess_ConditionalHalt(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"LINE1",
			"#IFDEF UNDEFINED",
			"#HALT",
			"#IFEND",
			"LINE2",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}
	// HALT is inside false conditional, should not trigger
	if len(lines) != 2 {
		t.Errorf("expected 2 lines, got %d", len(lines))
	}
}

func TestPreProcess_ComplexIncludeGuard(t *testing.T) {
	files := map[string][]string{
		"c64scr.c65": {
			"#IFNDEF __C64_SCR",
			"#DEFINE __C64_SCR = 1",
			"GOTO lib_c64scr_skip",
			"LABEL lib_c64scr_blank",
			"ASM",
			"  lda $d011",
			"ENDASM",
			"SUBEND",
			"LABEL lib_c64scr_skip",
			"#IFEND",
		},
		"main.c65": {
			"#INCLUDE c64scr.c65",
			"#INCLUDE c64scr.c65",
			"MAIN",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("main.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// First include should emit the library, second should be blocked
	count := 0
	for _, line := range lines {
		if strings.Contains(line.Text, "lda $d011") {
			count++
		}
	}

	if count != 1 {
		t.Errorf("expected 1 occurrence from multiple includes of same file, got %d", count)
	}
}

func TestPreProcess_UnbalancedConditionals(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE FOO",
			"#IFDEF FOO",
			"  LINE1",
			"  LINE2",
			// Missing #IFEND
		},
	}
	reader := NewMockFileReader(files)
	_, _, err := PreProcess("test.c65", reader)
	if err == nil {
		t.Error("expected error for unbalanced conditionals")
	}
	if !strings.Contains(err.Error(), "unbalanced") {
		t.Errorf("expected 'unbalanced' error, got: %v", err)
	}
}

func TestPreProcess_FilenameAndLineNumberTracking(t *testing.T) {
	files := map[string][]string{
		"main.c65": {
			"MAIN_LINE1",       // line 1
			"#INCLUDE lib.c65", // line 2 (directive, not emitted)
			"MAIN_LINE2",       // line 3
			"MAIN_LINE3",       // line 4
		},
		"lib.c65": {
			"LIB_LINE1", // line 1
			"LIB_LINE2", // line 2
			"LIB_LINE3", // line 3
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("main.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	expected := []struct {
		text     string
		filename string
		lineNo   int
	}{
		{"MAIN_LINE1", "main.c65", 1},
		{"LIB_LINE1", "lib.c65", 1},
		{"LIB_LINE2", "lib.c65", 2},
		{"LIB_LINE3", "lib.c65", 3},
		{"MAIN_LINE2", "main.c65", 3},
		{"MAIN_LINE3", "main.c65", 4},
	}

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

	for i, exp := range expected {
		if lines[i].Text != exp.text {
			t.Errorf("line %d: expected text %q, got %q", i, exp.text, lines[i].Text)
		}
		if lines[i].Filename != exp.filename {
			t.Errorf("line %d: expected filename %q, got %q", i, exp.filename, lines[i].Filename)
		}
		if lines[i].LineNo != exp.lineNo {
			t.Errorf("line %d: expected line number %d, got %d", i, exp.lineNo, lines[i].LineNo)
		}
	}
}

func TestPreProcess_PragmaTracking(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"LINE1",                     // no pragmas yet (index 0)
			"LINE2",                     // no pragmas yet (index 0)
			"#PRAGMA FEATURE1 enabled",  // directive, not emitted
			"LINE3",                     // FEATURE1=enabled (index 1)
			"LINE4",                     // FEATURE1=enabled (index 1)
			"#PRAGMA FEATURE2 123",      // directive, not emitted
			"LINE5",                     // FEATURE1=enabled, FEATURE2=123 (index 2)
			"#PRAGMA FEATURE1 disabled", // directive, not emitted
			"LINE6",                     // FEATURE1=disabled, FEATURE2=123 (index 3)
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	if len(lines) != 6 {
		t.Fatalf("expected 6 lines, got %d", len(lines))
	}

	// Create pragma instance to verify the indices
	pragma := NewPragma()
	pragma.AddPragma("FEATURE1", "enabled")
	pragma.AddPragma("FEATURE2", "123")
	pragma.AddPragma("FEATURE1", "disabled")

	tests := []struct {
		lineIdx      int
		text         string
		pragmaSetIdx int
		feature1     string
		feature2     string
	}{
		{0, "LINE1", 0, "", ""},
		{1, "LINE2", 0, "", ""},
		{2, "LINE3", 1, "enabled", ""},
		{3, "LINE4", 1, "enabled", ""},
		{4, "LINE5", 2, "enabled", "123"},
		{5, "LINE6", 3, "disabled", "123"},
	}

	for _, tt := range tests {
		line := lines[tt.lineIdx]

		if line.Text != tt.text {
			t.Errorf("line %d: expected text %q, got %q", tt.lineIdx, tt.text, line.Text)
		}

		if line.PragmaSetIndex != tt.pragmaSetIdx {
			t.Errorf("line %d: expected pragma set index %d, got %d",
				tt.lineIdx, tt.pragmaSetIdx, line.PragmaSetIndex)
		}

		// Verify we can retrieve pragma values using the index
		pragmaSet := pragma.GetPragmaSetByIndex(line.PragmaSetIndex)

		if got := pragmaSet.GetPragma("FEATURE1"); got != tt.feature1 {
			t.Errorf("line %d: expected FEATURE1=%q, got %q", tt.lineIdx, tt.feature1, got)
		}

		if got := pragmaSet.GetPragma("FEATURE2"); got != tt.feature2 {
			t.Errorf("line %d: expected FEATURE2=%q, got %q", tt.lineIdx, tt.feature2, got)
		}
	}
}

func TestPreProcess_Halt(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"LINE1",
			"LINE2",
			"#HALT",
			"LINE3",
			"LINE4",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)

	if err == nil {
		t.Fatal("expected HaltError, got nil")
	}

	if _, ok := err.(HaltError); !ok {
		t.Fatalf("expected HaltError, got %T: %v", err, err)
	}

	if len(lines) != 2 {
		t.Errorf("expected 2 lines before halt, got %d", len(lines))
	}

	if len(lines) >= 1 && lines[0].Text != "LINE1" {
		t.Errorf("expected LINE1, got %q", lines[0].Text)
	}

	if len(lines) >= 2 && lines[1].Text != "LINE2" {
		t.Errorf("expected LINE2, got %q", lines[1].Text)
	}
}

func TestPreProcess_HaltInInclude(t *testing.T) {
	files := map[string][]string{
		"main.c65": {
			"MAIN_LINE1",
			"#INCLUDE lib.c65",
			"MAIN_LINE2",
		},
		"lib.c65": {
			"LIB_LINE1",
			"#HALT",
			"LIB_LINE2",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("main.c65", reader)

	if err == nil {
		t.Fatal("expected HaltError, got nil")
	}

	if _, ok := err.(HaltError); !ok {
		t.Fatalf("expected HaltError, got %T: %v", err, err)
	}

	if len(lines) != 2 {
		t.Fatalf("expected 2 lines before halt, got %d", len(lines))
	}

	if lines[0].Text != "MAIN_LINE1" {
		t.Errorf("expected MAIN_LINE1, got %q", lines[0].Text)
	}

	if lines[1].Text != "LIB_LINE1" {
		t.Errorf("expected LIB_LINE1, got %q", lines[1].Text)
	}
}

func TestPreProcess_MixedBlocksAndComments(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"#DEFINE X = 10",
			"LDA #X // source comment",
			"ASM",
			"  lda #X // asm comment",
			"ENDASM",
			"SCRIPT",
			"  y = X // script comment",
			"ENDSCRIPT",
			"STA $D020 // another source comment",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	expected := []struct {
		text string
		kind LineKind
	}{
		{"LDA #10", Source},
		{"  lda #X // asm comment", Assembler},
		{"  y = X // script comment", Script},
		{"", Source}, // ENDSCRIPT boundary marker
		{"STA $D020", Source},
	}

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

	for i, exp := range expected {
		if lines[i].Text != exp.text {
			t.Errorf("line %d: expected text %q, got %q", i, exp.text, lines[i].Text)
		}
		if lines[i].Kind != exp.kind {
			t.Errorf("line %d: expected Kind=%v, got %v", i, exp.kind, lines[i].Kind)
		}
	}
}

func TestPreProcess_EmptyASMBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"ASM",
			"ENDASM",
			"NOP",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// Empty block produces no lines, just NOP
	if len(lines) != 1 {
		t.Fatalf("expected 1 line, got %d", len(lines))
	}

	if lines[0].Text != "NOP" {
		t.Errorf("expected 'NOP', got %q", lines[0].Text)
	}
}

func TestPreProcess_EmptyScriptBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"SCRIPT",
			"ENDSCRIPT",
			"NOP",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// Empty script emits an ENDSCRIPT boundary marker + NOP
	if len(lines) != 2 {
		t.Fatalf("expected 2 lines, got %d", len(lines))
	}

	if lines[0].Kind != Source || lines[0].Text != "" {
		t.Errorf("line 0: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[0].Kind, lines[0].Text)
	}

	if lines[1].Text != "NOP" {
		t.Errorf("expected 'NOP', got %q", lines[1].Text)
	}
}

func TestPreProcess_ScriptLibraryBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"SCRIPT LIBRARY",
			"def my_func():",
			"  print('nop')",
			"ENDSCRIPT",
			"NOP",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// Should have 2 script lines + 1 ENDSCRIPT boundary + 1 source line = 4 lines
	if len(lines) != 4 {
		t.Fatalf("expected 4 lines, got %d", len(lines))
	}

	// Script library lines should have ScriptLibrary kind
	if lines[0].Kind != ScriptLibrary {
		t.Errorf("expected Kind=ScriptLibrary, got %v", lines[0].Kind)
	}
	if lines[0].Text != "def my_func():" {
		t.Errorf("expected 'def my_func():', got %q", lines[0].Text)
	}

	if lines[1].Kind != ScriptLibrary {
		t.Errorf("expected Kind=ScriptLibrary, got %v", lines[1].Kind)
	}

	// Line 2 is the ENDSCRIPT boundary marker
	if lines[2].Kind != Source {
		t.Errorf("line 2: expected Kind=Source (ENDSCRIPT boundary), got %v", lines[2].Kind)
	}
	if lines[2].Text != "" {
		t.Errorf("line 2: expected empty text, got %q", lines[2].Text)
	}

	// Source line after ENDSCRIPT
	if lines[3].Kind != Source {
		t.Errorf("expected Kind=Source, got %v", lines[3].Kind)
	}
	if lines[3].Text != "NOP" {
		t.Errorf("expected 'NOP', got %q", lines[3].Text)
	}
}

func TestPreProcess_ScriptVsScriptLibrary(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"SCRIPT LIBRARY",
			"def foo(): pass",
			"ENDSCRIPT",
			"SCRIPT",
			"foo()",
			"ENDSCRIPT",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// Two blocks → 1 lib line + 1 lib ENDSCRIPT boundary + 1 script line + 1 script ENDSCRIPT boundary = 4 lines
	if len(lines) != 4 {
		t.Fatalf("expected 4 lines, got %d", len(lines))
	}

	// Line 0: SCRIPT LIBRARY content
	if lines[0].Kind != ScriptLibrary {
		t.Errorf("line 0: expected ScriptLibrary, got %v", lines[0].Kind)
	}

	// Line 1: ENDSCRIPT boundary for library
	if lines[1].Kind != Source || lines[1].Text != "" {
		t.Errorf("line 1: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[1].Kind, lines[1].Text)
	}

	// Line 2: regular SCRIPT content
	if lines[2].Kind != Script {
		t.Errorf("line 2: expected Script, got %v", lines[2].Kind)
	}

	// Line 3: ENDSCRIPT boundary for script
	if lines[3].Kind != Source || lines[3].Text != "" {
		t.Errorf("line 3: expected empty Source (ENDSCRIPT boundary), got Kind=%v Text=%q", lines[3].Kind, lines[3].Text)
	}
}

func TestPreProcess_ScriptMacroBlock(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"SCRIPT MACRO delay(cycles)",
			"  emit_delay(cycles)",
			"ENDSCRIPT",
			"NOP",
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	// Should have header + body line + ENDSCRIPT boundary + source line = 4 lines
	if len(lines) != 4 {
		t.Fatalf("expected 4 lines, got %d", len(lines))
	}

	// First line is the header (also ScriptMacroDef kind)
	if lines[0].Kind != ScriptMacroDef {
		t.Errorf("line 0: expected ScriptMacroDef, got %v", lines[0].Kind)
	}
	if lines[0].Text != "SCRIPT MACRO delay(cycles)" {
		t.Errorf("line 0: expected header, got %q", lines[0].Text)
	}

	// Second line is macro body
	if lines[1].Kind != ScriptMacroDef {
		t.Errorf("line 1: expected ScriptMacroDef, got %v", lines[1].Kind)
	}

	// Third line is ENDSCRIPT boundary marker
	if lines[2].Kind != Source {
		t.Errorf("line 2: expected Source (ENDSCRIPT boundary), got %v", lines[2].Kind)
	}
	if lines[2].Text != "" {
		t.Errorf("line 2: expected empty text, got %q", lines[2].Text)
	}

	// Fourth line is source
	if lines[3].Kind != Source {
		t.Errorf("line 3: expected Source, got %v", lines[3].Kind)
	}
	if lines[3].Text != "NOP" {
		t.Errorf("line 3: expected 'NOP', got %q", lines[3].Text)
	}
}

func TestPreProcess_DollarEscapeExpansion(t *testing.T) {
	files := map[string][]string{
		"test.c65": {
			"LDA #$0A",            // $0A -> newline (byte 10)
			"STA $D020",           // $$ -> literal $
			"JMP $5000",           // $$ -> literal $
			"#DEFINE ADDR $$C000", // $$ in define value
			"LDX ADDR",            // expands to $$C000 then to $C000
		},
	}
	reader := NewMockFileReader(files)
	lines, _, err := PreProcess("test.c65", reader)
	if err != nil {
		t.Fatalf("PreProcess failed: %v", err)
	}

	if len(lines) != 4 {
		t.Fatalf("expected 4 lines, got %d", len(lines))
	}

	// $0A should become newline character (byte 10)
	if lines[0].Text != "LDA #$0A" {
		t.Errorf("line 0: expected 'LDA #\\n', got %q", lines[0].Text)
	}

	// $$ should become single $
	if lines[1].Text != "STA $D020" {
		t.Errorf("line 1: expected 'STA $D020', got %q", lines[1].Text)
	}

	if lines[2].Text != "JMP $5000" {
		t.Errorf("line 2: expected 'JMP $5000', got %q", lines[2].Text)
	}

	// Define with $$ should expand to single $
	if lines[3].Text != "LDX $C000" {
		t.Errorf("line 3: expected 'LDX $C000', got %q", lines[3].Text)
	}
}
