package commands

import (
	"strings"
	"testing"

	"c65gm/internal/compiler"
	"c65gm/internal/preproc"
)

func TestPeekOldSyntax(t *testing.T) {
	tests := []struct {
		name      string
		line      string
		setupVars func(*compiler.SymbolTable)
		wantAsm   []string
	}{
		{
			name: "peek constant address to byte",
			line: "PEEK 1024 GIVING result",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tlda 1024",
				"\tsta result",
			},
		},
		{
			name: "peek ZP pointer to byte",
			line: "PEEK ptr GIVING result",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddAbsolute("ptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tldy #0",
				"\tlda (ptr),y",
				"\tsta result",
			},
		},
		{
			name: "peek ZP pointer with offset to byte",
			line: "PEEK ptr[5] GIVING result",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddAbsolute("ptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tldy #5",
				"\tlda (ptr),y",
				"\tsta result",
			},
		},
		{
			name: "peek with -> separator",
			line: "PEEK 2048 -> result",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tlda 2048",
				"\tsta result",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pragma := preproc.NewPragma()
			ctx := compiler.NewCompilerContext(pragma)
			tt.setupVars(ctx.SymbolTable)

			cmd := &PeekCommand{}
			line := preproc.Line{
				Text:           tt.line,
				Kind:           preproc.Source,
				PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
			}

			if err := cmd.Interpret(line, ctx); err != nil {
				t.Fatalf("Interpret() error = %v", err)
			}

			asm, err := cmd.Generate(ctx)
			if err != nil {
				t.Fatalf("Generate() error = %v", err)
			}

			if !equalAsm(asm, tt.wantAsm) {
				t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
					strings.Join(asm, "\n"),
					strings.Join(tt.wantAsm, "\n"))
			}
		})
	}
}

func TestPeekNewSyntax(t *testing.T) {
	tests := []struct {
		name      string
		line      string
		setupVars func(*compiler.SymbolTable)
		wantAsm   []string
	}{
		{
			name: "new syntax constant address to byte",
			line: "result = PEEK 1024",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tlda 1024",
				"\tsta result",
			},
		},
		{
			name: "new syntax ZP pointer to byte",
			line: "result = PEEK ptr",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddAbsolute("ptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tldy #0",
				"\tlda (ptr),y",
				"\tsta result",
			},
		},
		{
			name: "new syntax ZP pointer with offset",
			line: "result = PEEK ptr[10]",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddAbsolute("ptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tldy #10",
				"\tlda (ptr),y",
				"\tsta result",
			},
		},
		{
			name: "new syntax ZP pointer with variable offset",
			line: "result = PEEK ptr[idx]",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddAbsolute("ptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("idx", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tldy idx",
				"\tlda (ptr),y",
				"\tsta result",
			},
		},
		{
			name: "new syntax to word destination",
			line: "result = PEEK 2048",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tlda 2048",
				"\tsta result",
				"\tlda #0",
				"\tsta result+1",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pragma := preproc.NewPragma()
			ctx := compiler.NewCompilerContext(pragma)
			tt.setupVars(ctx.SymbolTable)

			cmd := &PeekCommand{}
			line := preproc.Line{
				Text:           tt.line,
				Kind:           preproc.Source,
				PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
			}

			if err := cmd.Interpret(line, ctx); err != nil {
				t.Fatalf("Interpret() error = %v", err)
			}

			asm, err := cmd.Generate(ctx)
			if err != nil {
				t.Fatalf("Generate() error = %v", err)
			}

			if !equalAsm(asm, tt.wantAsm) {
				t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
					strings.Join(asm, "\n"),
					strings.Join(tt.wantAsm, "\n"))
			}
		})
	}
}

func TestPeekWNewSyntax(t *testing.T) {
	tests := []struct {
		name      string
		line      string
		setupVars func(*compiler.SymbolTable)
		wantAsm   []string
	}{
		{
			name: "new syntax constant address",
			line: "result = PEEKW 1024",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tlda 1024",
				"\tsta result",
				"\tlda 1025",
				"\tsta result+1",
			},
		},
		{
			name: "new syntax ZP pointer",
			line: "result = PEEKW ptr",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddAbsolute("ptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tldy #0",
				"\tlda (ptr),y",
				"\tsta result",
				"\tiny",
				"\tlda (ptr),y",
				"\tsta result+1",
			},
		},
		{
			name: "new syntax ZP pointer with offset",
			line: "result = PEEKW ptr[8]",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddAbsolute("ptr", "", compiler.KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantAsm: []string{
				"\tldy #8",
				"\tlda (ptr),y",
				"\tsta result",
				"\tiny",
				"\tlda (ptr),y",
				"\tsta result+1",
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pragma := preproc.NewPragma()
			ctx := compiler.NewCompilerContext(pragma)
			tt.setupVars(ctx.SymbolTable)

			cmd := &PeekWCommand{}
			line := preproc.Line{
				Text:           tt.line,
				Kind:           preproc.Source,
				PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
			}

			if err := cmd.Interpret(line, ctx); err != nil {
				t.Fatalf("Interpret() error = %v", err)
			}

			asm, err := cmd.Generate(ctx)
			if err != nil {
				t.Fatalf("Generate() error = %v", err)
			}

			if !equalAsm(asm, tt.wantAsm) {
				t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s",
					strings.Join(asm, "\n"),
					strings.Join(tt.wantAsm, "\n"))
			}
		})
	}
}

func TestPeekErrors(t *testing.T) {
	tests := []struct {
		name      string
		line      string
		setupVars func(*compiler.SymbolTable)
		wantError string
	}{
		{
			name: "unknown destination",
			line: "PEEK 1024 GIVING unknown",
			setupVars: func(st *compiler.SymbolTable) {
			},
			wantError: "unknown destination variable",
		},
		{
			name: "constant destination",
			line: "PEEK 1024 GIVING CONST",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddConst("CONST", "", compiler.KindByte, 100, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantError: "cannot PEEK into constant",
		},
		{
			name: "new syntax unknown destination",
			line: "unknown = PEEK 1024",
			setupVars: func(st *compiler.SymbolTable) {
			},
			wantError: "unknown destination variable",
		},
		{
			name: "new syntax constant destination",
			line: "CONST = PEEK 1024",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddConst("CONST", "", compiler.KindByte, 100, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantError: "cannot PEEK into constant",
		},
		{
			name: "offset without ZP pointer old syntax",
			line: "PEEK addr[5] GIVING result",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantError: "offset",
		},
		{
			name: "offset without ZP pointer new syntax",
			line: "result = PEEK addr[5]",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantError: "offset",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pragma := preproc.NewPragma()
			ctx := compiler.NewCompilerContext(pragma)
			tt.setupVars(ctx.SymbolTable)

			cmd := &PeekCommand{}
			line := preproc.Line{
				Text:           tt.line,
				Kind:           preproc.Source,
				PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
			}

			err := cmd.Interpret(line, ctx)
			if err == nil {
				t.Fatal("Expected error but got none")
			}
			if !strings.Contains(err.Error(), tt.wantError) {
				t.Errorf("Error message mismatch\ngot: %v\nwant substring: %s", err, tt.wantError)
			}
		})
	}
}

func TestPeekWErrors(t *testing.T) {
	tests := []struct {
		name      string
		line      string
		setupVars func(*compiler.SymbolTable)
		wantError string
	}{
		{
			name: "byte destination",
			line: "PEEKW 1024 GIVING result",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantError: "must be word type",
		},
		{
			name: "new syntax byte destination",
			line: "result = PEEKW 1024",
			setupVars: func(st *compiler.SymbolTable) {
				st.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
			},
			wantError: "must be word type",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pragma := preproc.NewPragma()
			ctx := compiler.NewCompilerContext(pragma)
			tt.setupVars(ctx.SymbolTable)

			cmd := &PeekWCommand{}
			line := preproc.Line{
				Text:           tt.line,
				Kind:           preproc.Source,
				PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
			}

			err := cmd.Interpret(line, ctx)
			if err == nil {
				t.Fatal("Expected error but got none")
			}
			if !strings.Contains(err.Error(), tt.wantError) {
				t.Errorf("Error message mismatch\ngot: %v\nwant substring: %s", err, tt.wantError)
			}
		})
	}
}

func TestPeekImmutableCodePragma(t *testing.T) {
	// Test that _P_USE_IMMUTABLE_CODE pragma with value "1" causes error for variable address
	t.Run("Pragma enabled with 1", func(t *testing.T) {
		pragma := preproc.NewPragma()
		pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "1")
		ctx := compiler.NewCompilerContext(pragma)

		// Add a non-ZP word variable for address
		ctx.SymbolTable.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
		ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

		cmd := &PeekCommand{}
		line := preproc.Line{
			Text:           "PEEK addr GIVING result",
			Kind:           preproc.Source,
			PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
		}

		// First interpret the command
		err := cmd.Interpret(line, ctx)
		if err != nil {
			t.Fatalf("Interpret() error = %v", err)
		}
		
		// Then generate code - this is where the pragma check happens
		_, err = cmd.Generate(ctx)
		if err == nil {
			t.Error("Expected error when _P_USE_IMMUTABLE_CODE=1 and using variable address, got nil")
		} else if !strings.Contains(err.Error(), "USE_IMMUTABLE_CODE pragma set") {
			t.Errorf("Expected error about USE_IMMUTABLE_CODE pragma, got: %v", err)
		}
	})

	// Test that _P_USE_IMMUTABLE_CODE pragma with value "0" does NOT cause error
	t.Run("Pragma disabled with 0", func(t *testing.T) {
		pragma := preproc.NewPragma()
		pragma.AddPragma("_P_USE_IMMUTABLE_CODE", "0")
		ctx := compiler.NewCompilerContext(pragma)

		// Add a non-ZP word variable for address
		ctx.SymbolTable.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
		ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

		cmd := &PeekCommand{}
		line := preproc.Line{
			Text:           "PEEK addr GIVING result",
			Kind:           preproc.Source,
			PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
		}

		// First interpret the command
		err := cmd.Interpret(line, ctx)
		if err != nil {
			t.Fatalf("Interpret() error = %v", err)
		}
		
		// Then generate code - this is where the pragma check happens
		_, err = cmd.Generate(ctx)
		if err != nil {
			t.Errorf("Expected no error when _P_USE_IMMUTABLE_CODE=0, got: %v", err)
		}
	})

	// Test that _P_USE_IMMUTABLE_CODE pragma not set does NOT cause error
	t.Run("Pragma not set", func(t *testing.T) {
		pragma := preproc.NewPragma()
		// Don't add the pragma at all
		ctx := compiler.NewCompilerContext(pragma)

		// Add a non-ZP word variable for address
		ctx.SymbolTable.AddVar("addr", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
		ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

		cmd := &PeekCommand{}
		line := preproc.Line{
			Text:           "PEEK addr GIVING result",
			Kind:           preproc.Source,
			PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
		}

		// First interpret the command
		err := cmd.Interpret(line, ctx)
		if err != nil {
			t.Fatalf("Interpret() error = %v", err)
		}
		
		// Then generate code - this is where the pragma check happens
		_, err = cmd.Generate(ctx)
		if err != nil {
			t.Errorf("Expected no error when _P_USE_IMMUTABLE_CODE not set, got: %v", err)
		}
	})
}
