package compiler

import (
	"strings"
	"testing"
)

func TestSymbolFlags(t *testing.T) {
	tests := []struct {
		name    string
		flags   SymbolFlags
		isByte  bool
		isWord  bool
		isConst bool
		isAbs   bool
		isZP    bool
		isZPPtr bool
	}{
		{
			name:    "byte variable",
			flags:   FlagByte,
			isByte:  true,
			isWord:  false,
			isConst: false,
		},
		{
			name:    "word variable",
			flags:   FlagWord,
			isByte:  false,
			isWord:  true,
			isConst: false,
		},
		{
			name:    "byte constant",
			flags:   FlagByte | FlagConst,
			isByte:  true,
			isConst: true,
		},
		{
			name:    "absolute zero-page",
			flags:   FlagWord | FlagAbsolute | FlagZeroPage,
			isWord:  true,
			isAbs:   true,
			isZP:    true,
			isZPPtr: true,
		},
		{
			name:    "absolute non-zero-page",
			flags:   FlagWord | FlagAbsolute,
			isWord:  true,
			isAbs:   true,
			isZP:    false,
			isZPPtr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &Symbol{Flags: tt.flags}

			if s.IsByte() != tt.isByte {
				t.Errorf("IsByte() = %v, want %v", s.IsByte(), tt.isByte)
			}
			if s.IsWord() != tt.isWord {
				t.Errorf("IsWord() = %v, want %v", s.IsWord(), tt.isWord)
			}
			if s.IsConst() != tt.isConst {
				t.Errorf("IsConst() = %v, want %v", s.IsConst(), tt.isConst)
			}
			if s.IsAbsolute() != tt.isAbs {
				t.Errorf("IsAbsolute() = %v, want %v", s.IsAbsolute(), tt.isAbs)
			}
			if s.IsZeroPage() != tt.isZP {
				t.Errorf("IsZeroPage() = %v, want %v", s.IsZeroPage(), tt.isZP)
			}
			if s.IsZeroPagePointer() != tt.isZPPtr {
				t.Errorf("IsZeroPagePointer() = %v, want %v", s.IsZeroPagePointer(), tt.isZPPtr)
			}
		})
	}
}

func TestSymbolFullName(t *testing.T) {
	tests := []struct {
		name     string
		symName  string
		scope    string
		expected string
	}{
		{"global", "counter", "", "counter"},
		{"local", "temp", "main", "main_temp"},
		{"nested", "var", "outer_inner", "outer_inner_var"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := &Symbol{Name: tt.symName, Scope: tt.scope}
			if got := s.FullName(); got != tt.expected {
				t.Errorf("FullName() = %q, want %q", got, tt.expected)
			}
		})
	}
}

func TestAddVar(t *testing.T) {
	st := NewSymbolTable()

	// Add byte var
	err := st.AddVar("counter", "", KindByte, 0)
	if err != nil {
		t.Fatalf("AddVar() error = %v", err)
	}

	sym := st.Get("counter")
	if sym == nil {
		t.Fatal("symbol not found")
	}
	if !sym.IsByte() {
		t.Error("expected byte variable")
	}
	if sym.IsConst() {
		t.Error("should not be const")
	}

	// Add word var
	err = st.AddVar("ptr", "", KindWord, 0x1234)
	if err != nil {
		t.Fatalf("AddVar() error = %v", err)
	}

	sym = st.Get("ptr")
	if !sym.IsWord() {
		t.Error("expected word variable")
	}
	if sym.Value != 0x1234 {
		t.Errorf("Value = %d, want %d", sym.Value, 0x1234)
	}

	// Test byte value range check
	err = st.AddVar("bad", "", KindByte, 256)
	if err == nil {
		t.Error("expected error for byte value > 255")
	}
}

func TestAddConst(t *testing.T) {
	st := NewSymbolTable()

	err := st.AddConst("MAX", "", KindByte, 255)
	if err != nil {
		t.Fatalf("AddConst() error = %v", err)
	}

	sym := st.Get("MAX")
	if sym == nil {
		t.Fatal("symbol not found")
	}
	if !sym.IsConst() {
		t.Error("expected constant")
	}
	if !sym.IsByte() {
		t.Error("expected byte constant")
	}
	if sym.Value != 255 {
		t.Errorf("Value = %d, want 255", sym.Value)
	}

	// Test byte range check
	err = st.AddConst("BAD", "", KindByte, 300)
	if err == nil {
		t.Error("expected error for byte const > 255")
	}
}

func TestAddAbsolute(t *testing.T) {
	st := NewSymbolTable()

	// Zero-page byte
	err := st.AddAbsolute("ZP_VAR", "", KindByte, 0x80)
	if err != nil {
		t.Fatalf("AddAbsolute() error = %v", err)
	}

	sym := st.Get("ZP_VAR")
	if !sym.IsAbsolute() {
		t.Error("expected absolute")
	}
	if !sym.IsZeroPage() {
		t.Error("expected zero-page flag for addr < $100")
	}
	if sym.AbsAddr != 0x80 {
		t.Errorf("AbsAddr = $%04X, want $0080", sym.AbsAddr)
	}

	// Zero-page word pointer
	err = st.AddAbsolute("ZP_PTR", "", KindWord, 0xFE)
	if err != nil {
		t.Fatalf("AddAbsolute() error = %v", err)
	}

	sym = st.Get("ZP_PTR")
	if !sym.IsZeroPagePointer() {
		t.Error("expected zero-page pointer (word addr < $FF)")
	}

	// Non-zero-page
	err = st.AddAbsolute("VIC", "", KindWord, 0xD000)
	if err != nil {
		t.Fatalf("AddAbsolute() error = %v", err)
	}

	sym = st.Get("VIC")
	if !sym.IsAbsolute() {
		t.Error("expected absolute")
	}
	if sym.IsZeroPage() {
		t.Error("should not have zero-page flag")
	}
	if sym.AbsAddr != 0xD000 {
		t.Errorf("AbsAddr = $%04X, want $D000", sym.AbsAddr)
	}
}

func TestAddLabel(t *testing.T) {
	st := NewSymbolTable()

	err := st.AddLabel("handler", "", "irq_vector")
	if err != nil {
		t.Fatalf("AddLabel() error = %v", err)
	}

	sym := st.Get("handler")
	if sym == nil {
		t.Fatal("symbol not found")
	}
	if !sym.IsWord() {
		t.Error("label ref should be word")
	}
	if !sym.Has(FlagLabelRef) {
		t.Error("expected FlagLabelRef")
	}
	if sym.LabelRef != "irq_vector" {
		t.Errorf("LabelRef = %q, want %q", sym.LabelRef, "irq_vector")
	}
}

func TestRedeclaration(t *testing.T) {
	st := NewSymbolTable()

	err := st.AddVar("test", "", KindByte, 0)
	if err != nil {
		t.Fatalf("first AddVar() error = %v", err)
	}

	// Attempt redeclaration
	err = st.AddVar("test", "", KindByte, 0)
	if err == nil {
		t.Error("expected error on redeclaration")
	}

	// Different scope should be OK
	err = st.AddVar("test", "main", KindByte, 0)
	if err != nil {
		t.Errorf("AddVar() with different scope error = %v", err)
	}
}

func TestLookup(t *testing.T) {
	st := NewSymbolTable()

	// Global variable
	st.AddVar("global", "", KindByte, 0)

	// Local in main
	st.AddVar("local", "main", KindByte, 0)

	// Local in nested function
	st.AddVar("inner", "main_helper", KindByte, 0)

	tests := []struct {
		name          string
		searchName    string
		currentScopes []string
		expectFound   bool
		expectFull    string
	}{
		{
			name:          "find global from empty scope",
			searchName:    "global",
			currentScopes: []string{},
			expectFound:   true,
			expectFull:    "global",
		},
		{
			name:          "find global from main scope",
			searchName:    "global",
			currentScopes: []string{"main"},
			expectFound:   true,
			expectFull:    "global",
		},
		{
			name:          "find local from same scope",
			searchName:    "local",
			currentScopes: []string{"main"},
			expectFound:   true,
			expectFull:    "main_local",
		},
		{
			name:          "shadow global with local",
			searchName:    "local",
			currentScopes: []string{"main"},
			expectFound:   true,
			expectFull:    "main_local",
		},
		{
			name:          "find inner from nested scope",
			searchName:    "inner",
			currentScopes: []string{"main", "main_helper"},
			expectFound:   true,
			expectFull:    "main_helper_inner",
		},
		{
			name:          "not found",
			searchName:    "notexist",
			currentScopes: []string{},
			expectFound:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			sym := st.Lookup(tt.searchName, tt.currentScopes)

			if tt.expectFound {
				if sym == nil {
					t.Fatal("symbol not found")
				}
				if got := sym.FullName(); got != tt.expectFull {
					t.Errorf("FullName() = %q, want %q", got, tt.expectFull)
				}
			} else {
				if sym != nil {
					t.Errorf("expected not found, got %v", sym)
				}
			}
		})
	}
}

func TestExpandName(t *testing.T) {
	st := NewSymbolTable()

	st.AddVar("global", "", KindByte, 0)
	st.AddVar("local", "main", KindByte, 0)

	tests := []struct {
		name          string
		searchName    string
		currentScopes []string
		expected      string
	}{
		{"expand local", "local", []string{"main"}, "main_local"},
		{"expand global", "global", []string{"main"}, "global"},
		{"no expansion needed", "notfound", []string{}, "notfound"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := st.ExpandName(tt.searchName, tt.currentScopes)
			if got != tt.expected {
				t.Errorf("ExpandName() = %q, want %q", got, tt.expected)
			}
		})
	}
}

func TestInsertionOrder(t *testing.T) {
	st := NewSymbolTable()

	names := []string{"first", "second", "third"}
	for _, name := range names {
		st.AddVar(name, "", KindByte, 0)
	}

	symbols := st.Symbols()
	if len(symbols) != len(names) {
		t.Fatalf("Count = %d, want %d", len(symbols), len(names))
	}

	for i, name := range names {
		if symbols[i].Name != name {
			t.Errorf("symbols[%d].Name = %q, want %q", i, symbols[i].Name, name)
		}
	}
}

func TestCount(t *testing.T) {
	st := NewSymbolTable()

	if st.Count() != 0 {
		t.Errorf("initial Count() = %d, want 0", st.Count())
	}

	st.AddVar("a", "", KindByte, 0)
	st.AddVar("b", "", KindByte, 0)

	if st.Count() != 2 {
		t.Errorf("Count() = %d, want 2", st.Count())
	}
}

func TestSymbolString(t *testing.T) {
	tests := []struct {
		name     string
		setup    func(*SymbolTable) *Symbol
		contains []string
	}{
		{
			name: "byte variable",
			setup: func(st *SymbolTable) *Symbol {
				st.AddVar("test", "", KindByte, 0)
				return st.Get("test")
			},
			contains: []string{"Name=test", "BYTE"},
		},
		{
			name: "word constant",
			setup: func(st *SymbolTable) *Symbol {
				st.AddConst("MAX", "", KindWord, 65535)
				return st.Get("MAX")
			},
			contains: []string{"Name=MAX", "WORD", "CONST=65535"},
		},
		{
			name: "zero-page pointer",
			setup: func(st *SymbolTable) *Symbol {
				st.AddAbsolute("ptr", "", KindWord, 0x80)
				return st.Get("ptr")
			},
			contains: []string{"Name=ptr", "WORD", "@$0080", "ZP"},
		},
		{
			name: "label reference",
			setup: func(st *SymbolTable) *Symbol {
				st.AddLabel("handler", "", "irq")
				return st.Get("handler")
			},
			contains: []string{"Name=handler", "->irq"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			st := NewSymbolTable()
			sym := tt.setup(st)

			str := sym.String()
			for _, want := range tt.contains {
				if !containsSubstring(str, want) {
					t.Errorf("String() = %q, missing %q", str, want)
				}
			}
		})
	}
}

func containsSubstring(s, substr string) bool {
	return len(s) >= len(substr) &&
		(s == substr || len(s) > len(substr) && containsAt(s, substr))
}

func containsAt(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}

// Code generation tests

func TestGenerateConstants(t *testing.T) {
	st := NewSymbolTable()

	// Add some constants
	st.AddConst("MAX", "", KindByte, 255)
	st.AddConst("SIZE", "", KindWord, 0x1234)
	st.AddVar("notconst", "", KindByte, 0) // should be skipped

	lines := GenerateConstants(st)

	if len(lines) == 0 {
		t.Fatal("expected output lines")
	}

	// Check header
	if lines[0] != ";Constant values (from c65gm)" {
		t.Errorf("expected header comment, got %q", lines[0])
	}

	// Find constant definitions
	output := strings.Join(lines, "\n")

	if !strings.Contains(output, "MAX = $ff") {
		t.Error("expected byte constant with lowercase hex")
	}
	if !strings.Contains(output, "; 255") {
		t.Error("expected decimal comment for byte constant")
	}
	if !strings.Contains(output, "SIZE = $1234") {
		t.Error("expected word constant")
	}
	if strings.Contains(output, "notconst") {
		t.Error("regular variable should not appear in constants")
	}
}

func TestGenerateAbsolutes(t *testing.T) {
	st := NewSymbolTable()

	// Zero-page
	st.AddAbsolute("ZP_VAR", "", KindByte, 0x80)
	st.AddAbsolute("ZP_PTR", "", KindWord, 0xFE)

	// Non-zero-page
	st.AddAbsolute("VIC", "", KindWord, 0xD000)

	// Regular var (should be skipped)
	st.AddVar("regular", "", KindByte, 0)

	lines := GenerateAbsolutes(st)

	if len(lines) == 0 {
		t.Fatal("expected output lines")
	}

	// Check header
	if lines[0] != ";Absolute variable definitions (from c65gm)" {
		t.Errorf("expected header comment, got %q", lines[0])
	}

	output := strings.Join(lines, "\n")

	// Zero-page should use 2 hex digits
	if !strings.Contains(output, "ZP_VAR = $80") {
		t.Error("expected zero-page with 2 hex digits")
	}
	if !strings.Contains(output, "ZP_PTR = $fe") {
		t.Error("expected zero-page pointer with 2 hex digits")
	}

	// Non-zero-page should use 4 hex digits
	if !strings.Contains(output, "VIC = $d000") {
		t.Error("expected non-zero-page with 4 hex digits")
	}

	if strings.Contains(output, "regular") {
		t.Error("regular variable should not appear in absolutes")
	}
}

func TestGenerateVariables(t *testing.T) {
	st := NewSymbolTable()

	// Byte variable
	st.AddVar("counter", "", KindByte, 42)

	// Word variable
	st.AddVar("ptr", "", KindWord, 0x1234)

	// Label reference
	st.AddLabel("handler", "", "irq_routine")

	// Const (should be skipped)
	st.AddConst("SKIP", "", KindByte, 99)

	// Absolute (should be skipped)
	st.AddAbsolute("SKIP2", "", KindByte, 0x80)

	lines := GenerateVariables(st)

	if len(lines) == 0 {
		t.Fatal("expected output lines")
	}

	// Check header
	if lines[0] != ";Variables (from c65gm)" {
		t.Errorf("expected header comment, got %q", lines[0])
	}

	output := strings.Join(lines, "\n")

	// Byte variable with decimal comment
	if !strings.Contains(output, "counter\t!8 $2a") {
		t.Error("expected byte variable declaration")
	}
	if !strings.Contains(output, "; 42") {
		t.Error("expected decimal comment for byte")
	}

	// Word variable (low, high)
	if !strings.Contains(output, "ptr\t!8 $34, $12") {
		t.Error("expected word variable as two bytes (lo, hi)")
	}

	// Label reference
	if !strings.Contains(output, "handler\t!8 <irq_routine, >irq_routine") {
		t.Error("expected label reference with < and >")
	}

	// Should not contain constants or absolutes
	if strings.Contains(output, "SKIP") {
		t.Error("constants should not appear in variables")
	}
	if strings.Contains(output, "SKIP2") {
		t.Error("absolutes should not appear in variables")
	}
}

func TestGenerateEmpty(t *testing.T) {
	st := NewSymbolTable()

	// Empty table
	if lines := GenerateConstants(st); lines != nil {
		t.Error("expected nil for empty constants")
	}
	if lines := GenerateAbsolutes(st); lines != nil {
		t.Error("expected nil for empty absolutes")
	}
	if lines := GenerateVariables(st); lines != nil {
		t.Error("expected nil for empty variables")
	}

	// Only variables (no constants/absolutes)
	st.AddVar("test", "", KindByte, 0)

	if lines := GenerateConstants(st); lines != nil {
		t.Error("expected nil when no constants exist")
	}
	if lines := GenerateAbsolutes(st); lines != nil {
		t.Error("expected nil when no absolutes exist")
	}
}

func TestGenerateScopedVariables(t *testing.T) {
	st := NewSymbolTable()

	st.AddVar("global", "", KindByte, 0)
	st.AddVar("local", "main", KindByte, 0)
	st.AddVar("nested", "main_helper", KindByte, 0)

	lines := GenerateVariables(st)
	output := strings.Join(lines, "\n")

	// Check full names are used
	if !strings.Contains(output, "global\t!8") {
		t.Error("expected global variable")
	}
	if !strings.Contains(output, "main_local\t!8") {
		t.Error("expected scoped variable with full name")
	}
	if !strings.Contains(output, "main_helper_nested\t!8") {
		t.Error("expected nested scoped variable with full name")
	}
}

func TestGenerateHexLowercase(t *testing.T) {
	st := NewSymbolTable()

	st.AddConst("TEST", "", KindByte, 0xAB)
	st.AddAbsolute("ADDR", "", KindWord, 0xDEAD)
	st.AddVar("VAR", "", KindWord, 0xBEEF)

	constLines := GenerateConstants(st)
	absLines := GenerateAbsolutes(st)
	varLines := GenerateVariables(st)

	output := strings.Join(append(append(constLines, absLines...), varLines...), "\n")

	// Check all hex is lowercase
	if strings.Contains(output, "$AB") || strings.Contains(output, "$DEAD") || strings.Contains(output, "$BEEF") {
		t.Error("hex digits should be lowercase")
	}

	if !strings.Contains(output, "$ab") {
		t.Error("expected lowercase hex in constant")
	}
	if !strings.Contains(output, "$dead") {
		t.Error("expected lowercase hex in absolute")
	}
	if !strings.Contains(output, "$be") && !strings.Contains(output, "$ef") {
		t.Error("expected lowercase hex in variable")
	}
}
