//go:build test && dev

package bookings

// Package bookings contains tests for user-facing booking endpoints.
//
// Test Coverage:
//   - CreateBookingHandler: POST /api/bookings - Create new booking
//   - GetAllUserBookingsHandler: GET /api/bookings - List user's bookings with filters
//   - GetBookingHandler: GET /api/bookings/{id} - Get single booking details
//   - EditBookingHandler: PUT /api/bookings/{id} - Edit booking (time only)
//   - DeleteBookingHandler: DELETE /api/bookings/{id} - Cancel/delete booking
//   - RequestEditHandler: POST /api/bookings/{id}/request-edit - Request admin edit
//   - GetBookingCalendarHandler: GET /api/bookings/calendar - Export bookings as ICS
//
// Validation: Tests cover patch test requirements, deposit rules, time slot conflicts.
import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"crussell/clock"
	"crussell/db"
	"crussell/handlers/user"
	"crussell/internal/validators"
	"crussell/mw"
	"crussell/testutils"
	"crussell/testutils/fixtures"
	"crussell/testutils/jwt"

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

// nextWorkingHour returns a time within working hours (08:00-19:00) that is
// always < 24h from now. This avoids time-of-day flakiness in tests that need
// a booking within the 24-hour no-show window. Working hours are 08:00-20:00;
// we cap at 19:00 so a 60-minute service finishes before closing.
func nextWorkingHour() time.Time {
	now := clock.Now().In(londonLocation)
	soon := now.Add(2 * time.Hour).Truncate(time.Second)
	if soon.Hour() < 8 {
		return time.Date(soon.Year(), soon.Month(), soon.Day(), 9, 0, 0, 0, soon.Location()).UTC()
	}
	if soon.Hour() >= 19 {
		return time.Date(soon.Year(), soon.Month(), soon.Day()+1, 9, 0, 0, 0, soon.Location()).UTC()
	}
	return soon.UTC()
}

// helper function to make JSON request with JWT auth
// For authenticated requests, use makeAuthRequest which extracts user from JWT
func makeRequest(handler http.Handler, method, path string, body interface{}, token string, requestCtx ...context.Context) *httptest.ResponseRecorder {
	return makeAuthRequest(handler, method, path, body, token, "", requestCtx...)
}

// makeAuthRequest creates request with optional JWT auth and userID override.
// An optional requestCtx can be provided to carry a per-test transaction
// (from SetupTestTx) for PoolProxy routing. If nil, context.Background() is used.
func makeAuthRequest(handler http.Handler, method, path string, body interface{}, token, userIDOverride string, requestCtx ...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)
	}
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}

	// Use provided context (carries per-test tx) or fall back to req.Context()
	baseCtx := req.Context()
	if len(requestCtx) > 0 {
		baseCtx = requestCtx[0]
	}

	// Set up chi routing context for path params
	rctx := chi.NewRouteContext()
	if id, paramName := extractIDFromPath(path); id != "" {
		rctx.URLParams.Add(paramName, id)
	}
	ctx := context.WithValue(baseCtx, chi.RouteCtxKey, rctx)

	// Set user context - either from override or attempt to extract from token
	var userID, userRole string
	if userIDOverride != "" {
		userID = userIDOverride
		userRole = "verified_email"
	} else if token != "" {
		// For test JWTs, extract user info from token by parsing it
		// Use JWT secret to parse
		if info := extractUserFromTestJWT(token); info != nil {
			userID = info.userID
			userRole = info.role
		}
	}

	if userID != "" {
		ctx = context.WithValue(ctx, mw.UserIDKey, userID)
		ctx = context.WithValue(ctx, mw.UserRoleKey, userRole)
	}

	req = req.WithContext(ctx)

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

// userInfo holds extracted user from JWT
type userInfo struct {
	userID string
	role   string
}

// extractUserFromTestJWT extracts user info from test JWT
func extractUserFromTestJWT(token string) *userInfo {
	// Parse the JWT without verification for tests
	// JWT format: header.payload.signature
	parts := strings.Split(token, ".")
	if len(parts) != 3 {
		return nil
	}

	// Decode the payload (middle part)
	payload := parts[1]
	// Add padding if needed
	if len(payload)%4 != 0 {
		payload += strings.Repeat("=", 4-len(payload)%4)
	}

	decoded, err := base64URLDecode(payload)
	if err != nil {
		return nil
	}

	// Parse JSON to get claims
	var claims map[string]interface{}
	if err := json.Unmarshal(decoded, &claims); err != nil {
		return nil
	}

	// Extract user_id (not "sub" - auth.GenerateToken uses "user_id")
	userID, _ := claims["user_id"].(string)
	role, _ := claims["role"].(string)

	if userID == "" {
		return nil
	}

	return &userInfo{userID: userID, role: role}
}

func base64URLDecode(s string) ([]byte, error) {
	return base64.URLEncoding.DecodeString(s)
}

// extractIDFromPath extracts the ID from URL paths
func extractIDFromPath(path string) (string, string) {
	patterns := []struct {
		prefix    string
		paramName string
	}{
		{"/api/bookings/", "id"},
		{"/api/admin/bookings/", "id"},
		{"/api/services/", "id"},
	}
	for _, p := range patterns {
		if idx := findLastSegment(path, p.prefix); idx >= 0 {
			// Extract only up to next '/' or end of path
			endIdx := len(path)
			for i := idx; i < len(path); i++ {
				if path[i] == '/' {
					endIdx = i
					break
				}
			}
			return path[idx:endIdx], p.paramName
		}
	}
	return "", ""
}

func findLastSegment(path, prefix string) int {
	for i := len(path) - 1; i >= len(prefix); i-- {
		if len(path) > i && path[i-len(prefix):i] == prefix {
			return i
		}
	}
	return -1
}

// Helper to parse response body
func parseResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
	return json.Unmarshal(w.Body.Bytes(), dest)
}

// =============================================================================
// Create Booking Tests
// =============================================================================

// TestBookings_Create tests that a user can successfully create a new booking
// with a valid future time and at least one service. The test verifies the
// booking is created in the database and associated with the correct user.
func TestBookings_Create(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Seed working hours for booking tests

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

	// Note: deposits_required=0 by default now (was 3)
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Generate token for user
	token := jwt.GenerateUserToken(userID)

	// Create booking request - use future time (1h+ advance is enforced for USERS only)
	// Use 10:00 to ensure service fits within working hours (08:00-20:00)
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	// Verify booking was created in DB
	var count int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM bookings WHERE user_id = $1", userID).Scan(&count)
	if err != nil {
		t.Errorf("failed to query bookings: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 booking, got %d", count)
	}

	// Verify booking_services was created
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_services WHERE booking_id IN (SELECT id FROM bookings WHERE user_id = $1)", userID).Scan(&count)
	if err != nil {
		t.Errorf("failed to query booking_services: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 booking_service, got %d", count)
	}
}

// TestBookings_Create_InvalidInput verifies that booking creation fails
// with HTTP 400 when required fields are missing: start time or service IDs.
func TestBookings_Create_InvalidInput(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	tests := []struct {
		name string
		req  CreateBookingRequest
	}{
		{
			name: "missing start time",
			req: CreateBookingRequest{
				ServiceIDs: []string{"some-service-id"},
			},
		},
		{
			name: "missing service IDs",
			req: CreateBookingRequest{
				StartTime: clock.Now().Add(72 * time.Hour),
			},
		},
		{
			name: "empty service IDs",
			req: CreateBookingRequest{
				StartTime:  clock.Now().Add(72 * time.Hour),
				ServiceIDs: []string{},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := http.HandlerFunc(CreateBookingHandler)
			w := makeRequest(handler, "POST", "/api/bookings", tt.req, token, ctx)

			// Both missing start time and missing/empty service IDs should return 400
			if w.Code != http.StatusBadRequest {
				t.Errorf("expected status 400, got %d", w.Code)
			}
		})
	}
}

// =============================================================================
// List Bookings Tests
// =============================================================================

// TestBookings_List tests that a user can retrieve their list of bookings.
// The test verifies the response includes the correct total count and that
// bookings are properly returned.
func TestBookings_List(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetAllUserBookingsHandler)
	w := makeRequest(handler, "GET", "/api/bookings", nil, token, ctx)

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

	var resp BookingListResponse
	if err := parseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if len(resp.Bookings) != 1 {
		t.Errorf("expected 1 booking, got %d", len(resp.Bookings))
	}

	if resp.Total != 1 {
		t.Errorf("expected total 1, got %d", resp.Total)
	}
}

// TestBookings_List_FilterByStatus tests that booking list can be filtered
// by status (e.g., pending, completed). It verifies that non-matching statuses
// return empty results.
func TestBookings_List_FilterByStatus(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Create a pending booking
	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	token := jwt.GenerateUserToken(userID)

	// Test filtering by status
	handler := http.HandlerFunc(GetAllUserBookingsHandler)
	w := makeRequest(handler, "GET", "/api/bookings?status=pending", nil, token, ctx)

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

	var resp BookingListResponse
	if err := parseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if len(resp.Bookings) != 1 {
		t.Errorf("expected 1 booking, got %d", len(resp.Bookings))
	}

	// Test filtering by non-matching status
	w = makeRequest(handler, "GET", "/api/bookings?status=completed", nil, token, ctx)

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

	if err := parseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if len(resp.Bookings) != 0 {
		t.Errorf("expected 0 bookings for completed status, got %d", len(resp.Bookings))
	}
}

// =============================================================================
// Get Single Booking Tests
// =============================================================================

// TestBookings_Get tests that a user can retrieve a single booking by its ID.
// The test verifies the booking details including services are returned.
func TestBookings_Get(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetBookingHandler)
	w := makeRequest(handler, "GET", "/api/bookings/"+bookingID, nil, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if booking.ID != bookingID {
		t.Errorf("expected booking ID %s, got %s", bookingID, booking.ID)
	}

	if len(booking.Services) != 1 {
		t.Errorf("expected 1 service, got %d", len(booking.Services))
	}
}

// TestBookings_Get_NotFound verifies that requesting a non-existent booking
// returns HTTP 404 Not Found.
func TestBookings_Get_NotFound(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetBookingHandler)
	w := makeRequest(handler, "GET", "/api/bookings/nonexistent-id", nil, token, ctx)

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

// TestBookings_Get_AccessDenied tests that a user cannot access another user's
// booking. The test creates two users, one creates a booking, and the other
// attempts to access it - expecting HTTP 404 (not found/access denied).
func TestBookings_Get_AccessDenied(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Create two test users
	userID1, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user 1: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID1)

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

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

	// Create a booking for user1
	bookingID, err := fixtures.CreateTestBooking(tx, userID1, serviceID)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	// Try to access with user2's token
	token := jwt.GenerateUserToken(userID2)

	handler := http.HandlerFunc(GetBookingHandler)
	w := makeRequest(handler, "GET", "/api/bookings/"+bookingID, nil, token, ctx)

	// Should return not found (or access denied) since user2 doesn't own the booking
	if w.Code != http.StatusNotFound {
		t.Errorf("expected status 404, got %d", w.Code)
	}
}

// =============================================================================
// Get Calendar Tests
// =============================================================================

// TestBookings_GetCalendar tests that a user can export their booking
// as an ICS calendar file. It verifies the response has the correct
// text/calendar Content-Type and contains ICS-formatted data.
func TestBookings_GetCalendar(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetBookingCalendarHandler)
	w := makeRequest(handler, "GET", "/api/bookings/"+bookingID+"/calendar", nil, token, ctx)

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

	// Check that Content-Type is text/calendar
	contentType := w.Header().Get("Content-Type")
	if contentType != "text/calendar; charset=utf-8" {
		t.Errorf("expected Content-Type text/calendar, got %s", contentType)
	}

	// Check that response contains ICS data
	body := w.Body.String()
	if len(body) == 0 {
		t.Error("expected non-empty ICS response")
	}

	// Basic ICS validation
	if !bytes.Contains([]byte(body), []byte("BEGIN:VCALENDAR")) {
		t.Error("expected ICS to contain BEGIN:VCALENDAR")
	}
	if !bytes.Contains([]byte(body), []byte("BEGIN:VEVENT")) {
		t.Error("expected ICS to contain BEGIN:VEVENT")
	}
}

// TestBookings_GetCalendar_NotFound verifies that attempting to export
// a non-existent booking to calendar returns HTTP 404.
func TestBookings_GetCalendar_NotFound(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetBookingCalendarHandler)
	w := makeRequest(handler, "GET", "/api/bookings/nonexistent-id/calendar", nil, token, ctx)

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

// =============================================================================
// Edit Booking Tests
// =============================================================================

// TestBookings_Edit tests that a user can modify the start time of
// their existing booking. The test verifies the time is updated in the DB.
func TestBookings_Edit(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	// Update to a future time
	newStartTime := clock.Now().Add(96 * time.Hour).Truncate(time.Second)
	req := EditBookingRequest{
		StartTime: newStartTime,
	}

	handler := http.HandlerFunc(EditBookingHandler)
	w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	// Verify the start time was updated in DB
	var dbStartTime time.Time
	err = tx.QueryRow(ctx,
		"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime)
	if err != nil {
		t.Errorf("failed to query booking: %v", err)
	}

	// Times should match (truncated to seconds)
	if !dbStartTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected start_time %v, got %v", newStartTime, dbStartTime)
	}
}

// TestBookings_Edit_InvalidInput verifies that editing fails with HTTP 400
// when the start time is missing or is in the past.
func TestBookings_Edit_InvalidInput(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	tests := []struct {
		name string
		req  EditBookingRequest
	}{
		{
			name: "missing start time",
			req:  EditBookingRequest{},
		},
		{
			name: "past start time",
			req: EditBookingRequest{
				StartTime: clock.Now().Add(-1 * time.Hour),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := http.HandlerFunc(EditBookingHandler)
			w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, tt.req, token, ctx)

			if w.Code != http.StatusBadRequest {
				t.Errorf("expected status 400, got %d", w.Code)
			}
		})
	}
}

// TestBookings_Edit_NotFound verifies that editing a non-existent
// booking returns HTTP 404.
func TestBookings_Edit_NotFound(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	req := EditBookingRequest{
		StartTime: clock.Now().Add(96 * time.Hour),
	}

	handler := http.HandlerFunc(EditBookingHandler)
	w := makeRequest(handler, "PUT", "/api/bookings/nonexistent-id", req, token, ctx)

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

// =============================================================================
// Delete Booking Tests
// =============================================================================

// TestBookings_Delete tests that a user can delete (cancel) their booking.
// For bookings without payments, it performs a hard delete. The test verifies
// the booking is removed from the database.
func TestBookings_Delete(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	// Delete the booking
	handler := http.HandlerFunc(DeleteBookingHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)

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

	// Verify booking was deleted from DB
	var count int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM bookings WHERE id = $1", bookingID).Scan(&count)
	if err != nil {
		t.Errorf("failed to query bookings: %v", err)
	}
	if count != 0 {
		t.Errorf("expected booking to be deleted, but found %d", count)
	}
}

// TestBookings_Delete_WithAdminNotifications verifies that a booking with existing
// admin_notifications can be hard-deleted (regression test for FK constraint bug).
// Every booking in production has at least one admin_notification row, and the
// admin_notifications.booking_id FK has no ON DELETE CASCADE, so the DELETE must
// explicitly clean up notifications first.
func TestBookings_Delete_WithAdminNotifications(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	_, err = tx.Exec(ctx,
		`INSERT INTO admin_notifications (reason, booking_id, user_id) VALUES ($1, $2, $3)`,
		"pending_booking", bookingID, userID)
	if err != nil {
		t.Fatalf("failed to create admin_notification: %v", err)
	}
	_, err = tx.Exec(ctx,
		`INSERT INTO admin_notifications (reason, booking_id, user_id) VALUES ($1, $2, $3)`,
		"new_booking", bookingID, userID)
	if err != nil {
		t.Fatalf("failed to create second admin_notification: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(DeleteBookingHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)

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

	// Verify booking was deleted from DB
	var bookingCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM bookings WHERE id = $1", bookingID).Scan(&bookingCount)
	if err != nil {
		t.Fatalf("failed to query bookings: %v", err)
	}
	if bookingCount != 0 {
		t.Errorf("expected booking to be deleted, but found %d", bookingCount)
	}

	// Verify admin_notifications for this booking were also cleaned up
	var notifCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM admin_notifications WHERE booking_id = $1", bookingID).Scan(&notifCount)
	if err != nil {
		t.Fatalf("failed to query admin_notifications: %v", err)
	}
	if notifCount != 0 {
		t.Errorf("expected admin_notifications to be cleaned up, but found %d", notifCount)
	}
}

// TestBookings_Delete_WithReason verifies that cancelling a booking with
// an associated payment requires a reason (client_cancelled). Without a reason,
// the request fails with HTTP 400. With a reason, the booking is soft-deleted
// (status changed to client_cancelled).
func TestBookings_Delete_WithReason(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	// Add a payment to the booking (so it requires a reason)
	_, err = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		bookingID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Try to delete without reason - should fail
	handler := http.HandlerFunc(DeleteBookingHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for missing reason, got %d", w.Code)
	}

	// Delete with reason
	req := map[string]string{"reason": "client_cancelled"}
	w = makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, req, token, ctx)

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

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

// TestBookings_Delete_NotFound verifies that deleting a non-existent
// booking returns HTTP 404.
func TestBookings_Delete_NotFound(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(DeleteBookingHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/nonexistent-id", nil, token, ctx)

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

// TestBookings_Delete_NoShow24hThreshold tests the 24-hour no-show rule:
// - Cancellation < 24 hours before appointment: treated as no-show (deposits = 3)
// - Cancellation >= 24 hours before appointment: treated as late_cancellation
func TestBookings_Delete_NoShow24hThreshold(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

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

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

	token := jwt.GenerateUserToken(userID)

	// Create booking 23 hours from now (will be < 24h when deleted)
	soonTime := clock.Now().Add(23 * time.Hour).Truncate(time.Second)
	soonTime = time.Date(soonTime.Year(), soonTime.Month(), soonTime.Day(), 10, 0, 0, 0, soonTime.Location())

	bookingReq := CreateBookingRequest{
		StartTime:  soonTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token, ctx)

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking response: %v", err)
	}

	// Add payment so deletion works
	_, _ = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		booking.ID)

	// Delete within 24 hours (no forgiveness) - should result in no-show + deposits penalty
	delHandler := http.HandlerFunc(DeleteBookingHandler)
	delReq := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": false}
	w = makeRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, ctx)

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

	// Verify deposits were applied
	var deposits int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits != 0 {
		t.Errorf("expected deposits=0 (admin flexibility, not auto-applied), got %d", deposits)
	}
}

// TestBookings_Delete_NoShow_WithForgiveness tests that admin can forgive a no-show
func TestBookings_Delete_NoShow_WithForgiveness(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

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

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

	token := jwt.GenerateUserToken(userID)

	// Create booking 20 hours from now (will be < 24h)
	soonTime := clock.Now().Add(20 * time.Hour).Truncate(time.Second)
	soonTime = time.Date(soonTime.Year(), soonTime.Month(), soonTime.Day(), 10, 0, 0, 0, soonTime.Location())

	bookingReq := CreateBookingRequest{
		StartTime:  soonTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token, ctx)

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking response: %v", err)
	}

	// Add payment
	_, _ = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		booking.ID)

	// Delete within 24 hours WITH forgiveness
	trueVal := true
	delHandler := http.HandlerFunc(DeleteBookingHandler)
	delReq := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": trueVal}
	w = makeRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, ctx)

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

	// Verify NO deposits were applied (forgiveness worked)
	var deposits int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits != 0 {
		t.Errorf("expected deposits=0 after forgiveness, got %d", deposits)
	}
}

// =============================================================================
// Unauthorized Tests
// =============================================================================

// TestBookings_Unauthorized tests that all booking endpoints require
// authentication. It verifies that requests without a token are rejected with
// HTTP 401 for protected endpoints.
func TestBookings_Unauthorized(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	tests := []struct {
		name   string
		method string
		path   string
		body   interface{}
	}{
		{
			name:   "Create without token",
			method: "POST",
			path:   "/api/bookings",
			body:   CreateBookingRequest{ServiceIDs: []string{serviceID}},
		},
		{
			name:   "List without token",
			method: "GET",
			path:   "/api/bookings",
			body:   nil,
		},
		{
			name:   "Get without token",
			method: "GET",
			path:   "/api/bookings/" + bookingID,
			body:   nil,
		},
		{
			name:   "GetCalendar without token",
			method: "GET",
			path:   "/api/bookings/" + bookingID + "/calendar",
			body:   nil,
		},
		{
			name:   "Edit without token",
			method: "PUT",
			path:   "/api/bookings/" + bookingID,
			body:   EditBookingRequest{StartTime: clock.Now().Add(96 * time.Hour)},
		},
		{
			name:   "Delete without token",
			method: "DELETE",
			path:   "/api/bookings/" + bookingID,
			body:   nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Get the appropriate handler
			var handler http.HandlerFunc
			switch tt.path {
			case "/api/bookings":
				if tt.method == "POST" {
					handler = CreateBookingHandler
				} else {
					handler = GetAllUserBookingsHandler
				}
			case "/api/bookings/" + bookingID:
				if tt.method == "GET" {
					handler = GetBookingHandler
				} else if tt.method == "PUT" {
					handler = EditBookingHandler
				} else {
					handler = DeleteBookingHandler
				}
			case "/api/bookings/" + bookingID + "/calendar":
				handler = GetBookingCalendarHandler
			}

			w := makeRequest(http.HandlerFunc(handler), tt.method, tt.path, tt.body, "")

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

// =============================================================================
// Additional Edge Case Tests
// =============================================================================

// TestBookings_List_Empty tests that listing bookings for a user with no
// bookings returns an empty list with total 0.
func TestBookings_List_Empty(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetAllUserBookingsHandler)
	w := makeRequest(handler, "GET", "/api/bookings", nil, token, ctx)

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

	var resp BookingListResponse
	if err := parseResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if len(resp.Bookings) != 0 {
		t.Errorf("expected 0 bookings, got %d", len(resp.Bookings))
	}

	if resp.Total != 0 {
		t.Errorf("expected total 0, got %d", resp.Total)
	}
}

// TestBookings_Get_InvalidBookingID verifies that using an invalid
// booking ID format returns HTTP 404 or 400.
func TestBookings_Get_InvalidBookingID(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetBookingHandler)
	w := makeRequest(handler, "GET", "/api/bookings/", nil, token, ctx) // trailing slash

	// Should return 404 or 400 depending on routing
	if w.Code != http.StatusNotFound && w.Code != http.StatusBadRequest {
		t.Errorf("expected status 404 or 400, got %d", w.Code)
	}
}

// TestBookings_Create_PastDate verifies that creating a booking with a
// past start time fails with HTTP 400 Bad Request.
func TestBookings_Create_PastDate(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	pastTime := clock.Now().Add(-24 * time.Hour).Truncate(time.Second)
	req := CreateBookingRequest{
		StartTime:  pastTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for past date, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestBookings_Create_MinimumAdvance tests that bookings must be made at least
// 1 hour in advance (changed from 48h deposit requirement to universal 1h rule).
func TestBookings_Create_MinimumAdvance(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Seed working hours for booking tests

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

	// Note: deposits_required=0 by default now
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Test booking 1+ hour in advance - should succeed
	aheadTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	aheadTime = time.Date(aheadTime.Year(), aheadTime.Month(), aheadTime.Day(), 10, 0, 0, 0, aheadTime.Location())
	req := CreateBookingRequest{
		StartTime:  aheadTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if booking.DepositRequired {
		t.Error("expected deposit_required=false when user has deposits_required=0")
	}
}

// TestBookings_Create_WithNotes_StatusPending tests that when a booking is created with notes,
// the booking status is automatically set to 'pending' (requires admin approval).
func TestBookings_Create_WithNotes_StatusPending(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Seed working hours

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

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

	token := jwt.GenerateUserToken(userID)

	// Booking 2+ hours ahead with notes
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	notes := "Special treatment needed"
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
		Notes:      &notes,
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if booking.Status != "pending" {
		t.Errorf("expected status 'pending' when notes provided, got %q", booking.Status)
	}

	if booking.Notes == nil || *booking.Notes != notes {
		t.Error("notes not preserved in booking")
	}
}

// TestBookings_Create_WithoutNotes_StatusConfirmed tests that when a booking is created without notes,
// the booking status is automatically set to 'confirmed' (auto-approved).
func TestBookings_Create_WithoutNotes_StatusConfirmed(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Seed working hours

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

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

	token := jwt.GenerateUserToken(userID)

	// Booking 2+ hours ahead without notes
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
		Notes:      nil,
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if booking.Status != "confirmed" {
		t.Errorf("expected status 'confirmed' without notes, got %q", booking.Status)
	}
}

// TestBookings_Create_Within1Hour_ShouldFail tests that bookings less than 1 hour in advance are rejected.
func TestBookings_Create_Within1Hour_ShouldFail(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Seed working hours

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

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

	token := jwt.GenerateUserToken(userID)

	// Booking less than 1 hour in advance (30 minutes)
	sooonTime := clock.Now().Add(30 * time.Minute).Truncate(time.Second)
	req := CreateBookingRequest{
		StartTime:  sooonTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

// TestBookings_Create_MultipleServices verifies that a booking can include
// multiple services at once, and all services are properly associated with
// the booking in the database.
func TestBookings_Create_MultipleServices(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Seed working hours for booking tests

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	// Use 10:00 to ensure services fit within working hours (08:00-20:00)
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID1, serviceID2},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if len(booking.Services) != 2 {
		t.Errorf("expected 2 services in booking, got %d", len(booking.Services))
	}
}

// =============================================================================
// No-Show Deposit Logic Tests (Task 5)
// =============================================================================

// TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3 tests that when a confirmed
// booking is cancelled within 24 hours (with no forgiveness), the system
// overrides the cancellation to "no_show" and deposits_required stays 0.
func TestDeleteBooking_NoShowUnder24h_SetsDepositsTo3(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	soonTime := nextWorkingHour()

	bookingReq := CreateBookingRequest{
		StartTime:  soonTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token, ctx)

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking response: %v", err)
	}

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

	_, err = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		booking.ID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	delHandler := http.HandlerFunc(DeleteBookingHandler)
	delReq := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": false}
	w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, "", ctx)

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

	var deposits int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits != 0 {
		t.Errorf("expected deposits_required=0, got %d", deposits)
	}

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

// TestDeleteBooking_CancelOver24h_NoDepositPenalty tests that when a booking is
// cancelled with more than 24 hours notice (client_cancelled), no deposit penalty
// is applied and deposits_required remains 0.
func TestDeleteBooking_CancelOver24h_NoDepositPenalty(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

	// Create test user with deposits_required = 0
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Ensure deposits_required = 0
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Create booking with start_time = now + 48 hours (> 24h notice)
	laterTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	laterTime = time.Date(laterTime.Year(), laterTime.Month(), laterTime.Day(), 10, 0, 0, 0, laterTime.Location())

	bookingReq := CreateBookingRequest{
		StartTime:  laterTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token, ctx)

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking response: %v", err)
	}

	// Add a payment to the booking
	_, err = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		booking.ID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Delete booking with reason "client_cancelled" (>= 24h notice)
	delHandler := http.HandlerFunc(DeleteBookingHandler)
	delReq := map[string]interface{}{"reason": "client_cancelled"}
	w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, "", ctx)

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

	// Verify deposits_required = 0 (no penalty)
	var deposits int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits != 0 {
		t.Errorf("expected deposits_required=0, got %d", deposits)
	}

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

// TestDeleteBooking_NoShowWithForgiveness_NoPenalty tests that when a confirmed
// booking is cancelled within 24 hours with forgiveness, the system overrides to
// "client_cancelled" (no no-show penalty).
func TestDeleteBooking_NoShowWithForgiveness_NoPenalty(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	soonTime := nextWorkingHour()

	bookingReq := CreateBookingRequest{
		StartTime:  soonTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", bookingReq, token, ctx)

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking response: %v", err)
	}

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

	_, err = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		booking.ID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	delHandler := http.HandlerFunc(DeleteBookingHandler)
	delReq := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": true}
	w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, delReq, token, "", ctx)

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

	// Verify deposits_required = 0 (no penalty due to forgiveness)
	var deposits int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits != 0 {
		t.Errorf("expected deposits_required=0 after forgiveness, got %d", deposits)
	}

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

// TestDeleteBooking_SecondNoShow_StaysAt3 tests that when a user has their second
// no-show, the deposits_required stays at 3 (not 6). The handler sets deposits to 3
// on the first no-show and doesn't increment on subsequent no-shows.
func TestDeleteBooking_SecondNoShow_StaysAt3(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

	// Create test user with deposits_required = 0
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Ensure deposits_required = 0
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// === First booking: no-show ===
	soonTime1 := nextWorkingHour()

	bookingReq1 := CreateBookingRequest{
		StartTime:  soonTime1,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", bookingReq1, token, ctx)

	var booking1 Booking
	if err := parseResponseBody(w, &booking1); err != nil {
		t.Fatalf("failed to parse booking response: %v", err)
	}

	_, _ = tx.Exec(ctx,
		"UPDATE bookings SET status = 'confirmed' WHERE id = $1", booking1.ID)

	_, _ = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		booking1.ID)

	delHandler := http.HandlerFunc(DeleteBookingHandler)
	delReq1 := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": false}
	w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking1.ID, delReq1, token, "", ctx)

	if w.Code != http.StatusOK {
		t.Fatalf("first delete failed: %d body: %s", w.Code, w.Body.String())
	}

	var deposits1 int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits1)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits1 != 0 {
		t.Errorf("expected deposits=0 after first no-show, got %d", deposits1)
	}

	// === Second booking: no-show ===
	soonTime2 := nextWorkingHour().AddDate(0, 0, 1)

	bookingReq2 := CreateBookingRequest{
		StartTime:  soonTime2,
		ServiceIDs: []string{serviceID},
	}

	w = makeRequest(handler, "POST", "/api/bookings", bookingReq2, token, ctx)

	var booking2 Booking
	if err := parseResponseBody(w, &booking2); err != nil {
		t.Fatalf("failed to parse second booking response: %v", err)
	}

	_, _ = tx.Exec(ctx,
		"UPDATE bookings SET status = 'confirmed' WHERE id = $1", booking2.ID)

	_, _ = tx.Exec(ctx,
		"INSERT INTO payments (booking_id, payment_type, payment_method, status, amount) VALUES ($1, 'full', 'in_person_card', 'completed', 50.00)",
		booking2.ID)

	delReq2 := map[string]interface{}{"reason": "client_cancelled", "forgive_no_show": false}
	w = makeAuthRequest(delHandler, "DELETE", "/api/bookings/"+booking2.ID, delReq2, token, "", ctx)

	if w.Code != http.StatusOK {
		t.Fatalf("second delete failed: %d body: %s", w.Code, w.Body.String())
	}

	var deposits2 int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits2)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits2 != 0 {
		t.Errorf("expected deposits=0, got %d", deposits2)
	}
}

// TestCountUnforgivenNoShows_ExcludesForgiven tests that CountUnforgivenNoShows
// excludes bookings that have been forgiven (in forgiven_no_shows table).
func TestCountUnforgivenNoShows_ExcludesForgiven(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	// Create 3 bookings with status "no_show" within last 6 months
	now := clock.Now()
	for i := 0; i < 3; i++ {
		startTime := now.Add(time.Duration(i*30) * 24 * time.Hour) // 0, 30, 60 days ago
		var bookingID string
		err := tx.QueryRow(ctx, `
			INSERT INTO bookings (user_id, start_time, status, notes)
			VALUES ($1, $2, 'no_show', 'test no-show')
			RETURNING id
		`, userID, startTime).Scan(&bookingID)
		if err != nil {
			t.Fatalf("failed to create booking %d: %v", i, err)
		}

		// Link service
		_, err = tx.Exec(ctx,
			"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
			bookingID, serviceID)
		if err != nil {
			t.Fatalf("failed to link service: %v", err)
		}

		// Forgive the first one (i == 0)
		if i == 0 {
			_, err = tx.Exec(ctx,
				"INSERT INTO forgiven_no_shows (booking_id) VALUES ($1)",
				bookingID)
			if err != nil {
				t.Fatalf("failed to forgive no-show: %v", err)
			}
		}
	}

	// Call CountUnforgivenNoShows directly (unexported but in same package)
	count, err := CountUnforgivenNoShows(ctx, userID)
	if err != nil {
		t.Fatalf("CountUnforgivenNoShows failed: %v", err)
	}

	// Should be 2 (3 total - 1 forgiven = 2)
	if count != 2 {
		t.Errorf("expected count=2 (3 total - 1 forgiven), got %d", count)
	}
}

// TestCountUnforgivenNoShows_ExcludesOld tests that CountUnforgivenNoShows
// excludes no-shows older than 6 months.
func TestCountUnforgivenNoShows_ExcludesOld(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	now := clock.Now()

	// Create 1 booking with status "no_show" from 7 months ago (should be excluded)
	oldStartTime := now.Add(-7 * 30 * 24 * time.Hour)
	var oldBookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, notes)
		VALUES ($1, $2, 'no_show', 'old no-show')
		RETURNING id
	`, userID, oldStartTime).Scan(&oldBookingID)
	if err != nil {
		t.Fatalf("failed to create old booking: %v", err)
	}
	_, _ = tx.Exec(ctx,
		"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
		oldBookingID, serviceID)

	// Create 1 booking with status "no_show" from 1 month ago (should be included)
	recentStartTime := now.Add(-30 * 24 * time.Hour)
	var recentBookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, notes)
		VALUES ($1, $2, 'no_show', 'recent no-show')
		RETURNING id
	`, userID, recentStartTime).Scan(&recentBookingID)
	if err != nil {
		t.Fatalf("failed to create recent booking: %v", err)
	}
	_, _ = tx.Exec(ctx,
		"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
		recentBookingID, serviceID)

	// Call CountUnforgivenNoShows directly
	count, err := CountUnforgivenNoShows(ctx, userID)
	if err != nil {
		t.Fatalf("CountUnforgivenNoShows failed: %v", err)
	}

	// Should be 1 (only the recent one within 6 months)
	if count != 1 {
		t.Errorf("expected count=1 (only recent within 6 months), got %d", count)
	}
}

// TestApplyDepositsIfNeeded_AppliesAt2Plus tests that ApplyDepositsIfNeeded
// applies 3 deposits when user has 2 or more unforgiven no-shows in last 6 months.
func TestApplyDepositsIfNeeded_AppliesAt2Plus(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Create test user with deposits_required = 0
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Ensure deposits_required = 0
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Create 2 bookings with status "no_show" within last 6 months
	now := clock.Now()
	for i := 0; i < 2; i++ {
		startTime := now.Add(time.Duration(i*30) * 24 * time.Hour) // 0, 30 days ago
		var bookingID string
		err := tx.QueryRow(ctx, `
			INSERT INTO bookings (user_id, start_time, status, notes)
			VALUES ($1, $2, 'no_show', 'test no-show')
			RETURNING id
		`, userID, startTime).Scan(&bookingID)
		if err != nil {
			t.Fatalf("failed to create booking %d: %v", i, err)
		}

		// Link service
		_, err = tx.Exec(ctx,
			"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
			bookingID, serviceID)
		if err != nil {
			t.Fatalf("failed to link service: %v", err)
		}
	}

	// Call ApplyDepositsIfNeeded directly
	applied, err := ApplyDepositsIfNeeded(ctx, tx, userID)
	if err != nil {
		t.Fatalf("ApplyDepositsIfNeeded failed: %v", err)
	}

	// Should return true (deposits were applied)
	if !applied {
		t.Error("expected ApplyDepositsIfNeeded to return true when 2+ no-shows")
	}

	// Verify deposits_required = 3
	var deposits int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits != 3 {
		t.Errorf("expected deposits_required=3, got %d", deposits)
	}
}

// TestApplyDepositsIfNeeded_DoesNotApplyAt1 tests that ApplyDepositsIfNeeded
// does NOT apply deposits when user has only 1 unforgiven no-show.
func TestApplyDepositsIfNeeded_DoesNotApplyAt1(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Create test user with deposits_required = 0
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Ensure deposits_required = 0
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Create 1 booking with status "no_show" within last 6 months
	startTime := clock.Now().Add(-30 * 24 * time.Hour) // 30 days ago
	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status, notes)
		VALUES ($1, $2, 'no_show', 'test no-show')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	// Link service
	_, err = tx.Exec(ctx,
		"INSERT INTO booking_services (booking_id, service_id) VALUES ($1, $2)",
		bookingID, serviceID)
	if err != nil {
		t.Fatalf("failed to link service: %v", err)
	}

	// Call ApplyDepositsIfNeeded directly
	applied, err := ApplyDepositsIfNeeded(ctx, tx, userID)
	if err != nil {
		t.Fatalf("ApplyDepositsIfNeeded failed: %v", err)
	}

	// Should return false (deposits were NOT applied)
	if applied {
		t.Error("expected ApplyDepositsIfNeeded to return false when only 1 no-show")
	}

	// Verify deposits_required = 0 (unchanged)
	var deposits int
	err = tx.QueryRow(ctx, "SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&deposits)
	if err != nil {
		t.Errorf("failed to query deposits: %v", err)
	}
	if deposits != 0 {
		t.Errorf("expected deposits_required=0 (unchanged), got %d", deposits)
	}
}

// =============================================================================
// Notes Validation Tests
// =============================================================================

// TestNotesValidation_UnderLimit verifies that a Notes string of 999,999
// characters passes go-playground/validator validation (max=1000000).
func TestNotesValidation_UnderLimit(t *testing.T) {
	longNotes := strings.Repeat("a", 999999)
	req := CreateBookingRequest{
		StartTime:  clock.Now().Add(72 * time.Hour),
		ServiceIDs: []string{"test-service-id"},
		Notes:      &longNotes,
	}

	err := validators.Validate.Struct(&req)
	if err != nil {
		t.Errorf("expected validation to pass for 999999-char notes, got: %v", err)
	}
}

// TestNotesValidation_AtLimit verifies that a Notes string of exactly
// 1,000,000 characters passes validation (max=1000000, boundary test).
func TestNotesValidation_AtLimit(t *testing.T) {
	exactNotes := strings.Repeat("b", 1000000)
	req := CreateBookingRequest{
		StartTime:  clock.Now().Add(72 * time.Hour),
		ServiceIDs: []string{"test-service-id"},
		Notes:      &exactNotes,
	}

	err := validators.Validate.Struct(&req)
	if err != nil {
		t.Errorf("expected validation to pass for 1000000-char notes (boundary), got: %v", err)
	}
}

// TestNotesValidation_OverLimit verifies that a Notes string of 1,000,001
// characters fails validation with max=1000000.
func TestNotesValidation_OverLimit(t *testing.T) {
	tooLongNotes := strings.Repeat("c", 1000001)
	req := CreateBookingRequest{
		StartTime:  clock.Now().Add(72 * time.Hour),
		ServiceIDs: []string{"test-service-id"},
		Notes:      &tooLongNotes,
	}

	err := validators.Validate.Struct(&req)
	if err == nil {
		t.Error("expected validation to fail for 1000001-char notes (over limit)")
	}
}

// TestNotesValidation_NilPointer verifies that a nil Notes pointer passes
// validation (omitempty tag allows nil/empty).
func TestNotesValidation_NilPointer(t *testing.T) {
	req := CreateBookingRequest{
		StartTime:  clock.Now().Add(72 * time.Hour),
		ServiceIDs: []string{"test-service-id"},
		Notes:      nil,
	}

	err := validators.Validate.Struct(&req)
	if err != nil {
		t.Errorf("expected validation to pass for nil Notes (omitempty), got: %v", err)
	}
}

// TestNotesValidation_EmptyString verifies that an empty string Notes passes
// validation (omitempty and max=1000000 allows empty strings).
func TestNotesValidation_EmptyString(t *testing.T) {
	emptyNotes := ""
	req := CreateBookingRequest{
		StartTime:  clock.Now().Add(72 * time.Hour),
		ServiceIDs: []string{"test-service-id"},
		Notes:      &emptyNotes,
	}

	err := validators.Validate.Struct(&req)
	if err != nil {
		t.Errorf("expected validation to pass for empty string Notes, got: %v", err)
	}
}

// Import mw to avoid unused import
var _ = mw.UserIDKey

// =============================================================================
// Auth and Security Tests
// =============================================================================

// TestBookings_Get_NoAuthHeader confirms that accessing a booking without
// an Authorization header returns HTTP 401 Unauthorized.
func TestBookings_Get_NoAuthHeader(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	// Make request WITHOUT token (empty string passed as token parameter)
	handler := http.HandlerFunc(GetBookingHandler)
	w := makeRequest(handler, "GET", "/api/bookings/"+bookingID, nil, "", ctx)

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

// =============================================================================
// Calendar Export Tests - ICS Format Validation
// =============================================================================

// TestBookings_GetCalendar_ValidICS validates that the ICS calendar export
// contains all required fields: BEGIN:VCALENDAR, END:VCALENDAR, BEGIN:VEVENT,
// END:VEVENT, DTSTART, DTEND, and SUMMARY.
func TestBookings_GetCalendar_ValidICS(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(GetBookingCalendarHandler)
	w := makeRequest(handler, "GET", "/api/bookings/"+bookingID+"/calendar", nil, token, ctx)

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

	body := w.Body.String()

	// Validate ICS structure is present
	if !bytes.Contains(w.Body.Bytes(), []byte("BEGIN:VCALENDAR")) {
		t.Error("ICS response missing BEGIN:VCALENDAR")
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("END:VCALENDAR")) {
		t.Error("ICS response missing END:VCALENDAR")
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("BEGIN:VEVENT")) {
		t.Error("ICS response missing BEGIN:VEVENT")
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("END:VEVENT")) {
		t.Error("ICS response missing END:VEVENT")
	}
	// Validate basic ICS properties
	if !bytes.Contains(w.Body.Bytes(), []byte("DTSTART")) {
		t.Error("ICS response missing DTSTART")
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("DTEND")) {
		t.Error("ICS response missing DTEND")
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("SUMMARY")) {
		t.Error("ICS response missing SUMMARY")
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("DTSTART:")) {
		t.Error("ICS response missing DTSTART: marker")
	}
	// Verify DTSTART and DTEND have Z suffix (UTC timezone marker)
	if !bytes.Contains(w.Body.Bytes(), []byte("DTSTART:")) {
		t.Error("ICS response missing DTSTART: marker")
	}
	// Find DTSTART line and verify it ends with Z
	lines := strings.Split(body, "\r\n")
	for _, line := range lines {
		if strings.HasPrefix(line, "DTSTART:") && !strings.HasSuffix(line, "Z") {
			t.Errorf("DTSTART missing Z suffix (got: %s) — calendar apps will interpret as reader's local timezone", line)
		}
		if strings.HasPrefix(line, "DTEND:") && !strings.HasSuffix(line, "Z") {
			t.Errorf("DTEND missing Z suffix (got: %s)", line)
		}
	}
	if len(body) == 0 {
		t.Error("expected non-empty ICS response")
	}
}

// =============================================================================
// Cancellation and Notification Tests
// =============================================================================

// TestUserCancelBooking_ConfirmedCreatesNotification verifies that when a
// user cancels a confirmed booking (one with payments), an admin notification
// is created to alert staff of the cancellation.
func TestUserCancelBooking_ConfirmedCreatesNotification(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	// First, confirm the booking (so it's not pending)
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
	if err != nil {
		t.Fatalf("failed to confirm booking: %v", err)
	}

	// Add a payment to trigger soft delete path (bookings with payments use soft delete)
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (id, booking_id, payment_type, payment_method, amount, status, created_at)
		VALUES ($1, $2, 'deposit', 'in_person_card', 50.00, 'completed', NOW())
	`, bookingID[:8]+"pay", bookingID)
	if err != nil {
		t.Fatalf("failed to add payment: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Cancel the confirmed booking with a reason (required for soft delete)
	handler := http.HandlerFunc(DeleteBookingHandler)
	reqBody := map[string]string{"reason": "client_cancelled"}
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, reqBody, token, ctx)

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

	// Verify admin notification was created
	var notifCount int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'cancelled_booking'`,
		bookingID).Scan(&notifCount)
	if err != nil {
		t.Errorf("failed to query notifications: %v", err)
	}
	if notifCount != 1 {
		t.Errorf("expected 1 admin notification for confirmed->cancelled, got %d", notifCount)
	}
}

// TestUserCancelBooking_PendingNoNotification verifies that cancelling a
// pending booking (one without payments) does NOT create an admin notification,
// as pending cancellations don't require staff attention.
func TestUserCancelBooking_PendingNoNotification(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	// Verify booking is in 'pending' status
	var status string
	err = tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if status != "pending" {
		t.Fatalf("expected booking status 'pending', got %s", status)
	}

	token := jwt.GenerateUserToken(userID)

	// Cancel the pending booking
	handler := http.HandlerFunc(DeleteBookingHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)

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

	// Verify NO admin notification was created (pending cancellations don't notify)
	var notifCount int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'cancelled_booking'`,
		bookingID).Scan(&notifCount)
	if err != nil {
		t.Errorf("failed to query notifications: %v", err)
	}
	if notifCount != 0 {
		t.Errorf("expected NO admin notification for pending->cancelled, got %d", notifCount)
	}
}

// =============================================================================
// Transaction and Error Handling Tests
// =============================================================================

// TestUserCancelBooking_TransactionIntegrity verifies that if any part of the
// cancellation transaction fails, the booking status is NOT changed (rollback behavior)
func TestUserCancelBooking_TransactionIntegrity(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	// Confirm the booking
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
	if err != nil {
		t.Fatalf("failed to confirm booking: %v", err)
	}

	// Add a payment to trigger soft delete path (bookings with payments use soft delete)
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (id, booking_id, payment_type, payment_method, amount, status, created_at)
		VALUES ($1, $2, 'deposit', 'in_person_card', 50.00, 'completed', NOW())
	`, bookingID[:8]+"pay", bookingID)
	if err != nil {
		t.Fatalf("failed to add payment: %v", err)
	}

	// Verify initial state
	var statusBefore string
	err = tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&statusBefore)
	if statusBefore != "confirmed" {
		t.Fatalf("expected status 'confirmed' before cancel, got %s", statusBefore)
	}

	token := jwt.GenerateUserToken(userID)

	// Cancel the booking with a reason (required for soft delete)
	handler := http.HandlerFunc(DeleteBookingHandler)
	reqBody := map[string]string{"reason": "client_cancelled"}
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, reqBody, token, ctx)

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

	// Verify that status WAS changed (successful transaction commit)
	var statusAfter string
	err = tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&statusAfter)
	if statusAfter == "confirmed" {
		t.Error("booking status should have changed after cancellation (transaction should have committed)")
	}
}

// TestCreateEditRequest verifies that a user can request an edit to their
// confirmed booking (e.g., change time). This creates a booking_edit_request record
// and generates an admin notification for staff review.
func TestCreateEditRequest(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Use a start time ~36h from now so auto-approval (>=48h) does not fire
	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	// Confirm the booking
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
	if err != nil {
		t.Fatalf("failed to confirm booking: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Create edit request
	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": "Please change the time",
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

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

	// Verify edit request was created
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 1 {
		t.Errorf("expected 1 edit request, got %d", erCount)
	}

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

// TestCreateEditRequest_WithTimeChange verifies that a user can request an edit to
// change the booking time, and a time_blocker is created to reserve the new slot.
func TestCreateEditRequest_WithTimeChange(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Use a start time ~36h from now so auto-approval (>=48h) does not fire
	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	token := jwt.GenerateUserToken(userID)

	newStartTime := clock.Now().Add(24 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 14, 0, 0, 0, newStartTime.Location())

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

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

	var erNewTime time.Time
	err = tx.QueryRow(ctx,
		"SELECT new_start_time FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erNewTime)
	if err != nil {
		t.Fatalf("failed to query edit request: %v", err)
	}
	if !erNewTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected new_start_time %v, got %v", newStartTime, erNewTime)
	}

	var blockerCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers: %v", err)
	}
	if blockerCount != 1 {
		t.Errorf("expected 1 time_blocker for edit request, got %d", blockerCount)
	}

	var blockerStart time.Time
	var blockerDuration int
	err = tx.QueryRow(ctx,
		"SELECT start_time, duration_minutes FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerStart, &blockerDuration)
	if err != nil {
		t.Fatalf("failed to query time_blocker details: %v", err)
	}
	if !blockerStart.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected blocker start_time %v, got %v", newStartTime, blockerStart)
	}
	if blockerDuration < 15 {
		t.Errorf("expected blocker duration >= 15, got %d", blockerDuration)
	}
}

// TestDeleteEditRequest tests that user deleting their edit request deletes the admin notification
func TestDeleteEditRequest(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	// Confirm the booking
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
	if err != nil {
		t.Fatalf("failed to confirm booking: %v", err)
	}

	// Create edit request directly in DB (simulating user request)
	var editRequestID string
	err = tx.QueryRow(ctx,
		`INSERT INTO booking_edit_requests (booking_id, requested_by, notes)
		VALUES ($1, $2, 'Please change time')
		RETURNING id`,
		bookingID, userID).Scan(&editRequestID)
	if err != nil {
		t.Fatalf("failed to create edit request: %v", err)
	}

	// Create admin notification
	_, err = tx.Exec(ctx,
		`INSERT INTO admin_notifications (reason, booking_id, user_id)
		VALUES ('edit_requested', $1, $2)`,
		bookingID, userID)
	if err != nil {
		t.Fatalf("failed to create admin notification: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Delete edit request (user cancels their request)
	handler := http.HandlerFunc(DeleteEditRequestHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID+"/edit-request", nil, token, ctx)

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

	// Verify edit request was deleted
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected 0 edit requests after delete, got %d", erCount)
	}

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

// TestAdminApproveEditRequest tests that admin approving acknowledges the notification (not deletes)
func TestAdminApproveEditRequest(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

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

	// Confirm the booking
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
	if err != nil {
		t.Fatalf("failed to confirm booking: %v", err)
	}

	// Create edit request directly in DB
	var editRequestID string
	newTime := clock.Now().Add(24 * time.Hour).Truncate(time.Minute)
	var emptyServices []string
	err = tx.QueryRow(ctx,
		`INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, new_services, notes)
		VALUES ($1, $2, $3, $4, 'Please change time')
		RETURNING id`,
		bookingID, userID, newTime, emptyServices).Scan(&editRequestID)
	if err != nil {
		t.Fatalf("failed to create edit request: %v", err)
	}
	if err != nil {
		t.Fatalf("failed to create edit request: %v", err)
	}

	// Create admin notification
	_, err = tx.Exec(ctx,
		`INSERT INTO admin_notifications (reason, booking_id, user_id)
		VALUES ('edit_requested', $1, $2)`,
		bookingID, userID)
	if err != nil {
		t.Fatalf("failed to create admin notification: %v", err)
	}

	// Verify notification starts as unacknowledged
	var ackTime *time.Time
	err = tx.QueryRow(ctx,
		`SELECT acknowledged_at FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'edit_requested'`,
		bookingID).Scan(&ackTime)
	if err != nil {
		t.Fatalf("failed to query notification: %v", err)
	}
	if ackTime != nil {
		t.Fatalf("expected notification to be unacknowledged initially")
	}

	// Simulate admin approval
	adminToken := jwt.GenerateAdminToken()

	req := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve", nil)
	req.Header.Set("Authorization", "Bearer "+adminToken)
	reqCtx := ctx
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", bookingID)
	rctx.URLParams.Add("request_id", editRequestID)
	reqCtx = context.WithValue(reqCtx, chi.RouteCtxKey, rctx)
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
	reqCtx = context.WithValue(reqCtx, mw.UserIDKey, "admintest001")
	req = req.WithContext(reqCtx)

	w := httptest.NewRecorder()
	AdminApproveEditRequestHandler(w, req)

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

	// Verify edit request was deleted (approved)
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected 0 edit requests after approve, got %d", erCount)
	}

	// Verify admin notification was ACKNOWLEDGED (not deleted) - history preserved
	var ackTimeAfter *time.Time
	err = tx.QueryRow(ctx,
		`SELECT acknowledged_at FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'edit_requested'`,
		bookingID).Scan(&ackTimeAfter)
	if err != nil {
		t.Fatalf("failed to query notification: %v", err)
	}
	if ackTimeAfter == nil {
		t.Errorf("expected notification to be acknowledged after approve, but acknowledged_at is still NULL")
	}
}

// TestAdminRejectEditRequest tests that admin rejecting acknowledges the notification (not deletes)
func TestAdminRejectEditRequest(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

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

	// Confirm the booking
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
	if err != nil {
		t.Fatalf("failed to confirm booking: %v", err)
	}

	// Create edit request directly in DB
	var editRequestID string
	err = tx.QueryRow(ctx,
		`INSERT INTO booking_edit_requests (booking_id, requested_by, notes)
		VALUES ($1, $2, 'Please change time')
		RETURNING id`,
		bookingID, userID).Scan(&editRequestID)
	if err != nil {
		t.Fatalf("failed to create edit request: %v", err)
	}

	// Create admin notification
	_, err = tx.Exec(ctx,
		`INSERT INTO admin_notifications (reason, booking_id, user_id)
		VALUES ('edit_requested', $1, $2)`,
		bookingID, userID)
	if err != nil {
		t.Fatalf("failed to create admin notification: %v", err)
	}

	// Verify notification starts as unacknowledged
	var ackTime *time.Time
	err = tx.QueryRow(ctx,
		`SELECT acknowledged_at FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'edit_requested'`,
		bookingID).Scan(&ackTime)
	if err != nil {
		t.Fatalf("failed to query notification: %v", err)
	}
	if ackTime != nil {
		t.Fatalf("expected notification to be unacknowledged initially")
	}

	// Simulate admin denial
	adminToken := jwt.GenerateAdminToken()

	req := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/deny", nil)
	req.Header.Set("Authorization", "Bearer "+adminToken)
	reqCtx := ctx
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", bookingID)
	rctx.URLParams.Add("request_id", editRequestID)
	reqCtx = context.WithValue(reqCtx, chi.RouteCtxKey, rctx)
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
	req = req.WithContext(reqCtx)

	w := httptest.NewRecorder()
	AdminRejectEditRequestHandler(w, req)

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

	// Verify edit request was deleted (rejected)
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected 0 edit requests after reject, got %d", erCount)
	}

	// Verify admin notification was ACKNOWLEDGED (not deleted) - history preserved
	var ackTimeAfter *time.Time
	err = tx.QueryRow(ctx,
		`SELECT acknowledged_at FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'edit_requested'`,
		bookingID).Scan(&ackTimeAfter)
	if err != nil {
		t.Fatalf("failed to query notification: %v", err)
	}
	if ackTimeAfter == nil {
		t.Errorf("expected notification to be acknowledged after reject, but acknowledged_at is still NULL")
	}
}

// TestAdminApproveEditRequest_DeletesTimeBlocker verifies that when admin approves
// an edit request, the associated time_blocker reservation is deleted.
func TestAdminApproveEditRequest_DeletesTimeBlocker(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	userToken := jwt.GenerateUserToken(userID)

	newStartTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 14, 0, 0, 0, newStartTime.Location())

	createReq := http.HandlerFunc(RequestEditHandler)
	createBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(createReq, "POST", "/api/bookings/"+bookingID+"/edit-request", createBody, userToken, ctx)
	if w.Code != http.StatusCreated && w.Code != http.StatusOK {
		t.Fatalf("failed to create edit request: %d %s", w.Code, w.Body.String())
	}

	var blockerCountBefore int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCountBefore)
	if err != nil {
		t.Fatalf("failed to query blockers: %v", err)
	}
	if blockerCountBefore != 1 {
		t.Fatalf("expected 1 blocker before approval, got %d", blockerCountBefore)
	}

	var editRequestID string
	err = tx.QueryRow(ctx,
		"SELECT id FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&editRequestID)
	if err != nil {
		t.Fatalf("failed to get edit request ID: %v", err)
	}

	r := chi.NewRouter()
	r.Post("/api/admin/bookings/{id}/edit-requests/{request_id}/approve", AdminApproveEditRequestHandler)
	req := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve", nil)
	reqCtx := ctx
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
	reqCtx = context.WithValue(reqCtx, mw.UserIDKey, "admintest001")
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", bookingID)
	rctx.URLParams.Add("request_id", editRequestID)
	reqCtx = context.WithValue(reqCtx, chi.RouteCtxKey, rctx)
	req = req.WithContext(reqCtx)
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

	var blockerCountAfter int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCountAfter)
	if err != nil {
		t.Fatalf("failed to query blockers after approval: %v", err)
	}
	if blockerCountAfter != 0 {
		t.Errorf("expected 0 blockers after approval, got %d", blockerCountAfter)
	}
}

// TestAdminRejectEditRequest_DeletesTimeBlocker verifies that when admin rejects
// an edit request, the associated time_blocker reservation is deleted.
func TestAdminRejectEditRequest_DeletesTimeBlocker(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	userToken := jwt.GenerateUserToken(userID)

	newStartTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 14, 0, 0, 0, newStartTime.Location())

	createReq := http.HandlerFunc(RequestEditHandler)
	createBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(createReq, "POST", "/api/bookings/"+bookingID+"/edit-request", createBody, userToken, ctx)
	if w.Code != http.StatusCreated && w.Code != http.StatusOK {
		t.Fatalf("failed to create edit request: %d %s", w.Code, w.Body.String())
	}

	var blockerCountBefore int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCountBefore)
	if err != nil {
		t.Fatalf("failed to query blockers: %v", err)
	}
	if blockerCountBefore != 1 {
		t.Fatalf("expected 1 blocker before rejection, got %d", blockerCountBefore)
	}

	var editRequestID string
	err = tx.QueryRow(ctx,
		"SELECT id FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&editRequestID)
	if err != nil {
		t.Fatalf("failed to get edit request ID: %v", err)
	}

	r := chi.NewRouter()
	r.Post("/api/admin/bookings/{id}/edit-requests/{request_id}/deny", AdminRejectEditRequestHandler)
	req := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/deny", nil)
	reqCtx := ctx
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", bookingID)
	rctx.URLParams.Add("request_id", editRequestID)
	reqCtx = context.WithValue(reqCtx, chi.RouteCtxKey, rctx)
	req = req.WithContext(reqCtx)
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)

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

	var blockerCountAfter int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCountAfter)
	if err != nil {
		t.Fatalf("failed to query blockers after rejection: %v", err)
	}
	if blockerCountAfter != 0 {
		t.Errorf("expected 0 blockers after rejection, got %d", blockerCountAfter)
	}
}

// TestDeleteEditRequest_DeletesTimeBlocker verifies that when user cancels their
// own edit request, the associated time_blocker reservation is deleted.
func TestDeleteEditRequest_DeletesTimeBlocker(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	userToken := jwt.GenerateUserToken(userID)

	newStartTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 14, 0, 0, 0, newStartTime.Location())

	createReq := http.HandlerFunc(RequestEditHandler)
	createBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(createReq, "POST", "/api/bookings/"+bookingID+"/edit-request", createBody, userToken, ctx)
	if w.Code != http.StatusCreated && w.Code != http.StatusOK {
		t.Fatalf("failed to create edit request: %d %s", w.Code, w.Body.String())
	}

	var blockerCountBefore int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCountBefore)
	if err != nil {
		t.Fatalf("failed to query blockers: %v", err)
	}
	if blockerCountBefore != 1 {
		t.Fatalf("expected 1 blocker before delete, got %d", blockerCountBefore)
	}

	delHandler := http.HandlerFunc(DeleteEditRequestHandler)
	w = makeRequest(delHandler, "DELETE", "/api/bookings/"+bookingID+"/edit-request", nil, userToken, ctx)

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

	var blockerCountAfter int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCountAfter)
	if err != nil {
		t.Fatalf("failed to query blockers after delete: %v", err)
	}
	if blockerCountAfter != 0 {
		t.Errorf("expected 0 blockers after user delete, got %d", blockerCountAfter)
	}
}

// TestAdminApproveEditRequest_TimeBlockerOverlap tests that approving an edit
// request fails when the new time conflicts with an existing time_blocker.
func TestAdminApproveEditRequest_TimeBlockerOverlap(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	userToken := jwt.GenerateUserToken(userID)

	newStartTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 14, 0, 0, 0, newStartTime.Location())

	createReq := http.HandlerFunc(RequestEditHandler)
	createBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(createReq, "POST", "/api/bookings/"+bookingID+"/edit-request", createBody, userToken, ctx)
	if w.Code != http.StatusCreated && w.Code != http.StatusOK {
		t.Fatalf("failed to create edit request: %d %s", w.Code, w.Body.String())
	}

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

	var editRequestID string
	err = tx.QueryRow(ctx,
		"SELECT id FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&editRequestID)
	if err != nil {
		t.Fatalf("failed to get edit request ID: %v", err)
	}

	r := chi.NewRouter()
	r.Post("/api/admin/bookings/{id}/edit-requests/{request_id}/approve", AdminApproveEditRequestHandler)
	req := httptest.NewRequest("POST", "/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve", nil)
	reqCtx := ctx
	reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, "admin")
	reqCtx = context.WithValue(reqCtx, mw.UserIDKey, "admintest001")
	rctx := chi.NewRouteContext()
	rctx.URLParams.Add("id", bookingID)
	rctx.URLParams.Add("request_id", editRequestID)
	reqCtx = context.WithValue(reqCtx, chi.RouteCtxKey, rctx)
	req = req.WithContext(reqCtx)
	w = httptest.NewRecorder()
	r.ServeHTTP(w, req)

	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409 Conflict due to time_blocker overlap, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestBookings_RequestEdit_BookingNotFound tests that requesting an edit for a non-existent booking returns 404
func TestBookings_RequestEdit_BookingNotFound(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": "Please change the time",
	}
	w := makeRequest(handler, "POST", "/api/bookings/nonexistent-booking-id/edit-request", reqBody, token, ctx)

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

// TestBookings_RequestEdit_AlreadyHasPending tests that a user cannot create a second edit request while one already exists
func TestBookings_RequestEdit_AlreadyHasPending(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	// Confirm the booking
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", bookingID)
	if err != nil {
		t.Fatalf("failed to confirm booking: %v", err)
	}

	// Create a pending edit request directly in DB (pre-condition)
	_, err = tx.Exec(ctx,
		`INSERT INTO booking_edit_requests (booking_id, requested_by, notes)
		VALUES ($1, $2, 'Please change the time')`,
		bookingID, userID)
	if err != nil {
		t.Fatalf("failed to create initial edit request: %v", err)
	}

	// Create admin notification for the initial edit request
	_, err = tx.Exec(ctx,
		`INSERT INTO admin_notifications (reason, booking_id, user_id)
		VALUES ('edit_requested', $1, $2)`,
		bookingID, userID)
	if err != nil {
		t.Fatalf("failed to create admin notification: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Try to create another edit request via API
	// Note: The handler actually replaces (upserts) the existing request, not rejects it
	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": "Please change to a different day",
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

	// Expect HTTP 201 Created (handler replaces existing request)
	if w.Code != http.StatusCreated {
		t.Errorf("expected status 201, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify only 1 edit request exists in DB (the old one was replaced)
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 1 {
		t.Errorf("expected 1 edit request, got %d", erCount)
	}

	// Verify the notes were updated
	var notes string
	err = tx.QueryRow(ctx,
		"SELECT notes FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&notes)
	if err != nil {
		t.Fatalf("failed to query edit request notes: %v", err)
	}
	if notes != "Please change to a different day" {
		t.Errorf("expected notes 'Please change to a different day', got '%s'", notes)
	}
}

// =============================================================================
// Patch Test Validation Tests
// =============================================================================

// TestBookings_Create_PatchTestRequired_NoRecord verifies that a user without a patch test record
// cannot book a service that requires a patch test. The booking should be rejected with 400.
func TestBookings_Create_PatchTestRequired_NoRecord(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Create user and service with patch test requirement
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}
	defer fixtures.DeleteUser(tx, userID)

	// Set deposits_required=0 to avoid 48h requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	serviceID, patchTestID, err := fixtures.CreateTestServiceWithPatchTest(tx)
	if err != nil {
		t.Fatalf("failed to create test service with patch test: %v", err)
	}
	defer fixtures.DeleteService(tx, serviceID)
	_ = patchTestID // We don't delete patch tests, they cascade with service

	token := jwt.GenerateUserToken(userID)

	// Try to book service requiring patch test - user has no patch test record
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for missing patch test, got %d. body: %s", w.Code, w.Body.String())
	}

	if !bytes.Contains(w.Body.Bytes(), []byte("patch test")) {
		t.Errorf("expected error message about patch test, got: %s", w.Body.String())
	}
}

// TestBookings_Create_PatchTestRequired_WithinNoticePeriod verifies that a user
// cannot book within the notice period after completing a patch test (e.g., 24h wait).
func TestBookings_Create_PatchTestRequired_WithinNoticePeriod(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	serviceID, patchTestID, err := fixtures.CreateTestServiceWithPatchTest(tx)
	if err != nil {
		t.Fatalf("failed to create test service with patch test: %v", err)
	}
	defer fixtures.DeleteService(tx, serviceID)

	// Create patch test record with tested_at only 1 hour ago (notice is 24h)
	testedAt := clock.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")
	err = fixtures.CreateUserPatchTest(tx, userID, patchTestID, testedAt)
	if err != nil {
		t.Fatalf("failed to create user patch test: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Try to book within notice period (24h required, but only 1h passed).
	// Booking start_time must be before testedAt + noticeHours to trigger this.
	// Use a booking time in the future (passes 1-hour advance check) but before
	// eligibleFrom (testedAt + 24h = now + 23h).
	futureTime := clock.Now().Add(2 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), futureTime.Hour(), 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for within notice period, got %d. body: %s", w.Code, w.Body.String())
	}

	if !bytes.Contains(w.Body.Bytes(), []byte("notice period")) {
		t.Errorf("expected error message about notice period, got: %s", w.Body.String())
	}
}

// TestBookings_Create_PatchTestRequired_Expired verifies that a user
// with an expired patch test cannot book services requiring patch test.
func TestBookings_Create_PatchTestRequired_Expired(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	serviceID, patchTestID, err := fixtures.CreateTestServiceWithPatchTest(tx)
	if err != nil {
		t.Fatalf("failed to create test service with patch test: %v", err)
	}
	defer fixtures.DeleteService(tx, serviceID)

	// Create patch test record from 7 months ago (expiry is 6 months)
	testedAt := clock.Now().AddDate(0, -7, 0).Format("2006-01-02 15:04:05")
	err = fixtures.CreateUserPatchTest(tx, userID, patchTestID, testedAt)
	if err != nil {
		t.Fatalf("failed to create user patch test: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Try to book with expired patch test
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for expired patch test, got %d. body: %s", w.Code, w.Body.String())
	}

	if !bytes.Contains(w.Body.Bytes(), []byte("expired")) {
		t.Errorf("expected error message about expiry, got: %s", w.Body.String())
	}
}

// TestBookings_Create_PatchTestRequired_ValidRecord verifies that a user
// with a valid patch test record can successfully book services requiring patch test.
func TestBookings_Create_PatchTestRequired_ValidRecord(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	serviceID, patchTestID, err := fixtures.CreateTestServiceWithPatchTest(tx)
	if err != nil {
		t.Fatalf("failed to create test service with patch test: %v", err)
	}
	defer fixtures.DeleteService(tx, serviceID)

	// Create patch test record from 48 hours ago (notice is 24h, so valid now)
	testedAt := clock.Now().Add(-48 * time.Hour).Format("2006-01-02 15:04:05")
	err = fixtures.CreateUserPatchTest(tx, userID, patchTestID, testedAt)
	if err != nil {
		t.Fatalf("failed to create user patch test: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Book with valid patch test record (after notice period)
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	// Verify booking was created
	var count int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM bookings WHERE user_id = $1", userID).Scan(&count)
	if err != nil {
		t.Errorf("failed to query bookings: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 booking, got %d", count)
	}
}

// =============================================================================
// Deposit Requirement Tests
// =============================================================================

// TestBookings_Create_DepositRequired_WithinAdvanceWindow verifies that a user with deposits_required > 0
// cannot book within the deposit advance window. They must complete more appointments
// to remove this restriction.
func TestBookings_Create_DepositRequired_WithinAdvanceWindow(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=3 to trigger deposit advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Try to book within the advance window (should be blocked by deposit advance rule).
	// Use 10h advance: passes 1h minimum, fails deposit advance window.
	withinWindow := clock.Now().Add(10 * time.Hour)
	req := CreateBookingRequest{
		StartTime:  withinWindow,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for within advance-window booking with deposit requirement, got %d. body: %s", w.Code, w.Body.String())
	}

	if !bytes.Contains(w.Body.Bytes(), []byte("hours in advance")) {
		t.Errorf("expected error message about advance window, got: %s", w.Body.String())
	}
}

// TestBookings_Create_DepositRequired_After48Hours verifies that a user with deposits_required > 0
// CAN book if the start time is at least 48 hours in the future.
func TestBookings_Create_DepositRequired_After48Hours(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=3 to trigger 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Book more than 48 hours in advance (should succeed)
	after48h := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	after48h = time.Date(after48h.Year(), after48h.Month(), after48h.Day(), 10, 0, 0, 0, after48h.Location())
	req := CreateBookingRequest{
		StartTime:  after48h,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	// Verify booking was created
	var count int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM bookings WHERE user_id = $1", userID).Scan(&count)
	if err != nil {
		t.Errorf("failed to query bookings: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 booking, got %d", count)
	}
}

// TestBookings_Create_NoDepositRequired_Within48Hours verifies that a user with deposits_required=0
// can book at any time (no 48h restriction).
func TestBookings_Create_NoDepositRequired_Within48Hours(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

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

	// deposits_required=0 means no 48h restriction
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Book within 48 hours (should succeed since no deposit required)
	within48h := clock.Now().Add(24 * time.Hour).Truncate(time.Second)
	within48h = time.Date(within48h.Year(), within48h.Month(), within48h.Day(), 10, 0, 0, 0, within48h.Location())
	req := CreateBookingRequest{
		StartTime:  within48h,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

	if w.Code != http.StatusCreated {
		t.Errorf("expected status 201 for booking within 48h with no deposit required, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Deposit Snapshot and Field Tests
// =============================================================================

// TestBookings_Create_DepositSnapshot verifies that deposit_required is snapshotted
// at booking creation time from user's current deposits_required value.
func TestBookings_Create_DepositSnapshot(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=3 BEFORE creating booking
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Create booking after deposits_required is set
	after48h := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	after48h = time.Date(after48h.Year(), after48h.Month(), after48h.Day(), 10, 0, 0, 0, after48h.Location())
	req := CreateBookingRequest{
		StartTime:  after48h,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	// Verify deposit_required was snapshotted on the booking
	var depositRequired bool
	err = tx.QueryRow(ctx,
		"SELECT deposit_required FROM bookings WHERE user_id = $1", userID).Scan(&depositRequired)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !depositRequired {
		t.Error("expected deposit_required=true to be snapshotted on booking")
	}

	// Now change user's deposits_required to 0
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to update deposits_required: %v", err)
	}

	// Verify the booking's deposit_required is still true (snapshot is not updated)
	err = tx.QueryRow(ctx,
		"SELECT deposit_required FROM bookings WHERE user_id = $1", userID).Scan(&depositRequired)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !depositRequired {
		t.Error("expected deposit_required to remain true after user's deposits_required changed")
	}
}

// TestBookings_Create_DepositRequired_OneActiveBookingLimit verifies that a user
// with deposits_required > 0 can only have ONE active booking at a time.
func TestBookings_Create_DepositRequired_OneActiveBookingLimit(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=3 (triggers one-active-booking limit)
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Create first booking (should succeed)
	after48h := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	after48h = time.Date(after48h.Year(), after48h.Month(), after48h.Day(), 10, 0, 0, 0, after48h.Location())
	req1 := CreateBookingRequest{
		StartTime:  after48h,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req1, token, ctx)

	if w.Code != http.StatusCreated {
		t.Fatalf("expected first booking to succeed, got %d. body: %s", w.Code, w.Body.String())
	}

	// Try to create second booking (should fail - one active booking limit)
	after72h := clock.Now().Add(96 * time.Hour).Truncate(time.Second)
	after72h = time.Date(after72h.Year(), after72h.Month(), after72h.Day(), 10, 0, 0, 0, after72h.Location())
	req2 := CreateBookingRequest{
		StartTime:  after72h,
		ServiceIDs: []string{serviceID},
	}

	w = makeRequest(handler, "POST", "/api/bookings", req2, token, ctx)

	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409 for second booking attempt, got %d. body: %s", w.Code, w.Body.String())
	}

	if !bytes.Contains(w.Body.Bytes(), []byte("active booking")) {
		t.Errorf("expected error about active booking, got: %s", w.Body.String())
	}
}

// TestBookings_Get_DepositFieldsReturned verifies that GET /api/bookings returns
// the deposit-related fields (deposit_required, deposit_amount, deposit_paid, deposit_deadline).
func TestBookings_Get_DepositFieldsReturned(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=3 and create booking
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Create booking with deposit requirement
	after48h := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	after48h = time.Date(after48h.Year(), after48h.Month(), after48h.Day(), 10, 0, 0, 0, after48h.Location())
	req := CreateBookingRequest{
		StartTime:  after48h,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	// GET the booking and verify deposit fields
	w = makeRequest(http.HandlerFunc(GetAllUserBookingsHandler), "GET", "/api/bookings", nil, token, ctx)

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

	var resp BookingListResponse
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	if len(resp.Bookings) != 1 {
		t.Fatalf("expected 1 booking, got %d", len(resp.Bookings))
	}

	booking := resp.Bookings[0]

	// Verify deposit fields exist
	// Verify deposit fields exist
	if !booking.DepositRequired {
		t.Error("expected DepositRequired to be true")
	}
	if booking.DepositAmount <= 0 {
		t.Error("expected DepositAmount to be positive")
	}
	// DepositPaid is a bool, check it's set (should be false for new booking)
	// Just verify the field exists by accessing it
	_ = booking.DepositPaid
	if booking.DepositDeadline == nil {
		t.Error("expected DepositDeadline to be set")
	}
}

// =============================================================================
// Services End-to-End Tests
// =============================================================================

// TestBookings_Get_ServicesReturned verifies that GET /api/bookings returns
// services with correct name, price, and duration for each booking.
func TestBookings_Get_ServicesReturned(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	startTime := nextWeekday(time.Wednesday).Add(10 * time.Hour)

	token := jwt.GenerateUserToken(userID)

	// Create booking with standard service via the handler (production path)
	req := CreateBookingRequest{
		StartTime:  startTime,
		ServiceIDs: []string{serviceID},
	}
	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("expected 201, got %d. body: %s", w.Code, w.Body.String())
	}

	// Fetch bookings list
	w = makeRequest(http.HandlerFunc(GetAllUserBookingsHandler), "GET", "/api/bookings", nil, token, ctx)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp BookingListResponse
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if len(resp.Bookings) == 0 {
		t.Fatal("expected at least one booking")
	}

	booking := resp.Bookings[0]
	if booking.Services == nil {
		t.Fatal("expected services to be non-nil, got nil")
	}
	if len(booking.Services) == 0 {
		t.Fatal("expected at least one service in booking")
	}

	svc := booking.Services[0]
	if svc.ServiceID != serviceID {
		t.Errorf("expected service_id %s, got %s", serviceID, svc.ServiceID)
	}
	if svc.ServiceName == nil || *svc.ServiceName != "Test Service" {
		t.Errorf("expected service_name 'Test Service', got %v", svc.ServiceName)
	}
	if svc.Price == nil || *svc.Price != 50.00 {
		t.Errorf("expected price 50.00, got %v", svc.Price)
	}
	if svc.DurationMinutes == nil || *svc.DurationMinutes != 60 {
		t.Errorf("expected duration 60, got %v", svc.DurationMinutes)
	}
}

// TestBookings_Get_CustomServicesReturned verifies that GET /api/bookings
// returns custom services correctly.
func TestBookings_Get_CustomServicesReturned(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	csID, err := fixtures.CreateTestCustomService(tx)
	if err != nil {
		t.Fatalf("failed to create custom service: %v", err)
	}
	defer fixtures.DeleteCustomService(tx, csID)

	startTime := nextWeekday(time.Thursday).Add(10 * time.Hour)

	// Insert booking + custom service link directly
	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_custom_services (booking_id, custom_service_id)
		VALUES ($1, $2)
	`, bookingID, csID)
	if err != nil {
		t.Fatalf("failed to link custom service: %v", err)
	}

	token := jwt.GenerateUserToken(userID)
	w := makeRequest(http.HandlerFunc(GetAllUserBookingsHandler), "GET", "/api/bookings", nil, token, ctx)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp BookingListResponse
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if len(resp.Bookings) == 0 {
		t.Fatal("expected at least one booking")
	}

	booking := resp.Bookings[0]
	if booking.Services == nil {
		t.Fatal("expected services to be non-nil, got nil")
	}
	if len(booking.Services) == 0 {
		t.Fatal("expected at least one service in booking")
	}

	found := false
	for _, s := range booking.Services {
		if s.ServiceID == csID {
			found = true
			if s.ServiceName == nil || *s.ServiceName != "Test Custom Service" {
				t.Errorf("expected service_name 'Test Custom Service', got %v", s.ServiceName)
			}
			if s.Price == nil || *s.Price != 75.00 {
				t.Errorf("expected price 75.00, got %v", s.Price)
			}
			if s.DurationMinutes == nil || *s.DurationMinutes != 45 {
				t.Errorf("expected duration 45, got %v", s.DurationMinutes)
			}
			break
		}
	}
	if !found {
		t.Errorf("custom service %s not found in booking services", csID)
	}
}

// TestBookings_Get_EmptyServices verifies that GET /api/bookings returns an
// empty array (not null) for bookings with no services.
func TestBookings_Get_EmptyServices(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	startTime := nextWeekday(time.Friday).Add(10 * time.Hour)

	// Insert booking with NO services at all
	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	token := jwt.GenerateUserToken(userID)
	w := makeRequest(http.HandlerFunc(GetAllUserBookingsHandler), "GET", "/api/bookings", nil, token, ctx)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d. body: %s", w.Code, w.Body.String())
	}

	var resp BookingListResponse
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if len(resp.Bookings) == 0 {
		t.Fatal("expected at least one booking")
	}

	booking := resp.Bookings[0]
	if booking.Services == nil {
		t.Fatal("expected services to be non-nil (empty array), got nil")
	}
	if len(booking.Services) != 0 {
		t.Errorf("expected empty services array, got %d services", len(booking.Services))
	}
}

// TestBookings_Edit_ClosedDay_UserBlocked verifies that a regular user cannot edit a booking
// to fall on a closed day (exceptional hours marked as is_open=false).
func TestBookings_Edit_ClosedDay_UserBlocked(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	// Create exceptional hours group for holiday
	var groupID string
	err = tx.QueryRow(ctx,
		"INSERT INTO exceptional_working_hours_groups (name, description) VALUES ($1, $2) RETURNING id",
		"Holiday Closure", "Closed for holiday").Scan(&groupID)
	if err != nil {
		t.Fatalf("failed to create exceptional hours group: %v", err)
	}

	// Calculate week start for the booking target date
	targetDate := clock.Now().Add(96 * time.Hour)
	// DB convention: 0=Monday..6=Sunday; Go: 0=Sunday..6=Saturday. Convert.
	dbWeekday := (int(targetDate.Weekday()) + 6) % 7
	daysToMonday := int(targetDate.Weekday())
	if daysToMonday == 0 {
		daysToMonday = 7
	}
	mondayDate := targetDate.AddDate(0, 0, -daysToMonday+1)
	weekStart := time.Date(mondayDate.Year(), mondayDate.Month(), mondayDate.Day(), 0, 0, 0, 0, mondayDate.Location())

	// Apply group to this week
	_, err = tx.Exec(ctx,
		"INSERT INTO exceptional_group_applications (group_id, week_start) VALUES ($1, $2)",
		groupID, weekStart)
	if err != nil {
		t.Fatalf("failed to apply exceptional hours group: %v", err)
	}

	// Create closed exceptional hours for that weekday
	_, err = tx.Exec(ctx,
		`INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
		VALUES ($1, $2, '08:00:00', '20:00:00', false)`,
		groupID, dbWeekday)
	if err != nil {
		t.Fatalf("failed to create closed exceptional hours: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Try to edit booking to closed day
	newStartTime := targetDate.Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 10, 0, 0, 0, newStartTime.Location())
	req := EditBookingRequest{
		StartTime: newStartTime,
	}

	handler := http.HandlerFunc(EditBookingHandler)
	w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req, token, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for closed day edit, got %d. body: %s", w.Code, w.Body.String())
	}

	if !bytes.Contains(w.Body.Bytes(), []byte("closed day")) {
		t.Errorf("expected error message about closed day, got: %s", w.Body.String())
	}
}

// TestBookings_Edit_OpenDay_UserAllowed verifies that a user CAN edit a booking
// to a day that is marked as open in exceptional hours.
func TestBookings_Edit_OpenDay_UserAllowed(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	// Create exceptional hours group with OPEN hours (is_open=true)
	var groupID string
	err = tx.QueryRow(ctx,
		"INSERT INTO exceptional_working_hours_groups (name, description) VALUES ($1, $2) RETURNING id",
		"Special Opening", "Extended hours").Scan(&groupID)
	if err != nil {
		t.Fatalf("failed to create exceptional hours group: %v", err)
	}

	// Calculate week start for the booking target date
	targetDate := clock.Now().Add(96 * time.Hour)
	weekday := int(targetDate.Weekday())
	daysToMonday := weekday
	if daysToMonday == 0 {
		daysToMonday = 7
	}
	mondayDate := targetDate.AddDate(0, 0, -daysToMonday+1)
	weekStart := time.Date(mondayDate.Year(), mondayDate.Month(), mondayDate.Day(), 0, 0, 0, 0, mondayDate.Location())

	// Apply group to this week
	_, err = tx.Exec(ctx,
		"INSERT INTO exceptional_group_applications (group_id, week_start) VALUES ($1, $2)",
		groupID, weekStart)
	if err != nil {
		t.Fatalf("failed to apply exceptional hours group: %v", err)
	}

	// Create OPEN exceptional hours for that weekday
	_, err = tx.Exec(ctx,
		`INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
		VALUES ($1, $2, '08:00:00', '20:00:00', true)`,
		groupID, weekday)
	if err != nil {
		t.Fatalf("failed to create open exceptional hours: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Edit booking to open day (should succeed)
	newStartTime := targetDate.Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 10, 0, 0, 0, newStartTime.Location())
	req := EditBookingRequest{
		StartTime: newStartTime,
	}

	handler := http.HandlerFunc(EditBookingHandler)
	w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req, token, ctx)

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

// =============================================================================
// Time Blocker Tests for User Bookings
// =============================================================================

// TestBookings_Create_OverlappingBlocker_UserBlocked verifies that a regular user
// CANNOT create a booking that overlaps with a time blocker. They receive 409 Conflict.
func TestBookings_Create_OverlappingBlocker_UserBlocked(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Create a time blocker for a specific time
	blockerTime := time.Date(2099, 12, 31, 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 time blocker: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// User tries to create booking overlapping the blocker
	req := CreateBookingRequest{
		StartTime:  blockerTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

	// User should get 409 Conflict (not 201 Created)
	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify error message mentions the blocker
	if !bytes.Contains(w.Body.Bytes(), []byte("blocked")) {
		t.Errorf("expected error message to mention 'blocked', got: %s", w.Body.String())
	}

	// Verify NO booking was created
	var count int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM bookings WHERE user_id = $1", userID).Scan(&count)
	if err != nil {
		t.Fatalf("failed to query bookings: %v", err)
	}
	if count != 0 {
		t.Errorf("expected 0 bookings (user should be blocked), got %d", count)
	}
}

// TestBookings_Edit_OverlappingBlocker_UserBlocked verifies that a regular user
// CANNOT edit a booking to a time that overlaps with a time blocker.
func TestBookings_Edit_OverlappingBlocker_UserBlocked(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Set deposits_required=0 to avoid 48h advance booking requirement
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Create a booking first
	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	// Create a time blocker for a specific time
	blockerTime := time.Date(2099, 12, 31, 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 time blocker: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// User tries to edit booking to overlap the blocker
	req := EditBookingRequest{
		StartTime: blockerTime,
	}

	handler := http.HandlerFunc(EditBookingHandler)
	w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req, token, ctx)

	// User should get 409 Conflict
	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify error message mentions the blocker
	if !bytes.Contains(w.Body.Bytes(), []byte("blocked")) {
		t.Errorf("expected error message to mention 'blocked', got: %s", w.Body.String())
	}
}

// --- Guest Booking Tests ---

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

	req := map[string]string{
		"firstName": "Jane",
		"lastName":  "Doe",
		"email":     "jane.doe@test.com",
		"phone":     "07123456789",
	}

	handler := http.HandlerFunc(user.CreateGuestUserHandler)
	w := makeRequest(handler, "POST", "/api/users/guest", req, "", ctx)

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

	var resp map[string]string
	json.Unmarshal(w.Body.Bytes(), &resp)
	if resp["role"] != "guest" {
		t.Errorf("expected role 'guest', got '%s'", resp["role"])
	}
	if resp["id"] == "" {
		t.Error("expected non-empty user ID")
	}

	// Verify user exists in DB
	var role string
	err := tx.QueryRow(ctx, `SELECT account_role FROM users WHERE id = $1`, resp["id"]).Scan(&role)
	if err != nil {
		t.Fatalf("failed to query user: %v", err)
	}
	if role != "guest" {
		t.Errorf("expected role 'guest' in DB, got '%s'", role)
	}
}

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

	// First guest creation
	req := map[string]string{
		"firstName": "John",
		"lastName":  "Smith",
		"email":     "john@test.com",
		"phone":     "07123456789",
	}

	handler := http.HandlerFunc(user.CreateGuestUserHandler)
	w1 := makeRequest(handler, "POST", "/api/users/guest", req, "", ctx)
	if w1.Code != http.StatusCreated {
		t.Fatalf("first guest creation failed: %d", w1.Code)
	}
	var resp1 map[string]string
	json.Unmarshal(w1.Body.Bytes(), &resp1)

	// Second guest with same email — should create a NEW account
	req2 := map[string]string{
		"firstName": "Jane",
		"lastName":  "Smith",
		"email":     "john@test.com", // same email
		"phone":     "07123456780",
	}
	w2 := makeRequest(handler, "POST", "/api/users/guest", req2, "", ctx)
	if w2.Code != http.StatusCreated {
		t.Errorf("expected status 201 for second guest, got %d. body: %s", w2.Code, w2.Body.String())
	}
	var resp2 map[string]string
	json.Unmarshal(w2.Body.Bytes(), &resp2)

	if resp1["id"] == resp2["id"] {
		t.Error("expected different user IDs for duplicate email, got same ID")
	}

	// Verify two separate guest accounts exist
	var count int
	tx.QueryRow(ctx, `SELECT COUNT(*) FROM users WHERE email = $1 AND account_role = 'guest'`, "john@test.com").Scan(&count)
	if count != 2 {
		t.Errorf("expected 2 guest accounts with same email, got %d", count)
	}
}

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

	// Create a registered user with a known email
	registeredEmail := "registered@example.com"
	tx.Exec(ctx, `
		INSERT INTO users (n_first_name, n_last_name, email, phone, date_of_birth, account_role)
		VALUES ('Registered', 'User', $1, '07123456700', '1990-01-01', 'verified_email')
	`, registeredEmail)

	// Try to create a guest with same email as the registered user
	req := map[string]string{
		"firstName": "Evil",
		"lastName":  "Guest",
		"email":     registeredEmail,
		"phone":     "07123456799",
	}

	handler := http.HandlerFunc(user.CreateGuestUserHandler)
	w := makeRequest(handler, "POST", "/api/users/guest", req, "", ctx)

	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409 for registered email collision, got %d. body: %s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), "log in") {
		t.Errorf("expected error to mention 'log in', got: %s", w.Body.String())
	}
}

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

	// Create guest user
	guestReq := map[string]string{
		"firstName": "Guest",
		"lastName":  "User",
		"email":     "guest@test.com",
		"phone":     "07123456789",
	}
	handler := http.HandlerFunc(user.CreateGuestUserHandler)
	w := makeRequest(handler, "POST", "/api/users/guest", guestReq, "", ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create guest user: %d", w.Code)
	}
	var guestResp map[string]string
	json.Unmarshal(w.Body.Bytes(), &guestResp)
	guestID := guestResp["id"]

	// Create booking as guest
	serviceID, _ := fixtures.CreateTestService(tx)
	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())

	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
		UserID:     &guestID,
	}

	bookingHandler := http.HandlerFunc(CreateBookingHandler)
	w2 := makeRequest(bookingHandler, "POST", "/api/bookings", req, "", ctx)

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

	// Verify booking in DB
	var count int
	tx.QueryRow(ctx, `SELECT COUNT(*) FROM bookings WHERE user_id = $1`, guestID).Scan(&count)
	if count != 1 {
		t.Errorf("expected 1 booking for guest, got %d", count)
	}
}

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

	// Attempt booking without auth AND without user_id
	serviceID, _ := fixtures.CreateTestService(tx)
	futureTime := clock.Now().Add(72 * time.Hour)

	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, "", ctx)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("expected status 401 for missing user_id, got %d. body: %s", w.Code, w.Body.String())
	}
}

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

	// Create a registered (non-guest) user
	userID, _ := fixtures.CreateTestUser(tx)

	// Try to book using their user_id but without auth token
	serviceID, _ := fixtures.CreateTestService(tx)
	futureTime := clock.Now().Add(72 * time.Hour)

	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
		UserID:     &userID,
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, "", ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for non-guest user_id, got %d. body: %s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), "guest") {
		t.Errorf("expected error to mention 'guest', got: %s", w.Body.String())
	}
}

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

	// Create guest user
	guestReq := map[string]string{
		"firstName": "Guest",
		"lastName":  "Skipper",
		"email":     "skip@test.com",
		"phone":     "07123456788",
	}
	handler := http.HandlerFunc(user.CreateGuestUserHandler)
	w := makeRequest(handler, "POST", "/api/users/guest", guestReq, "", ctx)
	var guestResp map[string]string
	json.Unmarshal(w.Body.Bytes(), &guestResp)
	guestID := guestResp["id"]

	// Give them an active booking with deposit required
	serviceID, _ := fixtures.CreateTestService(tx)
	pastTime := clock.Now().Add(72 * time.Hour)
	tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, $2, 'confirmed'::booking_status, false)
	`, guestID, pastTime)

	// Guest should still be able to create a second booking (deposit check skipped)
	futureTime := clock.Now().Add(96 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 14, 0, 0, 0, futureTime.Location())

	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
		UserID:     &guestID,
	}

	bookingHandler := http.HandlerFunc(CreateBookingHandler)
	w2 := makeRequest(bookingHandler, "POST", "/api/bookings", req, "", ctx)

	if w2.Code != http.StatusCreated {
		t.Errorf("expected guest to bypass deposit check, got %d. body: %s", w2.Code, w2.Body.String())
	}
}

func TestGuestBooking_BypassesAdvanceWindow(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

	// Create guest user
	guestReq := map[string]string{
		"firstName": "Guest",
		"lastName":  "Walker",
		"email":     "walker-guest@test.com",
		"phone":     "07123456789",
	}
	handler := http.HandlerFunc(user.CreateGuestUserHandler)
	w := makeRequest(handler, "POST", "/api/users/guest", guestReq, "", ctx)
	var guestResp map[string]string
	json.Unmarshal(w.Body.Bytes(), &guestResp)
	guestID := guestResp["id"]

	// Set deposits_required on the guest user.
	if _, err := tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", guestID); err != nil {
		t.Fatalf("failed to set deposits_required on guest: %v", err)
	}

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

	// Guest should be able to book within the 36h advance window (bypasses deposit check).
	midday := clock.Now().Truncate(24 * time.Hour).Add(29 * time.Hour)
	if midday.Hour() < 8 || midday.Hour() >= 20 {
		midday = nextWeekday(clock.Now().Weekday()).Add(12 * time.Hour)
	}
	nearTime := midday.Truncate(time.Second)

	req := CreateBookingRequest{
		StartTime:  nearTime,
		ServiceIDs: []string{serviceID},
		UserID:     &guestID,
	}

	bookingHandler := http.HandlerFunc(CreateBookingHandler)
	w2 := makeRequest(bookingHandler, "POST", "/api/bookings", req, "", ctx)

	if w2.Code != http.StatusCreated {
		t.Errorf("expected guest to bypass 36h advance window, got %d. body: %s", w2.Code, w2.Body.String())
	}
}

// =============================================================================
// Booking Notification Creation Tests
// =============================================================================

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var bookingID string
	err = tx.QueryRow(ctx, "SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1", userID).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to get booking ID: %v", err)
	}

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

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	notes := "Please do French tips with gold foil"
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
		Notes:      &notes,
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var bookingID string
	err = tx.QueryRow(ctx, "SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1", userID).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to get booking ID: %v", err)
	}

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

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

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	futureTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	futureTime = time.Date(futureTime.Year(), futureTime.Month(), futureTime.Day(), 10, 0, 0, 0, futureTime.Location())
	req := CreateBookingRequest{
		StartTime:  futureTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var bookingID string
	err = tx.QueryRow(ctx, "SELECT id FROM bookings WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1", userID).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to get booking ID: %v", err)
	}

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

// =============================================================================
// Closing Hours, Advance Check, and Active Booking Limit Tests
// =============================================================================

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

	hours := []struct {
		weekday   int
		startTime string
		endTime   string
		isOpen    bool
	}{
		{0, "08:00", "17:00", true},
		{1, "08:00", "20:00", true},
		{2, "08:00", "20:00", true},
		{3, "08:00", "20:00", true},
		{4, "08:00", "20:00", true},
		{5, "08:00", "20:00", true},
		{6, "08:00", "20:00", true},
	}
	seedCustomWorkingHours(t, ctx, tx, hours)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	_, err = tx.Exec(ctx, "UPDATE services SET duration_minutes = 60 WHERE id = $1", serviceID)
	if err != nil {
		t.Fatalf("failed to set service duration: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	thursday := nextWeekday(time.Thursday)
	thursdayStart := time.Date(thursday.Year(), thursday.Month(), thursday.Day(), 17, 30, 0, 0, thursday.Location())
	req1 := CreateBookingRequest{
		StartTime:  thursdayStart,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w1 := makeRequest(handler, "POST", "/api/bookings", req1, token, ctx)

	if w1.Code != http.StatusCreated {
		t.Errorf("Thursday 17:30+60min should succeed (ends 18:30 < 20:00), got %d. body: %s", w1.Code, w1.Body.String())
	}

	monday := nextWeekday(time.Monday)
	mondayStart := time.Date(monday.Year(), monday.Month(), monday.Day(), 16, 30, 0, 0, monday.Location())
	req2 := CreateBookingRequest{
		StartTime:  mondayStart,
		ServiceIDs: []string{serviceID},
	}

	w2 := makeRequest(handler, "POST", "/api/bookings", req2, token, ctx)

	if w2.Code != http.StatusBadRequest {
		t.Errorf("Monday 16:30+60min should fail (ends 17:30 > 17:00), got %d. body: %s", w2.Code, w2.Body.String())
	}

	if !bytes.Contains(w2.Body.Bytes(), []byte("closing")) {
		t.Errorf("expected error about closing hours, got: %s", w2.Body.String())
	}
}

// =============================================================================
// T1: Duplicate Completion Test
// =============================================================================

// TestProgressBooking_DuplicateCompletion verifies that calling
// ProgressBookingHandler twice with "completed" does not award duplicate
// stamps or discounts.
func TestProgressBooking_DuplicateCompletion(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	// Reset loyalty_stamps to 0
	_, err = tx.Exec(ctx, "UPDATE users SET loyalty_stamps = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to reset loyalty_stamps: %v", err)
	}

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

	// Create booking (defaults to pending, far future)
	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	// Set total_amount > 0 so stamp is awarded
	_, err = tx.Exec(ctx, "UPDATE bookings SET total_amount = 50.00 WHERE id = $1", bookingID)
	if err != nil {
		t.Fatalf("failed to set total_amount: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// First completion call — awards the first stamp
	handler := http.HandlerFunc(ProgressBookingHandler)
	req := ProgressBookingRequest{Status: "completed"}
	w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID+"/progress", req, token, ctx)

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

	// Verify stamp count is 1 (awarded by the first completion)
	var stamps1 int
	err = tx.QueryRow(ctx, "SELECT loyalty_stamps FROM users WHERE id = $1", userID).Scan(&stamps1)
	if err != nil {
		t.Fatalf("failed to query stamps: %v", err)
	}
	if stamps1 != 1 {
		t.Errorf("after first completion: expected 1 stamp, got %d", stamps1)
	}

	// Second completion call (should be idempotent — booking is already completed)
	w2 := makeRequest(handler, "PUT", "/api/bookings/"+bookingID+"/progress", req, token, ctx)
	if w2.Code != http.StatusOK {
		t.Fatalf("second completion: expected 200, got %d. body: %s", w2.Code, w2.Body.String())
	}

	// Verify stamp count = 1 (still 1, not 2)
	var stamps2 int
	err = tx.QueryRow(ctx, "SELECT loyalty_stamps FROM users WHERE id = $1", userID).Scan(&stamps2)
	if err != nil {
		t.Fatalf("failed to query stamps: %v", err)
	}
	if stamps2 != 1 {
		t.Errorf("second completion: expected 1 stamp (duplicate prevented), got %d", stamps2)
	}
}

// =============================================================================
// T2: Empty Stamp Cap Test (Daily Limit)
// =============================================================================

// TestProgressBooking_DailyStampCap verifies that completing two bookings for
// the same user on the same day only awards 1 stamp (daily cap via SQL subquery).
func TestProgressBooking_DailyStampCap(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	// Reset loyalty_stamps to 0
	_, err = tx.Exec(ctx, "UPDATE users SET loyalty_stamps = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to reset loyalty_stamps: %v", err)
	}

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

	// Create two bookings on the same day (different times)
	today := clock.Now().Truncate(24 * time.Hour)
	booking1Time := today.Add(10 * time.Hour) // 10:00 today
	booking2Time := today.Add(14 * time.Hour) // 14:00 today

	booking1ID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, booking1Time)
	if err != nil {
		t.Fatalf("failed to create booking 1: %v", err)
	}
	defer fixtures.DeleteBooking(tx, booking1ID)

	booking2ID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, booking2Time)
	if err != nil {
		t.Fatalf("failed to create booking 2: %v", err)
	}
	defer fixtures.DeleteBooking(tx, booking2ID)

	// Set total_amount > 0 for both
	_, err = tx.Exec(ctx, "UPDATE bookings SET total_amount = 50.00 WHERE id = $1", booking1ID)
	if err != nil {
		t.Fatalf("failed to set total_amount 1: %v", err)
	}
	_, err = tx.Exec(ctx, "UPDATE bookings SET total_amount = 50.00 WHERE id = $1", booking2ID)
	if err != nil {
		t.Fatalf("failed to set total_amount 2: %v", err)
	}

	// Move both to confirmed → in_progress
	for _, bid := range []string{booking1ID, booking2ID} {
		_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'confirmed' WHERE id = $1", bid)
		if err != nil {
			t.Fatalf("failed to confirm booking %s: %v", bid, err)
		}
		_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'in_progress' WHERE id = $1", bid)
		if err != nil {
			t.Fatalf("failed to set in_progress for booking %s: %v", bid, err)
		}
	}

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(ProgressBookingHandler)
	req := ProgressBookingRequest{Status: "completed"}

	// Complete first booking via handler — awards the first stamp
	w1 := makeRequest(handler, "PUT", "/api/bookings/"+booking1ID+"/progress", req, token, ctx)
	if w1.Code != http.StatusOK {
		t.Fatalf("first completion: expected 200, got %d. body: %s", w1.Code, w1.Body.String())
	}

	var stamps1 int
	err = tx.QueryRow(ctx, "SELECT loyalty_stamps FROM users WHERE id = $1", userID).Scan(&stamps1)
	if err != nil {
		t.Fatalf("failed to query stamps: %v", err)
	}
	if stamps1 != 1 {
		t.Errorf("after first completion: expected 1 stamp, got %d", stamps1)
	}

	// Complete second booking (same day, daily cap should prevent another stamp)
	w2 := makeRequest(handler, "PUT", "/api/bookings/"+booking2ID+"/progress", req, token, ctx)
	if w2.Code != http.StatusOK {
		t.Fatalf("second completion: expected 200, got %d. body: %s", w2.Code, w2.Body.String())
	}

	var stamps2 int
	err = tx.QueryRow(ctx, "SELECT loyalty_stamps FROM users WHERE id = $1", userID).Scan(&stamps2)
	if err != nil {
		t.Fatalf("failed to query stamps: %v", err)
	}
	if stamps2 != 1 {
		t.Errorf("after second completion (same day): expected 1 stamp (daily cap), got %d", stamps2)
	}
}

// =============================================================================
// T3: Invalid Status Transition Test
// =============================================================================

// TestProgressBooking_InvalidTransitions verifies that ProgressBookingHandler
// rejects invalid status transitions with HTTP 400.
func TestProgressBooking_InvalidTransitions(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	t.Run("no_show_to_completed", func(t *testing.T) {
		bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
		if err != nil {
			t.Fatalf("failed to create booking: %v", err)
		}
		defer fixtures.DeleteBooking(tx, bookingID)

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

		handler := http.HandlerFunc(ProgressBookingHandler)
		req := ProgressBookingRequest{Status: "completed"}
		w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID+"/progress", req, token, ctx)

		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400 for no_show→completed, got %d. body: %s", w.Code, w.Body.String())
		}
	})

	t.Run("client_cancelled_to_in_progress", func(t *testing.T) {
		bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
		if err != nil {
			t.Fatalf("failed to create booking: %v", err)
		}
		defer fixtures.DeleteBooking(tx, bookingID)

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

		handler := http.HandlerFunc(ProgressBookingHandler)
		req := ProgressBookingRequest{Status: "in_progress"}
		w := makeRequest(handler, "PUT", "/api/bookings/"+bookingID+"/progress", req, token, ctx)

		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400 for client_cancelled→in_progress, got %d. body: %s", w.Code, w.Body.String())
		}
	})
}

// =============================================================================
// T4: Stale Duration Edit Test
// =============================================================================

// TestBookings_Edit_SequentialEdit verifies that EditBookingHandler correctly
// updates the booking when called sequentially (not concurrent).
func TestBookings_Edit_SequentialEdit(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	// First edit: change start_time to a new future time
	newStartTime1 := clock.Now().Add(96 * time.Hour).Truncate(time.Second)
	req1 := EditBookingRequest{
		StartTime: newStartTime1,
	}

	handler := http.HandlerFunc(EditBookingHandler)
	w1 := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req1, token, ctx)

	if w1.Code != http.StatusOK {
		t.Fatalf("first edit: expected 200, got %d. body: %s", w1.Code, w1.Body.String())
	}

	// Verify the edit took effect
	var dbStartTime1 time.Time
	err = tx.QueryRow(ctx, "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime1)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !dbStartTime1.Truncate(time.Second).Equal(newStartTime1) {
		t.Errorf("after first edit: expected %v, got %v", newStartTime1, dbStartTime1)
	}

	// Second edit: change to a different future time
	newStartTime2 := newStartTime1.AddDate(0, 0, 1).Truncate(time.Second)
	req2 := EditBookingRequest{
		StartTime: newStartTime2,
	}

	w2 := makeRequest(handler, "PUT", "/api/bookings/"+bookingID, req2, token, ctx)

	if w2.Code != http.StatusOK {
		t.Fatalf("second edit: expected 200, got %d. body: %s", w2.Code, w2.Body.String())
	}

	// Verify the second edit took effect
	var dbStartTime2 time.Time
	err = tx.QueryRow(ctx, "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime2)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !dbStartTime2.Truncate(time.Second).Equal(newStartTime2) {
		t.Errorf("after second edit: expected %v, got %v", newStartTime2, dbStartTime2)
	}
}

// =============================================================================
// T5: Refund-Fails-Then-Delete-Test (TODO only)
// =============================================================================

// TODO(RefundFailsThenDelete): Test that when a refund fails during booking
// cancellation, the booking status is NOT changed (transaction rollback).
// This is hard to test without mocking the payment service because the refund
// is processed outside the cancellation transaction. Steps if mocking were
// available:
//   1. Create a booking with a completed payment
//   2. Make ProcessCancellationRefund return an error (requires mocking
//      payments.NewPaymentService or the underlying DB calls)
//   3. Call DeleteBookingHandler with client_cancelled reason
//   4. Verify the booking status was NOT changed (still 'confirmed')
//   5. Verify no refund records were created
// Currently skipped: no mocking framework is set up for this project.

// =============================================================================
// T6: Timezone Independence Test
// =============================================================================

// TestBookings_TimezoneIndependence verifies that times are stored and
// retrieved correctly regardless of timezone — the stored time matches the
// requested time with no timezone shift.
func TestBookings_TimezoneIndependence(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Create a booking with a specific UTC time
	utcTime := time.Date(2099, 6, 15, 14, 30, 0, 0, time.UTC)
	req := CreateBookingRequest{
		StartTime:  utcTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	// Verify the start time in the response matches (ignoring monotonic clock)
	if !booking.StartTime.Equal(utcTime) {
		t.Errorf("expected start_time %v (UTC), got %v", utcTime, booking.StartTime)
	}

	// Verify the time is stored correctly in the database
	var dbStartTime time.Time
	err = tx.QueryRow(ctx, "SELECT start_time FROM bookings WHERE id = $1", booking.ID).Scan(&dbStartTime)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !dbStartTime.Equal(utcTime) {
		t.Errorf("DB start_time: expected %v (UTC), got %v", utcTime, dbStartTime)
	}

	// Now retrieve via GetBookingHandler
	w2 := makeRequest(http.HandlerFunc(GetBookingHandler), "GET", "/api/bookings/"+booking.ID, nil, token, ctx)
	if w2.Code != http.StatusOK {
		t.Fatalf("get booking: expected 200, got %d. body: %s", w2.Code, w2.Body.String())
	}

	var fetched Booking
	if err := parseResponseBody(w2, &fetched); err != nil {
		t.Fatalf("failed to parse get response: %v", err)
	}
	if !fetched.StartTime.Equal(utcTime) {
		t.Errorf("GET start_time: expected %v (UTC), got %v", utcTime, fetched.StartTime)
	}
}

// =============================================================================
// T10: Duplicate Stamp Test at Daily Limit (explicit SQL subquery)
// =============================================================================

// TestProgressBooking_DailyStampCap_SQLSubquery verifies that the SQL subquery
// in ProgressBookingHandler correctly prevents duplicate stamps on the same
// day by testing the underlying database constraint directly.
func TestProgressBooking_DailyStampCap_SQLSubquery(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0, loyalty_stamps = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set user fields: %v", err)
	}

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

	// Create two bookings on the same day
	today := clock.Now().Truncate(24 * time.Hour)
	b1Time := today.Add(9 * time.Hour)
	b2Time := today.Add(15 * time.Hour)

	b1ID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, b1Time)
	if err != nil {
		t.Fatalf("failed to create booking 1: %v", err)
	}
	defer fixtures.DeleteBooking(tx, b1ID)

	b2ID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, b2Time)
	if err != nil {
		t.Fatalf("failed to create booking 2: %v", err)
	}
	defer fixtures.DeleteBooking(tx, b2ID)

	// Ensure total_amount > 0 so stamps are awarded
	_, err = tx.Exec(ctx, "UPDATE bookings SET total_amount = 50.00, status = 'completed', updated_at = NOW() WHERE id = $1", b1ID)
	if err != nil {
		t.Fatalf("failed to complete booking 1: %v", err)
	}

	// Directly test the SQL subquery: award 1 stamp via the same logic used in ProgressBookingHandler
	var stampCount int
	err = tx.QueryRow(ctx, `
		UPDATE users
		SET loyalty_stamps = loyalty_stamps + 1
		WHERE id = $1
		AND NOT EXISTS (
			SELECT 1 FROM bookings b
			WHERE b.user_id = users.id
			AND b.status = 'completed'
			AND b.updated_at >= CURRENT_DATE - INTERVAL '1 day'
			AND b.id != $2
		)
		RETURNING loyalty_stamps
	`, userID, b1ID).Scan(&stampCount)
	if err != nil {
		if err.Error() == "no rows in result set" {
			// No rows means the subquery blocked the update — that's the cap working
			stampCount = 0
		} else {
			t.Fatalf("first stamp query failed: %v", err)
		}
	}

	if stampCount != 1 {
		t.Errorf("expected 1 stamp after first booking completion, got %d", stampCount)
	}

	// Now complete the second booking — the subquery should see that b1 was
	// completed today and block the second stamp
	_, err = tx.Exec(ctx, "UPDATE bookings SET total_amount = 50.00, status = 'completed', updated_at = NOW() WHERE id = $1", b2ID)
	if err != nil {
		t.Fatalf("failed to complete booking 2: %v", err)
	}

	// Try to award stamp for b2 — should be blocked by daily cap
	var stampCount2 int
	err = tx.QueryRow(ctx, `
		UPDATE users
		SET loyalty_stamps = loyalty_stamps + 1
		WHERE id = $1
		AND NOT EXISTS (
			SELECT 1 FROM bookings b
			WHERE b.user_id = users.id
			AND b.status = 'completed'
			AND b.updated_at >= CURRENT_DATE - INTERVAL '1 day'
			AND b.id != $2
		)
		RETURNING loyalty_stamps
	`, userID, b2ID).Scan(&stampCount2)
	if err != nil {
		if err.Error() == "no rows in result set" {
			stampCount2 = 0
		} else {
			t.Fatalf("second stamp query failed: %v", err)
		}
	}

	if stampCount2 != 0 {
		t.Errorf("expected 0 stamps (daily cap blocked), got %d", stampCount2)
	}

	// Verify total stamps is still 1
	var totalStamps int
	err = tx.QueryRow(ctx, "SELECT loyalty_stamps FROM users WHERE id = $1", userID).Scan(&totalStamps)
	if err != nil {
		t.Fatalf("failed to query total stamps: %v", err)
	}
	if totalStamps != 1 {
		t.Errorf("expected total 1 stamp, got %d", totalStamps)
	}
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(CreateBookingHandler)

	soonTime := clock.Now().Add(30 * time.Minute).Truncate(time.Second)
	req1 := CreateBookingRequest{
		StartTime:  soonTime,
		ServiceIDs: []string{serviceID},
	}

	w1 := makeRequest(handler, "POST", "/api/bookings", req1, token, ctx)

	if w1.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 for 30-min advance booking, got %d. body: %s", w1.Code, w1.Body.String())
	}

	if !bytes.Contains(w1.Body.Bytes(), []byte("at least 1 hour")) {
		t.Errorf("expected error about 1 hour advance, got: %s", w1.Body.String())
	}

	aheadTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	aheadTime = time.Date(aheadTime.Year(), aheadTime.Month(), aheadTime.Day(), 10, 0, 0, 0, aheadTime.Location())
	req2 := CreateBookingRequest{
		StartTime:  aheadTime,
		ServiceIDs: []string{serviceID},
	}

	w2 := makeRequest(handler, "POST", "/api/bookings", req2, token, ctx)

	if w2.Code != http.StatusCreated {
		t.Errorf("expected status 201 for 2h+ advance booking, got %d. body: %s", w2.Code, w2.Body.String())
	}
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(CreateBookingHandler)

	firstTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	firstTime = time.Date(firstTime.Year(), firstTime.Month(), firstTime.Day(), 10, 0, 0, 0, firstTime.Location())
	req1 := CreateBookingRequest{
		StartTime:  firstTime,
		ServiceIDs: []string{serviceID},
	}

	w1 := makeRequest(handler, "POST", "/api/bookings", req1, token, ctx)

	if w1.Code != http.StatusCreated {
		t.Fatalf("expected first booking to succeed, got %d. body: %s", w1.Code, w1.Body.String())
	}

	var booking1 Booking
	if err := parseResponseBody(w1, &booking1); err != nil {
		t.Fatalf("failed to parse first booking: %v", err)
	}

	secondTime := clock.Now().Add(96 * time.Hour).Truncate(time.Second)
	secondTime = time.Date(secondTime.Year(), secondTime.Month(), secondTime.Day(), 14, 0, 0, 0, secondTime.Location())
	req2 := CreateBookingRequest{
		StartTime:  secondTime,
		ServiceIDs: []string{serviceID},
	}

	w2 := makeRequest(handler, "POST", "/api/bookings", req2, token, ctx)

	if w2.Code != http.StatusConflict {
		t.Errorf("expected status 409 for second booking with active booking, got %d. body: %s", w2.Code, w2.Body.String())
	}

	if !bytes.Contains(w2.Body.Bytes(), []byte("active booking")) {
		t.Errorf("expected error about active booking, got: %s", w2.Body.String())
	}

	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = 'client_cancelled' WHERE id = $1", booking1.ID)
	if err != nil {
		t.Fatalf("failed to cancel first booking: %v", err)
	}

	w3 := makeRequest(handler, "POST", "/api/bookings", req2, token, ctx)

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

func TestNextWeekdayHelper(t *testing.T) {
	tests := []struct {
		name    string
		weekday time.Weekday
	}{
		{"Monday", time.Monday},
		{"Tuesday", time.Tuesday},
		{"Wednesday", time.Wednesday},
		{"Thursday", time.Thursday},
		{"Friday", time.Friday},
		{"Saturday", time.Saturday},
		{"Sunday", time.Sunday},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := nextWeekday(tt.weekday)

			if result.Weekday() != tt.weekday {
				t.Errorf("expected weekday %s, got %s", tt.weekday, result.Weekday())
			}

			now := clock.Now()
			today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC)
			resultDay := time.Date(result.Year(), result.Month(), result.Day(), 0, 0, 0, 0, time.UTC)
			daysDiff := int(resultDay.Sub(today).Hours() / 24)
			if daysDiff < 2 {
				t.Errorf("expected result to be at least 2 calendar days ahead, got %d", daysDiff)
			}
		})
	}
}

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

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Use next weekday >48h from now during working hours (well past 36h window).
	// Use 13:00 (1pm) to ensure the 36h deposit window from ANY overnight test time
	// is always cleared — 36h from midnight UTC Wednesday = 12:00 UTC Thursday.
	bookingTime := nextWeekday(time.Thursday).Add(13 * time.Hour)

	req := CreateBookingRequest{
		StartTime:  bookingTime,
		ServiceIDs: []string{serviceID},
	}

	handler := http.HandlerFunc(CreateBookingHandler)
	w := makeRequest(handler, "POST", "/api/bookings", req, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking response: %v", err)
	}

	if !booking.DepositRequired {
		t.Error("expected deposit_required=true on first booking")
	}

	var depositRequired bool
	err = tx.QueryRow(ctx,
		"SELECT deposit_required FROM bookings WHERE id = $1", booking.ID).Scan(&depositRequired)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !depositRequired {
		t.Error("expected deposit_required=true in DB for first booking")
	}

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to update deposits_required: %v", err)
	}

	err = tx.QueryRow(ctx,
		"SELECT deposit_required FROM bookings WHERE id = $1", booking.ID).Scan(&depositRequired)
	if err != nil {
		t.Fatalf("failed to query booking after user update: %v", err)
	}
	if !depositRequired {
		t.Error("expected booking deposit_required to remain true after user change")
	}

	bookingTime2 := nextWeekday(time.Tuesday).Add(10 * time.Hour)
	req2 := CreateBookingRequest{
		StartTime:  bookingTime2,
		ServiceIDs: []string{serviceID},
	}

	w2 := makeRequest(handler, "POST", "/api/bookings", req2, token, ctx)
	if w2.Code != http.StatusCreated {
		t.Fatalf("expected status 201 for second booking, got %d. body: %s", w2.Code, w2.Body.String())
	}

	var booking2 Booking
	if err := parseResponseBody(w2, &booking2); err != nil {
		t.Fatalf("failed to parse second booking response: %v", err)
	}

	if booking2.DepositRequired {
		t.Error("expected deposit_required=false on second booking after user deposits_required=0")
	}

	var depositRequired2 bool
	err = tx.QueryRow(ctx,
		"SELECT deposit_required FROM bookings WHERE id = $1", booking2.ID).Scan(&depositRequired2)
	if err != nil {
		t.Fatalf("failed to query second booking: %v", err)
	}
	if depositRequired2 {
		t.Error("expected second booking deposit_required=false in DB")
	}
}

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

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

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

	token := jwt.GenerateUserToken(userID)

	// Create completed booking
	bookingTime := nextWeekday(time.Wednesday).Add(10 * time.Hour)
	bookingID := createCompletedBookingWithTime(t, tx, ctx, userID, serviceID, bookingTime, 50.00)

	// Create a discount campaign and apply it
	var campaignID string
	err = tx.QueryRow(ctx, `
		INSERT INTO discount_campaigns (name, campaign_type, discount_percent, status, start_date, end_date)
		VALUES ($1, 'time_based', 10.0, 'active', NOW() - INTERVAL '1 day', NOW() + INTERVAL '1 day')
		RETURNING id
	`, "Test Campaign").Scan(&campaignID)
	if err != nil {
		t.Fatalf("failed to create campaign: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, discount_percent, original_total, discount_amount)
		VALUES ($1, $2, 'campaign', $3, 'time_based', 10.0, 50.00, 5.00)
	`, bookingID, userID, campaignID)
	if err != nil {
		t.Fatalf("failed to create booking discount: %v", err)
	}

	// Call GetBookingHandler
	handler := http.HandlerFunc(GetBookingHandler)
	w := makeRequest(handler, "GET", "/api/bookings/"+bookingID, nil, token, ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking: %v", err)
	}

	if len(booking.Discounts) != 1 {
		t.Fatalf("expected 1 discount, got %d", len(booking.Discounts))
	}

	d := booking.Discounts[0]
	if d.CampaignName == nil || *d.CampaignName != "Test Campaign" {
		t.Errorf("expected campaign name 'Test Campaign', got %v", d.CampaignName)
	}
	if d.DiscountAmount != 5.00 {
		t.Errorf("expected discount amount 5.00, got %.2f", d.DiscountAmount)
	}
}

func createCompletedBookingWithTime(t *testing.T, tx db.Querier, ctx context.Context, userID, serviceID string, startTime time.Time, price float64) string {
	t.Helper()
	var bookingID string
	err := tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'completed')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create completed booking: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_services (booking_id, service_id, override_price)
		VALUES ($1, $2, $3)
	`, bookingID, serviceID, price)
	if err != nil {
		t.Fatalf("failed to link service: %v", err)
	}

	return bookingID
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	csID, err := fixtures.CreateTestCustomService(tx)
	if err != nil {
		t.Fatalf("failed to create custom service: %v", err)
	}
	defer fixtures.DeleteCustomService(tx, csID)

	startTime := nextWeekday(time.Monday).Add(10 * time.Hour)

	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_services (booking_id, service_id)
		VALUES ($1, $2)
	`, bookingID, serviceID)
	if err != nil {
		t.Fatalf("failed to link service: %v", err)
	}

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_custom_services (booking_id, custom_service_id)
		VALUES ($1, $2)
	`, bookingID, csID)
	if err != nil {
		t.Fatalf("failed to link custom service: %v", err)
	}

	overridePrice := 100.0
	overrideDuration := 60
	svcOverridePrice := 55.0
	svcOverrideDuration := 45

	req := ConfirmBookingRequest{
		ServiceOverrides: []ServiceOverride{
			{ServiceID: serviceID, OverridePrice: &svcOverridePrice, OverrideDurationMinutes: &svcOverrideDuration},
		},
		CustomServiceOverrides: []ServiceOverride{
			{ServiceID: csID, OverridePrice: &overridePrice, OverrideDurationMinutes: &overrideDuration},
		},
	}

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(ConfirmBookingHandler)
	w := makeAuthRequest(handler, "POST", "/api/bookings/"+bookingID+"/confirm", req, token, "", ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if booking.Status != "confirmed" {
		t.Errorf("expected status 'confirmed', got %s", booking.Status)
	}

	var storedOverridePrice *float64
	var storedOverrideDuration *int
	err = tx.QueryRow(ctx, `
		SELECT override_price, override_duration_minutes
		FROM booking_custom_services
		WHERE booking_id = $1 AND custom_service_id = $2
	`, bookingID, csID).Scan(&storedOverridePrice, &storedOverrideDuration)
	if err != nil {
		t.Fatalf("failed to query custom service override: %v", err)
	}
	if storedOverridePrice == nil || *storedOverridePrice != overridePrice {
		t.Errorf("expected override_price %.2f, got %v", overridePrice, storedOverridePrice)
	}
	if storedOverrideDuration == nil || *storedOverrideDuration != overrideDuration {
		t.Errorf("expected override_duration_minutes %d, got %v", overrideDuration, storedOverrideDuration)
	}

	var svcStoredPrice *float64
	var svcStoredDuration *int
	err = tx.QueryRow(ctx, `
		SELECT override_price, override_duration_minutes
		FROM booking_services
		WHERE booking_id = $1 AND service_id = $2
	`, bookingID, serviceID).Scan(&svcStoredPrice, &svcStoredDuration)
	if err != nil {
		t.Fatalf("failed to query service override: %v", err)
	}
	if svcStoredPrice == nil || *svcStoredPrice != svcOverridePrice {
		t.Errorf("expected service override_price %.2f, got %v", svcOverridePrice, svcStoredPrice)
	}
	if svcStoredDuration == nil || *svcStoredDuration != svcOverrideDuration {
		t.Errorf("expected service override_duration_minutes %d, got %v", svcOverrideDuration, svcStoredDuration)
	}
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	csID, err := fixtures.CreateTestCustomService(tx)
	if err != nil {
		t.Fatalf("failed to create custom service: %v", err)
	}
	defer fixtures.DeleteCustomService(tx, csID)

	startTime := nextWeekday(time.Tuesday).Add(10 * time.Hour)

	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_custom_services (booking_id, custom_service_id)
		VALUES ($1, $2)
	`, bookingID, csID)
	if err != nil {
		t.Fatalf("failed to link custom service: %v", err)
	}

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(GetBookingHandler)
	w := makeAuthRequest(handler, "GET", "/api/bookings/"+bookingID, nil, token, "", ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking: %v", err)
	}

	if len(booking.Services) == 0 {
		t.Fatal("expected at least one service in response")
	}

	found := false
	for _, s := range booking.Services {
		if s.ServiceID == csID {
			found = true
			if s.ServiceName == nil || *s.ServiceName != "Test Custom Service" {
				t.Errorf("expected service name 'Test Custom Service', got %v", s.ServiceName)
			}
			if s.Price == nil || *s.Price != 75.00 {
				t.Errorf("expected price 75.00, got %v", s.Price)
			}
			if s.DurationMinutes == nil || *s.DurationMinutes != 45 {
				t.Errorf("expected duration 45, got %v", s.DurationMinutes)
			}
			break
		}
	}
	if !found {
		t.Errorf("custom service %s not found in booking services", csID)
	}
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	csID, err := fixtures.CreateTestCustomService(tx)
	if err != nil {
		t.Fatalf("failed to create custom service: %v", err)
	}
	defer fixtures.DeleteCustomService(tx, csID)

	startTime := nextWeekday(time.Wednesday).Add(10 * time.Hour)

	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_custom_services (booking_id, custom_service_id)
		VALUES ($1, $2)
	`, bookingID, csID)
	if err != nil {
		t.Fatalf("failed to link custom service: %v", err)
	}

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(ConfirmBookingHandler)

	t.Run("negative override price", func(t *testing.T) {
		negPrice := -10.0
		validDur := 30
		req := ConfirmBookingRequest{
			CustomServiceOverrides: []ServiceOverride{
				{ServiceID: csID, OverridePrice: &negPrice, OverrideDurationMinutes: &validDur},
			},
		}
		w := makeAuthRequest(handler, "POST", "/api/bookings/"+bookingID+"/confirm", req, token, "", ctx)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400 for negative price, got %d. body: %s", w.Code, w.Body.String())
		}
	})

	t.Run("non-positive override duration", func(t *testing.T) {
		validPrice := 50.0
		zeroDur := 0
		req := ConfirmBookingRequest{
			CustomServiceOverrides: []ServiceOverride{
				{ServiceID: csID, OverridePrice: &validPrice, OverrideDurationMinutes: &zeroDur},
			},
		}
		w := makeAuthRequest(handler, "POST", "/api/bookings/"+bookingID+"/confirm", req, token, "", ctx)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400 for zero duration, got %d. body: %s", w.Code, w.Body.String())
		}
	})

	t.Run("negative override duration", func(t *testing.T) {
		validPrice := 50.0
		negDur := -10
		req := ConfirmBookingRequest{
			CustomServiceOverrides: []ServiceOverride{
				{ServiceID: csID, OverridePrice: &validPrice, OverrideDurationMinutes: &negDur},
			},
		}
		w := makeAuthRequest(handler, "POST", "/api/bookings/"+bookingID+"/confirm", req, token, "", ctx)
		if w.Code != http.StatusBadRequest {
			t.Errorf("expected 400 for negative duration, got %d. body: %s", w.Code, w.Body.String())
		}
	})
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	csID, err := fixtures.CreateTestCustomService(tx)
	if err != nil {
		t.Fatalf("failed to create custom service: %v", err)
	}
	defer fixtures.DeleteCustomService(tx, csID)

	startTime := nextWeekday(time.Thursday).Add(10 * time.Hour)

	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_services (booking_id, service_id)
		VALUES ($1, $2)
	`, bookingID, serviceID)
	if err != nil {
		t.Fatalf("failed to link service: %v", err)
	}

	overridePrice := 80.0
	overrideDuration := 50
	req := ConfirmBookingRequest{
		CustomServiceOverrides: []ServiceOverride{
			{ServiceID: csID, OverridePrice: &overridePrice, OverrideDurationMinutes: &overrideDuration},
		},
	}

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(ConfirmBookingHandler)
	w := makeAuthRequest(handler, "POST", "/api/bookings/"+bookingID+"/confirm", req, token, "", ctx)

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

	if !bytes.Contains(w.Body.Bytes(), []byte("do not belong to this booking")) {
		t.Errorf("expected error about custom service not belonging to booking, got: %s", w.Body.String())
	}

	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" {
		t.Errorf("expected booking to remain 'pending' after failed confirm, got '%s'", status)
	}
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

	csID, err := fixtures.CreateTestCustomService(tx)
	if err != nil {
		t.Fatalf("failed to create custom service: %v", err)
	}
	defer fixtures.DeleteCustomService(tx, csID)

	startTime := nextWeekday(time.Friday).Add(10 * time.Hour)

	var bookingID string
	err = tx.QueryRow(ctx, `
		INSERT INTO bookings (user_id, start_time, status)
		VALUES ($1, $2, 'pending')
		RETURNING id
	`, userID, startTime).Scan(&bookingID)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_custom_services (booking_id, custom_service_id)
		VALUES ($1, $2)
	`, bookingID, csID)
	if err != nil {
		t.Fatalf("failed to link custom service: %v", err)
	}

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(ProgressBookingHandler)

	req := ProgressBookingRequest{Status: "completed"}
	w := makeAuthRequest(handler, "PUT", "/api/bookings/"+bookingID+"/progress", req, token, "", ctx)

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

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if booking.Status != "completed" {
		t.Errorf("expected booking status 'completed', got '%s'", booking.Status)
	}

	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 != "completed" {
		t.Errorf("expected DB status 'completed', got '%s'", status)
	}
}

// =============================================================================
// Refund Processing Tests (DeleteBookingHandler with payments)
// =============================================================================

// TestDeleteBooking_WithPayments_ProcessesRefund verifies that when a user
// cancels a booking that has completed payments, the refund is processed
// BEFORE the cancellation, and refund records are created in the DB.
func TestDeleteBooking_WithPayments_ProcessesRefund(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Use a booking far enough in the future that >72h notice applies (full refund)
	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	paymentID := bookingID[:8] + "pmt"
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (id, booking_id, payment_type, payment_method, amount, status, created_at)
		VALUES ($1, $2, 'full', 'in_person_card', 50.00, 'completed', NOW())
	`, paymentID, bookingID)
	if err != nil {
		t.Fatalf("failed to add payment: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(DeleteBookingHandler)
	reqBody := map[string]string{"reason": "client_cancelled"}
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, reqBody, token, ctx)

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

	// Verify booking was cancelled
	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 != "client_cancelled" {
		t.Errorf("expected status 'client_cancelled', got '%s'", status)
	}

	// Verify refund records were created
	var refundCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM refunds WHERE booking_id = $1", bookingID).Scan(&refundCount)
	if err != nil {
		t.Fatalf("failed to query refunds: %v", err)
	}
	if refundCount == 0 {
		t.Error("expected at least 1 refund record to be created")
	}

	// Verify refund_calculation in response body
	var respBody map[string]interface{}
	if err := parseResponseBody(w, &respBody); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if _, ok := respBody["refund_calculation"]; !ok {
		t.Error("expected refund_calculation in response body")
	}

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

// TestDeleteBooking_NoPayments_HardDelete verifies that when a booking has no
// payments, cancelling performs a hard delete (removes the row entirely).
func TestDeleteBooking_NoPayments_HardDelete(t *testing.T) {

	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Use next available working hour for booking creation
	soonTime := nextWorkingHour()
	bookingReq := CreateBookingRequest{
		StartTime:  soonTime,
		ServiceIDs: []string{serviceID},
	}
	createHandler := http.HandlerFunc(CreateBookingHandler)
	token := jwt.GenerateUserToken(userID)
	w := makeRequest(createHandler, "POST", "/api/bookings", bookingReq, token, ctx)

	var booking Booking
	if err := parseResponseBody(w, &booking); err != nil {
		t.Fatalf("failed to parse booking: %v", err)
	}

	// Cancel the pending booking (no payments → hard delete)
	delHandler := http.HandlerFunc(DeleteBookingHandler)
	w = makeRequest(delHandler, "DELETE", "/api/bookings/"+booking.ID, nil, token, ctx)

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

	// Verify booking row was hard-deleted
	var rowCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM bookings WHERE id = $1", booking.ID).Scan(&rowCount)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if rowCount != 0 {
		t.Error("expected booking row to be hard-deleted")
	}
}

// =============================================================================
// Past Booking No-Show Guard Test
// =============================================================================

// TestDeleteBooking_PastConfirmed_NoNoShow verifies that cancelling a past
// confirmed booking does NOT trigger no-show logic. The startTime.After(clock.Now())
// guard prevents retroactive no-show penalties for bookings that happen to
// still be "confirmed" after their start time.
func TestDeleteBooking_PastConfirmed_NoNoShow(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Create a booking in the past (1 hour ago) and set to confirmed.
	pastTime := clock.Now().Add(-1 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, pastTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	// Add a payment so the cancellation path is the paid-cancellation route.
	paymentID := bookingID[:8] + "pmt"
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (id, booking_id, payment_type, payment_method, amount, status, created_at)
		VALUES ($1, $2, 'full', 'in_person_card', 50.00, 'completed', NOW())
	`, paymentID, bookingID)
	if err != nil {
		t.Fatalf("failed to add payment: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(DeleteBookingHandler)
	reqBody := map[string]string{"reason": "client_cancelled"}
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID, reqBody, token, ctx)

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

	// Verify booking was NOT marked as no_show — past confirmed bookings
	// should cancel cleanly without triggering the <24h no-show penalty.
	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 == "no_show" {
		t.Error("past confirmed booking should NOT be marked as no_show — the startTime.After(clock.Now()) guard should prevent retroactive no-show")
	}
	// Should be client_cancelled (the requested reason).
	if status != "client_cancelled" {
		t.Errorf("expected status 'client_cancelled', got '%s'", status)
	}
}

// =============================================================================
// Auto-Approval Tests (RequestEditHandler)
// =============================================================================

// TestRequestEditHandler_AutoApproves_NoPayments_FarFuture verifies that when a
// user requests an edit on a booking with no payments and >48h until the
// appointment, the edit request is auto-approved without admin intervention.
func TestRequestEditHandler_AutoApproves_NoPayments_FarFuture(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// >48h from now + no payments → triggers auto-approval
	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	token := jwt.GenerateUserToken(userID)

	// Request a notes-only edit
	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": "Please add gel polish",
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

	// Auto-approval returns 200 OK with auto_approved flag
	if w.Code != http.StatusOK {
		t.Fatalf("expected status 200 (auto-approved), got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify auto_approved flag in response
	var respBody map[string]interface{}
	if err := parseResponseBody(w, &respBody); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	autoApproved, ok := respBody["auto_approved"].(bool)
	if !ok || !autoApproved {
		t.Error("expected auto_approved=true in response")
	}

	// Verify no edit request row exists (was auto-approved and deleted)
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected 0 edit request rows (auto-approved), got %d", erCount)
	}

	// Verify no 'edit_requested' notification was created (acknowledged on auto-approve)
	var notifCount int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'edit_requested' AND acknowledged_at IS NULL`,
		bookingID).Scan(&notifCount)
	if err != nil {
		t.Fatalf("failed to query notifications: %v", err)
	}
	if notifCount != 0 {
		t.Errorf("expected 0 unacknowledged edit_requested notifications (auto-approved), got %d", notifCount)
	}

	// Verify notes were applied to the booking
	var dbNotes string
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(notes, '') FROM bookings WHERE id = $1", bookingID).Scan(&dbNotes)
	if err != nil {
		t.Fatalf("failed to query booking notes: %v", err)
	}
	if dbNotes != "Please add gel polish" {
		t.Errorf("expected booking notes to be updated to 'Please add gel polish', got '%s'", dbNotes)
	}
}

// TestRequestEditHandler_AutoApproves_WithTimeChange verifies that auto-approval
// correctly updates the start_time when the edit request includes a time change.
func TestRequestEditHandler_AutoApproves_WithTimeChange(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// >48h from now, no payments → auto-approval
	bookingID, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	token := jwt.GenerateUserToken(userID)

	// Request a time change to 48h from now
	newStartTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 10, 0, 0, 0, newStartTime.Location())

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

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

	// Verify booking start_time was updated
	var updatedStartTime time.Time
	err = tx.QueryRow(ctx,
		"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&updatedStartTime)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !updatedStartTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected start_time %v, got %v", newStartTime, updatedStartTime)
	}

	// Verify no time_blocker reservation remains (cleaned up on auto-approve)
	var blockerCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers: %v", err)
	}
	if blockerCount != 0 {
		t.Errorf("expected 0 time_blocker reservations (cleaned up on auto-approve), got %d", blockerCount)
	}
}

// TestRequestEditHandler_NoAutoApproval_WithPayments verifies that when a
// booking has completed payments, the edit request stays pending for admin
// approval regardless of how far in the future the booking is.
func TestRequestEditHandler_NoAutoApproval_WithPayments(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

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

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

	// Add a payment to the booking
	_, err = tx.Exec(ctx, `
		INSERT INTO payments (id, booking_id, payment_type, payment_method, amount, status, created_at)
		VALUES ($1, $2, 'deposit', 'in_person_card', 25.00, 'completed', NOW())
	`, bookingID[:8]+"pay", bookingID)
	if err != nil {
		t.Fatalf("failed to add payment: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": "Please change the time",
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

	// Should NOT auto-approve (has payments), so expect 201 Created
	if w.Code != http.StatusCreated {
		t.Fatalf("expected status 201 (pending), got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify edit request row exists
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 1 {
		t.Errorf("expected 1 edit request row (pending admin approval), got %d", erCount)
	}
}

// TestRequestEditHandler_NoAutoApproval_Within48h verifies that when a booking
// has no payments but is within 48h of the appointment, the edit request stays
// pending for admin approval.
func TestRequestEditHandler_NoAutoApproval_Within48h(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	// Use a start time within 24h so auto-approval (>=48h) does not fire
	// Also >24h so the time-change block (>=24h for no-payment bookings) does not fire
	bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": "Please change the service",
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

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

	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit requests: %v", err)
	}
	if erCount != 1 {
		t.Errorf("expected 1 edit request row (pending admin approval), got %d", erCount)
	}
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	bookingTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	_, err = tx.Exec(ctx, `
		INSERT INTO booking_discounts (booking_id, user_id, discount_source, discount_percent, original_total, discount_amount)
		VALUES ($1, $2, 'loyalty', 10, 5000, 500)
	`, bookingID, userID)
	if err != nil {
		t.Fatalf("failed to add discount: %v", err)
	}

	token := jwt.GenerateUserToken(userID)
	newStartTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 14, 0, 0, 0, newStartTime.Location())

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

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

	// Edit request should still exist (not auto-approved) since discounts block it.
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit request: %v", err)
	}
	if erCount != 1 {
		t.Errorf("expected 1 edit request row (auto-approve blocked by discounts), got %d", erCount)
	}
}

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

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

	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	bookingTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}
	defer fixtures.DeleteBooking(tx, bookingID)

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

	token := jwt.GenerateUserToken(userID)
	newStartTime := clock.Now().Add(48 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 14, 0, 0, 0, newStartTime.Location())

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

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

	// With no discounts, auto-approve deletes the edit request row.
	var erCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query edit request: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected 0 edit request rows (auto-approved and cleaned up), got %d", erCount)
	}
}

// =============================================================================
// Pagination — nextCursor is only set when there are more pages
// =============================================================================

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

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

	// Disable deposit requirement for this user so bookings auto-confirm.
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Create 5 bookings with staggered created_at values so cursor ordering
	// is deterministic. Direct UPDATE ensures each booking has a unique
	// timestamp — otherwise all 5 batch-inserted rows share the same NOW().
	now := clock.Now().In(time.UTC).Truncate(time.Second)
	bookingIDs := make([]string, 5)
	for i := 0; i < 5; i++ {
		start := now.Add(time.Duration(48+i*24) * time.Hour)
		id, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, start)
		if err != nil {
			t.Fatalf("failed to create booking %d: %v", i, err)
		}
		bookingIDs[i] = id
		defer fixtures.DeleteBooking(tx, id)
		// Stagger created_at so cursor (created_at DESC, id DESC) is predictable.
		_, _ = tx.Exec(ctx,
			"UPDATE bookings SET created_at = $1 WHERE id = $2",
			now.Add(time.Duration(i)*time.Second), id)
	}

	handler := http.HandlerFunc(GetAllUserBookingsHandler)

	// Page 1: fetch 2 items, expect nextCursor
	w1 := makeRequest(handler, "GET", "/api/bookings?per_page=2", nil, token, ctx)
	if w1.Code != http.StatusOK {
		t.Fatalf("page 1 expected 200, got %d: %s", w1.Code, w1.Body.String())
	}
	var page1 BookingListResponse
	if err := json.Unmarshal(w1.Body.Bytes(), &page1); err != nil {
		t.Fatalf("failed to parse page 1: %v", err)
	}
	if len(page1.Bookings) != 2 {
		t.Errorf("page 1 expected 2 bookings, got %d", len(page1.Bookings))
	}
	if page1.NextCursor == nil {
		t.Error("page 1 expected nextCursor (2 more pages remain), got nil")
	}

	// Page 2: use cursor from page 1 (URL-encode to protect '+' in timezone offset).
	page2URL := "/api/bookings?per_page=2&cursor=" + url.QueryEscape(*page1.NextCursor)
	w2 := makeRequest(handler, "GET", page2URL, nil, token, ctx)
	if w2.Code != http.StatusOK {
		t.Fatalf("page 2 expected 200, got %d: %s", w2.Code, w2.Body.String())
	}
	var page2 BookingListResponse
	if err := json.Unmarshal(w2.Body.Bytes(), &page2); err != nil {
		t.Fatalf("failed to parse page 2: %v", err)
	}
	if len(page2.Bookings) != 2 {
		t.Errorf("page 2 expected 2 bookings, got %d", len(page2.Bookings))
	}
	if page2.NextCursor == nil {
		t.Error("page 2 expected nextCursor (1 more page remains), got nil")
	}

	// Page 3: use cursor from page 2 — this is the last page
	page3URL := "/api/bookings?per_page=2&cursor=" + url.QueryEscape(*page2.NextCursor)
	w3 := makeRequest(handler, "GET", page3URL, nil, token, ctx)
	if w3.Code != http.StatusOK {
		t.Fatalf("page 3 expected 200, got %d: %s", w3.Code, w3.Body.String())
	}
	var page3 BookingListResponse
	if err := json.Unmarshal(w3.Body.Bytes(), &page3); err != nil {
		t.Fatalf("failed to parse page 3: %v", err)
	}
	if len(page3.Bookings) != 1 {
		t.Errorf("last page expected 1 booking, got %d", len(page3.Bookings))
	}
	if page3.NextCursor != nil {
		t.Errorf("last page expected nil nextCursor, got %q", *page3.NextCursor)
	}

	// Verify no duplicate bookings across pages
	seen := make(map[string]bool)
	for _, b := range page1.Bookings {
		seen[b.ID] = true
	}
	for _, b := range page2.Bookings {
		if seen[b.ID] {
			t.Errorf("duplicate booking %s on page 2", b.ID)
		}
		seen[b.ID] = true
	}
	for _, b := range page3.Bookings {
		if seen[b.ID] {
			t.Errorf("duplicate booking %s on page 3", b.ID)
		}
		seen[b.ID] = true
	}
	if len(seen) != 5 {
		t.Errorf("expected 5 unique bookings across all pages, got %d", len(seen))
	}
}

// =============================================================================
// Count query accuracy — total field in paginated responses
// =============================================================================

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

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

	// Disable deposit requirement so bookings auto-confirm.
	_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)

	// Create 3 bookings with staggered created_at for deterministic cursor ordering.
	now := clock.Now().In(time.UTC).Truncate(time.Second)
	for i := 0; i < 3; i++ {
		start := now.Add(time.Duration(72+i*24) * time.Hour)
		id, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, start)
		if err != nil {
			t.Fatalf("failed to create booking %d: %v", i, err)
		}
		defer fixtures.DeleteBooking(tx, id)
		// Stagger created_at so cursor-ordering is deterministic.
		_, _ = tx.Exec(ctx,
			"UPDATE bookings SET created_at = $1 WHERE id = $2",
			now.Add(time.Duration(i)*time.Second), id)
	}

	handler := http.HandlerFunc(GetAllUserBookingsHandler)

	// Fetch all bookings with per_page=10 (fits all).
	w := makeRequest(handler, "GET", "/api/bookings?per_page=10", nil, token, ctx)
	if w.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
	}
	var resp BookingListResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp.Total != 3 {
		t.Errorf("expected total 3, got %d", resp.Total)
	}
	if len(resp.Bookings) != 3 {
		t.Errorf("expected 3 bookings, got %d", len(resp.Bookings))
	}
	if resp.NextCursor != nil {
		t.Errorf("expected no nextCursor (all items fit), got %q", *resp.NextCursor)
	}

	// Fetch with per_page=2 — should still report total=3.
	w2 := makeRequest(handler, "GET", "/api/bookings?per_page=2", nil, token, ctx)
	if w2.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w2.Code, w2.Body.String())
	}
	var resp2 BookingListResponse
	if err := json.Unmarshal(w2.Body.Bytes(), &resp2); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp2.Total != 3 {
		t.Errorf("expected total 3 on page 1, got %d", resp2.Total)
	}
	if len(resp2.Bookings) != 2 {
		t.Errorf("expected 2 bookings on page 1, got %d", len(resp2.Bookings))
	}
	if resp2.NextCursor == nil {
		t.Fatal("expected nextCursor on page 1 (more items remain)")
	}

	// Page 2: still total=3 but only 1 booking.
	w3 := makeRequest(handler, "GET", "/api/bookings?per_page=2&cursor="+url.QueryEscape(*resp2.NextCursor), nil, token, ctx)
	if w3.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d: %s", w3.Code, w3.Body.String())
	}
	var resp3 BookingListResponse
	if err := json.Unmarshal(w3.Body.Bytes(), &resp3); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp3.Total != 3 {
		t.Errorf("expected total 3 on page 2, got %d", resp3.Total)
	}
	if len(resp3.Bookings) != 1 {
		t.Errorf("expected 1 booking on page 2, got %d", len(resp3.Bookings))
	}
	if resp3.NextCursor != nil {
		t.Errorf("expected no nextCursor on last page, got %q", *resp3.NextCursor)
	}
}
