package renderer

import (
	"bytes"
	"crypto/sha1"
	"fmt"
	"image/png"
	"testing"

	"gitea.dockr.co/kev/go-jdenticon/internal/engine"
)

// TestPNGRenderer_VisualRegression tests that PNG output matches expected characteristics
func TestPNGRenderer_VisualRegression(t *testing.T) {
	testCases := []struct {
		name     string
		size     int
		bg       string
		bgOp     float64
		shapes   []testShape
		checksum string // Expected checksum of PNG data
	}{
		{
			name: "simple_red_square",
			size: 50,
			bg:   "#ffffff",
			bgOp: 1.0,
			shapes: []testShape{
				{
					color: "#ff0000",
					polygons: [][]engine.Point{
						{
							{X: 10, Y: 10},
							{X: 40, Y: 10},
							{X: 40, Y: 40},
							{X: 10, Y: 40},
						},
					},
				},
			},
		},
		{
			name: "blue_circle",
			size: 60,
			bg:   "#f0f0f0",
			bgOp: 1.0,
			shapes: []testShape{
				{
					color: "#0000ff",
					circles: []testCircle{
						{center: engine.Point{X: 30, Y: 30}, radius: 20, invert: false},
					},
				},
			},
		},
		{
			name: "transparent_background",
			size: 40,
			bg:   "#000000",
			bgOp: 0.0,
			shapes: []testShape{
				{
					color: "#00ff00",
					polygons: [][]engine.Point{
						{
							{X: 5, Y: 5},
							{X: 35, Y: 5},
							{X: 20, Y: 35},
						},
					},
				},
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			renderer := NewPNGRenderer(tc.size)

			if tc.bgOp > 0 {
				renderer.SetBackground(tc.bg, tc.bgOp)
			}

			for _, shape := range tc.shapes {
				renderer.BeginShape(shape.color)

				for _, points := range shape.polygons {
					renderer.AddPolygon(points)
				}

				for _, circle := range shape.circles {
					renderer.AddCircle(circle.center, circle.radius, circle.invert)
				}

				renderer.EndShape()
			}

			pngData, err := renderer.ToPNG()
			if err != nil {
				t.Fatalf("Failed to generate PNG: %v", err)
			}

			// Verify PNG is valid
			reader := bytes.NewReader(pngData)
			img, err := png.Decode(reader)
			if err != nil {
				t.Fatalf("Failed to decode PNG: %v", err)
			}

			bounds := img.Bounds()
			if bounds.Max.X != tc.size || bounds.Max.Y != tc.size {
				t.Errorf("Image size = %dx%d, want %dx%d",
					bounds.Max.X, bounds.Max.Y, tc.size, tc.size)
			}

			// Calculate checksum for regression testing
			checksum := fmt.Sprintf("%x", sha1.Sum(pngData))
			t.Logf("PNG checksum for %s: %s", tc.name, checksum)

			// Basic size validation
			if len(pngData) < 100 {
				t.Errorf("PNG data too small: %d bytes", len(pngData))
			}
		})
	}
}

// testShape represents a shape to be drawn for testing
type testShape struct {
	color    string
	polygons [][]engine.Point
	circles  []testCircle
}

type testCircle struct {
	center engine.Point
	radius float64
	invert bool
}

// TestPNGRenderer_ComplexIcon tests rendering a more complex icon pattern
func TestPNGRenderer_ComplexIcon(t *testing.T) {
	renderer := NewPNGRenderer(100)
	renderer.SetBackground("#f8f8f8", 1.0)

	// Simulate a complex icon with multiple shapes and colors
	// This mimics the patterns that would be generated by the actual jdenticon algorithm

	// Outer shapes (corners)
	renderer.BeginShape("#3f7cac")
	// Top-left triangle
	renderer.AddPolygon([]engine.Point{
		{X: 0, Y: 0}, {X: 25, Y: 0}, {X: 0, Y: 25},
	})
	// Top-right triangle
	renderer.AddPolygon([]engine.Point{
		{X: 75, Y: 0}, {X: 100, Y: 0}, {X: 100, Y: 25},
	})
	// Bottom-left triangle
	renderer.AddPolygon([]engine.Point{
		{X: 0, Y: 75}, {X: 0, Y: 100}, {X: 25, Y: 100},
	})
	// Bottom-right triangle
	renderer.AddPolygon([]engine.Point{
		{X: 75, Y: 100}, {X: 100, Y: 100}, {X: 100, Y: 75},
	})
	renderer.EndShape()

	// Middle shapes
	renderer.BeginShape("#95b3d0")
	// Left rhombus
	renderer.AddPolygon([]engine.Point{
		{X: 12.5, Y: 37.5}, {X: 25, Y: 50}, {X: 12.5, Y: 62.5}, {X: 0, Y: 50},
	})
	// Right rhombus
	renderer.AddPolygon([]engine.Point{
		{X: 87.5, Y: 37.5}, {X: 100, Y: 50}, {X: 87.5, Y: 62.5}, {X: 75, Y: 50},
	})
	// Top rhombus
	renderer.AddPolygon([]engine.Point{
		{X: 37.5, Y: 12.5}, {X: 50, Y: 0}, {X: 62.5, Y: 12.5}, {X: 50, Y: 25},
	})
	// Bottom rhombus
	renderer.AddPolygon([]engine.Point{
		{X: 37.5, Y: 87.5}, {X: 50, Y: 75}, {X: 62.5, Y: 87.5}, {X: 50, Y: 100},
	})
	renderer.EndShape()

	// Center shape
	renderer.BeginShape("#2f5f8f")
	renderer.AddCircle(engine.Point{X: 50, Y: 50}, 15, false)
	renderer.EndShape()

	pngData, err := renderer.ToPNG()
	if err != nil {
		t.Fatalf("Failed to generate PNG: %v", err)
	}

	// Verify the complex icon renders successfully
	reader := bytes.NewReader(pngData)
	img, err := png.Decode(reader)
	if err != nil {
		t.Fatalf("Failed to decode complex PNG: %v", err)
	}

	bounds := img.Bounds()
	if bounds.Max.X != 100 || bounds.Max.Y != 100 {
		t.Errorf("Complex icon size = %dx%d, want 100x100", bounds.Max.X, bounds.Max.Y)
	}

	// Ensure PNG is reasonable size (not too large, not too small)
	if len(pngData) < 500 || len(pngData) > 50000 {
		t.Errorf("Complex PNG size %d bytes seems unreasonable", len(pngData))
	}

	t.Logf("Complex icon PNG size: %d bytes", len(pngData))
}

// TestRendererInterface_Consistency tests that both SVG and PNG renderers
// implement the Renderer interface consistently
func TestRendererInterface_Consistency(t *testing.T) {
	testCases := []struct {
		name     string
		size     int
		bg       string
		bgOp     float64
		testFunc func(Renderer)
	}{
		{
			name: "basic_shapes",
			size: 100,
			bg:   "#ffffff",
			bgOp: 1.0,
			testFunc: func(r Renderer) {
				r.BeginShape("#ff0000")
				r.AddRectangle(10, 10, 30, 30)
				r.EndShape()

				r.BeginShape("#00ff00")
				r.AddCircle(engine.Point{X: 70, Y: 70}, 15, false)
				r.EndShape()

				r.BeginShape("#0000ff")
				r.AddTriangle(
					engine.Point{X: 20, Y: 80},
					engine.Point{X: 40, Y: 80},
					engine.Point{X: 30, Y: 60},
				)
				r.EndShape()
			},
		},
		{
			name: "complex_polygon",
			size: 80,
			bg:   "#f8f8f8",
			bgOp: 0.8,
			testFunc: func(r Renderer) {
				r.BeginShape("#8B4513")
				// Star shape
				points := []engine.Point{
					{X: 40, Y: 10},
					{X: 45, Y: 25},
					{X: 60, Y: 25},
					{X: 50, Y: 35},
					{X: 55, Y: 50},
					{X: 40, Y: 40},
					{X: 25, Y: 50},
					{X: 30, Y: 35},
					{X: 20, Y: 25},
					{X: 35, Y: 25},
				}
				r.AddPolygon(points)
				r.EndShape()
			},
		},
		{
			name: "primitive_drawing",
			size: 60,
			bg:   "",
			bgOp: 0,
			testFunc: func(r Renderer) {
				r.BeginShape("#FF6B35")
				r.MoveTo(10, 10)
				r.LineTo(50, 10)
				r.LineTo(50, 50)
				r.CurveTo(45, 55, 35, 55, 30, 50)
				r.LineTo(10, 50)
				r.ClosePath()
				r.Fill("#FF6B35")
				r.EndShape()
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// Test with PNG renderer
			t.Run("png", func(t *testing.T) {
				renderer := NewPNGRenderer(tc.size)
				if tc.bgOp > 0 {
					renderer.SetBackground(tc.bg, tc.bgOp)
				}

				tc.testFunc(renderer)

				// Verify PNG output
				pngData, err := renderer.ToPNG()
				if err != nil {
					t.Fatalf("Failed to generate PNG: %v", err)
				}
				if len(pngData) == 0 {
					t.Error("PNG renderer produced no data")
				}

				reader := bytes.NewReader(pngData)
				img, err := png.Decode(reader)
				if err != nil {
					t.Fatalf("PNG decode failed: %v", err)
				}

				bounds := img.Bounds()
				if bounds.Max.X != tc.size || bounds.Max.Y != tc.size {
					t.Errorf("PNG size = %dx%d, want %dx%d",
						bounds.Max.X, bounds.Max.Y, tc.size, tc.size)
				}
			})

			// Test with SVG renderer
			t.Run("svg", func(t *testing.T) {
				renderer := NewSVGRenderer(tc.size)
				if tc.bgOp > 0 {
					renderer.SetBackground(tc.bg, tc.bgOp)
				}

				tc.testFunc(renderer)

				// Verify SVG output
				svgData := renderer.ToSVG()
				if len(svgData) == 0 {
					t.Error("SVG renderer produced no data")
				}

				// Basic SVG validation
				if !bytes.Contains([]byte(svgData), []byte("<svg")) {
					t.Error("SVG output missing opening tag")
				}
				if !bytes.Contains([]byte(svgData), []byte("</svg>")) {
					t.Error("SVG output missing closing tag")
				}

				// Check size attributes
				expectedWidth := fmt.Sprintf(`width="%d"`, tc.size)
				expectedHeight := fmt.Sprintf(`height="%d"`, tc.size)
				if !bytes.Contains([]byte(svgData), []byte(expectedWidth)) {
					t.Errorf("SVG missing width attribute: %s", expectedWidth)
				}
				if !bytes.Contains([]byte(svgData), []byte(expectedHeight)) {
					t.Errorf("SVG missing height attribute: %s", expectedHeight)
				}
			})
		})
	}
}

// TestRendererInterface_BaseRendererMethods tests that renderers properly use BaseRenderer methods
func TestRendererInterface_BaseRendererMethods(t *testing.T) {
	renderers := []struct {
		name     string
		renderer Renderer
	}{
		{"svg", NewSVGRenderer(50)},
		{"png", NewPNGRenderer(50)},
	}

	for _, r := range renderers {
		t.Run(r.name, func(t *testing.T) {
			renderer := r.renderer

			// Test size getter
			if renderer.GetSize() != 50 {
				t.Errorf("GetSize() = %d, want 50", renderer.GetSize())
			}

			// Test background setting
			renderer.SetBackground("#123456", 0.75)
			if svgRenderer, ok := renderer.(*SVGRenderer); ok {
				if bg, op := svgRenderer.GetBackground(); bg != "#123456" || op != 0.75 {
					t.Errorf("SVG GetBackground() = %s, %f, want #123456, 0.75", bg, op)
				}
			}
			if pngRenderer, ok := renderer.(*PNGRenderer); ok {
				if bg, op := pngRenderer.GetBackground(); bg != "#123456" || op != 0.75 {
					t.Errorf("PNG GetBackground() = %s, %f, want #123456, 0.75", bg, op)
				}
			}

			// Test shape management
			renderer.BeginShape("#ff0000")
			if svgRenderer, ok := renderer.(*SVGRenderer); ok {
				if color := svgRenderer.GetCurrentColor(); color != "#ff0000" {
					t.Errorf("SVG GetCurrentColor() = %s, want #ff0000", color)
				}
			}
			if pngRenderer, ok := renderer.(*PNGRenderer); ok {
				if color := pngRenderer.GetCurrentColor(); color != "#ff0000" {
					t.Errorf("PNG GetCurrentColor() = %s, want #ff0000", color)
				}
			}

			// Test clearing
			renderer.Clear()
			if svgRenderer, ok := renderer.(*SVGRenderer); ok {
				if color := svgRenderer.GetCurrentColor(); color != "" {
					t.Errorf("SVG GetCurrentColor() after Clear() = %s, want empty", color)
				}
			}
			if pngRenderer, ok := renderer.(*PNGRenderer); ok {
				if color := pngRenderer.GetCurrentColor(); color != "" {
					t.Errorf("PNG GetCurrentColor() after Clear() = %s, want empty", color)
				}
			}
		})
	}
}

// TestRendererInterface_CompatibilityWithJavaScript tests patterns from JavaScript reference
func TestRendererInterface_CompatibilityWithJavaScript(t *testing.T) {
	// This test replicates patterns that would be used by the JavaScript jdenticon library
	// to ensure our Go implementation is compatible

	testJavaScriptPattern := func(r Renderer) {
		// Simulate the JavaScript renderer usage pattern
		r.SetBackground("#f0f0f0", 1.0)

		// Pattern similar to what iconGenerator.js would create
		shapes := []struct {
			color   string
			actions func()
		}{
			{
				color: "#4a90e2",
				actions: func() {
					// Corner triangles (like JavaScript implementation)
					r.AddPolygon([]engine.Point{
						{X: 0, Y: 0}, {X: 20, Y: 0}, {X: 0, Y: 20},
					})
					r.AddPolygon([]engine.Point{
						{X: 80, Y: 0}, {X: 100, Y: 0}, {X: 100, Y: 20},
					})
					r.AddPolygon([]engine.Point{
						{X: 0, Y: 80}, {X: 0, Y: 100}, {X: 20, Y: 100},
					})
					r.AddPolygon([]engine.Point{
						{X: 80, Y: 100}, {X: 100, Y: 100}, {X: 100, Y: 80},
					})
				},
			},
			{
				color: "#7fc383",
				actions: func() {
					// Center circle
					r.AddCircle(engine.Point{X: 50, Y: 50}, 25, false)
				},
			},
			{
				color: "#e94b3c",
				actions: func() {
					// Side rhombs
					r.AddPolygon([]engine.Point{
						{X: 25, Y: 37.5}, {X: 37.5, Y: 50}, {X: 25, Y: 62.5}, {X: 12.5, Y: 50},
					})
					r.AddPolygon([]engine.Point{
						{X: 75, Y: 37.5}, {X: 87.5, Y: 50}, {X: 75, Y: 62.5}, {X: 62.5, Y: 50},
					})
				},
			},
		}

		for _, shape := range shapes {
			r.BeginShape(shape.color)
			shape.actions()
			r.EndShape()
		}
	}

	t.Run("svg_javascript_pattern", func(t *testing.T) {
		renderer := NewSVGRenderer(100)
		testJavaScriptPattern(renderer)

		svgData := renderer.ToSVG()

		// Should contain multiple paths with different colors
		for _, color := range []string{"#4a90e2", "#7fc383", "#e94b3c"} {
			expected := fmt.Sprintf(`fill="%s"`, color)
			if !bytes.Contains([]byte(svgData), []byte(expected)) {
				t.Errorf("SVG missing expected color: %s", color)
			}
		}

		// Should contain background
		if !bytes.Contains([]byte(svgData), []byte("#f0f0f0")) {
			t.Error("SVG missing background color")
		}
	})

	t.Run("png_javascript_pattern", func(t *testing.T) {
		renderer := NewPNGRenderer(100)
		testJavaScriptPattern(renderer)

		pngData, err := renderer.ToPNG()
		if err != nil {
			t.Fatalf("Failed to generate PNG: %v", err)
		}

		// Verify valid PNG
		reader := bytes.NewReader(pngData)
		img, err := png.Decode(reader)
		if err != nil {
			t.Fatalf("PNG decode failed: %v", err)
		}

		bounds := img.Bounds()
		if bounds.Max.X != 100 || bounds.Max.Y != 100 {
			t.Errorf("PNG size = %dx%d, want 100x100", bounds.Max.X, bounds.Max.Y)
		}
	})
}

// TestPNGRenderer_EdgeCases tests various edge cases
func TestPNGRenderer_EdgeCases(t *testing.T) {
	t.Run("very_small_icon", func(t *testing.T) {
		renderer := NewPNGRenderer(1)
		renderer.BeginShape("#ff0000")
		renderer.AddPolygon([]engine.Point{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 0, Y: 1}})
		renderer.EndShape()

		pngData, err := renderer.ToPNG()
		if err != nil {
			t.Fatalf("Failed to generate PNG: %v", err)
		}
		if len(pngData) == 0 {
			t.Error("1x1 PNG should generate data")
		}
	})

	t.Run("large_icon", func(t *testing.T) {
		renderer := NewPNGRenderer(512)
		renderer.SetBackground("#ffffff", 1.0)
		renderer.BeginShape("#000000")
		renderer.AddCircle(engine.Point{X: 256, Y: 256}, 200, false)
		renderer.EndShape()

		pngData, err := renderer.ToPNG()
		if err != nil {
			t.Fatalf("Failed to generate PNG: %v", err)
		}
		if len(pngData) == 0 {
			t.Error("512x512 PNG should generate data")
		}

		// Large images should compress well due to simple content
		t.Logf("512x512 PNG size: %d bytes", len(pngData))
	})

	t.Run("shapes_outside_bounds", func(t *testing.T) {
		renderer := NewPNGRenderer(50)
		renderer.BeginShape("#ff0000")

		// Add shapes that extend outside the image bounds
		renderer.AddPolygon([]engine.Point{
			{X: -10, Y: -10}, {X: 60, Y: -10}, {X: 60, Y: 60}, {X: -10, Y: 60},
		})
		renderer.AddCircle(engine.Point{X: 25, Y: 25}, 50, false)
		renderer.EndShape()

		// Should not panic and should produce valid PNG
		pngData, err := renderer.ToPNG()
		if err != nil {
			t.Fatalf("Failed to generate PNG: %v", err)
		}
		reader := bytes.NewReader(pngData)
		_, err = png.Decode(reader)
		if err != nil {
			t.Errorf("Failed to decode PNG with out-of-bounds shapes: %v", err)
		}
	})
}
