//go:build test

package scheduling

// Package scheduling contains tests for time blockers CRUD handlers.
//
// Test Coverage:
//   - ListTimeBlockers: GET /api/admin/time-blockers - List all blockers
//   - CreateTimeBlocker: POST /api/admin/time-blockers - Create blocker (admin)
//   - DeleteTimeBlocker: DELETE /api/admin/time-blockers/{id} - Delete blocker (admin)
//   - CheckTimeBlockerOverlap: Helper to check for overlapping blockers
//   - GetTimeBlockersInRange: Helper to get blockers in date range
//
// Authentication: Create/Delete endpoints require admin role.

import (
	"bytes"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"crussell/clock"
	"crussell/mw"
	"crussell/testutils/fixtures"

	"github.com/go-chi/chi/v5"
)

func makeTimeBlockerRequest(handler http.HandlerFunc, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}
	req = req.WithContext(ctx)
	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)
	return w
}

func makeTimeBlockerAuthRequest(handler http.HandlerFunc, method, path string, body interface{}, ctx context.Context) *httptest.ResponseRecorder {
	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest(method, path, nil)
	}

	// Layer admin context on top of test transaction context
	chiCtx := context.WithValue(ctx, mw.UserRoleKey, "admin")
	req = req.WithContext(chiCtx)

	w := httptest.NewRecorder()
	handler.ServeHTTP(w, req)
	return w
}

// --- Tests for ListTimeBlockers ---

// TestTimeBlockers_List verifies that all time blockers can be listed.
// Returns 200 OK with an array of blockers.
func TestTimeBlockers_List(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	blockerTime1 := clock.Now().Add(7 * 24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
	blockerTime2 := clock.Now().Add(8 * 24 * time.Hour).Truncate(24 * time.Hour).Add(14 * time.Hour)

	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Blocker 1', NULL),
		       ($2, 30, 'Blocker 2', NULL)
	`, blockerTime1, blockerTime2)
	if err != nil {
		t.Fatalf("failed to create time blockers: %v", err)
	}

	handler := http.HandlerFunc(ListTimeBlockers)
	w := makeTimeBlockerRequest(handler, "GET", "/api/admin/time-blockers", nil, ctx)

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

	var response []TimeBlocker
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if len(response) != 2 {
		t.Errorf("expected 2 blockers, got %d", len(response))
	}
}

// TestTimeBlockers_ListWithDateFilter verifies that time blockers can be
// filtered by start/end query parameters.
func TestTimeBlockers_ListWithDateFilter(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create blockers on different dates
	blockerTime1 := time.Date(2026, 3, 10, 10, 0, 0, 0, time.UTC) // In range
	blockerTime2 := time.Date(2026, 3, 15, 14, 0, 0, 0, time.UTC) // Out of range
	blockerTime3 := time.Date(2026, 3, 12, 9, 0, 0, 0, time.UTC)  // In range

	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'In Range 1', NULL),
		       ($2, 30, 'Out of Range', NULL),
		       ($3, 45, 'In Range 2', NULL)
	`, blockerTime1, blockerTime2, blockerTime3)
	if err != nil {
		t.Fatalf("failed to create time blockers: %v", err)
	}
	handler := http.HandlerFunc(ListTimeBlockers)
	w := makeTimeBlockerRequest(handler, "GET", "/api/admin/time-blockers?start=2026-03-10&end=2026-03-13", nil, ctx)

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

	var response []TimeBlocker
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	// Should return only blockers within the date range (2026-03-10 to 2026-03-13)
	if len(response) != 2 {
		t.Errorf("expected 2 blockers in range, got %d", len(response))
	}
}

// --- Tests for CreateTimeBlocker ---

// TestTimeBlockers_Create verifies that an admin can create a new time blocker.
func TestTimeBlockers_Create(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	blockerTime := time.Date(2026, 3, 20, 10, 0, 0, 0, time.UTC)

	reqBody := CreateTimeBlockerRequest{
		StartTime:       blockerTime,
		DurationMinutes: 60,
		Description:     "Test blocker",
	}

	handler := http.HandlerFunc(CreateTimeBlocker)
	w := makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody, ctx)

	if w.Code != http.StatusCreated {
		t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
	}

	var response TimeBlocker
	if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil {
		t.Fatalf("failed to unmarshal response: %v", err)
	}

	if response.DurationMinutes != 60 {
		t.Errorf("expected duration 60, got %d", response.DurationMinutes)
	}

	if response.Description != "Test blocker" {
		t.Errorf("expected description 'Test blocker', got %s", response.Description)
	}

	// Verify it exists in DB
	var count int
	err := tx.QueryRow(ctx, `SELECT COUNT(*) FROM time_blockers WHERE id = $1`, response.ID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to verify blocker in DB: %v", err)
	}
	if count != 1 {
		t.Error("expected blocker to exist in DB")
	}
}

// TestTimeBlockers_Create_ValidationErrors verifies that missing or invalid
// fields result in 400 Bad Request.
func TestTimeBlockers_Create_ValidationErrors(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	handler := http.HandlerFunc(CreateTimeBlocker)

	// Test missing start_time
	reqBody1 := map[string]interface{}{
		"duration_minutes": 60,
		"description":      "Test",
	}
	w := makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody1, ctx)
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for missing start_time, got %d. body: %s", w.Code, w.Body.String())
	}

	// Test missing duration_minutes
	blockerTime := time.Date(2026, 3, 20, 10, 0, 0, 0, time.UTC)
	reqBody2 := map[string]interface{}{
		"start_time":  blockerTime,
		"description": "Test",
	}
	w = makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody2, ctx)
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for missing duration_minutes, got %d. body: %s", w.Code, w.Body.String())
	}

	// Test invalid duration_minutes (zero)
	reqBody3 := map[string]interface{}{
		"start_time":       blockerTime,
		"duration_minutes": 0,
		"description":      "Test",
	}
	w = makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody3, ctx)
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for zero duration_minutes, got %d. body: %s", w.Code, w.Body.String())
	}

	// Test invalid duration_minutes (negative)
	reqBody4 := map[string]interface{}{
		"start_time":       blockerTime,
		"duration_minutes": -10,
		"description":      "Test",
	}
	w = makeTimeBlockerAuthRequest(handler, "POST", "/api/admin/time-blockers", reqBody4, ctx)
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for negative duration_minutes, got %d. body: %s", w.Code, w.Body.String())
	}
}

// --- Tests for DeleteTimeBlocker ---

// TestTimeBlockers_Delete verifies that an admin can delete a time blocker.
func TestTimeBlockers_Delete(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	blockerTime := time.Date(2026, 3, 25, 10, 0, 0, 0, time.UTC)

	// Create a blocker to delete
	var blockerID string
	err := tx.QueryRow(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'To be deleted', NULL)
		RETURNING id
	`, blockerTime).Scan(&blockerID)
	if err != nil {
		t.Fatalf("failed to create blocker: %v", err)
	}

	// Set up chi router for URL param
	r := chi.NewRouter()
	r.Delete("/api/admin/time-blockers/{id}", DeleteTimeBlocker)
	// Create request with chi context
	req := httptest.NewRequest("DELETE", "/api/admin/time-blockers/"+blockerID, nil)
	reqCtx := context.WithValue(ctx, mw.UserIDKey, "admin001")
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", blockerID)
	reqCtx = context.WithValue(reqCtx, chi.RouteCtxKey, rctx)

	req = req.WithContext(reqCtx)

	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	if w.Code != http.StatusNoContent {
		t.Errorf("expected status 204, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify blocker was deleted
	var count int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM time_blockers WHERE id = $1`, blockerID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to check blocker: %v", err)
	}
	if count != 0 {
		t.Error("expected blocker to be deleted from DB")
	}
}

// TestTimeBlockers_Delete_NotFound verifies that attempting to delete a
// non-existent blocker returns 404 Not Found.
func TestTimeBlockers_Delete_NotFound(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

	// Set up chi router for URL param
	r := chi.NewRouter()
	r.Delete("/api/admin/time-blockers/{id}", DeleteTimeBlocker)

	// Create request with non-existent ID
	req := httptest.NewRequest("DELETE", "/api/admin/time-blockers/nonexistent-id", nil)
	reqCtx := context.WithValue(ctx, mw.UserIDKey, "admin001")
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", "nonexistent-id")
	reqCtx = context.WithValue(reqCtx, chi.RouteCtxKey, rctx)
	req = req.WithContext(reqCtx)

	w := httptest.NewRecorder()
	r.ServeHTTP(w, req)

	if w.Code != http.StatusNotFound {
		t.Errorf("expected status 404, got %d. body: %s", w.Code, w.Body.String())
	}
}

// --- Tests for CheckTimeBlockerOverlap ---

// TestCheckTimeBlockerOverlap verifies that the overlap detection function
// correctly identifies overlapping time ranges.
func TestCheckTimeBlockerOverlap(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create blocker for 10:00-11:00 (60 minutes)
	blockerTime := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)

	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Existing blocker', NULL)
	`, blockerTime)
	if err != nil {
		t.Fatalf("failed to create blocker: %v", err)
	}

	// Test case 1: Exact overlap (10:00-11:00)
	hasOverlap, desc, err := CheckTimeBlockerOverlap(ctx,
		time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC),
		time.Date(2026, 3, 15, 11, 0, 0, 0, time.UTC), nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if !hasOverlap {
		t.Error("expected overlap for exact match booking 10:00-11:00")
	}
	if desc != "Existing blocker" {
		t.Errorf("expected description 'Existing blocker', got %s", desc)
	}

	// Test case 2: No overlap (09:00-10:00 - ends exactly when blocker starts)
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
		time.Date(2026, 3, 15, 9, 0, 0, 0, time.UTC),
		time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC), nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if hasOverlap {
		t.Error("expected no overlap for booking 09:00-10:00 (ends exactly when blocker starts)")
	}

	// Test case 3: Partial overlap (10:30-11:30 - starts during blocker)
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
		time.Date(2026, 3, 15, 10, 30, 0, 0, time.UTC),
		time.Date(2026, 3, 15, 11, 30, 0, 0, time.UTC), nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if !hasOverlap {
		t.Error("expected overlap for partial overlap booking 10:30-11:30")
	}

	// Test case 4: Partial overlap (09:30-10:30 - ends during blocker)
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
		time.Date(2026, 3, 15, 9, 30, 0, 0, time.UTC),
		time.Date(2026, 3, 15, 10, 30, 0, 0, time.UTC), nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if !hasOverlap {
		t.Error("expected overlap for partial overlap booking 09:30-10:30")
	}

	// Test case 5: No overlap (completely before blocker)
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
		time.Date(2026, 3, 15, 8, 0, 0, 0, time.UTC),
		time.Date(2026, 3, 15, 9, 0, 0, 0, time.UTC), nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if hasOverlap {
		t.Error("expected no overlap for booking completely before blocker")
	}

	// Test case 6: No overlap (completely after blocker)
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx,
		time.Date(2026, 3, 15, 14, 0, 0, 0, time.UTC),
		time.Date(2026, 3, 15, 15, 0, 0, 0, time.UTC), nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if hasOverlap {
		t.Error("expected no overlap for booking completely after blocker")
	}
}

// --- Tests for GetTimeBlockersInRange ---

// TestGetTimeBlockersInRange verifies that blockers can be retrieved
// for a specific date range.
func TestGetTimeBlockersInRange(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create blockers on different dates
	blocker1 := time.Date(2026, 3, 10, 10, 0, 0, 0, time.UTC)
	blocker2 := time.Date(2026, 3, 15, 14, 0, 0, 0, time.UTC)
	blocker3 := time.Date(2026, 3, 20, 9, 0, 0, 0, time.UTC)

	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Day 10', NULL),
		       ($2, 30, 'Day 15', NULL),
		       ($3, 45, 'Day 20', NULL)
	`, blocker1, blocker2, blocker3)
	if err != nil {
		t.Fatalf("failed to create blockers: %v", err)
	}

	// Query range that includes blocker1 and blocker2 but not blocker3
	start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2026, 3, 16, 23, 59, 59, 0, time.UTC)

	blockers, err := GetTimeBlockersInRange(ctx, start, end, nil)
	if err != nil {
		t.Fatalf("GetTimeBlockersInRange failed: %v", err)
	}

	// Should return 2 blockers (March 10 and March 15)
	if len(blockers) != 2 {
		t.Errorf("expected 2 blockers in range, got %d", len(blockers))
	}

	// Verify correct blockers returned
	found := make(map[string]bool)
	for _, b := range blockers {
		found[b.Description] = true
	}

	if !found["Day 10"] {
		t.Error("expected blocker 'Day 10' in range")
	}
	if !found["Day 15"] {
		t.Error("expected blocker 'Day 15' in range")
	}
	if found["Day 20"] {
		t.Error("did not expect blocker 'Day 20' in range (March 20)")
	}
}

// TestGetTimeBlockersInRange_Empty verifies that an empty array is
// returned when no blockers exist in the range.
func TestGetTimeBlockersInRange_Empty(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create a blocker
	blockerTime := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'March 15', NULL)
	`, blockerTime)
	if err != nil {
		t.Fatalf("failed to create blocker: %v", err)
	}

	// Query range with no blockers
	start := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2026, 4, 30, 23, 59, 59, 0, time.UTC)

	blockers, err := GetTimeBlockersInRange(ctx, start, end, nil)
	if err != nil {
		t.Fatalf("GetTimeBlockersInRange failed: %v", err)
	}

	if len(blockers) != 0 {
		t.Errorf("expected 0 blockers in range, got %d", len(blockers))
	}
}

// TestGetTimeBlockersInRange_IncludesRecurring verifies that recurring blockers
// are expanded to actual occurrences within the query range.
func TestGetTimeBlockersInRange_IncludesRecurring(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create one-off blocker for March 15
	oneOffTime := time.Date(2026, 3, 15, 10, 0, 0, 0, time.UTC)
	// Cron: every Monday at 10:00 (0 10 * * 1)
	cronExpr := "0 10 * * 1"

	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, cron_expression, created_by)
		VALUES ($1, 60, 'One-off', NULL, NULL),
		       ($2, 60, 'Recurring Monday', $3, NULL)
	`, oneOffTime, oneOffTime, cronExpr)
	if err != nil {
		t.Fatalf("failed to create blockers: %v", err)
	}

	// Query range: March 1-31, 2026
	start := time.Date(2026, 3, 1, 0, 0, 0, 0, time.UTC)
	end := time.Date(2026, 3, 31, 23, 59, 59, 0, time.UTC)

	blockers, err := GetTimeBlockersInRange(ctx, start, end, nil)
	if err != nil {
		t.Fatalf("GetTimeBlockersInRange failed: %v", err)
	}

	// March 2026 has 5 Mondays: 2nd, 9th, 16th, 23rd, 30th
	// So we expect: 1 one-off + 5 recurring occurrences = 6 total
	if len(blockers) != 6 {
		t.Errorf("expected 6 blockers (1 one-off + 5 recurring), got %d", len(blockers))
		for i, b := range blockers {
			t.Logf("Blocker %d: %s at %v", i, b.Description, b.StartTime)
		}
	}

	// Verify we have the one-off
	foundOneOff := false
	foundRecurring := 0
	for _, b := range blockers {
		if b.Description == "One-off" {
			foundOneOff = true
		}
		if b.Description == "Recurring Monday" {
			foundRecurring++
		}
	}

	if !foundOneOff {
		t.Error("expected to find one-off blocker")
	}
	if foundRecurring != 5 {
		t.Errorf("expected 5 recurring Monday occurrences, got %d", foundRecurring)
	}
}

// TestCleanupOldReservations verifies that reservation blockers older than 1 hour
// are automatically deleted, while recent ones are kept.
func TestCleanupOldReservations(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create fixture users for the test
	oldUserID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create old user: %v", err)
	}

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

	// Create old reservation (> 1 hour old)
	oldTime := clock.Now().Add(-2 * time.Hour)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, $2, $3)`, oldTime, fmt.Sprintf("RESERVATION:user:%s:%d", oldUserID, clock.Now().UnixNano()), oldUserID)
	if err != nil {
		t.Fatalf("failed to create old reservation: %v", err)
	}

	// Set old created_at to make it eligible for cleanup (> 1 hour old)
	_, err = tx.Exec(ctx, `UPDATE time_blockers SET created_at = $1 WHERE description LIKE 'RESERVATION:user:%' AND start_time = $2`, clock.Now().Add(-2*time.Hour), oldTime)
	if err != nil {
		t.Fatalf("failed to update old reservation created_at: %v", err)
	}

	// Create recent reservation (< 1 hour old)
	recentTime := clock.Now().Add(-30 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, $2, $3)`, recentTime, fmt.Sprintf("RESERVATION:user:%s:%d", recentUserID, clock.Now().UnixNano()), recentUserID)
	if err != nil {
		t.Fatalf("failed to create recent reservation: %v", err)
	}

	// Set recent created_at to recent (< 1 hour old) so it's NOT deleted
	_, err = tx.Exec(ctx, `UPDATE time_blockers SET created_at = $1 WHERE description LIKE 'RESERVATION:user:%' AND start_time = $2`, clock.Now().Add(-30*time.Minute), recentTime)
	if err != nil {
		t.Fatalf("failed to update recent reservation created_at: %v", err)
	}

	// Create non-reservation blocker (should never be deleted)
	nonResTime := clock.Now().Add(-2 * time.Hour)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, $2, NULL)`, nonResTime, "Admin Blocked Time")
	if err != nil {
		t.Fatalf("failed to create non-reservation blocker: %v", err)
	}

	// Verify we have 3 blockers before cleanup
	var countBefore int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers").Scan(&countBefore)
	if err != nil {
		t.Fatalf("failed to count blockers before cleanup: %v", err)
	}
	if countBefore != 3 {
		t.Errorf("expected 3 blockers before cleanup, got %d", countBefore)
	}

	// Run cleanup
	_, err = CleanupOldReservations(ctx)
	if err != nil {
		t.Fatalf("CleanupOldReservations failed: %v", err)
	}

	// Verify old reservation was deleted
	var oldCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:%' AND start_time = $1", oldTime).Scan(&oldCount)
	if err == nil && oldCount > 0 {
		t.Error("expected old reservation to be deleted")
	}

	// Verify recent reservation still exists
	var recentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:%' AND start_time = $1", recentTime).Scan(&recentCount)
	if err != nil || recentCount == 0 {
		t.Error("expected recent reservation to still exist")
	}

	// Verify non-reservation blocker still exists
	var nonResCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'Admin Blocked Time'").Scan(&nonResCount)
	if err != nil || nonResCount == 0 {
		t.Error("expected non-reservation blocker to still exist")
	}

	// Verify final count (should be 2: recent reservation + non-reservation blocker)
	var countAfter int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers").Scan(&countAfter)
	if err != nil {
		t.Fatalf("failed to count blockers after cleanup: %v", err)
	}
	if countAfter != 2 {
		t.Errorf("expected 2 blockers after cleanup (1 old deletion), got %d", countAfter)
	}
}

// --- Tests for CleanupOldReservations (Admin Walk-In) ---

// TestCleanupOldReservations_AdminWalkIn verifies that admin walk-in reservations
// older than 15 minutes are deleted, while recent ones are preserved.
func TestCleanupOldReservations_AdminWalkIn(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create old walk-in reservation (>15 min old)
	oldTime := clock.Now().Add(-16 * time.Minute)
	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:walkin:guest:123', $2)
	`, oldTime, clock.Now().Add(-16*time.Minute))
	if err != nil {
		t.Fatalf("failed to create old walk-in reservation: %v", err)
	}

	// Create recent walk-in reservation (<15 min old)
	recentTime := clock.Now().Add(-14 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:walkin:guest:456', $2)
	`, recentTime, clock.Now().Add(-14*time.Minute))
	if err != nil {
		t.Fatalf("failed to create recent walk-in reservation: %v", err)
	}

	// Run cleanup
	_, err = CleanupOldReservations(ctx)
	if err != nil {
		t.Fatalf("CleanupOldReservations failed: %v", err)
	}

	// Verify old reservation was deleted
	var oldCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:walkin:guest:123'").Scan(&oldCount)
	if err != nil {
		t.Fatalf("failed to check old reservation: %v", err)
	}
	if oldCount != 0 {
		t.Error("expected old walk-in reservation (16 min) to be deleted")
	}

	// Verify recent reservation still exists
	var recentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:walkin:guest:456'").Scan(&recentCount)
	if err != nil {
		t.Fatalf("failed to check recent reservation: %v", err)
	}
	if recentCount != 1 {
		t.Error("expected recent walk-in reservation (14 min) to be preserved")
	}
}

// --- Tests for CleanupOldReservations (Admin Call-In) ---

// TestCleanupOldReservations_AdminCallIn verifies that admin call-in reservations
// older than 15 minutes are deleted, while recent ones are preserved.
func TestCleanupOldReservations_AdminCallIn(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create old call-in reservation (>15 min old)
	oldTime := clock.Now().Add(-16 * time.Minute)
	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:callin:guest:123', $2)
	`, oldTime, clock.Now().Add(-16*time.Minute))
	if err != nil {
		t.Fatalf("failed to create old call-in reservation: %v", err)
	}

	// Create recent call-in reservation (<15 min old)
	recentTime := clock.Now().Add(-14 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:callin:guest:456', $2)
	`, recentTime, clock.Now().Add(-14*time.Minute))
	if err != nil {
		t.Fatalf("failed to create recent call-in reservation: %v", err)
	}

	// Run cleanup
	_, err = CleanupOldReservations(ctx)
	if err != nil {
		t.Fatalf("CleanupOldReservations failed: %v", err)
	}

	// Verify old reservation was deleted
	var oldCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:callin:guest:123'").Scan(&oldCount)
	if err != nil {
		t.Fatalf("failed to check old reservation: %v", err)
	}
	if oldCount != 0 {
		t.Error("expected old call-in reservation (16 min) to be deleted")
	}

	// Verify recent reservation still exists
	var recentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:callin:guest:456'").Scan(&recentCount)
	if err != nil {
		t.Fatalf("failed to check recent reservation: %v", err)
	}
	if recentCount != 1 {
		t.Error("expected recent call-in reservation (14 min) to be preserved")
	}
}

// --- Tests for CleanupOldReservations (Mixed Types) ---

// TestCleanupOldReservations_MixedTypes verifies that cleanup correctly handles
// all reservation types with their respective TTLs.
func TestCleanupOldReservations_MixedTypes(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create old user reservation (>1 hour old)
	oldUserTime := clock.Now().Add(-2 * time.Hour)
	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:user:old', $2)
	`, oldUserTime, clock.Now().Add(-2*time.Hour))
	if err != nil {
		t.Fatalf("failed to create old user reservation: %v", err)
	}

	// Create recent user reservation (<1 hour old)
	recentUserTime := clock.Now().Add(-30 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:user:recent', $2)
	`, recentUserTime, clock.Now().Add(-30*time.Minute))
	if err != nil {
		t.Fatalf("failed to create recent user reservation: %v", err)
	}

	// Create old anon reservation (>10 min old)
	oldAnonTime := clock.Now().Add(-15 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:anon:old', $2)
	`, oldAnonTime, clock.Now().Add(-15*time.Minute))
	if err != nil {
		t.Fatalf("failed to create old anon reservation: %v", err)
	}

	// Create recent anon reservation (<10 min old)
	recentAnonTime := clock.Now().Add(-5 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:anon:recent', $2)
	`, recentAnonTime, clock.Now().Add(-5*time.Minute))
	if err != nil {
		t.Fatalf("failed to create recent anon reservation: %v", err)
	}

	// Create old admin walk-in reservation (>15 min old)
	oldWalkinTime := clock.Now().Add(-20 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:walkin:old', $2)
	`, oldWalkinTime, clock.Now().Add(-20*time.Minute))
	if err != nil {
		t.Fatalf("failed to create old walk-in reservation: %v", err)
	}

	// Create recent admin walk-in reservation (<15 min old)
	recentWalkinTime := clock.Now().Add(-10 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:walkin:recent', $2)
	`, recentWalkinTime, clock.Now().Add(-10*time.Minute))
	if err != nil {
		t.Fatalf("failed to create recent walk-in reservation: %v", err)
	}

	// Create old admin call-in reservation (>15 min old)
	oldCallinTime := clock.Now().Add(-20 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:callin:old', $2)
	`, oldCallinTime, clock.Now().Add(-20*time.Minute))
	if err != nil {
		t.Fatalf("failed to create old call-in reservation: %v", err)
	}

	// Create recent admin call-in reservation (<15 min old)
	recentCallinTime := clock.Now().Add(-10 * time.Minute)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:admin:callin:recent', $2)
	`, recentCallinTime, clock.Now().Add(-10*time.Minute))
	if err != nil {
		t.Fatalf("failed to create recent call-in reservation: %v", err)
	}

	// Verify we have 8 reservations before cleanup
	var countBefore int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:%'").Scan(&countBefore)
	if err != nil {
		t.Fatalf("failed to count reservations before cleanup: %v", err)
	}
	if countBefore != 8 {
		t.Errorf("expected 8 reservations before cleanup, got %d", countBefore)
	}

	// Run cleanup
	_, err = CleanupOldReservations(ctx)
	if err != nil {
		t.Fatalf("CleanupOldReservations failed: %v", err)
	}

	// Verify old reservations were deleted (4 old ones)
	var oldUserCount, oldAnonCount, oldWalkinCount, oldCallinCount int
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:user:old'").Scan(&oldUserCount)
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:anon:old'").Scan(&oldAnonCount)
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:walkin:old'").Scan(&oldWalkinCount)
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:callin:old'").Scan(&oldCallinCount)

	if oldUserCount != 0 {
		t.Error("expected old user reservation to be deleted")
	}
	if oldAnonCount != 0 {
		t.Error("expected old anon reservation to be deleted")
	}
	if oldWalkinCount != 0 {
		t.Error("expected old walk-in reservation to be deleted")
	}
	if oldCallinCount != 0 {
		t.Error("expected old call-in reservation to be deleted")
	}

	// Verify recent reservations still exist (4 recent ones)
	var recentUserCount, recentAnonCount, recentWalkinCount, recentCallinCount int
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:user:recent'").Scan(&recentUserCount)
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:anon:recent'").Scan(&recentAnonCount)
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:walkin:recent'").Scan(&recentWalkinCount)
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:admin:callin:recent'").Scan(&recentCallinCount)

	if recentUserCount != 1 {
		t.Error("expected recent user reservation to be preserved")
	}
	if recentAnonCount != 1 {
		t.Error("expected recent anon reservation to be preserved")
	}
	if recentWalkinCount != 1 {
		t.Error("expected recent walk-in reservation to be preserved")
	}
	if recentCallinCount != 1 {
		t.Error("expected recent call-in reservation to be preserved")
	}
}

// --- Tests for GetTimeBlockersInRange (Excludes Reservations) ---

// TestGetTimeBlockersInRange_IncludesReservations verifies that reservation
// blockers ARE included in the results (needed for available-hours to correctly
// exclude reserved slots).
func TestGetTimeBlockersInRange_IncludesReservations(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create a regular blocker for tomorrow at 10:00
	tomorrow := clock.Now().Add(24 * time.Hour)
	blockerTime := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 10, 0, 0, 0, time.UTC)
	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Staff meeting', NULL)
	`, blockerTime)
	if err != nil {
		t.Fatalf("failed to create regular blocker: %v", err)
	}

	// Create a reservation for tomorrow at 11:00
	reservationTime := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 11, 0, 0, 0, time.UTC)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:abc:123', NULL)
	`, reservationTime)
	if err != nil {
		t.Fatalf("failed to create reservation: %v", err)
	}

	// Query range covering both times
	start := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, time.UTC)
	end := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 23, 59, 59, 0, time.UTC)

	blockers, err := GetTimeBlockersInRange(ctx, start, end, nil)
	if err != nil {
		t.Fatalf("GetTimeBlockersInRange failed: %v", err)
	}

	// Should return both blockers (regular + reservation)
	if len(blockers) != 2 {
		t.Errorf("expected 2 blockers (including reservation), got %d", len(blockers))
	}
}

// --- Tests for GetTimeBlockersInRange (Exclude User Reservations) ---

// TestGetTimeBlockersInRange_ExcludeOwnReservation verifies that when
// excludeUserID is set, RESERVATION entries owned by that user are excluded
// from results, while other blockers (including other users' reservations) remain.
func TestGetTimeBlockersInRange_ExcludeOwnReservation(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	tomorrow := clock.Now().Add(24 * time.Hour)
	dayStart := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, time.UTC)
	dayEnd := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 23, 59, 59, 0, time.UTC)

	// Create a regular blocker (always returned)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Staff meeting', NULL)
	`, dayStart.Add(9*time.Hour))
	if err != nil {
		t.Fatalf("failed to create regular blocker: %v", err)
	}

	// Create a RESERVATION for userA
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:' || $2 || ':123', $2)
	`, dayStart.Add(10*time.Hour), userAID)
	if err != nil {
		t.Fatalf("failed to create userA reservation: %v", err)
	}

	// Create a RESERVATION for userB
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:' || $2 || ':456', $2)
	`, dayStart.Add(11*time.Hour), userBID)
	if err != nil {
		t.Fatalf("failed to create userB reservation: %v", err)
	}

	// Test 1: excludeUserID = userA — should see regular + userB, but NOT userA's reservation
	blockers, err := GetTimeBlockersInRange(ctx, dayStart, dayEnd, &userAID)
	if err != nil {
		t.Fatalf("GetTimeBlockersInRange failed: %v", err)
	}
	if len(blockers) != 2 {
		t.Errorf("expected 2 blockers (regular + userB) when excluding userA, got %d", len(blockers))
		for _, b := range blockers {
			t.Logf("  blocker: %s created_by=%v", b.Description, b.CreatedBy)
		}
	}

	// Test 2: excludeUserID = nil — should see all 3 (regular + userA + userB)
	blockers, err = GetTimeBlockersInRange(ctx, dayStart, dayEnd, nil)
	if err != nil {
		t.Fatalf("GetTimeBlockersInRange failed: %v", err)
	}
	if len(blockers) != 3 {
		t.Errorf("expected 3 blockers (all) when excludeUserID=nil, got %d", len(blockers))
	}

	// Test 3: excludeUserID = userB — should see regular + userA, but NOT userB's reservation
	blockers, err = GetTimeBlockersInRange(ctx, dayStart, dayEnd, &userBID)
	if err != nil {
		t.Fatalf("GetTimeBlockersInRange failed: %v", err)
	}
	if len(blockers) != 2 {
		t.Errorf("expected 2 blockers (regular + userA) when excluding userB, got %d", len(blockers))
	}
}

// --- Tests for CheckTimeBlockerOverlap (Exclude User Reservations) ---

// TestCheckTimeBlockerOverlap_ExcludeOwnReservation verifies that RESERVATION
// entries are not considered overlapping when excludeUserID matches the owner.
func TestCheckTimeBlockerOverlap_ExcludeOwnReservation(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	tomorrow := clock.Now().Add(24 * time.Hour)
	slotStart := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 10, 0, 0, 0, time.UTC)
	slotEnd := slotStart.Add(1 * time.Hour)

	// Create a regular blocker (always detected)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Existing blocker', NULL)
	`, slotStart)
	if err != nil {
		t.Fatalf("failed to create regular blocker: %v", err)
	}

	// Create a RESERVATION for this user at a different time
	reservationStart := slotStart.Add(2 * time.Hour)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:' || $2 || ':123', $2)
	`, reservationStart, userID)
	if err != nil {
		t.Fatalf("failed to create user reservation: %v", err)
	}

	// Test 1: excludeUserID = userID — the regular blocker at slotStart should
	// still be detected, but the user's own RESERVATION at reservationStart
	// should be ignored.
	hasOverlap, desc, err := CheckTimeBlockerOverlap(ctx, slotStart, slotEnd, &userID)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if !hasOverlap {
		t.Error("expected overlap with regular blocker even when excluding own reservation")
	}
	if desc != "Existing blocker" {
		t.Errorf("expected description 'Existing blocker', got %s", desc)
	}

	// Test 2: excludeUserID = nil — the regular blocker should still be detected
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx, slotStart, slotEnd, nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if !hasOverlap {
		t.Error("expected overlap with regular blocker when excludeUserID=nil")
	}

	// Test 3: Overlap with own RESERVATION at reservationStart — with excludeUserID
	// set, this should NOT be detected as overlap
	resEnd := reservationStart.Add(1 * time.Hour)
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx, reservationStart, resEnd, &userID)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if hasOverlap {
		t.Error("expected NO overlap with own RESERVATION when excludeUserID matches")
	}

	// Test 4: Without excludeUserID, the own RESERVATION SHOULD be detected
	hasOverlap, _, err = CheckTimeBlockerOverlap(ctx, reservationStart, resEnd, nil)
	if err != nil {
		t.Fatalf("CheckTimeBlockerOverlap failed: %v", err)
	}
	if !hasOverlap {
		t.Error("expected overlap with own RESERVATION when excludeUserID=nil")
	}
}

// --- Tests for AnonymizeStaleGuestAccounts ---

// TestAnonymizeStaleGuestAccounts_Exactly6Months verifies that a guest with
// a booking exactly 6 months ago is anonymized.
func TestAnonymizeStaleGuestAccounts_Exactly6Months(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Set account_role to guest
	_, err = tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guestID)
	if err != nil {
		t.Fatalf("failed to set guest role: %v", err)
	}

	// Create booking with start_time more than 6 months ago
	// (strictly less than NOW() - INTERVAL '6 months' per the SQL condition)
	_, err = tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, NOW() - INTERVAL '7 months', 'completed', false)
	`, guestID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	// Run anonymization
	_, err = AnonymizeStaleGuestAccounts(ctx)
	if err != nil {
		t.Fatalf("AnonymizeStaleGuestAccounts failed: %v", err)
	}

	// Verify guest was anonymized
	var firstName, lastName, email string
	err = tx.QueryRow(ctx, `SELECT n_first_name, n_last_name, email FROM users WHERE id = $1`, guestID).Scan(&firstName, &lastName, &email)
	if err != nil {
		t.Fatalf("failed to query anonymized user: %v", err)
	}

	if firstName != "Guest" {
		t.Errorf("expected first_name 'Guest', got '%s'", firstName)
	}
	if lastName != "Anonymized" {
		t.Errorf("expected last_name 'Anonymized', got '%s'", lastName)
	}
	if !strings.HasPrefix(email, "anon-") {
		t.Errorf("expected email to start with 'anon-', got '%s'", email)
	}
}

// TestAnonymizeStaleGuestAccounts_ActiveBooking_Skipped verifies that a guest
// with an active (future) booking is NOT anonymized even if they have a past booking.
func TestAnonymizeStaleGuestAccounts_ActiveBooking_Skipped(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Set account_role to guest
	_, err = tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guestID)
	if err != nil {
		t.Fatalf("failed to set guest role: %v", err)
	}

	// Create past booking (7 months ago)
	_, err = tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, NOW() - INTERVAL '7 months', 'completed', false)
	`, guestID)
	if err != nil {
		t.Fatalf("failed to create past booking: %v", err)
	}

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

	// Run anonymization
	_, err = AnonymizeStaleGuestAccounts(ctx)
	if err != nil {
		t.Fatalf("AnonymizeStaleGuestAccounts failed: %v", err)
	}

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

	// The first name should NOT be "Guest" (it should retain original name)
	if firstName == "Guest" {
		t.Error("expected guest with active booking to NOT be anonymized")
	}
}

// TestAnonymizeStaleGuestAccounts_NoBookings verifies that a guest with
// no bookings is NOT anonymized.
func TestAnonymizeStaleGuestAccounts_NoBookings(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create guest user with no bookings
	guestID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create guest user: %v", err)
	}

	// Set account_role to guest
	_, err = tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guestID)
	if err != nil {
		t.Fatalf("failed to set guest role: %v", err)
	}

	// Run anonymization
	_, err = AnonymizeStaleGuestAccounts(ctx)
	if err != nil {
		t.Fatalf("AnonymizeStaleGuestAccounts failed: %v", err)
	}

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

	// The first name should NOT be "Guest" (it should retain original name)
	if firstName == "Guest" {
		t.Error("expected guest with no bookings to NOT be anonymized")
	}
}

// --- Tests for CleanupExpiredFinancialRecords ---

// TestCleanupExpiredFinancialRecords_PaymentOlderThan7Years verifies that a
// payment older than 7 years is aggregated and deleted.
func TestCleanupExpiredFinancialRecords_PaymentOlderThan7Years(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create 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)
	}

	eightYearsAgo := clock.Now().AddDate(-8, 0, 0)
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 50.00, $2)
	`, bookingID, eightYearsAgo)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Anonymize user so they qualify for deletion (>1 year ago, guest role, anon email)
	_, err = tx.Exec(ctx, `
		UPDATE users SET account_role = 'guest', email = 'anon-' || id || '@anon.invalid', updated_at = NOW() - INTERVAL '2 years'
		WHERE id = $1
	`, userID)
	if err != nil {
		t.Fatalf("failed to anonymize user: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
	}

	// Verify payment was deleted
	var paymentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&paymentCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if paymentCount != 0 {
		t.Errorf("expected payment to be deleted, got %d payment(s)", paymentCount)
	}

	// Verify financial_aggregates has 1 row
	var aggCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
	if err != nil {
		t.Fatalf("failed to count aggregates: %v", err)
	}
	if aggCount != 1 {
		t.Errorf("expected 1 financial_aggregates row, got %d", aggCount)
	}

	// Verify aggregate has correct month and total
	expectedMonth := time.Date(eightYearsAgo.Year(), eightYearsAgo.Month(), 1, 0, 0, 0, 0, time.UTC)
	var aggMonth time.Time
	var totalPayments float64
	err = tx.QueryRow(ctx, "SELECT month, total_payments FROM financial_aggregates").Scan(&aggMonth, &totalPayments)
	if err != nil {
		t.Fatalf("failed to query aggregate: %v", err)
	}
	if !aggMonth.Equal(expectedMonth) {
		t.Errorf("expected month %s, got %s", expectedMonth.Format("2006-01-02"), aggMonth.Format("2006-01-02"))
	}
	if totalPayments != 50.00 {
		t.Errorf("expected total_payments 50.00, got %.2f", totalPayments)
	}
}

// TestCleanupExpiredFinancialRecords_AnonUserWithin1YearBuffer verifies that
// an anonymized user's payment is NOT deleted when the 1-year buffer hasn't
// elapsed, even if the payment is older than 7 years.
func TestCleanupExpiredFinancialRecords_AnonUserWithin1YearBuffer(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	_, err = tx.Exec(ctx, `
		UPDATE users
		SET account_role = 'guest',
		    email = 'anon-' || id || '@anon.invalid',
		    updated_at = NOW() - INTERVAL '6 months'
		WHERE id = $1
	`, guestID)
	if err != nil {
		t.Fatalf("failed to anonymize user: %v", err)
	}

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

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

	fourYearsAgo := clock.Now().AddDate(-4, 0, 0)
	var paymentID string
	err = tx.QueryRow(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 75.00, $2)
		RETURNING id
	`, bookingID, fourYearsAgo).Scan(&paymentID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
	}

	// Verify payment still exists (neither 7yr nor 1yr conditions satisfied)
	var paymentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if paymentCount != 1 {
		t.Error("expected payment to still exist")
	}

	// Verify no aggregate was created
	var aggCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
	if err != nil {
		t.Fatalf("failed to count aggregates: %v", err)
	}
	if aggCount != 0 {
		t.Errorf("expected no aggregates, got %d", aggCount)
	}
}

// TestCleanupExpiredFinancialRecords_PaymentOlderThan9Years verifies that a
// payment older than 9 years is always deleted regardless of user status.
func TestCleanupExpiredFinancialRecords_PaymentOlderThan9Years(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create 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)
	}

	// Payment created 9 years ago
	nineYearsAgo := clock.Now().AddDate(-9, 0, 0)
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 60.00, $2)
	`, bookingID, nineYearsAgo)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Anonymize user so record qualifies for deletion
	_, err = tx.Exec(ctx, `
		UPDATE users SET account_role = 'guest', email = 'anon-' || id || '@anon.invalid', updated_at = NOW() - INTERVAL '2 years'
		WHERE id = $1
	`, userID)
	if err != nil {
		t.Fatalf("failed to anonymize user: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
	}

	// Verify payment was deleted
	var paymentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&paymentCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if paymentCount != 0 {
		t.Errorf("expected payment to be deleted, got %d payment(s)", paymentCount)
	}

	// Verify financial_aggregates has 1 row
	var aggCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
	if err != nil {
		t.Fatalf("failed to count aggregates: %v", err)
	}
	if aggCount != 1 {
		t.Errorf("expected 1 financial_aggregates row, got %d", aggCount)
	}
}

// TestCleanupExpiredFinancialRecords_AggregationCorrectTotals verifies that
// multiple payments in the same month are correctly aggregated by method and type.
func TestCleanupExpiredFinancialRecords_AggregationCorrectTotals(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create 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)
	}

	// 3 payments 8 years ago, all in the same month
	sameMonth := clock.Now().AddDate(-8, 0, 0)
	_ = sameMonth // used for all payments

	// Payment 1: £50 cash
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, fees, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 50.00, 0, $2)
	`, bookingID, sameMonth)
	if err != nil {
		t.Fatalf("failed to create cash payment: %v", err)
	}

	// Payment 2: £30 online_square with £2.50 fees
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, fees, created_at)
		VALUES ($1, 'full', 'online_square', 'completed', 30.00, 2.50, $2)
	`, bookingID, sameMonth)
	if err != nil {
		t.Fatalf("failed to create online payment: %v", err)
	}

	// Payment 3: £20 in_person_card
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, fees, created_at)
		VALUES ($1, 'full', 'in_person_card', 'completed', 20.00, 0, $2)
	`, bookingID, sameMonth)
	if err != nil {
		t.Fatalf("failed to create card payment: %v", err)
	}

	// Anonymize user so records qualify for deletion
	_, err = tx.Exec(ctx, `
		UPDATE users SET account_role = 'guest', email = 'anon-' || id || '@anon.invalid', updated_at = NOW() - INTERVAL '2 years'
		WHERE id = $1
	`, userID)
	if err != nil {
		t.Fatalf("failed to anonymize user: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
	}

	// Verify aggregate totals
	var totalPayments, totalCash, totalOnline, totalInPerson, totalSquareFees float64
	var bookingCount int
	err = tx.QueryRow(ctx, `
		SELECT total_payments, total_cash, total_online, total_in_person, total_square_fees, booking_count
		FROM financial_aggregates
	`).Scan(&totalPayments, &totalCash, &totalOnline, &totalInPerson, &totalSquareFees, &bookingCount)
	if err != nil {
		t.Fatalf("failed to query aggregate: %v", err)
	}

	if totalPayments != 100.00 {
		t.Errorf("expected total_payments 100.00, got %.2f", totalPayments)
	}
	if totalCash != 50.00 {
		t.Errorf("expected total_cash 50.00, got %.2f", totalCash)
	}
	if totalOnline != 30.00 {
		t.Errorf("expected total_online 30.00, got %.2f", totalOnline)
	}
	if totalInPerson != 20.00 {
		t.Errorf("expected total_in_person 20.00, got %.2f", totalInPerson)
	}
	if totalSquareFees != 2.50 {
		t.Errorf("expected total_square_fees 2.50, got %.2f", totalSquareFees)
	}
	if bookingCount != 1 {
		t.Errorf("expected booking_count 1, got %d", bookingCount)
	}
}

// TestCleanupExpiredFinancialRecords_Idempotent verifies that running the
// cleanup function twice produces the same result (no double-counting).
func TestCleanupExpiredFinancialRecords_Idempotent(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create 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)
	}

	// Payment 8 years ago
	eightYearsAgo := clock.Now().AddDate(-8, 0, 0)
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 100.00, $2)
	`, bookingID, eightYearsAgo)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Anonymize user so record qualifies for deletion
	_, err = tx.Exec(ctx, `
		UPDATE users SET account_role = 'guest', email = 'anon-' || id || '@anon.invalid', updated_at = NOW() - INTERVAL '2 years'
		WHERE id = $1
	`, userID)
	if err != nil {
		t.Fatalf("failed to anonymize user: %v", err)
	}

	// First run
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("first cleanup failed: %v", err)
	}

	// Capture aggregate values after first run
	var aggCount1 int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount1)
	if err != nil {
		t.Fatalf("failed to count aggregates: %v", err)
	}
	var totalPayments1 float64
	var aggMonth1 time.Time
	err = tx.QueryRow(ctx, "SELECT month, total_payments FROM financial_aggregates").Scan(&aggMonth1, &totalPayments1)
	if err != nil {
		t.Fatalf("failed to query aggregate after first run: %v", err)
	}

	// Second run
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("second cleanup failed: %v", err)
	}

	// Verify same aggregate count and values
	var aggCount2 int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount2)
	if err != nil {
		t.Fatalf("failed to count aggregates: %v", err)
	}
	if aggCount2 != aggCount1 {
		t.Errorf("expected %d aggregates after second run, got %d", aggCount1, aggCount2)
	}

	var totalPayments2 float64
	var aggMonth2 time.Time
	err = tx.QueryRow(ctx, "SELECT month, total_payments FROM financial_aggregates").Scan(&aggMonth2, &totalPayments2)
	if err != nil {
		t.Fatalf("failed to query aggregate after second run: %v", err)
	}
	if !aggMonth2.Equal(aggMonth1) {
		t.Errorf("expected month %s, got %s", aggMonth1.Format("2006-01-02"), aggMonth2.Format("2006-01-02"))
	}
	if totalPayments2 != totalPayments1 {
		t.Errorf("expected total_payments %.2f after second run, got %.2f", totalPayments1, totalPayments2)
	}

	// Verify payments are still deleted
	var paymentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1", bookingID).Scan(&paymentCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if paymentCount != 0 {
		t.Errorf("expected payments to be deleted, got %d", paymentCount)
	}
}

// TestCleanupExpiredFinancialRecords_ActiveUserWithin7Years verifies that
// a payment newer than 7 years for an active user is NOT deleted.
func TestCleanupExpiredFinancialRecords_ActiveUserWithin7Years(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create 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)
	}

	// Payment created 3 years ago (< 7 years)
	threeYearsAgo := clock.Now().AddDate(-3, 0, 0)
	var paymentID string
	err = tx.QueryRow(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 40.00, $2)
		RETURNING id
	`, bookingID, threeYearsAgo).Scan(&paymentID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
	}

	// Verify payment still exists
	var paymentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if paymentCount != 1 {
		t.Error("expected payment to still exist")
	}

	// Verify no aggregate was created
	var aggCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
	if err != nil {
		t.Fatalf("failed to count aggregates: %v", err)
	}
	if aggCount != 0 {
		t.Errorf("expected no aggregates, got %d", aggCount)
	}
}

// TestCleanupExpiredFinancialRecords_AnonUserBothThresholdsElapsed verifies that
// an anonymized user's payment IS deleted when BOTH the 7-year rule AND the
// 1-year post-anonymization buffer have elapsed.
func TestCleanupExpiredFinancialRecords_AnonUserBothThresholdsElapsed(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Anonymize user 2 years ago
	_, err = tx.Exec(ctx, `
		UPDATE users
		SET account_role = 'guest',
		    email = 'anon-' || id || '@anon.invalid',
		    updated_at = NOW() - INTERVAL '2 years'
		WHERE id = $1
	`, guestID)
	if err != nil {
		t.Fatalf("failed to anonymize user: %v", err)
	}

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

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

	// Payment created 8 years ago (past 7yr rule)
	// User anonymized 2 years ago (past 1yr buffer)
	// Both conditions met → should be deleted
	eightYearsAgo := clock.Now().AddDate(-8, 0, 0)
	var paymentID string
	err = tx.QueryRow(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 75.00, $2)
		RETURNING id
	`, bookingID, eightYearsAgo).Scan(&paymentID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
	}

	// Verify payment was deleted
	var paymentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if paymentCount != 0 {
		t.Error("expected payment to be deleted (both 7yr and 1yr+ thresholds elapsed)")
	}

	// Verify aggregate was created
	var aggCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM financial_aggregates").Scan(&aggCount)
	if err != nil {
		t.Fatalf("failed to count aggregates: %v", err)
	}
	if aggCount != 1 {
		t.Errorf("expected 1 financial_aggregates row, got %d", aggCount)
	}
}

// TestCleanupExpiredFinancialRecords_RefundAggregatedAndDeleted verifies that
// refunds are aggregated and deleted alongside their parent payment when
// retention expires.
func TestCleanupExpiredFinancialRecords_RefundAggregatedAndDeleted(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create 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)
	}

	// Payment 8 years ago
	eightYearsAgo := clock.Now().AddDate(-8, 0, 0)
	var paymentID string
	err = tx.QueryRow(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'full', 'cash', 'completed', 100.00, $2)
		RETURNING id
	`, bookingID, eightYearsAgo).Scan(&paymentID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Refund 8 years ago (same month as payment)
	var refundID string
	err = tx.QueryRow(ctx, `
		INSERT INTO refunds (payment_id, booking_id, amount, status, reason, created_at)
		VALUES ($1, $2, 30.00, 'completed', 'test refund', $3)
		RETURNING id
	`, paymentID, bookingID, eightYearsAgo).Scan(&refundID)
	if err != nil {
		t.Fatalf("failed to create refund: %v", err)
	}

	// Anonymize user so records qualify for deletion
	_, err = tx.Exec(ctx, `
		UPDATE users SET account_role = 'guest', email = 'anon-' || id || '@anon.invalid', updated_at = NOW() - INTERVAL '2 years'
		WHERE id = $1
	`, userID)
	if err != nil {
		t.Fatalf("failed to anonymize user: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredFinancialRecords(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredFinancialRecords failed: %v", err)
	}

	// Verify payment was deleted
	var paymentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", paymentID).Scan(&paymentCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if paymentCount != 0 {
		t.Error("expected payment to be deleted")
	}

	// Verify refund was deleted
	var refundCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM refunds WHERE id = $1", refundID).Scan(&refundCount)
	if err != nil {
		t.Fatalf("failed to count refunds: %v", err)
	}
	if refundCount != 0 {
		t.Error("expected refund to be deleted")
	}

	// Verify aggregate has both payment and refund totals
	var totalPayments, totalRefunds float64
	err = tx.QueryRow(ctx, "SELECT total_payments, total_refunds FROM financial_aggregates").Scan(&totalPayments, &totalRefunds)
	if err != nil {
		t.Fatalf("failed to query aggregate: %v", err)
	}
	if totalPayments != 100.00 {
		t.Errorf("expected total_payments 100.00, got %.2f", totalPayments)
	}
	if totalRefunds != 30.00 {
		t.Errorf("expected total_refunds 30.00, got %.2f", totalRefunds)
	}
}

// Ensure bytes is used to avoid unused import error
var _ = bytes.Buffer{}

func TestAnonymizeStaleGuestAccounts(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Guest 1: last booking 7 months ago — should be anonymized
	guest1ID, _ := fixtures.CreateTestUser(tx)
	tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guest1ID)
	tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, NOW() - INTERVAL '7 months', 'completed', false)
	`, guest1ID)

	// Guest 2: last booking 3 months ago — should NOT be anonymized
	guest2ID, _ := fixtures.CreateTestUser(tx)
	tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guest2ID)
	tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, NOW() - INTERVAL '3 months', 'completed', false)
	`, guest2ID)

	// Guest 3: has a pending booking — should NOT be anonymized (regardless of booking age)
	guest3ID, _ := fixtures.CreateTestUser(tx)
	tx.Exec(ctx, `UPDATE users SET account_role = 'guest' WHERE id = $1`, guest3ID)
	tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, NOW() + INTERVAL '2 days', 'pending', false)
	`, guest3ID)

	// Run anonymization
	_, err := AnonymizeStaleGuestAccounts(ctx)
	if err != nil {
		t.Fatalf("AnonymizeStaleGuestAccounts failed: %v", err)
	}

	// Guest 1 should be anonymized
	var g1Name string
	tx.QueryRow(ctx, `SELECT n_first_name FROM users WHERE id = $1`, guest1ID).Scan(&g1Name)
	if g1Name != "Guest" {
		t.Errorf("expected guest 1 to be anonymized, got first_name='%s'", g1Name)
	}

	// Guest 2 should NOT be anonymized
	var g2Name string
	tx.QueryRow(ctx, `SELECT n_first_name FROM users WHERE id = $1`, guest2ID).Scan(&g2Name)
	if g2Name == "Guest" {
		t.Error("expected guest 2 to NOT be anonymized (booking too recent)")
	}

	// Guest 3 should NOT be anonymized (has pending booking)
	var g3Name string
	tx.QueryRow(ctx, `SELECT n_first_name FROM users WHERE id = $1`, guest3ID).Scan(&g3Name)
	if g3Name == "Guest" {
		t.Error("expected guest 3 to NOT be anonymized (has pending booking)")
	}

	// Verify guest 1's email was anonymized
	var g1Email string
	tx.QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, guest1ID).Scan(&g1Email)
	if !strings.HasPrefix(g1Email, "anon-") {
		t.Errorf("expected guest 1 email to start with 'anon-', got '%s'", g1Email)
	}
}

// --- Tests for CleanupOldReservations (Edit Request) ---

// TestCleanupOldReservations_EditRequest verifies that edit request reservations
// older than 24 hours are deleted, while recent ones are preserved.
func TestCleanupOldReservations_EditRequest(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create old edit_request reservation (>24 hours old)
	oldTime := clock.Now().Add(-25 * time.Hour)
	_, err := tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:edit_request:bk123', $2)
	`, oldTime, clock.Now().Add(-25*time.Hour))
	if err != nil {
		t.Fatalf("failed to create old edit_request reservation: %v", err)
	}

	// Create recent edit_request reservation (<24 hours old)
	recentTime := clock.Now().Add(-12 * time.Hour)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at)
		VALUES ($1, 60, 'RESERVATION:edit_request:bk456', $2)
	`, recentTime, clock.Now().Add(-12*time.Hour))
	if err != nil {
		t.Fatalf("failed to create recent edit_request reservation: %v", err)
	}

	// Run cleanup
	_, err = CleanupOldReservations(ctx)
	if err != nil {
		t.Fatalf("CleanupOldReservations failed: %v", err)
	}

	// Verify old reservation was deleted
	var oldCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:edit_request:bk123'").Scan(&oldCount)
	if err != nil {
		t.Fatalf("failed to check old reservation: %v", err)
	}
	if oldCount != 0 {
		t.Error("expected old edit_request reservation (25h) to be deleted")
	}

	// Verify recent reservation still exists
	var recentCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = 'RESERVATION:edit_request:bk456'").Scan(&recentCount)
	if err != nil {
		t.Fatalf("failed to check recent reservation: %v", err)
	}
	if recentCount != 1 {
		t.Error("expected recent edit_request reservation (12h) to be preserved")
	}
}

// --- Tests for CleanupExpiredDeposits ---

func TestCleanupExpiredDeposits_ExpiredConfirmed(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	startTime := clock.Now().Add(12 * time.Hour)
	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, $2, 'confirmed', true)
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description)
		VALUES ($1, 60, $2)
	`, startTime, "RESERVATION:user:"+userID+":bk123")
	if err != nil {
		t.Fatalf("failed to create reservation time blocker: %v", err)
	}

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

	var status string
	err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if status != "pending_release" {
		t.Errorf("expected status 'pending_release', got '%s'", status)
	}

	var notifCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM admin_notifications WHERE booking_id = $1 AND reason = 'deposit_not_paid_by_deadline'", bookingID).Scan(&notifCount)
	if err != nil {
		t.Fatalf("failed to query admin notifications: %v", err)
	}
	if notifCount != 1 {
		t.Errorf("expected 1 deposit_not_paid_by_deadline notification, got %d", notifCount)
	}

	var tbCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = $1", "RESERVATION:user:"+userID+":bk123").Scan(&tbCount)
	if err != nil {
		t.Fatalf("failed to query time blockers: %v", err)
	}
	if tbCount != 0 {
		t.Error("expected reservation time blocker to be deleted")
	}
}

func TestCleanupExpiredDeposits_ExpiredPending(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	startTime := clock.Now().Add(12 * time.Hour)
	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, $2, 'pending', true)
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description)
		VALUES ($1, 60, $2)
	`, startTime, "RESERVATION:user:"+userID+":bk123")
	if err != nil {
		t.Fatalf("failed to create reservation time blocker: %v", err)
	}

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

	var status string
	err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if status != "pending_release" {
		t.Errorf("expected status 'pending_release', got '%s'", status)
	}

	var notifCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM admin_notifications WHERE booking_id = $1 AND reason = 'deposit_not_paid_by_deadline'", bookingID).Scan(&notifCount)
	if err != nil {
		t.Fatalf("failed to query admin notifications: %v", err)
	}
	if notifCount != 1 {
		t.Errorf("expected 1 deposit_not_paid_by_deadline notification, got %d", notifCount)
	}

	var tbCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = $1", "RESERVATION:user:"+userID+":bk123").Scan(&tbCount)
	if err != nil {
		t.Fatalf("failed to query time blockers: %v", err)
	}
	if tbCount != 0 {
		t.Error("expected reservation time blocker to be deleted")
	}
}

// TestCleanupExpiredDeposits_PaidDepositPreserved verifies that a booking
// past its deposit deadline with completed payment is NOT cancelled.
func TestCleanupExpiredDeposits_PaidDepositPreserved(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	startTime := clock.Now().Add(12 * time.Hour)
	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, $2, 'confirmed', true)
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	// Add completed payment
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, created_at)
		VALUES ($1, 'deposit', 'online_square', 'completed', 20.00, NOW() - INTERVAL '1 hour')
	`, bookingID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Create reservation time blocker
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description)
		VALUES ($1, 60, $2)
	`, startTime, "RESERVATION:user:"+userID+":bk123")
	if err != nil {
		t.Fatalf("failed to create reservation time blocker: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredDeposits(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredDeposits failed: %v", err)
	}

	// Verify status remains 'confirmed'
	var status string
	err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if status != "confirmed" {
		t.Errorf("expected status 'confirmed', got '%s'", status)
	}

	// Verify reservation time blocker remains
	var tbCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = $1", "RESERVATION:user:"+userID+":bk123").Scan(&tbCount)
	if err != nil {
		t.Fatalf("failed to query time blockers: %v", err)
	}
	if tbCount != 1 {
		t.Error("expected reservation time blocker to still exist")
	}
}

// TestCleanupExpiredDeposits_FutureDeadlinePreserved verifies that a booking
// with deposit deadline in the future (e.g. starting 48h from now, deadline is 24h from now)
// is NOT cancelled.
func TestCleanupExpiredDeposits_FutureDeadlinePreserved(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	startTime := clock.Now().Add(48 * time.Hour)
	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, $2, 'confirmed', true)
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	// Create reservation time blocker
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description)
		VALUES ($1, 60, $2)
	`, startTime, "RESERVATION:user:"+userID+":bk123")
	if err != nil {
		t.Fatalf("failed to create reservation time blocker: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredDeposits(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredDeposits failed: %v", err)
	}

	// Verify status remains 'confirmed'
	var status string
	err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if status != "confirmed" {
		t.Errorf("expected status 'confirmed', got '%s'", status)
	}

	// Verify reservation time blocker remains
	var tbCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM time_blockers WHERE description = $1", "RESERVATION:user:"+userID+":bk123").Scan(&tbCount)
	if err != nil {
		t.Fatalf("failed to query time blockers: %v", err)
	}
	if tbCount != 1 {
		t.Error("expected reservation time blocker to still exist")
	}
}

// --- Tests for CleanupExpiredGiftCards ---

// TestCleanupExpiredGiftCards verifies that gift cards unused for 24+ months
// are expired: amount_remaining set to 0, moved to gift_card_expired_balances,
// and an 'expire' transaction is recorded.
func TestCleanupExpiredGiftCards(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
	// for unredeemed gift cards (no user account to reference).
	_, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`)
	if err != nil {
		t.Fatalf("failed to alter gift_card_expired_balances: %v", err)
	}

	// Create expired gift card (unused for 25 months)
	var expiredCardID string
	err = tx.QueryRow(ctx, `
		INSERT INTO gift_cards (total_funds_added, amount_remaining, last_used_at)
		VALUES (100.00, 50.00, NOW() - INTERVAL '25 months')
		RETURNING id
	`).Scan(&expiredCardID)
	if err != nil {
		t.Fatalf("failed to create expired gift card: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredGiftCards(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredGiftCards failed: %v", err)
	}

	// Verify expired card's amount_remaining is 0
	var amountRemaining float64
	err = tx.QueryRow(ctx, `SELECT amount_remaining FROM gift_cards WHERE id = $1`, expiredCardID).Scan(&amountRemaining)
	if err != nil {
		t.Fatalf("failed to query gift card: %v", err)
	}
	if amountRemaining != 0 {
		t.Errorf("expected amount_remaining 0 for expired card, got %.2f", amountRemaining)
	}

	// Verify gift_card_expired_balances has a record (account_id IS NULL for gift cards)
	var ebCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM gift_card_expired_balances WHERE account_id IS NULL`).Scan(&ebCount)
	if err != nil {
		t.Fatalf("failed to count expired balances: %v", err)
	}
	if ebCount != 1 {
		t.Errorf("expected 1 expired balance record, got %d", ebCount)
	}

	// Verify the expired balance amount
	var originalBalance float64
	err = tx.QueryRow(ctx, `SELECT original_balance FROM gift_card_expired_balances WHERE account_id IS NULL`).Scan(&originalBalance)
	if err != nil {
		t.Fatalf("failed to query expired balance: %v", err)
	}
	if originalBalance != 50.00 {
		t.Errorf("expected original_balance 50.00, got %.2f", originalBalance)
	}

	// Verify gift_card_transactions has an 'expire' transaction
	var txCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM gift_card_transactions WHERE gift_card_id = $1 AND transaction_type = 'expire'`, expiredCardID).Scan(&txCount)
	if err != nil {
		t.Fatalf("failed to count transactions: %v", err)
	}
	if txCount != 1 {
		t.Errorf("expected 1 expire transaction, got %d", txCount)
	}
}

// TestCleanupExpiredGiftCards_SkipRecentlyUsed verifies that gift cards used
// recently (last_used_at = NOW()) are NOT expired.
func TestCleanupExpiredGiftCards_SkipRecentlyUsed(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
	// for unredeemed gift cards (no user account to reference).
	_, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`)
	if err != nil {
		t.Fatalf("failed to alter gift_card_expired_balances: %v", err)
	}

	// Create recently used gift card
	var recentCardID string
	err = tx.QueryRow(ctx, `
		INSERT INTO gift_cards (total_funds_added, amount_remaining, last_used_at)
		VALUES (100.00, 75.00, NOW())
		RETURNING id
	`).Scan(&recentCardID)
	if err != nil {
		t.Fatalf("failed to create recent gift card: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredGiftCards(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredGiftCards failed: %v", err)
	}

	// Verify recent card's amount_remaining unchanged
	var amountRemaining float64
	err = tx.QueryRow(ctx, `SELECT amount_remaining FROM gift_cards WHERE id = $1`, recentCardID).Scan(&amountRemaining)
	if err != nil {
		t.Fatalf("failed to query gift card: %v", err)
	}
	if amountRemaining != 75.00 {
		t.Errorf("expected amount_remaining 75.00 for recent card, got %.2f", amountRemaining)
	}

	// Verify no expired balances created
	var ebCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM gift_card_expired_balances`).Scan(&ebCount)
	if err != nil {
		t.Fatalf("failed to count expired balances: %v", err)
	}
	if ebCount != 0 {
		t.Errorf("expected 0 expired balance records, got %d", ebCount)
	}
}

// TestCleanupExpiredGiftCards_SkipRedeemed verifies that gift cards already
// redeemed to an account are NOT expired (they're already claimed).
func TestCleanupExpiredGiftCards_SkipRedeemed(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL
	// for unredeemed gift cards (no user account to reference). Even though this
	// card is redeemed, the function may also match other cards; ensure schema allows it.
	_, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`)
	if err != nil {
		t.Fatalf("failed to alter gift_card_expired_balances: %v", err)
	}

	// Create a user to be the redeemer
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create user: %v", err)
	}

	// Create redeemed gift card (redeemed_by IS NOT NULL) that is otherwise expired
	var redeemedCardID string
	err = tx.QueryRow(ctx, `
		INSERT INTO gift_cards (total_funds_added, amount_remaining, redeemed_by, redeemed_at, last_used_at)
		VALUES (100.00, 25.00, $1, NOW() - INTERVAL '25 months', NOW() - INTERVAL '25 months')
		RETURNING id
	`, userID).Scan(&redeemedCardID)
	if err != nil {
		t.Fatalf("failed to create redeemed gift card: %v", err)
	}

	// Run cleanup
	_, err = CleanupExpiredGiftCards(ctx)
	if err != nil {
		t.Fatalf("CleanupExpiredGiftCards failed: %v", err)
	}

	// Verify redeemed card's amount_remaining unchanged
	var amountRemaining float64
	err = tx.QueryRow(ctx, `SELECT amount_remaining FROM gift_cards WHERE id = $1`, redeemedCardID).Scan(&amountRemaining)
	if err != nil {
		t.Fatalf("failed to query gift card: %v", err)
	}
	if amountRemaining != 25.00 {
		t.Errorf("expected amount_remaining 25.00 for redeemed card, got %.2f", amountRemaining)
	}

	// Verify no expired balances created (redeemed cards are skipped)
	var ebCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM gift_card_expired_balances`).Scan(&ebCount)
	if err != nil {
		t.Fatalf("failed to count expired balances: %v", err)
	}
	if ebCount != 0 {
		t.Errorf("expected 0 expired balance records, got %d", ebCount)
	}
}

// --- Tests for CleanupIdleAccounts ---

// TestCleanupIdleAccounts_WithBalance verifies that an account idle for 5+ years
// with a balance is anonymized and the balance is moved to gift_card_expired_balances.
func TestCleanupIdleAccounts_WithBalance(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Set last_login_at to 6 years ago (past the 5yr threshold)
	_, err = tx.Exec(ctx, `UPDATE users SET last_login_at = NOW() - INTERVAL '6 years' WHERE id = $1`, userID)
	if err != nil {
		t.Fatalf("failed to set last_login_at: %v", err)
	}

	// Create a gift card balance for this user
	_, err = tx.Exec(ctx, `
		INSERT INTO user_giftcard_balances (user_id, balance)
		VALUES ($1, 150.00)
	`, userID)
	if err != nil {
		t.Fatalf("failed to create user giftcard balance: %v", err)
	}

	// Run cleanup
	_, err = CleanupIdleAccounts(ctx)
	if err != nil {
		t.Fatalf("CleanupIdleAccounts failed: %v", err)
	}

	// Verify balance was zeroed
	var balance float64
	err = tx.QueryRow(ctx, `SELECT balance FROM user_giftcard_balances WHERE user_id = $1`, userID).Scan(&balance)
	if err != nil {
		t.Fatalf("failed to query balance: %v", err)
	}
	if balance != 0 {
		t.Errorf("expected balance 0, got %.2f", balance)
	}

	// Verify gift_card_expired_balances has a record with the correct amount
	var originalBalance float64
	var ebCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*), COALESCE(SUM(original_balance), 0) FROM gift_card_expired_balances WHERE account_id = $1`, userID).Scan(&ebCount, &originalBalance)
	if err != nil {
		t.Fatalf("failed to query expired balances: %v", err)
	}
	if ebCount != 1 {
		t.Errorf("expected 1 expired balance record, got %d", ebCount)
	}
	if originalBalance != 150.00 {
		t.Errorf("expected original_balance 150.00, got %.2f", originalBalance)
	}

	// Verify user was anonymized
	var email string
	err = tx.QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, userID).Scan(&email)
	if err != nil {
		t.Fatalf("failed to query user email: %v", err)
	}
	if !strings.Contains(email, "deleted+") || !strings.HasSuffix(email, "@deleted.invalid") {
		t.Errorf("expected anonymized email 'deleted+%s@deleted.invalid', got '%s'", userID, email)
	}
}

// TestCleanupIdleAccounts_NoBalance verifies that an account idle for 2+ years
// with no balance is anonymized.
func TestCleanupIdleAccounts_NoBalance(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Set last_login_at to 3 years ago (past the 2yr threshold)
	_, err = tx.Exec(ctx, `UPDATE users SET last_login_at = NOW() - INTERVAL '3 years' WHERE id = $1`, userID)
	if err != nil {
		t.Fatalf("failed to set last_login_at: %v", err)
	}

	// Run cleanup
	_, err = CleanupIdleAccounts(ctx)
	if err != nil {
		t.Fatalf("CleanupIdleAccounts failed: %v", err)
	}

	// Verify user was anonymized
	var email string
	err = tx.QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, userID).Scan(&email)
	if err != nil {
		t.Fatalf("failed to query user email: %v", err)
	}
	if !strings.Contains(email, "deleted+") || !strings.HasSuffix(email, "@deleted.invalid") {
		t.Errorf("expected anonymized email 'deleted+%s@deleted.invalid', got '%s'", userID, email)
	}
}

// TestCleanupIdleAccounts_SkipActive verifies that recently active accounts
// are NOT anonymized.
func TestCleanupIdleAccounts_SkipActive(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create a user with recent last_login
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create user: %v", err)
	}

	// Set last_login_at to NOW() (active account)
	_, err = tx.Exec(ctx, `UPDATE users SET last_login_at = NOW() WHERE id = $1`, userID)
	if err != nil {
		t.Fatalf("failed to set last_login_at: %v", err)
	}

	// Run cleanup
	_, err = CleanupIdleAccounts(ctx)
	if err != nil {
		t.Fatalf("CleanupIdleAccounts failed: %v", err)
	}

	// Verify user was NOT anonymized
	var email string
	err = tx.QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, userID).Scan(&email)
	if err != nil {
		t.Fatalf("failed to query user email: %v", err)
	}
	if strings.Contains(email, "deleted+") {
		t.Errorf("expected active user to NOT be anonymized, got email '%s'", email)
	}
}

// TestCleanupIdleAccounts_SkipAdminGuest verifies that admin and guest accounts
// are NOT anonymized regardless of inactivity.
func TestCleanupIdleAccounts_SkipAdminGuest(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create admin user with old last_login
	adminID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create admin user: %v", err)
	}
	_, err = tx.Exec(ctx, `UPDATE users SET account_role = 'admin', last_login_at = NOW() - INTERVAL '10 years' WHERE id = $1`, adminID)
	if err != nil {
		t.Fatalf("failed to set admin role and last_login: %v", err)
	}

	// Create guest user with old last_login
	guestID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create guest user: %v", err)
	}
	_, err = tx.Exec(ctx, `UPDATE users SET account_role = 'guest', last_login_at = NOW() - INTERVAL '10 years' WHERE id = $1`, guestID)
	if err != nil {
		t.Fatalf("failed to set guest role and last_login: %v", err)
	}

	// Run cleanup
	_, err = CleanupIdleAccounts(ctx)
	if err != nil {
		t.Fatalf("CleanupIdleAccounts failed: %v", err)
	}

	// Verify admin was NOT anonymized
	var adminEmail string
	err = tx.QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, adminID).Scan(&adminEmail)
	if err != nil {
		t.Fatalf("failed to query admin email: %v", err)
	}
	if strings.Contains(adminEmail, "deleted+") {
		t.Errorf("expected admin to NOT be anonymized, got email '%s'", adminEmail)
	}

	// Verify guest was NOT anonymized
	var guestEmail string
	err = tx.QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, guestID).Scan(&guestEmail)
	if err != nil {
		t.Fatalf("failed to query guest email: %v", err)
	}
	if strings.Contains(guestEmail, "deleted+") {
		t.Errorf("expected guest to NOT be anonymized, got email '%s'", guestEmail)
	}
}

// --- Tests for CleanupOldIdempotencyKeys ---

func TestCleanupOldIdempotencyKeys_ClearsOldBookings(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

	// Create an old booking (created 48h ago, status = 'completed') with idempotency_key
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create user: %v", err)
	}

	var oldBookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, idempotency_key, created_at)
		VALUES ($1, NOW() - INTERVAL '1 hour', 'completed', 'old-key-001', NOW() - INTERVAL '48 hours')
		RETURNING id
	`, userID).Scan(&oldBookingID)
	if err != nil {
		t.Fatalf("failed to create old booking: %v", err)
	}

	// Create a recent booking (< 24h) with idempotency_key
	var recentBookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, idempotency_key, created_at)
		VALUES ($1, NOW(), 'completed', 'recent-key-002', NOW() - INTERVAL '2 hours')
		RETURNING id
	`, userID).Scan(&recentBookingID)
	if err != nil {
		t.Fatalf("failed to create recent booking: %v", err)
	}

	// Create a pending old booking (should NOT be cleared)
	var pendingBookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, idempotency_key, created_at)
		VALUES ($1, NOW() - INTERVAL '1 hour', 'pending', 'pending-key-003', NOW() - INTERVAL '48 hours')
		RETURNING id
	`, userID).Scan(&pendingBookingID)
	if err != nil {
		t.Fatalf("failed to create pending booking: %v", err)
	}

	// Run cleanup
	_, err = CleanupOldIdempotencyKeys(ctx)
	if err != nil {
		t.Fatalf("CleanupOldIdempotencyKeys failed: %v", err)
	}

	// Verify old booking's key was cleared
	var oldKey sql.NullString
	err = tx.QueryRow(ctx, `SELECT idempotency_key FROM bookings WHERE id = $1`, oldBookingID).Scan(&oldKey)
	if err != nil {
		t.Fatalf("failed to query old booking: %v", err)
	}
	if oldKey.Valid {
		t.Error("expected old booking's idempotency_key to be cleared")
	}

	// Verify recent booking's key was preserved
	var recentKey sql.NullString
	err = tx.QueryRow(ctx, `SELECT idempotency_key FROM bookings WHERE id = $1`, recentBookingID).Scan(&recentKey)
	if err != nil {
		t.Fatalf("failed to query recent booking: %v", err)
	}
	if !recentKey.Valid || recentKey.String != "recent-key-002" {
		t.Errorf("expected recent booking's idempotency_key to be preserved, got %v", recentKey)
	}

	// Verify pending old booking's key was preserved
	var pendingKey sql.NullString
	err = tx.QueryRow(ctx, `SELECT idempotency_key FROM bookings WHERE id = $1`, pendingBookingID).Scan(&pendingKey)
	if err != nil {
		t.Fatalf("failed to query pending booking: %v", err)
	}
	if !pendingKey.Valid || pendingKey.String != "pending-key-003" {
		t.Errorf("expected pending booking's idempotency_key to be preserved, got %v", pendingKey)
	}
}

func TestCleanupOldIdempotencyKeys_ClearsOldPayments(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Create an old completed payment with idempotency_key
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (payment_type, payment_method, status, amount, idempotency_key, created_by, created_at)
		VALUES ('full', 'online_square', 'completed', 50.00, 'old-pay-key', $1, NOW() - INTERVAL '48 hours')
	`, userID)
	if err != nil {
		t.Fatalf("failed to create old payment: %v", err)
	}

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

	var key sql.NullString
	err = tx.QueryRow(ctx, `SELECT idempotency_key FROM payments WHERE idempotency_key = 'old-pay-key'`).Scan(&key)
	if err == nil {
		t.Error("expected old payment's idempotency_key to be cleared")
	}
}

func TestCleanupOldIdempotencyKeys_ClearsOldTillSales(t *testing.T) {

	ctx, tx := resetTestData(t)

	// Create an admin user for till_sales.created_by
	adminID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create admin: %v", err)
	}

	// Create an old till_sale with idempotency_key
	_, err = tx.Exec(ctx, `
		INSERT INTO till_sales (item_type, total_amount, unit_price, status, payment_method, idempotency_key, created_by, created_at)
		VALUES ('gift_card', 25.00, 25.00, 'completed', 'cash', 'old-till-key', $1, NOW() - INTERVAL '48 hours')
	`, adminID)
	if err != nil {
		t.Fatalf("failed to create old till_sale: %v", err)
	}

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

	var key sql.NullString
	err = tx.QueryRow(ctx, `SELECT idempotency_key FROM till_sales WHERE idempotency_key = 'old-till-key'`).Scan(&key)
	if err == nil {
		t.Error("expected old till_sale's idempotency_key to be cleared")
	}
}

// =============================================================================
// CleanupExpiredDeposits - pending_release status
// =============================================================================

func TestCleanupExpiredDeposits_SetsPendingRelease(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

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

	soon := clock.Now().Add(1 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, soon)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	t.Cleanup(func() { fixtures.DeleteBooking(tx, bookingID) })

	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = 'confirmed', deposit_required = true WHERE id = $1", bookingID)
	if err != nil {
		t.Fatalf("failed to set booking: %v", err)
	}

	if _, err := CleanupExpiredDeposits(ctx); err != nil {
		t.Fatalf("CleanupExpiredDeposits failed: %v", err)
	}

	var status string
	err = tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking status: %v", err)
	}
	if status != "pending_release" {
		t.Errorf("expected status 'pending_release', got %q", status)
	}
}

func TestCleanupExpiredDeposits_DoesNotAffectPaidBookings(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

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

	soon := clock.Now().Add(1 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, soon)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	t.Cleanup(func() { fixtures.DeleteBooking(tx, bookingID) })

	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = 'confirmed', deposit_required = true WHERE id = $1", bookingID)
	if err != nil {
		t.Fatalf("failed to set booking: %v", err)
	}

	paymentID, err := fixtures.CreateTestPayment(tx, bookingID, 50, "online_square", "deposit", "completed")
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}
	t.Cleanup(func() { fixtures.DeletePayment(tx, paymentID) })

	if _, err := CleanupExpiredDeposits(ctx); err != nil {
		t.Fatalf("CleanupExpiredDeposits failed: %v", err)
	}

	var status string
	err = tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking status: %v", err)
	}
	if status != "confirmed" {
		t.Errorf("expected status 'confirmed' (deposit was paid), got %q", status)
	}
}

// =============================================================================
// CleanupExpiredLoyaltyRedemptions Tests
// =============================================================================

func TestCleanupExpiredLoyaltyRedemptions_DeletesExpiredPending(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Insert expired pending redemption
	_, err = tx.Exec(ctx, `
		INSERT INTO loyalty_redemptions (user_id, status, expires_at)
		VALUES ($1, 'pending', NOW() - INTERVAL '1 day')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert expired redemption: %v", err)
	}

	// Insert non-expired pending redemption (should be preserved)
	_, err = tx.Exec(ctx, `
		INSERT INTO loyalty_redemptions (user_id, status, expires_at)
		VALUES ($1, 'pending', NOW() + INTERVAL '7 days')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert active redemption: %v", err)
	}

	// Insert applied redemption (different status, should be preserved)
	_, err = tx.Exec(ctx, `
		INSERT INTO loyalty_redemptions (user_id, status, expires_at)
		VALUES ($1, 'applied', NOW() - INTERVAL '1 day')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert applied redemption: %v", err)
	}

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

	var remaining int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM loyalty_redemptions").Scan(&remaining)
	if err != nil {
		t.Fatalf("failed to count redemptions: %v", err)
	}
	if remaining != 2 {
		t.Errorf("expected 2 remaining redemptions (active + applied), got %d", remaining)
	}
}

func TestCleanupExpiredLoyaltyRedemptions_NoExpired(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Only active redemptions
	_, err = tx.Exec(ctx, `
		INSERT INTO loyalty_redemptions (user_id, status, expires_at)
		VALUES ($1, 'pending', NOW() + INTERVAL '7 days')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert active redemption: %v", err)
	}

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

	var count int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM loyalty_redemptions").Scan(&count)
	if err != nil {
		t.Fatalf("failed to count: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 remaining (no expired), got %d", count)
	}
}

func TestCleanupExpiredLoyaltyRedemptions_Empty(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

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

// =============================================================================
// CleanupOldNameHistory Tests
// =============================================================================

func TestCleanupOldNameHistory_DeletesOldEntries(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Insert an old name_history entry (7 months ago)
	_, err = tx.Exec(ctx, `
		INSERT INTO name_history (user_id, previous_first_name, previous_last_name, changed_at)
		VALUES ($1, 'Old', 'Name', NOW() - INTERVAL '7 months')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert old name_history: %v", err)
	}

	// Insert a recent name_history entry (1 month ago — should be preserved)
	_, err = tx.Exec(ctx, `
		INSERT INTO name_history (user_id, previous_first_name, previous_last_name, changed_at)
		VALUES ($1, 'Recent', 'Name', NOW() - INTERVAL '1 month')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert recent name_history: %v", err)
	}

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

	// Verify old entry was deleted
	var oldCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM name_history WHERE previous_first_name = 'Old'`).Scan(&oldCount)
	if err != nil {
		t.Fatalf("failed to count old entries: %v", err)
	}
	if oldCount != 0 {
		t.Errorf("expected old name_history entry to be deleted, got %d entries", oldCount)
	}

	// Verify recent entry was preserved
	var recentCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM name_history WHERE previous_first_name = 'Recent'`).Scan(&recentCount)
	if err != nil {
		t.Fatalf("failed to count recent entries: %v", err)
	}
	if recentCount != 1 {
		t.Errorf("expected recent name_history entry to be preserved, got %d entries", recentCount)
	}

	// Verify total entries: 1 preserved (recent), old was deleted
	var totalCount int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM name_history`).Scan(&totalCount)
	if err != nil {
		t.Fatalf("failed to count total entries: %v", err)
	}
	if totalCount != 1 {
		t.Errorf("expected 1 total entry (only recent preserved), got %d", totalCount)
	}
}

func TestCleanupOldNameHistory_EmptyTable(t *testing.T) {
	t.Parallel()
	ctx, _ := resetTestData(t)

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

func TestCleanupOldNameHistory_Idempotent(t *testing.T) {
	t.Parallel()
	ctx, tx := resetTestData(t)

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

	// Insert an old entry
	_, err = tx.Exec(ctx, `
		INSERT INTO name_history (user_id, previous_first_name, previous_last_name, changed_at)
		VALUES ($1, 'Old', 'Name', NOW() - INTERVAL '7 months')
	`, userID)
	if err != nil {
		t.Fatalf("failed to insert name_history: %v", err)
	}

	// Run cleanup twice
	_, err = CleanupOldNameHistory(ctx)
	if err != nil {
		t.Fatalf("first CleanupOldNameHistory failed: %v", err)
	}

	_, err = CleanupOldNameHistory(ctx)
	if err != nil {
		t.Fatalf("second CleanupOldNameHistory failed: %v", err)
	}

	// Verify no errors and still clean
	var count int
	err = tx.QueryRow(ctx, `SELECT COUNT(*) FROM name_history`).Scan(&count)
	if err != nil {
		t.Fatalf("failed to count: %v", err)
	}
	if count != 0 {
		t.Errorf("expected 0 entries after idempotent cleanup, got %d", count)
	}
}
