package engine import ( "math" "testing" ) func TestHSLToRGB(t *testing.T) { tests := []struct { name string h, s, l float64 r, g, b uint8 }{ { name: "pure red", h: 0.0, s: 1.0, l: 0.5, r: 255, g: 0, b: 0, }, { name: "pure green", h: 1.0/3.0, s: 1.0, l: 0.5, r: 0, g: 255, b: 0, }, { name: "pure blue", h: 2.0/3.0, s: 1.0, l: 0.5, r: 0, g: 0, b: 255, }, { name: "white", h: 0.0, s: 0.0, l: 1.0, r: 255, g: 255, b: 255, }, { name: "black", h: 0.0, s: 0.0, l: 0.0, r: 0, g: 0, b: 0, }, { name: "gray", h: 0.0, s: 0.0, l: 0.5, r: 127, g: 127, b: 127, }, { name: "dark red", h: 0.0, s: 1.0, l: 0.25, r: 127, g: 0, b: 0, }, { name: "light blue", h: 2.0/3.0, s: 1.0, l: 0.75, r: 127, g: 127, b: 255, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r, g, b := HSLToRGB(tt.h, tt.s, tt.l) // Allow small tolerance due to floating point arithmetic tolerance := uint8(2) if abs(int(r), int(tt.r)) > int(tolerance) || abs(int(g), int(tt.g)) > int(tolerance) || abs(int(b), int(tt.b)) > int(tolerance) { t.Errorf("HSLToRGB(%f, %f, %f) = (%d, %d, %d), want (%d, %d, %d)", tt.h, tt.s, tt.l, r, g, b, tt.r, tt.g, tt.b) } }) } } func TestCorrectedHSLToRGB(t *testing.T) { // Test that corrected HSL produces valid RGB values testCases := []struct { name string h, s, l float64 }{ {"Red", 0.0, 1.0, 0.5}, {"Green", 0.33, 1.0, 0.5}, {"Blue", 0.67, 1.0, 0.5}, {"Gray", 0.0, 0.0, 0.5}, {"DarkCyan", 0.5, 0.7, 0.3}, {"LightMagenta", 0.8, 0.8, 0.8}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { r, g, b := CorrectedHSLToRGB(tc.h, tc.s, tc.l) // Verify RGB values are in valid range if r > 255 || g > 255 || b > 255 { t.Errorf("CorrectedHSLToRGB(%f, %f, %f) = (%d, %d, %d), RGB values should be <= 255", tc.h, tc.s, tc.l, r, g, b) } }) } } func TestRGBToHex(t *testing.T) { tests := []struct { name string r, g, b uint8 expected string }{ {"black", 0, 0, 0, "#000000"}, {"white", 255, 255, 255, "#ffffff"}, {"red", 255, 0, 0, "#ff0000"}, {"green", 0, 255, 0, "#00ff00"}, {"blue", 0, 0, 255, "#0000ff"}, {"gray", 128, 128, 128, "#808080"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := RGBToHex(tt.r, tt.g, tt.b) if result != tt.expected { t.Errorf("RGBToHex(%d, %d, %d) = %s, want %s", tt.r, tt.g, tt.b, result, tt.expected) } }) } } func TestHexToByte(t *testing.T) { tests := []struct { name string input string expected uint8 expectError bool }{ { name: "valid hex 00", input: "00", expected: 0, }, { name: "valid hex ff", input: "ff", expected: 255, }, { name: "valid hex a5", input: "a5", expected: 165, }, { name: "valid hex A5 uppercase", input: "A5", expected: 165, }, { name: "invalid length - too short", input: "f", expectError: true, }, { name: "invalid length - too long", input: "fff", expectError: true, }, { name: "invalid character x", input: "fx", expectError: true, }, { name: "invalid character z", input: "zz", expectError: true, }, { name: "empty string", input: "", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := hexToByte(tt.input) if tt.expectError { if err == nil { t.Errorf("hexToByte(%s) expected error, got nil", tt.input) } return } if err != nil { t.Errorf("hexToByte(%s) unexpected error: %v", tt.input, err) return } if result != tt.expected { t.Errorf("hexToByte(%s) = %d, want %d", tt.input, result, tt.expected) } }) } } func TestParseHexColor(t *testing.T) { tests := []struct { name string input string expectError bool r, g, b, a uint8 }{ { name: "3-char hex", input: "#f0a", r: 255, g: 0, b: 170, a: 255, }, { name: "6-char hex", input: "#ff00aa", r: 255, g: 0, b: 170, a: 255, }, { name: "8-char hex with alpha", input: "#ff00aa80", r: 255, g: 0, b: 170, a: 128, }, { name: "black", input: "#000", r: 0, g: 0, b: 0, a: 255, }, { name: "white", input: "#fff", r: 255, g: 255, b: 255, a: 255, }, { name: "invalid format - no hash", input: "ff0000", expectError: true, }, { name: "invalid format - too short", input: "#f", expectError: true, }, { name: "invalid format - too long", input: "#ff00aa12345", expectError: true, }, { name: "invalid hex character in 3-char", input: "#fxz", expectError: true, }, { name: "invalid hex character in 6-char", input: "#ff00xz", expectError: true, }, { name: "invalid hex character in 8-char", input: "#ff00aaxz", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r, g, b, a, err := ParseHexColor(tt.input) if tt.expectError { if err == nil { t.Errorf("ParseHexColor(%s) expected error, got nil", tt.input) } return } if err != nil { t.Errorf("ParseHexColor(%s) unexpected error: %v", tt.input, err) return } if r != tt.r || g != tt.g || b != tt.b || a != tt.a { t.Errorf("ParseHexColor(%s) = (%d, %d, %d, %d), want (%d, %d, %d, %d)", tt.input, r, g, b, a, tt.r, tt.g, tt.b, tt.a) } }) } } func TestClamp(t *testing.T) { tests := []struct { value, min, max, expected float64 }{ {0.5, 0.0, 1.0, 0.5}, // within range {-0.5, 0.0, 1.0, 0.0}, // below min {1.5, 0.0, 1.0, 1.0}, // above max {0.0, 0.0, 1.0, 0.0}, // at min {1.0, 0.0, 1.0, 1.0}, // at max } for _, tt := range tests { result := clamp(tt.value, tt.min, tt.max) if result != tt.expected { t.Errorf("clamp(%f, %f, %f) = %f, want %f", tt.value, tt.min, tt.max, result, tt.expected) } } } func TestNewColorHSL(t *testing.T) { color := NewColorHSL(0.0, 1.0, 0.5) // Pure red if color.H != 0.0 || color.S != 1.0 || color.L != 0.5 { t.Errorf("NewColorHSL(0.0, 1.0, 0.5) HSL = (%f, %f, %f), want (0.0, 1.0, 0.5)", color.H, color.S, color.L) } if color.R != 255 || color.G != 0 || color.B != 0 { t.Errorf("NewColorHSL(0.0, 1.0, 0.5) RGB = (%d, %d, %d), want (255, 0, 0)", color.R, color.G, color.B) } if color.A != 255 { t.Errorf("NewColorHSL(0.0, 1.0, 0.5) A = %d, want 255", color.A) } } func TestNewColorRGB(t *testing.T) { color := NewColorRGB(255, 0, 0) // Pure red if color.R != 255 || color.G != 0 || color.B != 0 { t.Errorf("NewColorRGB(255, 0, 0) RGB = (%d, %d, %d), want (255, 0, 0)", color.R, color.G, color.B) } // HSL values should be approximately (0, 1, 0.5) for pure red tolerance := 0.01 if math.Abs(color.H-0.0) > tolerance || math.Abs(color.S-1.0) > tolerance || math.Abs(color.L-0.5) > tolerance { t.Errorf("NewColorRGB(255, 0, 0) HSL = (%f, %f, %f), want approximately (0.0, 1.0, 0.5)", color.H, color.S, color.L) } } func TestColorString(t *testing.T) { tests := []struct { name string color Color expected string }{ { name: "red without alpha", color: NewColorRGB(255, 0, 0), expected: "#ff0000", }, { name: "blue with alpha", color: NewColorRGBA(0, 0, 255, 128), expected: "#0000ff80", }, { name: "black", color: NewColorRGB(0, 0, 0), expected: "#000000", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.color.String() if result != tt.expected { t.Errorf("Color.String() = %s, want %s", result, tt.expected) } }) } } func TestColorEquals(t *testing.T) { color1 := NewColorRGB(255, 0, 0) color2 := NewColorRGB(255, 0, 0) color3 := NewColorRGB(0, 255, 0) color4 := NewColorRGBA(255, 0, 0, 128) if !color1.Equals(color2) { t.Error("Expected equal colors to be equal") } if color1.Equals(color3) { t.Error("Expected different colors to not be equal") } if color1.Equals(color4) { t.Error("Expected colors with different alpha to not be equal") } } func TestColorWithAlpha(t *testing.T) { color := NewColorRGB(255, 0, 0) newColor := color.WithAlpha(128) if newColor.A != 128 { t.Errorf("WithAlpha(128) A = %d, want 128", newColor.A) } // RGB and HSL should remain the same if newColor.R != color.R || newColor.G != color.G || newColor.B != color.B { t.Error("WithAlpha should not change RGB values") } if newColor.H != color.H || newColor.S != color.S || newColor.L != color.L { t.Error("WithAlpha should not change HSL values") } } func TestColorIsGrayscale(t *testing.T) { grayColor := NewColorRGB(128, 128, 128) redColor := NewColorRGB(255, 0, 0) if !grayColor.IsGrayscale() { t.Error("Expected gray color to be identified as grayscale") } if redColor.IsGrayscale() { t.Error("Expected red color to not be identified as grayscale") } } func TestColorDarkenLighten(t *testing.T) { color := NewColorHSL(0.0, 1.0, 0.5) // Pure red darker := color.Darken(0.2) if darker.L >= color.L { t.Error("Darken should reduce lightness") } lighter := color.Lighten(0.2) if lighter.L <= color.L { t.Error("Lighten should increase lightness") } // Test clamping veryDark := color.Darken(1.0) if veryDark.L != 0.0 { t.Errorf("Darken with large amount should clamp to 0, got %f", veryDark.L) } veryLight := color.Lighten(1.0) if veryLight.L != 1.0 { t.Errorf("Lighten with large amount should clamp to 1, got %f", veryLight.L) } } func TestRGBToHSL(t *testing.T) { tests := []struct { name string r, g, b uint8 h, s, l float64 }{ { name: "red", r: 255, g: 0, b: 0, h: 0.0, s: 1.0, l: 0.5, }, { name: "green", r: 0, g: 255, b: 0, h: 1.0/3.0, s: 1.0, l: 0.5, }, { name: "blue", r: 0, g: 0, b: 255, h: 2.0/3.0, s: 1.0, l: 0.5, }, { name: "white", r: 255, g: 255, b: 255, h: 0.0, s: 0.0, l: 1.0, }, { name: "black", r: 0, g: 0, b: 0, h: 0.0, s: 0.0, l: 0.0, }, { name: "gray", r: 128, g: 128, b: 128, h: 0.0, s: 0.0, l: 0.502, // approximately 0.5 }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { h, s, l := RGBToHSL(tt.r, tt.g, tt.b) tolerance := 0.01 if math.Abs(h-tt.h) > tolerance || math.Abs(s-tt.s) > tolerance || math.Abs(l-tt.l) > tolerance { t.Errorf("RGBToHSL(%d, %d, %d) = (%f, %f, %f), want approximately (%f, %f, %f)", tt.r, tt.g, tt.b, h, s, l, tt.h, tt.s, tt.l) } }) } } func TestGenerateColor(t *testing.T) { config := DefaultColorConfig() // Test color generation with mid-range lightness color := GenerateColor(0.0, config, 0.5) // Red hue, mid lightness // Should be approximately red with default saturation (0.5) and mid lightness (0.6) expectedLightness := config.ColorLightness.GetLightness(0.5) // Should be 0.6 tolerance := 0.01 if math.Abs(color.H-0.0) > tolerance { t.Errorf("GenerateColor hue = %f, want approximately 0.0", color.H) } if math.Abs(color.S-config.ColorSaturation) > tolerance { t.Errorf("GenerateColor saturation = %f, want %f", color.S, config.ColorSaturation) } if math.Abs(color.L-expectedLightness) > tolerance { t.Errorf("GenerateColor lightness = %f, want approximately %f", color.L, expectedLightness) } } func TestGenerateGrayscale(t *testing.T) { config := DefaultColorConfig() // Test grayscale generation color := GenerateGrayscale(config, 0.5) // Should be grayscale (saturation 0) with mid lightness expectedLightness := config.GrayscaleLightness.GetLightness(0.5) // Should be 0.6 tolerance := 0.01 if math.Abs(color.S-config.GrayscaleSaturation) > tolerance { t.Errorf("GenerateGrayscale saturation = %f, want %f", color.S, config.GrayscaleSaturation) } if math.Abs(color.L-expectedLightness) > tolerance { t.Errorf("GenerateGrayscale lightness = %f, want approximately %f", color.L, expectedLightness) } if !color.IsGrayscale() { t.Error("GenerateGrayscale should produce a grayscale color") } } func TestGenerateColorTheme(t *testing.T) { config := DefaultColorConfig() hue := 0.25 // Green-ish hue theme := GenerateColorTheme(hue, config) // Should have exactly 5 colors if len(theme) != 5 { t.Errorf("GenerateColorTheme returned %d colors, want 5", len(theme)) } // Test color indices according to JavaScript implementation: // 0: Dark gray, 1: Mid color, 2: Light gray, 3: Light color, 4: Dark color // Index 0: Dark gray (grayscale with lightness 0) darkGray := theme[0] if !darkGray.IsGrayscale() { t.Error("Theme color 0 should be grayscale (dark gray)") } expectedLightness := config.GrayscaleLightness.GetLightness(0) if math.Abs(darkGray.L-expectedLightness) > 0.01 { t.Errorf("Dark gray lightness = %f, want %f", darkGray.L, expectedLightness) } // Index 1: Mid color (normal color with lightness 0.5) midColor := theme[1] if midColor.IsGrayscale() { t.Error("Theme color 1 should not be grayscale (mid color)") } expectedLightness = config.ColorLightness.GetLightness(0.5) if math.Abs(midColor.L-expectedLightness) > 0.01 { t.Errorf("Mid color lightness = %f, want %f", midColor.L, expectedLightness) } // Index 2: Light gray (grayscale with lightness 1) lightGray := theme[2] if !lightGray.IsGrayscale() { t.Error("Theme color 2 should be grayscale (light gray)") } expectedLightness = config.GrayscaleLightness.GetLightness(1) if math.Abs(lightGray.L-expectedLightness) > 0.01 { t.Errorf("Light gray lightness = %f, want %f", lightGray.L, expectedLightness) } // Index 3: Light color (normal color with lightness 1) lightColor := theme[3] if lightColor.IsGrayscale() { t.Error("Theme color 3 should not be grayscale (light color)") } expectedLightness = config.ColorLightness.GetLightness(1) if math.Abs(lightColor.L-expectedLightness) > 0.01 { t.Errorf("Light color lightness = %f, want %f", lightColor.L, expectedLightness) } // Index 4: Dark color (normal color with lightness 0) darkColor := theme[4] if darkColor.IsGrayscale() { t.Error("Theme color 4 should not be grayscale (dark color)") } expectedLightness = config.ColorLightness.GetLightness(0) if math.Abs(darkColor.L-expectedLightness) > 0.01 { t.Errorf("Dark color lightness = %f, want %f", darkColor.L, expectedLightness) } // All colors should have the same hue (or close to it for grayscale) for i, color := range theme { if !color.IsGrayscale() { // Only check hue for non-grayscale colors if math.Abs(color.H-hue) > 0.01 { t.Errorf("Theme color %d hue = %f, want approximately %f", i, color.H, hue) } } } } func TestGenerateColorThemeWithHueRestriction(t *testing.T) { // Test with hue restriction config := NewColorConfigBuilder(). WithHues(180). // Only allow cyan (180 degrees = 0.5 turns) Build() theme := GenerateColorTheme(0.25, config) // Request green, should get cyan for i, color := range theme { if !color.IsGrayscale() { // Only check hue for non-grayscale colors if math.Abs(color.H-0.5) > 0.01 { t.Errorf("Theme color %d hue = %f, want approximately 0.5 (restricted)", i, color.H) } } } } func TestGenerateColorWithConfiguration(t *testing.T) { // Test with custom configuration config := NewColorConfigBuilder(). WithColorSaturation(0.8). WithColorLightness(0.2, 0.6). Build() color := GenerateColor(0.33, config, 1.0) // Green hue, max lightness tolerance := 0.01 if math.Abs(color.S-0.8) > tolerance { t.Errorf("Custom config saturation = %f, want 0.8", color.S) } expectedLightness := config.ColorLightness.GetLightness(1.0) // Should be 0.6 if math.Abs(color.L-expectedLightness) > tolerance { t.Errorf("Custom config lightness = %f, want %f", color.L, expectedLightness) } } // Helper function for absolute difference func abs(a, b int) int { if a > b { return a - b } return b - a }