package main

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"gitea.dockr.co/kev/go-jdenticon/jdenticon"
)

// TestBatchCommand tests the batch command functionality
func TestBatchCommand(t *testing.T) {
	// Create temporary directory for test files
	tempDir, err := os.MkdirTemp("", "jdenticon-batch-test")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	// Create test input file
	inputFile := filepath.Join(tempDir, "input.txt")
	testInputs := []string{
		"user1@example.com",
		"user2@example.com",
		"test-user",
		"unicode-üser",
		"", // Empty line should be skipped
		"special@chars!",
	}
	inputContent := strings.Join(testInputs, "\n")
	if err := os.WriteFile(inputFile, []byte(inputContent), 0644); err != nil {
		t.Fatalf("Failed to create input file: %v", err)
	}

	outputDir := filepath.Join(tempDir, "output")

	tests := []struct {
		name        string
		args        []string
		wantErr     bool
		outputCheck func(t *testing.T, outputDir string)
	}{
		{
			name:    "batch generate SVG",
			args:    []string{"batch", inputFile, "--output-dir", outputDir},
			wantErr: false,
			outputCheck: func(t *testing.T, outputDir string) {
				// Check that SVG files were created
				expectedFiles := []string{
					"user1_at_example.com.svg",
					"user2_at_example.com.svg",
					"test-user.svg",
					"unicode-_ser.svg",      // Unicode characters get sanitized
					"special_at_chars_.svg", // Special chars get sanitized
				}

				for _, filename := range expectedFiles {
					filepath := filepath.Join(outputDir, filename)
					if _, err := os.Stat(filepath); os.IsNotExist(err) {
						t.Errorf("Expected file %s to be created", filename)
						continue
					}

					// Check file content
					content, err := os.ReadFile(filepath)
					if err != nil {
						t.Errorf("Failed to read file %s: %v", filename, err)
						continue
					}

					if !strings.Contains(string(content), "<svg") {
						t.Errorf("File %s should contain SVG content", filename)
					}
				}
			},
		},
		{
			name:    "batch generate PNG",
			args:    []string{"batch", "--format", "png", inputFile, "--output-dir", outputDir + "-png"},
			wantErr: false,
			outputCheck: func(t *testing.T, outputDir string) {
				// Check that PNG files were created
				expectedFiles := []string{
					"user1_at_example.com.png",
					"user2_at_example.com.png",
				}

				for _, filename := range expectedFiles {
					filepath := filepath.Join(outputDir, filename)
					if _, err := os.Stat(filepath); os.IsNotExist(err) {
						t.Errorf("Expected file %s to be created", filename)
						continue
					}

					// Check PNG magic bytes
					content, err := os.ReadFile(filepath)
					if err != nil {
						t.Errorf("Failed to read file %s: %v", filename, err)
						continue
					}

					if len(content) < 8 || string(content[:4]) != "\x89PNG" {
						t.Errorf("File %s should contain PNG content", filename)
					}
				}
			},
		},
		{
			name:    "missing output-dir",
			args:    []string{"batch", inputFile},
			wantErr: true,
			outputCheck: func(t *testing.T, outputDir string) {
				// Should not create any files
			},
		},
		{
			name:    "missing input file",
			args:    []string{"batch", "nonexistent.txt", "--output-dir", outputDir},
			wantErr: true,
			outputCheck: func(t *testing.T, outputDir string) {
				// Should not create any files
			},
		},
		{
			name:    "no arguments",
			args:    []string{},
			wantErr: false, // Root command shows help, doesn't error
			outputCheck: func(t *testing.T, outputDir string) {
				// Should not create any files
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			viper.Reset()

			cmd := createTestBatchCommand()
			cmd.SetArgs(tt.args)

			err := cmd.Execute()

			if (err != nil) != tt.wantErr {
				t.Errorf("batchCmd.Execute() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if !tt.wantErr && tt.outputCheck != nil {
				// Extract output dir from args
				var testOutputDir string
				for i, arg := range tt.args {
					if arg == "--output-dir" && i+1 < len(tt.args) {
						testOutputDir = tt.args[i+1]
						break
					}
				}
				if testOutputDir != "" {
					tt.outputCheck(t, testOutputDir)
				}
			}
		})
	}
}

// TestBatchSanitizeFilename tests filename sanitization
func TestBatchSanitizeFilename(t *testing.T) {
	tests := []struct {
		input    string
		expected string
	}{
		{
			input:    "user@example.com",
			expected: "user_at_example.com",
		},
		{
			input:    "test-user_123",
			expected: "test-user_123",
		},
		{
			input:    "unicode-üser",
			expected: "unicode-_ser",
		},
		{
			input:    "special!@#$%^&*()",
			expected: "special__at__",
		},
		{
			input: "very-long-username-with-many-characters-that-exceeds-normal-length-limits-and-should-be-truncated-to-prevent-filesystem-issues",
			expected: func() string {
				longStr := "very-long-username-with-many-characters-that-exceeds-normal-length-limits-and-should-be-truncated-to-prevent-filesystem-issues"
				if len(longStr) > 200 {
					return longStr[:200]
				}
				return longStr
			}(),
		},
		{
			input:    "",
			expected: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.input, func(t *testing.T) {
			result := sanitizeFilename(tt.input)
			if result != tt.expected {
				t.Errorf("sanitizeFilename(%q) = %q, want %q", tt.input, result, tt.expected)
			}
		})
	}
}

// TestBatchWithCustomConfig tests batch generation with custom configuration
func TestBatchWithCustomConfig(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "jdenticon-batch-config-test")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	// Create simple input file
	inputFile := filepath.Join(tempDir, "input.txt")
	if err := os.WriteFile(inputFile, []byte("test@example.com\n"), 0644); err != nil {
		t.Fatalf("Failed to create input file: %v", err)
	}

	outputDir := filepath.Join(tempDir, "output")

	tests := []struct {
		name        string
		args        []string
		wantErr     bool
		outputCheck func(t *testing.T, outputPath string)
	}{
		{
			name:    "custom size",
			args:    []string{"batch", "--size", "128", inputFile, "--output-dir", outputDir + "-size"},
			wantErr: false,
			outputCheck: func(t *testing.T, outputPath string) {
				content, err := os.ReadFile(outputPath)
				if err != nil {
					t.Fatalf("Failed to read output: %v", err)
				}
				if !strings.Contains(string(content), "width=\"128\"") {
					t.Error("Expected SVG to have custom size")
				}
			},
		},
		{
			name:    "custom background color",
			args:    []string{"batch", "--bg-color", "#ff0000", inputFile, "--output-dir", outputDir + "-bg"},
			wantErr: false,
			outputCheck: func(t *testing.T, outputPath string) {
				content, err := os.ReadFile(outputPath)
				if err != nil {
					t.Fatalf("Failed to read output: %v", err)
				}
				if !strings.Contains(string(content), "#ff0000") {
					t.Error("Expected SVG to have custom background color")
				}
			},
		},
		{
			name:    "custom padding",
			args:    []string{"batch", "--padding", "0.2", inputFile, "--output-dir", outputDir + "-pad"},
			wantErr: false,
			outputCheck: func(t *testing.T, outputPath string) {
				content, err := os.ReadFile(outputPath)
				if err != nil {
					t.Fatalf("Failed to read output: %v", err)
				}
				// Should still generate valid SVG
				if !strings.Contains(string(content), "<svg") {
					t.Error("Expected valid SVG output")
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			viper.Reset()

			cmd := createTestBatchCommand()
			cmd.SetArgs(tt.args)

			err := cmd.Execute()

			if (err != nil) != tt.wantErr {
				t.Errorf("batchCmd.Execute() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if !tt.wantErr && tt.outputCheck != nil {
				// Find the output directory and check the generated file
				var testOutputDir string
				for i, arg := range tt.args {
					if arg == "--output-dir" && i+1 < len(tt.args) {
						testOutputDir = tt.args[i+1]
						break
					}
				}

				if testOutputDir != "" {
					expectedFile := filepath.Join(testOutputDir, "test_at_example.com.svg")
					tt.outputCheck(t, expectedFile)
				}
			}
		})
	}
}

// createTestBatchCommand creates a batch command for testing
func createTestBatchCommand() *cobra.Command {
	// Create root command with flags
	rootCmd := &cobra.Command{
		Use:   "jdenticon",
		Short: "Generate identicons from any input string",
	}

	// Initialize root flags
	initTestFlags(rootCmd)

	// Create batch command similar to the actual one
	batchCmd := &cobra.Command{
		Use:   "batch <input-file>",
		Short: "Generate multiple identicons from a list",
		Args:  cobra.ExactArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			return runConcurrentBatch(cmd, args)
		},
	}

	// Add batch-specific flags
	batchCmd.Flags().StringP("output-dir", "d", "", "Output directory for generated identicons (required)")
	batchCmd.MarkFlagRequired("output-dir")

	// Concurrency control
	batchCmd.Flags().IntP("concurrency", "c", runtime.NumCPU(),
		fmt.Sprintf("Number of concurrent workers (default: %d)", runtime.NumCPU()))

	// Add to root command
	rootCmd.AddCommand(batchCmd)

	return rootCmd
}

// TestConcurrentBatchProcessing tests the concurrent batch processing functionality
func TestConcurrentBatchProcessing(t *testing.T) {
	// Create temporary directory for test files
	tempDir, err := os.MkdirTemp("", "jdenticon-concurrent-test")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	// Create test input file with more entries to test concurrency
	inputFile := filepath.Join(tempDir, "input.txt")
	var inputs []string
	for i := 0; i < 50; i++ {
		inputs = append(inputs, fmt.Sprintf("user%d@example.com", i))
	}
	inputContent := strings.Join(inputs, "\n")
	if err := os.WriteFile(inputFile, []byte(inputContent), 0644); err != nil {
		t.Fatalf("Failed to create input file: %v", err)
	}

	outputDir := filepath.Join(tempDir, "output")

	tests := []struct {
		name        string
		concurrency int
		expectFiles int
	}{
		{"sequential", 1, 50},
		{"small_pool", 2, 50},
		{"medium_pool", 4, 50},
		{"large_pool", runtime.NumCPU(), 50},
		{"over_provisioned", runtime.NumCPU() * 2, 50},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Clean output directory
			os.RemoveAll(outputDir)

			// Test the concurrent batch command
			cmd := createTestBatchCommand()
			args := []string{"batch", inputFile, "--output-dir", outputDir, "--concurrency", fmt.Sprintf("%d", tt.concurrency)}
			cmd.SetArgs(args)

			start := time.Now()
			err := cmd.Execute()
			duration := time.Since(start)

			if err != nil {
				t.Fatalf("Command failed: %v", err)
			}

			// Verify output files
			files, err := os.ReadDir(outputDir)
			if err != nil {
				t.Fatalf("Failed to read output directory: %v", err)
			}

			if len(files) != tt.expectFiles {
				t.Errorf("Expected %d files, got %d", tt.expectFiles, len(files))
			}

			// Verify all files are valid SVG
			for _, file := range files {
				if !strings.HasSuffix(file.Name(), ".svg") {
					t.Errorf("Expected SVG file, got %s", file.Name())
					continue
				}

				content, err := os.ReadFile(filepath.Join(outputDir, file.Name()))
				if err != nil {
					t.Errorf("Failed to read file %s: %v", file.Name(), err)
					continue
				}

				if !strings.Contains(string(content), "<svg") {
					t.Errorf("File %s does not contain SVG content", file.Name())
				}
			}

			t.Logf("Processed %d files with %d workers in %v", tt.expectFiles, tt.concurrency, duration)
		})
	}
}

// TestJobPreparation tests the job preparation functionality
func TestJobPreparation(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "jdenticon-job-prep-test")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	// Test with various input scenarios
	tests := []struct {
		name        string
		content     string
		expectJobs  int
		expectError bool
	}{
		{
			name:       "normal_inputs",
			content:    "user1@example.com\nuser2@example.com\nuser3@example.com",
			expectJobs: 3,
		},
		{
			name:       "with_empty_lines",
			content:    "user1@example.com\n\nuser2@example.com\n\n",
			expectJobs: 2,
		},
		{
			name:       "only_empty_lines",
			content:    "\n\n\n",
			expectJobs: 0,
		},
		{
			name:       "mixed_content",
			content:    "user1@example.com\n  \nuser2@example.com\n\t\nuser3@example.com",
			expectJobs: 3,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			inputFile := filepath.Join(tempDir, "input.txt")
			if err := os.WriteFile(inputFile, []byte(tt.content), 0644); err != nil {
				t.Fatalf("Failed to create input file: %v", err)
			}

			outputDir := filepath.Join(tempDir, "output")
			jobs, total, err := prepareJobs(inputFile, outputDir, FormatSVG, 64)

			if tt.expectError && err == nil {
				t.Errorf("Expected error, got none")
			}
			if !tt.expectError && err != nil {
				t.Errorf("Unexpected error: %v", err)
			}

			if total != tt.expectJobs {
				t.Errorf("Expected %d jobs, got %d", tt.expectJobs, total)
			}

			if len(jobs) != tt.expectJobs {
				t.Errorf("Expected %d jobs in slice, got %d", tt.expectJobs, len(jobs))
			}
		})
	}
}

// TestBatchWorkerShutdown tests graceful shutdown of batch workers
func TestBatchWorkerShutdown(t *testing.T) {
	generator, err := jdenticon.NewGeneratorWithConfig(jdenticon.DefaultConfig(), 100)
	if err != nil {
		t.Fatalf("Failed to create generator: %v", err)
	}

	// Create a temp directory for output files (cross-platform)
	tempDir := t.TempDir()

	// Create a context that will be canceled
	ctx, cancel := context.WithCancel(context.Background())

	// Create job channel with some jobs
	jobChan := make(chan batchJob, 10)
	for i := 0; i < 5; i++ {
		jobChan <- batchJob{
			value:      fmt.Sprintf("user%d@example.com", i),
			outputPath: filepath.Join(tempDir, fmt.Sprintf("test%d.svg", i)),
			size:       64,
		}
	}

	stats := &batchStats{}

	// Start worker
	done := make(chan struct{})
	go func() {
		defer close(done)
		batchWorker(ctx, 1, jobChan, generator, FormatSVG, stats, nil)
	}()

	// Let it process a bit, then cancel
	time.Sleep(10 * time.Millisecond)
	cancel()

	// Wait for worker to shutdown (should be quick)
	select {
	case <-done:
		// Good, worker shut down gracefully
	case <-time.After(1 * time.Second):
		t.Error("Worker did not shut down within timeout")
	}

	// Verify some jobs were processed
	processed := atomic.LoadInt64(&stats.processed)
	t.Logf("Processed %d jobs before shutdown", processed)
}

// TestConcurrencyFlagValidation tests the validation of concurrency flag
func TestConcurrencyFlagValidation(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "jdenticon-concurrency-test")
	if err != nil {
		t.Fatalf("Failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	// Create minimal input file
	inputFile := filepath.Join(tempDir, "input.txt")
	if err := os.WriteFile(inputFile, []byte("test@example.com\n"), 0644); err != nil {
		t.Fatalf("Failed to create input file: %v", err)
	}

	outputDir := filepath.Join(tempDir, "output")

	tests := []struct {
		name        string
		concurrency string
		expectError bool
	}{
		{"valid_positive", "4", false},
		{"valid_one", "1", false},
		{"invalid_zero", "0", true},
		{"invalid_negative", "-1", true},
		{"invalid_non_numeric", "abc", true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cmd := createTestBatchCommand()
			args := []string{"batch", inputFile, "--output-dir", outputDir, "--concurrency", tt.concurrency}
			cmd.SetArgs(args)

			err := cmd.Execute()

			if tt.expectError && err == nil {
				t.Errorf("Expected error for concurrency %s, got none", tt.concurrency)
			}
			if !tt.expectError && err != nil {
				t.Errorf("Unexpected error for concurrency %s: %v", tt.concurrency, err)
			}
		})
	}
}
