package tests

import (
	"context"
	"os"
	"path/filepath"
	"testing"

	"dbbackup/internal/config"
	"dbbackup/internal/logger"
	"dbbackup/internal/pitr"
	"dbbackup/internal/wal"
)

// TestRecoveryTargetValidation tests recovery target parsing and validation
func TestRecoveryTargetValidation(t *testing.T) {
	tests := []struct {
		name        string
		targetTime  string
		targetXID   string
		targetLSN   string
		targetName  string
		immediate   bool
		action      string
		timeline    string
		inclusive   bool
		expectError bool
		errorMsg    string
	}{
		{
			name:        "Valid time target",
			targetTime:  "2024-11-26 12:00:00",
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: false,
		},
		{
			name:        "Valid XID target",
			targetXID:   "1000000",
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: false,
		},
		{
			name:        "Valid LSN target",
			targetLSN:   "0/3000000",
			action:      "pause",
			timeline:    "latest",
			inclusive:   false,
			expectError: false,
		},
		{
			name:        "Valid name target",
			targetName:  "my_restore_point",
			action:      "promote",
			timeline:    "2",
			inclusive:   true,
			expectError: false,
		},
		{
			name:        "Valid immediate target",
			immediate:   true,
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: false,
		},
		{
			name:        "No target specified",
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: true,
			errorMsg:    "no recovery target specified",
		},
		{
			name:        "Multiple targets",
			targetTime:  "2024-11-26 12:00:00",
			targetXID:   "1000000",
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: true,
			errorMsg:    "multiple recovery targets",
		},
		{
			name:        "Invalid time format",
			targetTime:  "invalid-time",
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: true,
			errorMsg:    "invalid timestamp format",
		},
		{
			name:        "Invalid XID (negative)",
			targetXID:   "-1000",
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: true,
			errorMsg:    "invalid transaction ID",
		},
		{
			name:        "Invalid LSN format",
			targetLSN:   "invalid-lsn",
			action:      "promote",
			timeline:    "latest",
			inclusive:   true,
			expectError: true,
			errorMsg:    "invalid LSN format",
		},
		{
			name:        "Invalid action",
			targetTime:  "2024-11-26 12:00:00",
			action:      "invalid",
			timeline:    "latest",
			inclusive:   true,
			expectError: true,
			errorMsg:    "invalid recovery action",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			target, err := pitr.ParseRecoveryTarget(
				tt.targetTime,
				tt.targetXID,
				tt.targetLSN,
				tt.targetName,
				tt.immediate,
				tt.action,
				tt.timeline,
				tt.inclusive,
			)

			if tt.expectError {
				if err == nil {
					t.Errorf("Expected error containing '%s', got nil", tt.errorMsg)
				} else if tt.errorMsg != "" && !contains(err.Error(), tt.errorMsg) {
					t.Errorf("Expected error containing '%s', got '%s'", tt.errorMsg, err.Error())
				}
			} else {
				if err != nil {
					t.Errorf("Unexpected error: %v", err)
				}
				if target == nil {
					t.Error("Expected target, got nil")
				}
			}
		})
	}
}

// TestRecoveryTargetToConfig tests conversion to PostgreSQL config
func TestRecoveryTargetToConfig(t *testing.T) {
	tests := []struct {
		name           string
		target         *pitr.RecoveryTarget
		expectedKeys   []string
		expectedValues map[string]string
	}{
		{
			name: "Time target",
			target: &pitr.RecoveryTarget{
				Type:      "time",
				Value:     "2024-11-26 12:00:00",
				Action:    "promote",
				Timeline:  "latest",
				Inclusive: true,
			},
			expectedKeys: []string{"recovery_target_time", "recovery_target_action", "recovery_target_timeline", "recovery_target_inclusive"},
			expectedValues: map[string]string{
				"recovery_target_time":      "2024-11-26 12:00:00",
				"recovery_target_action":    "promote",
				"recovery_target_timeline":  "latest",
				"recovery_target_inclusive": "true",
			},
		},
		{
			name: "XID target",
			target: &pitr.RecoveryTarget{
				Type:      "xid",
				Value:     "1000000",
				Action:    "pause",
				Timeline:  "2",
				Inclusive: false,
			},
			expectedKeys: []string{"recovery_target_xid", "recovery_target_action", "recovery_target_timeline", "recovery_target_inclusive"},
			expectedValues: map[string]string{
				"recovery_target_xid":       "1000000",
				"recovery_target_action":    "pause",
				"recovery_target_timeline":  "2",
				"recovery_target_inclusive": "false",
			},
		},
		{
			name: "Immediate target",
			target: &pitr.RecoveryTarget{
				Type:     "immediate",
				Value:    "immediate",
				Action:   "promote",
				Timeline: "latest",
			},
			expectedKeys: []string{"recovery_target", "recovery_target_action", "recovery_target_timeline"},
			expectedValues: map[string]string{
				"recovery_target":          "immediate",
				"recovery_target_action":   "promote",
				"recovery_target_timeline": "latest",
			},
		},
	}

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

			// Check all expected keys are present
			for _, key := range tt.expectedKeys {
				if _, exists := config[key]; !exists {
					t.Errorf("Missing expected key: %s", key)
				}
			}

			// Check expected values
			for key, expectedValue := range tt.expectedValues {
				if actualValue, exists := config[key]; !exists {
					t.Errorf("Missing key: %s", key)
				} else if actualValue != expectedValue {
					t.Errorf("Key %s: expected '%s', got '%s'", key, expectedValue, actualValue)
				}
			}
		})
	}
}

// TestWALArchiving tests WAL file archiving
func TestWALArchiving(t *testing.T) {
	// Create temp directories
	tempDir := t.TempDir()
	walArchiveDir := filepath.Join(tempDir, "wal_archive")
	if err := os.MkdirAll(walArchiveDir, 0700); err != nil {
		t.Fatalf("Failed to create WAL archive dir: %v", err)
	}

	// Create a mock WAL file
	walDir := filepath.Join(tempDir, "wal")
	if err := os.MkdirAll(walDir, 0700); err != nil {
		t.Fatalf("Failed to create WAL dir: %v", err)
	}

	walFileName := "000000010000000000000001"
	walFilePath := filepath.Join(walDir, walFileName)
	walContent := []byte("mock WAL file content for testing")
	if err := os.WriteFile(walFilePath, walContent, 0600); err != nil {
		t.Fatalf("Failed to create mock WAL file: %v", err)
	}

	// Create archiver
	cfg := &config.Config{}
	log := logger.New("info", "text")
	archiver := wal.NewArchiver(cfg, log)

	// Test plain archiving
	t.Run("Plain archiving", func(t *testing.T) {
		archiveConfig := wal.ArchiveConfig{
			ArchiveDir:  walArchiveDir,
			CompressWAL: false,
			EncryptWAL:  false,
		}

		ctx := context.Background()
		info, err := archiver.ArchiveWALFile(ctx, walFilePath, walFileName, archiveConfig)
		if err != nil {
			t.Fatalf("Archiving failed: %v", err)
		}

		if info.WALFileName != walFileName {
			t.Errorf("Expected WAL filename %s, got %s", walFileName, info.WALFileName)
		}

		if info.OriginalSize != int64(len(walContent)) {
			t.Errorf("Expected size %d, got %d", len(walContent), info.OriginalSize)
		}

		// Verify archived file exists
		archivedPath := filepath.Join(walArchiveDir, walFileName)
		if _, err := os.Stat(archivedPath); err != nil {
			t.Errorf("Archived file not found: %v", err)
		}
	})

	// Test compressed archiving
	t.Run("Compressed archiving", func(t *testing.T) {
		walFileName2 := "000000010000000000000002"
		walFilePath2 := filepath.Join(walDir, walFileName2)
		if err := os.WriteFile(walFilePath2, walContent, 0600); err != nil {
			t.Fatalf("Failed to create mock WAL file: %v", err)
		}

		archiveConfig := wal.ArchiveConfig{
			ArchiveDir:  walArchiveDir,
			CompressWAL: true,
			EncryptWAL:  false,
		}

		ctx := context.Background()
		info, err := archiver.ArchiveWALFile(ctx, walFilePath2, walFileName2, archiveConfig)
		if err != nil {
			t.Fatalf("Compressed archiving failed: %v", err)
		}

		if !info.Compressed {
			t.Error("Expected compressed flag to be true")
		}

		// Verify compressed file exists
		archivedPath := filepath.Join(walArchiveDir, walFileName2+".gz")
		if _, err := os.Stat(archivedPath); err != nil {
			t.Errorf("Compressed archived file not found: %v", err)
		}
	})
}

// TestWALParsing tests WAL filename parsing
func TestWALParsing(t *testing.T) {
	tests := []struct {
		name             string
		walFileName      string
		expectedTimeline uint32
		expectedSegment  uint64
		expectError      bool
	}{
		{
			name:             "Valid WAL filename",
			walFileName:      "000000010000000000000001",
			expectedTimeline: 1,
			expectedSegment:  1,
			expectError:      false,
		},
		{
			name:             "Timeline 2",
			walFileName:      "000000020000000000000005",
			expectedTimeline: 2,
			expectedSegment:  5,
			expectError:      false,
		},
		{
			name:             "High segment number",
			walFileName:      "00000001000000000000FFFF",
			expectedTimeline: 1,
			expectedSegment:  0xFFFF,
			expectError:      false,
		},
		{
			name:        "Too short",
			walFileName: "00000001",
			expectError: true,
		},
		{
			name:        "Invalid hex",
			walFileName: "GGGGGGGGGGGGGGGGGGGGGGGG",
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			timeline, segment, err := wal.ParseWALFileName(tt.walFileName)

			if tt.expectError {
				if err == nil {
					t.Error("Expected error, got nil")
				}
			} else {
				if err != nil {
					t.Errorf("Unexpected error: %v", err)
				}
				if timeline != tt.expectedTimeline {
					t.Errorf("Expected timeline %d, got %d", tt.expectedTimeline, timeline)
				}
				if segment != tt.expectedSegment {
					t.Errorf("Expected segment %d, got %d", tt.expectedSegment, segment)
				}
			}
		})
	}
}

// TestTimelineManagement tests timeline parsing and validation
func TestTimelineManagement(t *testing.T) {
	// Create temp directory with mock timeline files
	tempDir := t.TempDir()

	// Create timeline history files
	history2 := "1\t0/3000000\tno recovery target specified\n"
	if err := os.WriteFile(filepath.Join(tempDir, "00000002.history"), []byte(history2), 0600); err != nil {
		t.Fatalf("Failed to create history file: %v", err)
	}

	history3 := "2\t0/5000000\trecovery target reached\n"
	if err := os.WriteFile(filepath.Join(tempDir, "00000003.history"), []byte(history3), 0600); err != nil {
		t.Fatalf("Failed to create history file: %v", err)
	}

	// Create mock WAL files
	walFiles := []string{
		"000000010000000000000001",
		"000000010000000000000002",
		"000000020000000000000001",
		"000000030000000000000001",
	}
	for _, walFile := range walFiles {
		if err := os.WriteFile(filepath.Join(tempDir, walFile), []byte("mock"), 0600); err != nil {
			t.Fatalf("Failed to create WAL file: %v", err)
		}
	}

	// Create timeline manager
	log := logger.New("info", "text")
	tm := wal.NewTimelineManager(log)

	// Parse timeline history
	ctx := context.Background()
	history, err := tm.ParseTimelineHistory(ctx, tempDir)
	if err != nil {
		t.Fatalf("Failed to parse timeline history: %v", err)
	}

	// Validate timeline count
	if len(history.Timelines) < 3 {
		t.Errorf("Expected at least 3 timelines, got %d", len(history.Timelines))
	}

	// Validate timeline 2
	tl2, exists := history.TimelineMap[2]
	if !exists {
		t.Fatal("Timeline 2 not found")
	}
	if tl2.ParentTimeline != 1 {
		t.Errorf("Expected timeline 2 parent to be 1, got %d", tl2.ParentTimeline)
	}
	if tl2.SwitchPoint != "0/3000000" {
		t.Errorf("Expected switch point '0/3000000', got '%s'", tl2.SwitchPoint)
	}

	// Validate timeline 3
	tl3, exists := history.TimelineMap[3]
	if !exists {
		t.Fatal("Timeline 3 not found")
	}
	if tl3.ParentTimeline != 2 {
		t.Errorf("Expected timeline 3 parent to be 2, got %d", tl3.ParentTimeline)
	}

	// Validate consistency
	if err := tm.ValidateTimelineConsistency(ctx, history); err != nil {
		t.Errorf("Timeline consistency validation failed: %v", err)
	}

	// Test timeline path
	path, err := tm.GetTimelinePath(history, 3)
	if err != nil {
		t.Fatalf("Failed to get timeline path: %v", err)
	}
	if len(path) != 3 {
		t.Errorf("Expected timeline path length 3, got %d", len(path))
	}
	if path[0].TimelineID != 1 || path[1].TimelineID != 2 || path[2].TimelineID != 3 {
		t.Error("Timeline path order incorrect")
	}
}

// TestRecoveryConfigGeneration tests recovery configuration file generation
func TestRecoveryConfigGeneration(t *testing.T) {
	tempDir := t.TempDir()

	// Create mock PostgreSQL data directory
	dataDir := filepath.Join(tempDir, "pgdata")
	if err := os.MkdirAll(dataDir, 0700); err != nil {
		t.Fatalf("Failed to create data dir: %v", err)
	}

	// Create PG_VERSION file
	if err := os.WriteFile(filepath.Join(dataDir, "PG_VERSION"), []byte("14\n"), 0600); err != nil {
		t.Fatalf("Failed to create PG_VERSION: %v", err)
	}

	log := logger.New("info", "text")
	configGen := pitr.NewRecoveryConfigGenerator(log)

	// Test version detection
	t.Run("Version detection", func(t *testing.T) {
		version, err := configGen.DetectPostgreSQLVersion(dataDir)
		if err != nil {
			t.Fatalf("Version detection failed: %v", err)
		}
		if version != 14 {
			t.Errorf("Expected version 14, got %d", version)
		}
	})

	// Test modern config generation (PG 12+)
	t.Run("Modern config generation", func(t *testing.T) {
		target := &pitr.RecoveryTarget{
			Type:      "time",
			Value:     "2024-11-26 12:00:00",
			Action:    "promote",
			Timeline:  "latest",
			Inclusive: true,
		}

		config := &pitr.RecoveryConfig{
			Target:            target,
			WALArchiveDir:     "/tmp/wal",
			PostgreSQLVersion: 14,
			DataDir:           dataDir,
		}

		err := configGen.GenerateRecoveryConfig(config)
		if err != nil {
			t.Fatalf("Config generation failed: %v", err)
		}

		// Verify recovery.signal exists
		recoverySignal := filepath.Join(dataDir, "recovery.signal")
		if _, err := os.Stat(recoverySignal); err != nil {
			t.Errorf("recovery.signal not created: %v", err)
		}

		// Verify postgresql.auto.conf exists
		autoConf := filepath.Join(dataDir, "postgresql.auto.conf")
		if _, err := os.Stat(autoConf); err != nil {
			t.Errorf("postgresql.auto.conf not created: %v", err)
		}

		// Read and verify content
		content, err := os.ReadFile(autoConf)
		if err != nil {
			t.Fatalf("Failed to read postgresql.auto.conf: %v", err)
		}

		contentStr := string(content)
		if !contains(contentStr, "recovery_target_time") {
			t.Error("Config missing recovery_target_time")
		}
		if !contains(contentStr, "recovery_target_action") {
			t.Error("Config missing recovery_target_action")
		}
	})

	// Test legacy config generation (PG < 12)
	t.Run("Legacy config generation", func(t *testing.T) {
		dataDir11 := filepath.Join(tempDir, "pgdata11")
		if err := os.MkdirAll(dataDir11, 0700); err != nil {
			t.Fatalf("Failed to create data dir: %v", err)
		}
		if err := os.WriteFile(filepath.Join(dataDir11, "PG_VERSION"), []byte("11\n"), 0600); err != nil {
			t.Fatalf("Failed to create PG_VERSION: %v", err)
		}

		target := &pitr.RecoveryTarget{
			Type:      "xid",
			Value:     "1000000",
			Action:    "pause",
			Timeline:  "latest",
			Inclusive: false,
		}

		config := &pitr.RecoveryConfig{
			Target:            target,
			WALArchiveDir:     "/tmp/wal",
			PostgreSQLVersion: 11,
			DataDir:           dataDir11,
		}

		err := configGen.GenerateRecoveryConfig(config)
		if err != nil {
			t.Fatalf("Legacy config generation failed: %v", err)
		}

		// Verify recovery.conf exists
		recoveryConf := filepath.Join(dataDir11, "recovery.conf")
		if _, err := os.Stat(recoveryConf); err != nil {
			t.Errorf("recovery.conf not created: %v", err)
		}

		// Read and verify content
		content, err := os.ReadFile(recoveryConf)
		if err != nil {
			t.Fatalf("Failed to read recovery.conf: %v", err)
		}

		contentStr := string(content)
		if !contains(contentStr, "recovery_target_xid") {
			t.Error("Config missing recovery_target_xid")
		}
		if !contains(contentStr, "1000000") {
			t.Error("Config missing XID value")
		}
	})
}

// TestDataDirectoryValidation tests data directory validation
func TestDataDirectoryValidation(t *testing.T) {
	log := logger.New("info", "text")
	configGen := pitr.NewRecoveryConfigGenerator(log)

	t.Run("Valid empty directory", func(t *testing.T) {
		tempDir := t.TempDir()
		dataDir := filepath.Join(tempDir, "pgdata")
		if err := os.MkdirAll(dataDir, 0700); err != nil {
			t.Fatalf("Failed to create data dir: %v", err)
		}

		// Create PG_VERSION to make it look like a PG directory
		if err := os.WriteFile(filepath.Join(dataDir, "PG_VERSION"), []byte("14\n"), 0600); err != nil {
			t.Fatalf("Failed to create PG_VERSION: %v", err)
		}

		err := configGen.ValidateDataDirectory(dataDir)
		if err != nil {
			t.Errorf("Validation failed for valid directory: %v", err)
		}
	})

	t.Run("Non-existent directory", func(t *testing.T) {
		err := configGen.ValidateDataDirectory("/nonexistent/path")
		if err == nil {
			t.Error("Expected error for non-existent directory")
		}
	})

	t.Run("PostgreSQL running", func(t *testing.T) {
		tempDir := t.TempDir()
		dataDir := filepath.Join(tempDir, "pgdata_running")
		if err := os.MkdirAll(dataDir, 0700); err != nil {
			t.Fatalf("Failed to create data dir: %v", err)
		}

		// Create postmaster.pid to simulate running PostgreSQL
		if err := os.WriteFile(filepath.Join(dataDir, "postmaster.pid"), []byte("12345\n"), 0600); err != nil {
			t.Fatalf("Failed to create postmaster.pid: %v", err)
		}

		err := configGen.ValidateDataDirectory(dataDir)
		if err == nil {
			t.Error("Expected error for running PostgreSQL")
		}
		if !contains(err.Error(), "currently running") {
			t.Errorf("Expected 'currently running' error, got: %v", err)
		}
	})
}

// Helper function
func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
		(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
			len(s) > len(substr)+1 && containsMiddle(s, substr)))
}

func containsMiddle(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}

// Benchmark tests
func BenchmarkWALArchiving(b *testing.B) {
	tempDir := b.TempDir()
	walArchiveDir := filepath.Join(tempDir, "wal_archive")
	os.MkdirAll(walArchiveDir, 0700)

	walDir := filepath.Join(tempDir, "wal")
	os.MkdirAll(walDir, 0700)

	// Create a 16MB mock WAL file (typical size)
	walContent := make([]byte, 16*1024*1024)
	walFilePath := filepath.Join(walDir, "000000010000000000000001")
	os.WriteFile(walFilePath, walContent, 0600)

	cfg := &config.Config{}
	log := logger.New("info", "text")
	archiver := wal.NewArchiver(cfg, log)

	archiveConfig := wal.ArchiveConfig{
		ArchiveDir:  walArchiveDir,
		CompressWAL: false,
		EncryptWAL:  false,
	}

	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		ctx := context.Background()
		archiver.ArchiveWALFile(ctx, walFilePath, "000000010000000000000001", archiveConfig)
	}
}

func BenchmarkRecoveryTargetParsing(b *testing.B) {
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		pitr.ParseRecoveryTarget(
			"2024-11-26 12:00:00",
			"",
			"",
			"",
			false,
			"promote",
			"latest",
			true,
		)
	}
}
