package engine

import (
	"context"
	"runtime"
	"strings"
	"testing"
	"time"

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

// TestResourceExhaustionProtection tests that the generator properly blocks
// attempts to create extremely large icons that could cause memory exhaustion.
func TestResourceExhaustionProtection(t *testing.T) {
	tests := []struct {
		name          string
		maxIconSize   int
		requestedSize float64
		expectError   bool
		errorContains string
	}{
		{
			name:          "valid size within default limit",
			maxIconSize:   0, // Use default
			requestedSize: 1024,
			expectError:   false,
		},
		{
			name:          "valid size at exact default limit",
			maxIconSize:   0, // Use default
			requestedSize: constants.DefaultMaxIconSize,
			expectError:   false,
		},
		{
			name:          "invalid size exceeds default limit by 1",
			maxIconSize:   0, // Use default
			requestedSize: constants.DefaultMaxIconSize + 1,
			expectError:   true,
			errorContains: "exceeds maximum allowed size",
		},
		{
			name:          "extremely large size should be blocked",
			maxIconSize:   0, // Use default
			requestedSize: 100000,
			expectError:   true,
			errorContains: "exceeds maximum allowed size",
		},
		{
			name:          "custom limit - valid size",
			maxIconSize:   1000,
			requestedSize: 1000,
			expectError:   false,
		},
		{
			name:          "custom limit - invalid size",
			maxIconSize:   1000,
			requestedSize: 1001,
			expectError:   true,
			errorContains: "exceeds maximum allowed size",
		},
		{
			name:          "disabled limit allows oversized requests",
			maxIconSize:   -1, // Disabled
			requestedSize: constants.DefaultMaxIconSize + 1000,
			expectError:   false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			config := DefaultGeneratorConfig()
			config.MaxIconSize = tt.maxIconSize

			generator, err := NewGeneratorWithConfig(config)
			if err != nil {
				t.Fatalf("Failed to create generator: %v", err)
			}

			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
			defer cancel()

			// Use a simple hash for testing
			testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b"

			icon, err := generator.Generate(ctx, testHash, tt.requestedSize)

			if tt.expectError {
				if err == nil {
					t.Errorf("Expected error for size %f, but got none", tt.requestedSize)
					return
				}
				if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
					t.Errorf("Expected error to contain '%s', but got: %v", tt.errorContains, err)
				}
				if icon != nil {
					t.Errorf("Expected nil icon when error occurs, but got non-nil")
				}
			} else {
				if err != nil {
					t.Errorf("Unexpected error for size %f: %v", tt.requestedSize, err)
					return
				}
				if icon == nil {
					t.Errorf("Expected non-nil icon for valid size %f", tt.requestedSize)
				}
			}
		})
	}
}

// TestMemoryUsageDoesNotSpikeOnRejection verifies that memory usage doesn't
// spike when oversized icon requests are rejected, proving that the validation
// happens before any memory allocation.
func TestMemoryUsageDoesNotSpikeOnRejection(t *testing.T) {
	generator, err := NewGeneratorWithConfig(DefaultGeneratorConfig())
	if err != nil {
		t.Fatalf("Failed to create generator: %v", err)
	}

	// Force garbage collection and get baseline memory stats
	runtime.GC()
	runtime.GC() // Run twice to ensure clean baseline

	var m1 runtime.MemStats
	runtime.ReadMemStats(&m1)
	baselineAlloc := m1.Alloc

	ctx := context.Background()
	testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b"

	// Attempt to generate an extremely large icon (should be rejected)
	oversizedRequest := float64(constants.DefaultMaxIconSize * 10) // 10x the limit

	icon, err := generator.Generate(ctx, testHash, oversizedRequest)

	// Verify the request was properly rejected
	if err == nil {
		t.Fatalf("Expected error for oversized request, but got none")
	}
	if icon != nil {
		t.Fatalf("Expected nil icon for oversized request, but got non-nil")
	}
	if !strings.Contains(err.Error(), "exceeds maximum allowed size") {
		t.Fatalf("Expected specific error message, got: %v", err)
	}

	// Check memory usage after the rejected request
	runtime.GC()
	var m2 runtime.MemStats
	runtime.ReadMemStats(&m2)
	postRejectionAlloc := m2.Alloc

	// Calculate memory increase (allow for some variance due to test overhead)
	memoryIncrease := postRejectionAlloc - baselineAlloc
	maxAcceptableIncrease := uint64(1024 * 1024) // 1MB tolerance for test overhead

	if memoryIncrease > maxAcceptableIncrease {
		t.Errorf("Memory usage spiked by %d bytes after rejection (baseline: %d, post: %d). "+
			"This suggests memory allocation occurred before validation.",
			memoryIncrease, baselineAlloc, postRejectionAlloc)
	}

	t.Logf("Memory baseline: %d bytes, post-rejection: %d bytes, increase: %d bytes",
		baselineAlloc, postRejectionAlloc, memoryIncrease)
}

// TestConfigurationDefaults verifies that the default MaxIconSize is properly applied
// when not explicitly set in the configuration.
func TestConfigurationDefaults(t *testing.T) {
	tests := []struct {
		name        string
		configSize  int
		expectedMax int
	}{
		{
			name:        "zero config uses default",
			configSize:  0,
			expectedMax: constants.DefaultMaxIconSize,
		},
		{
			name:        "other negative config uses default",
			configSize:  -5,
			expectedMax: constants.DefaultMaxIconSize,
		},
		{
			name:        "custom config is respected",
			configSize:  2000,
			expectedMax: 2000,
		},
		{
			name:        "disabled config is respected",
			configSize:  -1,
			expectedMax: -1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			config := DefaultGeneratorConfig()
			config.MaxIconSize = tt.configSize

			generator, err := NewGeneratorWithConfig(config)
			if err != nil {
				t.Fatalf("Failed to create generator: %v", err)
			}

			// Check that the effective max size was set correctly
			if generator.maxIconSize != tt.expectedMax {
				t.Errorf("Expected maxIconSize to be %d, but got %d", tt.expectedMax, generator.maxIconSize)
			}

			// Verify the limit is enforced (skip if disabled)
			if tt.expectedMax > 0 {
				ctx := context.Background()
				testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b"

				// Try a size just over the limit
				oversizedRequest := float64(tt.expectedMax + 1)
				icon, err := generator.Generate(ctx, testHash, oversizedRequest)

				if err == nil {
					t.Errorf("Expected error for size %f (limit: %d), but got none", oversizedRequest, tt.expectedMax)
				}
				if icon != nil {
					t.Errorf("Expected nil icon for oversized request")
				}
			} else if tt.expectedMax == -1 {
				// Test that disabled limit allows large sizes
				ctx := context.Background()
				testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b"

				// Try a very large size that would normally be blocked
				largeRequest := float64(constants.DefaultMaxIconSize + 1000)
				icon, err := generator.Generate(ctx, testHash, largeRequest)

				if err != nil {
					t.Errorf("Unexpected error for large size with disabled limit: %v", err)
				}
				if icon == nil {
					t.Errorf("Expected non-nil icon for large size with disabled limit")
				}
			}
		})
	}
}

// TestBoundaryConditions tests edge cases around the size limit boundaries
func TestBoundaryConditions(t *testing.T) {
	config := DefaultGeneratorConfig()
	config.MaxIconSize = 1000

	generator, err := NewGeneratorWithConfig(config)
	if err != nil {
		t.Fatalf("Failed to create generator: %v", err)
	}

	ctx := context.Background()
	testHash := "7b824bb99b5b4a4b7b824bb99b5b4a4b7b824bb99b5b4a4b"

	tests := []struct {
		name        string
		size        float64
		expectError bool
	}{
		{"size at exact limit", 1000, false},
		{"size just under limit", 999, false},
		{"size just over limit", 1001, true},
		{"floating point at limit", 1000.0, false},
		{"floating point just over", 1001.0, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			icon, err := generator.Generate(ctx, testHash, tt.size)

			if tt.expectError {
				if err == nil {
					t.Errorf("Expected error for size %f, but got none", tt.size)
				}
				if icon != nil {
					t.Errorf("Expected nil icon for oversized request")
				}
			} else {
				if err != nil {
					t.Errorf("Unexpected error for size %f: %v", tt.size, err)
				}
				if icon == nil {
					t.Errorf("Expected non-nil icon for valid size %f", tt.size)
				}
			}
		})
	}
}
