//go:build test

package user

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"crussell/clock"
	"crussell/mw"
	"crussell/testutils"
	"crussell/testutils/fixtures"
	"crussell/testutils/jwt"
)

// ============================================================
// GetGDPRExportHandler Tests
// ============================================================

func TestGDPRExport_NoAuth(t *testing.T) {
	t.Parallel()

	req := httptest.NewRequest(http.MethodGet, "/api/user/gdpr-export", nil)
	rr := httptest.NewRecorder()
	GetGDPRExportHandler(rr, req)

	if rr.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401, got %d", rr.Code)
	}
}

func TestGDPRExport_CacheMiss(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	req := httptest.NewRequest(http.MethodGet, "/api/user/gdpr-export", nil)
	req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
	req.Header.Set("Authorization", "Bearer "+token)

	rr := httptest.NewRecorder()
	GetGDPRExportHandler(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
		t.Logf("response body: %s", rr.Body.String())
		return
	}

	cacheHeader := rr.Header().Get("X-Cache")
	if cacheHeader != "MISS" {
		t.Errorf("expected X-Cache header 'MISS', got %q", cacheHeader)
	}

	var resp map[string]string
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}
	if resp["status"] != "generating" {
		t.Errorf("expected status 'generating', got %q", resp["status"])
	}
}

func TestGDPRExport_CacheHit(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	testData := json.RawMessage(`{"user_profile":{"id":"` + userID + `","first_name":"Test"}}`)
	gdprExportCacheMu.Lock()
	gdprExportCache[userID] = &gdprCacheEntry{
		data:      testData,
		expiresAt: clock.Now().Add(12 * time.Hour),
	}
	gdprExportCacheMu.Unlock()

	req := httptest.NewRequest(http.MethodGet, "/api/user/gdpr-export", nil)
	req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
	req.Header.Set("Authorization", "Bearer "+token)

	rr := httptest.NewRecorder()
	GetGDPRExportHandler(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
		t.Logf("response body: %s", rr.Body.String())
		return
	}

	cacheHeader := rr.Header().Get("X-Cache")
	if cacheHeader != "HIT" {
		t.Errorf("expected X-Cache header 'HIT', got %q", cacheHeader)
	}

	if rr.Body.String() != string(testData) {
		t.Errorf("expected cached data, got %s", rr.Body.String())
	}
}

func TestGDPRExport_CacheGenerating(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	gdprExportCacheMu.Lock()
	gdprExportCache[userID] = &gdprCacheEntry{
		generating: true,
		expiresAt:  clock.Now().Add(12 * time.Hour),
	}
	gdprExportCacheMu.Unlock()

	req := httptest.NewRequest(http.MethodGet, "/api/user/gdpr-export", nil)
	req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
	req.Header.Set("Authorization", "Bearer "+token)

	rr := httptest.NewRecorder()
	GetGDPRExportHandler(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
		t.Logf("response body: %s", rr.Body.String())
		return
	}

	cacheHeader := rr.Header().Get("X-Cache")
	if cacheHeader != "GENERATING" {
		t.Errorf("expected X-Cache header 'GENERATING', got %q", cacheHeader)
	}

	var resp map[string]string
	if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}
	if resp["status"] != "generating" {
		t.Errorf("expected status 'generating', got %q", resp["status"])
	}
}

func TestGDPRExport_ExpiredCacheTriggersRegeneration(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	gdprExportCacheMu.Lock()
	gdprExportCache[userID] = &gdprCacheEntry{
		data:      json.RawMessage(`{"old":"data"}`),
		expiresAt: clock.Now().Add(-1 * time.Hour),
	}
	gdprExportCacheMu.Unlock()

	req := httptest.NewRequest(http.MethodGet, "/api/user/gdpr-export", nil)
	req = req.WithContext(context.WithValue(ctx, mw.UserIDKey, userID))
	req.Header.Set("Authorization", "Bearer "+token)

	rr := httptest.NewRecorder()
	GetGDPRExportHandler(rr, req)

	if rr.Code != http.StatusOK {
		t.Errorf("expected status 200, got %d", rr.Code)
		return
	}

	cacheHeader := rr.Header().Get("X-Cache")
	if cacheHeader != "MISS" {
		t.Errorf("expected X-Cache header 'MISS' for expired cache, got %q", cacheHeader)
	}
}

// ============================================================
// SQL anonymize_user() Child Table Scrubbing Tests
// ============================================================

func TestAnonymizeUser_ScrubsSocialLogins(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO user_social_logins (user_id, provider, immutable_id)
		VALUES ($1, 'google', 'google-123'), ($1, 'microsoft', 'ms-456')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert social logins: %v", err)
	}

	var count int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM user_social_logins WHERE user_id = $1`, userID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to count social logins: %v", err)
	}
	if count != 2 {
		t.Fatalf("expected 2 social logins before anonymization, got %d", count)
	}

	_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
	if err != nil {
		t.Fatalf("anonymize_user failed: %v", err)
	}

	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM user_social_logins WHERE user_id = $1`, userID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to count social logins after anonymization: %v", err)
	}
	if count != 0 {
		t.Errorf("expected 0 social logins after anonymization, got %d", count)
	}
}

func TestAnonymizeUser_SoftDeletesSavedCards(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO user_saved_cards (user_id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default)
		VALUES ($1, 'sq_card_123', 'Visa', '4242', 12, 2030, 'fp_abc123', true)
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert saved card: %v", err)
	}

	_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
	if err != nil {
		t.Fatalf("anonymize_user failed: %v", err)
	}

	var deletedAt, fingerprint interface{}
	var last4 string
	var expMonth, expYear int
	err = tx.QueryRow(ctx, `
		SELECT deleted_at, last_4, fingerprint, exp_month, exp_year
		FROM user_saved_cards WHERE user_id = $1
	`, userID).Scan(&deletedAt, &last4, &fingerprint, &expMonth, &expYear)
	if err != nil {
		t.Fatalf("failed to query saved card after anonymization: %v", err)
	}

	if deletedAt == nil {
		t.Error("expected deleted_at to be set after anonymization")
	}
	if last4 != "XXXX" {
		t.Errorf("expected last_4 to be 'XXXX', got %q", last4)
	}
	if fingerprint != nil {
		t.Errorf("expected fingerprint to be NULL, got %v", fingerprint)
	}
	if expMonth != 1 {
		t.Errorf("expected exp_month to be 1, got %d", expMonth)
	}
	if expYear != 2000 {
		t.Errorf("expected exp_year to be 2000, got %d", expYear)
	}
}

func TestAnonymizeUser_ExpiresVerificationCodes(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO verification_codes (user_id, purpose, code, used_at, expires_at)
		VALUES ($1, 'email_verify', 'CODE1', NOW(), NOW() + INTERVAL '1 hour'),
		       ($1, 'password_reset', 'CODE2', NULL, NOW() + INTERVAL '1 hour')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert verification codes: %v", err)
	}

	_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
	if err != nil {
		t.Fatalf("anonymize_user failed: %v", err)
	}

	var pendingCount int
	err = tx.QueryRow(ctx, `
		SELECT COUNT(*) FROM verification_codes WHERE user_id = $1 AND used_at IS NULL
	`, userID).Scan(&pendingCount)
	if err != nil {
		t.Fatalf("failed to count pending verification codes: %v", err)
	}
	if pendingCount != 0 {
		t.Errorf("expected 0 pending verification codes after anonymization, got %d", pendingCount)
	}
}

func TestAnonymizeUser_ScrubsTimeBlockerReservations(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES (NOW(), 60, 'RESERVATION:user:slot123', $1),
		       (NOW() + INTERVAL '1 hour', 30, 'RESERVATION:user:slot456', $1),
		       (NOW() + INTERVAL '2 hours', 45, 'Admin lunch break', $1)
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert time blockers: %v", err)
	}

	_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
	if err != nil {
		t.Fatalf("anonymize_user failed: %v", err)
	}

	var reservationCount int
	err = tx.QueryRow(ctx, `
		SELECT COUNT(*) FROM time_blockers WHERE created_by = $1 AND description LIKE 'RESERVATION:user:%%'
	`, userID).Scan(&reservationCount)
	if err != nil {
		t.Fatalf("failed to count reservation time blockers: %v", err)
	}
	if reservationCount != 0 {
		t.Errorf("expected 0 RESERVATION time blockers after anonymization, got %d", reservationCount)
	}

	var adminBreakDesc string
	err = tx.QueryRow(ctx, `
		SELECT description FROM time_blockers WHERE created_by = $1 AND description = 'Admin lunch break'
	`, userID).Scan(&adminBreakDesc)
	if err != nil {
		t.Errorf("expected non-RESERVATION time blocker to be preserved, got error: %v", err)
	}
	if adminBreakDesc != "Admin lunch break" {
		t.Errorf("expected 'Admin lunch break' to be preserved, got %q", adminBreakDesc)
	}
}

func TestAnonymizeUser_ScrubsEditRequestNotes(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	serviceID, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create service: %v", err)
	}

	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, notes)
		VALUES ($1, $2, NOW() + INTERVAL '1 day', 'Please move my appointment, I have a conflict')
	`, bookingID, userID)
	if err != nil {
		t.Fatalf("failed to insert edit request: %v", err)
	}

	_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
	if err != nil {
		t.Fatalf("anonymize_user failed: %v", err)
	}

	var notes interface{}
	err = tx.QueryRow(ctx, `
		SELECT notes FROM booking_edit_requests WHERE requested_by = $1
	`, userID).Scan(&notes)
	if err != nil {
		t.Fatalf("failed to query edit request notes: %v", err)
	}
	if notes != nil {
		t.Errorf("expected edit request notes to be NULL after anonymization, got %v", notes)
	}
}

func TestAnonymizeUser_ClearsNotificationPrefs(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO user_notification_preferences (user_id, email_enabled, sms_enabled, browser_push_enabled)
		VALUES ($1, true, false, true)
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert notification preferences: %v", err)
	}

	_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
	if err != nil {
		t.Fatalf("anonymize_user failed: %v", err)
	}

	var count int
	err = tx.QueryRow(ctx, `
		SELECT COUNT(*) FROM user_notification_preferences WHERE user_id = $1
	`, userID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to count notification preferences: %v", err)
	}
	if count != 0 {
		t.Errorf("expected 0 notification preferences after anonymization, got %d", count)
	}
}

func TestAnonymizeUser_DoesNotAffectGuests(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestGuestUser(tx)
	if err != nil {
		t.Fatalf("failed to create guest user: %v", err)
	}

	var firstName string
	err = tx.QueryRow(ctx, `SELECT n_first_name FROM users WHERE id = $1`, userID).Scan(&firstName)
	if err != nil {
		t.Fatalf("failed to query guest user before anonymization: %v", err)
	}

	_, err = tx.Exec(ctx, `SELECT anonymize_user($1)`, userID)
	if err != nil {
		t.Fatalf("anonymize_user failed: %v", err)
	}

	var firstNameAfter string
	err = tx.QueryRow(ctx, `SELECT n_first_name FROM users WHERE id = $1`, userID).Scan(&firstNameAfter)
	if err != nil {
		t.Fatalf("failed to query guest user after anonymization: %v", err)
	}

	if firstNameAfter != firstName {
		t.Errorf("expected guest user first name to remain %q, got %q", firstName, firstNameAfter)
	}
}

// ============================================================
// SQL export_all_user_data() Comprehensive Export Tests
// ============================================================

func TestExportAllUserData_BasicProfile(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	profile, ok := data["user_profile"].(map[string]interface{})
	if !ok {
		t.Fatal("expected user_profile section in export")
	}
	if profile["id"] != userID {
		t.Errorf("expected user_profile.id to be %q, got %v", userID, profile["id"])
	}
	if profile["first_name"] != "Test" {
		t.Errorf("expected user_profile.first_name to be 'Test', got %v", profile["first_name"])
	}
	if profile["last_name"] != "User" {
		t.Errorf("expected user_profile.last_name to be 'User', got %v", profile["last_name"])
	}
}

func TestExportAllUserData_BookingsWithOverrides(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	serviceID, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create service: %v", err)
	}

	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		UPDATE booking_services SET override_price = 75.00 WHERE booking_id = $1 AND service_id = $2
	`, bookingID, serviceID)
	if err != nil {
		t.Fatalf("failed to set override price: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	bookings, ok := data["bookings"].([]interface{})
	if !ok {
		t.Fatal("expected bookings section in export")
	}
	if len(bookings) == 0 {
		t.Fatal("expected at least one booking in export")
	}

	booking := bookings[0].(map[string]interface{})
	if booking["booking_id"] != bookingID {
		t.Errorf("expected booking_id %q, got %v", bookingID, booking["booking_id"])
	}

	totalPrice := booking["total_price"]
	switch v := totalPrice.(type) {
	case string:
		if v != "75.00" {
			t.Errorf("expected total_price '75.00' (override), got %q", v)
		}
	case float64:
		if v != 75.00 {
			t.Errorf("expected total_price 75.00 (override), got %v", v)
		}
	default:
		t.Errorf("unexpected total_price type %T: %v", totalPrice, totalPrice)
	}

	services := booking["services"].([]interface{})
	if len(services) == 0 {
		t.Fatal("expected at least one service in booking")
	}
	service := services[0].(map[string]interface{})
	svcPrice := service["price"]
	switch v := svcPrice.(type) {
	case string:
		if v != "75.00" {
			t.Errorf("expected service price '75.00' (override), got %v", v)
		}
	case float64:
		if v != 75.00 {
			t.Errorf("expected service price 75.00 (override), got %v", v)
		}
	default:
		t.Errorf("unexpected service price type %T: %v", svcPrice, svcPrice)
	}
}

func TestExportAllUserData_PaymentsAndRefunds(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	serviceID, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create service: %v", err)
	}

	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	paymentID, err := fixtures.CreateTestPayment(tx, bookingID, 50.00, "cash", "full", "completed")
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	refundID, err := fixtures.CreateTestRefund(tx, paymentID, bookingID, 25.00)
	if err != nil {
		t.Fatalf("failed to create refund: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	payments, ok := data["payments"].([]interface{})
	if !ok {
		t.Fatal("expected payments section in export")
	}
	if len(payments) != 1 {
		t.Fatalf("expected 1 payment, got %d", len(payments))
	}

	payment := payments[0].(map[string]interface{})
	if payment["payment_id"] != paymentID {
		t.Errorf("expected payment_id %q, got %v", paymentID, payment["payment_id"])
	}

	refunds, ok := data["refunds"].([]interface{})
	if !ok {
		t.Fatal("expected refunds section in export")
	}
	if len(refunds) != 1 {
		t.Fatalf("expected 1 refund, got %d", len(refunds))
	}

	refund := refunds[0].(map[string]interface{})
	if refund["refund_id"] != refundID {
		t.Errorf("expected refund_id %q, got %v", refundID, refund["refund_id"])
	}
}

func TestExportAllUserData_SavedCards(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = fixtures.CreateTestPaymentMethod(tx, userID, "sq_card_test", "Visa", "4242")
	if err != nil {
		t.Fatalf("failed to create payment method: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	cards, ok := data["saved_cards"].([]interface{})
	if !ok {
		t.Fatal("expected saved_cards section in export")
	}
	if len(cards) != 1 {
		t.Fatalf("expected 1 saved card, got %d", len(cards))
	}

	card := cards[0].(map[string]interface{})
	if card["brand"] != "Visa" {
		t.Errorf("expected card brand 'Visa', got %v", card["brand"])
	}
	if card["last_4"] != "4242" {
		t.Errorf("expected card last_4 '4242', got %v", card["last_4"])
	}
}

func TestExportAllUserData_EmptySectionsReturnEmptyArrays(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	emptySections := []string{
		"bookings", "payments", "patch_tests", "saved_cards", "refunds",
		"social_logins", "loyalty_redemptions", "booking_discounts",
		"edit_requests", "affiliate_payouts", "forgiven_no_shows", "gift_cards", "admin_audit_log",
		"gift_card_transactions", "name_history", "referral_discounts",
		"login_audit", "refresh_tokens",
	}

	for _, section := range emptySections {
		val, ok := data[section]
		if !ok {
			t.Errorf("expected %q section in export", section)
			continue
		}
		arr, ok := val.([]interface{})
		if !ok {
			t.Errorf("expected %q to be an array, got %T", section, val)
			continue
		}
		if len(arr) != 0 {
			t.Errorf("expected %q to be empty array, got %d items", section, len(arr))
		}
	}

	// gift_card_balance is a JSON object (not array), check separately.
	if gb, ok := data["gift_card_balance"]; !ok {
		t.Error("expected 'gift_card_balance' section in export")
	} else if gbMap, ok := gb.(map[string]interface{}); !ok {
		t.Errorf("expected 'gift_card_balance' to be an object, got %T", gb)
	} else if gbMap["balance"] != nil && gbMap["balance"].(float64) != 0 {
		t.Errorf("expected gift_card_balance to be 0, got %v", gbMap["balance"])
	}
}

func TestExportAllUserData_ExportMetadata(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	metadata, ok := data["export_metadata"].(map[string]interface{})
	if !ok {
		t.Fatal("expected export_metadata section in export")
	}
	if metadata["user_id"] != userID {
		t.Errorf("expected export_metadata.user_id %q, got %v", userID, metadata["user_id"])
	}
	if metadata["format_version"] != "1.0" {
		t.Errorf("expected format_version '1.0', got %v", metadata["format_version"])
	}
	if metadata["exported_by"] != "system" {
		t.Errorf("expected exported_by 'system', got %v", metadata["exported_by"])
	}
	if metadata["exported_at"] == nil {
		t.Error("expected exported_at to be set")
	}
}

func TestExportAllUserData_NotificationPreferences(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO user_notification_preferences (user_id, email_enabled, sms_enabled, browser_push_enabled)
		VALUES ($1, true, false, true)
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert notification preferences: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	prefs, ok := data["notification_preferences"].([]interface{})
	if !ok {
		t.Fatal("expected notification_preferences section in export")
	}
	if len(prefs) != 1 {
		t.Fatalf("expected 1 notification preference, got %d", len(prefs))
	}

	pref := prefs[0].(map[string]interface{})
	if pref["email_enabled"] != true {
		t.Errorf("expected email_enabled true, got %v", pref["email_enabled"])
	}
	if pref["sms_enabled"] != false {
		t.Errorf("expected sms_enabled false, got %v", pref["sms_enabled"])
	}
	if pref["browser_push_enabled"] != true {
		t.Errorf("expected browser_push_enabled true, got %v", pref["browser_push_enabled"])
	}
}

func TestExportAllUserData_VerificationCodes(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO verification_codes (user_id, purpose, code, used_at, expires_at)
		VALUES ($1, 'email_verify', 'CODE1', NOW(), NOW() + INTERVAL '1 hour'),
		       ($1, 'password_reset', 'CODE2', NULL, NOW() + INTERVAL '1 hour')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert verification codes: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	// Verification codes are authentication tokens, not personal data,
	// so they SHOULD be excluded from the SAR export (GDPR Art 15).
	_, exists := data["verification_codes"]
	if exists {
		t.Fatal("verification_codes should not be included in GDPR export (authentication tokens)")
	}
}

func TestExportAllUserData_EditRequests(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	serviceID, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create service: %v", err)
	}

	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, notes, has_overrides)
		VALUES ($1, $2, NOW() + INTERVAL '1 day', 'Please reschedule', false)
	`, bookingID, userID)
	if err != nil {
		t.Fatalf("failed to insert edit request: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	editRequests, ok := data["edit_requests"].([]interface{})
	if !ok {
		t.Fatal("expected edit_requests section in export")
	}
	if len(editRequests) != 1 {
		t.Fatalf("expected 1 edit request, got %d", len(editRequests))
	}

	er := editRequests[0].(map[string]interface{})
	if er["notes"] != "Please reschedule" {
		t.Errorf("expected edit request notes 'Please reschedule', got %v", er["notes"])
	}
}

func TestExportAllUserData_ForgivenNoShows(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	serviceID, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create service: %v", err)
	}

	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO forgiven_no_shows (booking_id, created_at)
		VALUES ($1, NOW())
	`, bookingID)
	if err != nil {
		t.Fatalf("failed to insert forgiven no-show: %v", err)
	}

	var result json.RawMessage
	err = tx.QueryRow(ctx, `SELECT export_all_user_data($1)`, userID).Scan(&result)
	if err != nil {
		t.Fatalf("export_all_user_data failed: %v", err)
	}

	var data map[string]interface{}
	if err := json.Unmarshal(result, &data); err != nil {
		t.Fatalf("failed to unmarshal export result: %v", err)
	}

	noShows, ok := data["forgiven_no_shows"].([]interface{})
	if !ok {
		t.Fatal("expected forgiven_no_shows section in export")
	}
	if len(noShows) != 1 {
		t.Fatalf("expected 1 forgiven no-show, got %d", len(noShows))
	}

	ns := noShows[0].(map[string]interface{})
	if ns["booking_id"] != bookingID {
		t.Errorf("expected forgiven_no_show booking_id %q, got %v", bookingID, ns["booking_id"])
	}
}

// ============================================================
// AnonymizeStaleGuestAccounts Tests (SQL behavior)
// ============================================================

func TestAnonymizeStaleGuestAccounts_ScrubsAdditionalFields(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	guestID, err := fixtures.CreateTestGuestUser(tx)
	if err != nil {
		t.Fatalf("failed to create guest user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		UPDATE users SET
			profile_pic_url = 'https://example.com/pic.jpg',
			referral_code = 'ABCDEF123456',
			notes = 'Some personal notes about this guest',
			data_retention_consent = TRUE
		WHERE id = $1
	`, guestID)
	if err != nil {
		t.Fatalf("failed to update guest fields: %v", err)
	}

	staleTime := clock.Now().Add(-7 * 30 * 24 * time.Hour)
	_, err = tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'completed')
	`, guestID, staleTime)
	if err != nil {
		t.Fatalf("failed to create stale booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		UPDATE users SET
			n_first_name = 'Guest',
			n_last_name = 'Anonymized',
			email = 'anon-' || id || '@anon.invalid',
			phone = '000000000000',
			date_of_birth = '1900-01-01',
			profile_pic_url = NULL,
			referral_code = NULL,
			notes = NULL,
			data_retention_consent = FALSE,
			updated_at = NOW()
		WHERE account_role = 'guest'
		  AND id NOT IN (
			  SELECT user_id FROM bookings WHERE status IN ('pending', 'confirmed')
		  )
		  AND id IN (
			  SELECT user_id FROM bookings WHERE user_id IS NOT NULL
			  GROUP BY user_id HAVING MAX(start_time) < NOW() - INTERVAL '6 months'
		  )
	`)
	if err != nil {
		t.Fatalf("stale guest anonymization SQL failed: %v", err)
	}

	var profilePicURL, referralCode, notes interface{}
	var dataRetentionConsent bool
	err = tx.QueryRow(ctx, `
		SELECT profile_pic_url, referral_code, notes, data_retention_consent
		FROM users WHERE id = $1
	`, guestID).Scan(&profilePicURL, &referralCode, &notes, &dataRetentionConsent)
	if err != nil {
		t.Fatalf("failed to query guest user after anonymization: %v", err)
	}

	if profilePicURL != nil {
		t.Errorf("expected profile_pic_url to be NULL, got %v", profilePicURL)
	}
	if referralCode != nil {
		t.Errorf("expected referral_code to be NULL, got %v", referralCode)
	}
	if notes != nil {
		t.Errorf("expected notes to be NULL, got %v", notes)
	}
	if dataRetentionConsent != false {
		t.Errorf("expected data_retention_consent to be FALSE, got %v", dataRetentionConsent)
	}
}

func TestAnonymizeStaleGuestAccounts_DoesNotAffectActiveGuests(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	guestID, err := fixtures.CreateTestGuestUser(tx)
	if err != nil {
		t.Fatalf("failed to create guest user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		UPDATE users SET
			profile_pic_url = 'https://example.com/pic.jpg',
			referral_code = 'ABCDEF123456',
			data_retention_consent = TRUE
		WHERE id = $1
	`, guestID)
	if err != nil {
		t.Fatalf("failed to update guest fields: %v", err)
	}

	recentTime := clock.Now().Add(24 * time.Hour)
	_, err = tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
	`, guestID, recentTime)
	if err != nil {
		t.Fatalf("failed to create recent booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		UPDATE users SET
			n_first_name = 'Guest',
			n_last_name = 'Anonymized',
			email = 'anon-' || id || '@anon.invalid',
			phone = '000000000000',
			date_of_birth = '1900-01-01',
			profile_pic_url = NULL,
			referral_code = NULL,
			notes = NULL,
			data_retention_consent = FALSE,
			updated_at = NOW()
		WHERE account_role = 'guest'
		  AND id NOT IN (
			  SELECT user_id FROM bookings WHERE status IN ('pending', 'confirmed')
		  )
		  AND id IN (
			  SELECT user_id FROM bookings WHERE user_id IS NOT NULL
			  GROUP BY user_id HAVING MAX(start_time) < NOW() - INTERVAL '6 months'
		  )
	`)
	if err != nil {
		t.Fatalf("stale guest anonymization SQL failed: %v", err)
	}

	var profilePicURL interface{}
	var referralCode string
	var dataRetentionConsent bool
	err = tx.QueryRow(ctx, `
		SELECT profile_pic_url, referral_code, data_retention_consent
		FROM users WHERE id = $1
	`, guestID).Scan(&profilePicURL, &referralCode, &dataRetentionConsent)
	if err != nil {
		t.Fatalf("failed to query guest user: %v", err)
	}

	if profilePicURL == nil {
		t.Error("expected profile_pic_url to be preserved for active guest")
	}
	if referralCode != "ABCDEF123456" {
		t.Errorf("expected referral_code to be preserved, got %v", referralCode)
	}
	if dataRetentionConsent != true {
		t.Errorf("expected data_retention_consent to be TRUE, got %v", dataRetentionConsent)
	}
}

func TestAnonymizeStaleGuestAccounts_DoesNotAffectRegisteredUsers(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

	_, err = tx.Exec(ctx, `
		UPDATE users SET
			profile_pic_url = 'https://example.com/pic.jpg',
			referral_code = 'ABCDEF123456',
			data_retention_consent = TRUE
		WHERE id = $1
	`, userID)
	if err != nil {
		t.Fatalf("failed to update user fields: %v", err)
	}

	_, err = tx.Exec(ctx, `
		UPDATE users SET
			n_first_name = 'Guest',
			n_last_name = 'Anonymized',
			email = 'anon-' || id || '@anon.invalid',
			phone = '000000000000',
			date_of_birth = '1900-01-01',
			profile_pic_url = NULL,
			referral_code = NULL,
			notes = NULL,
			data_retention_consent = FALSE,
			updated_at = NOW()
		WHERE account_role = 'guest'
		  AND id NOT IN (
			  SELECT user_id FROM bookings WHERE status IN ('pending', 'confirmed')
		  )
		  AND id IN (
			  SELECT user_id FROM bookings WHERE user_id IS NOT NULL
			  GROUP BY user_id HAVING MAX(start_time) < NOW() - INTERVAL '6 months'
		  )
	`)
	if err != nil {
		t.Fatalf("stale guest anonymization SQL failed: %v", err)
	}

	var profilePicURL interface{}
	var referralCode string
	err = tx.QueryRow(ctx, `
		SELECT profile_pic_url, referral_code
		FROM users WHERE id = $1
	`, userID).Scan(&profilePicURL, &referralCode)
	if err != nil {
		t.Fatalf("failed to query user: %v", err)
	}

	if profilePicURL == nil {
		t.Error("expected profile_pic_url to be preserved for registered user")
	}
	if referralCode != "ABCDEF123456" {
		t.Errorf("expected referral_code to be preserved, got %v", referralCode)
	}
}

// ============================================================
// CleanupGDPRExportCache Tests
// ============================================================

func TestCleanupGDPRExportCache_Empty(t *testing.T) {
	t.Parallel()

	gdprExportCacheMu.Lock()
	gdprExportCache = make(map[string]*gdprCacheEntry)
	gdprExportCacheMu.Unlock()

	_, err := CleanupGDPRExportCache(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}
}

func TestCleanupGDPRExportCache_RemovesExpired(t *testing.T) {
	t.Parallel()

	gdprExportCacheMu.Lock()
	gdprExportCache = map[string]*gdprCacheEntry{
		"user-expired": {expiresAt: clock.Now().Add(-1 * time.Hour)},
	}
	gdprExportCacheMu.Unlock()

	_, err := CleanupGDPRExportCache(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}

	gdprExportCacheMu.RLock()
	_, exists := gdprExportCache["user-expired"]
	gdprExportCacheMu.RUnlock()
	if exists {
		t.Error("expected expired entry to be removed")
	}
}

func TestCleanupGDPRExportCache_PreservesValid(t *testing.T) {
	t.Parallel()

	gdprExportCacheMu.Lock()
	gdprExportCache = map[string]*gdprCacheEntry{
		"user-valid": {expiresAt: clock.Now().Add(1 * time.Hour)},
	}
	gdprExportCacheMu.Unlock()

	_, err := CleanupGDPRExportCache(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}

	gdprExportCacheMu.RLock()
	entry, exists := gdprExportCache["user-valid"]
	gdprExportCacheMu.RUnlock()
	if !exists {
		t.Fatal("expected valid entry to be preserved")
	}
	if entry == nil {
		t.Error("expected non-nil entry")
	}
}

func TestCleanupGDPRExportCache_Mixed(t *testing.T) {
	t.Parallel()

	gdprExportCacheMu.Lock()
	gdprExportCache = map[string]*gdprCacheEntry{
		"user-expired":  {expiresAt: clock.Now().Add(-2 * time.Hour)},
		"user-valid":    {expiresAt: clock.Now().Add(2 * time.Hour)},
		"user-expired2": {expiresAt: clock.Now().Add(-30 * time.Minute)},
	}
	gdprExportCacheMu.Unlock()

	_, err := CleanupGDPRExportCache(context.Background())
	if err != nil {
		t.Errorf("expected nil error, got %v", err)
	}

	gdprExportCacheMu.RLock()
	_, expiredExists := gdprExportCache["user-expired"]
	_, validExists := gdprExportCache["user-valid"]
	_, expired2Exists := gdprExportCache["user-expired2"]
	gdprExportCacheMu.RUnlock()

	if expiredExists {
		t.Error("expected user-expired to be removed")
	}
	if expired2Exists {
		t.Error("expected user-expired2 to be removed")
	}
	if !validExists {
		t.Error("expected user-valid to be preserved")
	}
}
