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, err := HSLToRGB(tt.h, tt.s, tt.l)
			if err != nil {
				t.Fatalf("HSLToRGB failed: %v", err)
			}

			// 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, err := CorrectedHSLToRGB(tc.h, tc.s, tc.l)
			if err != nil {
				t.Fatalf("CorrectedHSLToRGB failed: %v", err)
			}
			// RGB values are uint8, so they're guaranteed to be in 0-255 range
			_ = 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 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) {
			rgba, err := ParseHexColorToRGBA(tt.input)
			r, g, b, a := rgba.R, rgba.G, rgba.B, rgba.A

			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)
	}

	r, g, b, err := color.ToRGB()
	if err != nil {
		t.Fatalf("ToRGB failed: %v", err)
	}
	if r != 255 || g != 0 || b != 0 {
		t.Errorf("NewColorHSL(0.0, 1.0, 0.5) RGB = (%d, %d, %d), want (255, 0, 0)",
			r, g, 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

	r, g, b, err := color.ToRGB()
	if err != nil {
		t.Fatalf("ToRGB failed: %v", err)
	}
	if r != 255 || g != 0 || b != 0 {
		t.Errorf("NewColorRGB(255, 0, 0) RGB = (%d, %d, %d), want (255, 0, 0)",
			r, g, 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
	newColorR, newColorG, newColorB, err1 := newColor.ToRGB()
	if err1 != nil {
		t.Fatalf("newColor.ToRGB failed: %v", err1)
	}
	colorR, colorG, colorB, err2 := color.ToRGB()
	if err2 != nil {
		t.Fatalf("color.ToRGB failed: %v", err2)
	}
	if newColorR != colorR || newColorG != colorG || newColorB != colorB {
		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 := DefaultColorConfig()
	config.Hues = []float64{180} // Only allow cyan (180 degrees = 0.5 turns)

	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 := DefaultColorConfig()
	config.ColorSaturation = 0.8
	config.ColorLightness = LightnessRange{Min: 0.2, Max: 0.6}

	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
}
