//go:build test && dev

package bookings

// Package bookings contains tests for the booking edit request system.
//
// Test Coverage:
//   - RequestEditHandler: POST /api/bookings/{id}/edit-request - Create/upsert edit request
//   - DeleteEditRequestHandler: DELETE /api/bookings/{id}/edit-request  - Delete edit request
//   - GetMyEditRequestHandler: GET /api/bookings/{id}/edit-request  - View own request for booking
//   - GetMyEditRequestsHandler: GET /api/bookings/edit-requests  - List all own requests
//   - AdminListEditRequestsHandler: GET /api/admin/bookings/{id}/edit-requests  - Admin list with pagination
//   - AdminListAllEditRequestsHandler: GET /api/admin/bookings/edit-requests  - Admin list all enriched
//   - AdminGetBookingEditRequestHandler: GET /api/admin/bookings/{id}/edit-request  - Admin get for booking
//   - AdminApproveEditRequestHandler: POST /api/admin/bookings/{id}/edit-requests/{request_id}/approve
//   - AdminRejectEditRequestHandler: POST /api/admin/bookings/{id}/edit-requests/{request_id}/deny
//   - UserCancelBookingHandler: DELETE /api/bookings/{id} - Cleans up edit requests on cancel
//   - Time blocker reservation lifecycle
//   - Working hours validation (exceptional closed hours block on approve)
//   - Enriched response: end-time calculation, cross-user isolation, override handling
//   - Notification upsert on edit request replace (delete + recreate)

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

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

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

// =============================================================================
// Test Helpers
// =============================================================================

// strPtr returns a pointer to the given string.
func strPtr(s string) *string {
	return &s
}

// setupEditRequestTest creates a user, service, confirmed booking, and returns
// their IDs along with an auth token. Also seeds working hours and sets deposits=0.
func setupEditRequestTest(t *testing.T, ctx context.Context, tx db.Querier) (userID, serviceID, bookingID, token string) {
	t.Helper()

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

	_, 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)
	}

	// 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)
	}

	// Confirm the booking (edit requests require confirmed 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)
	return
}

// setupTwoUserEditRequestTest creates two users: one who owns a booking and
// another who doesn't. Returns both user IDs, service ID, booking ID, and tokens.
func setupTwoUserEditRequestTest(t *testing.T, ctx context.Context, tx db.Querier) (ownerID, otherUserID, serviceID, bookingID, ownerToken string) {
	t.Helper()

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

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

	for _, uid := range []string{ownerID, otherUserID} {
		_, err = tx.Exec(ctx, "UPDATE users SET deposits_required = 0 WHERE id = $1", uid)
		if err != nil {
			t.Fatalf("failed to set deposits_required for %s: %v", uid, err)
		}
	}

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

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

	_, 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)
	}

	ownerToken = jwt.GenerateUserToken(ownerID)
	return
}

// createEditRequestDirectly inserts an edit request into the DB and returns its ID.
func createEditRequestDirectly(t *testing.T, ctx context.Context, tx db.Querier, bookingID, userID string, newStartTime *time.Time, newServices []string, notes *string) string {
	t.Helper()
	var editRequestID string
	err := tx.QueryRow(ctx, `
		INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, new_services, notes, has_overrides)
		VALUES ($1, $2, $3, $4, $5, false)
		RETURNING id
	`, bookingID, userID, newStartTime, newServices, notes).Scan(&editRequestID)
	if err != nil {
		t.Fatalf("failed to create edit request directly: %v", err)
	}
	return editRequestID
}

// getEditRequestIDFromDB retrieves the edit request ID for a booking.
func getEditRequestIDFromDB(t *testing.T, ctx context.Context, tx db.Querier, bookingID string) string {
	t.Helper()
	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)
	}
	return editRequestID
}

// createAdminNotification creates an admin notification for an edit request.
func createAdminNotification(t *testing.T, ctx context.Context, tx db.Querier, bookingID, userID string) {
	t.Helper()
	_, 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)
	}
}

// makeAdminEditRequest creates an admin request with chi routing context for
// admin edit request endpoints (approve/deny). This is the most robust pattern.
func makeAdminEditRequest(method, path, routePattern string, body interface{}) *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)
	}

	adminToken := jwt.GenerateAdminToken()
	req.Header.Set("Authorization", "Bearer "+adminToken)

	rctx := chi.NewRouteContext()
	// Parse route pattern like "/api/admin/bookings/{id}/edit-requests/{request_id}/approve"
	patternParts := strings.Split(strings.Trim(routePattern, "/"), "/")
	pathParts := strings.Split(strings.Trim(path, "/"), "/")
	for i, pp := range patternParts {
		if strings.HasPrefix(pp, "{") && strings.HasSuffix(pp, "}") {
			paramName := pp[1 : len(pp)-1]
			if i < len(pathParts) {
				rctx.URLParams.Add(paramName, pathParts[i])
			}
		}
	}

	ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
	ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
	if info := extractUserFromTestJWT(adminToken); info != nil {
		ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
	}
	req = req.WithContext(ctx)

	w := httptest.NewRecorder()
	return w
}

// serveChiHandler sets up a chi router with the given route and serves a request.
// Supports an optional JSON body (pass nil for no body). An optional base context
// can be provided as the last argument to carry a per-test transaction.
// Returns the response recorder.
func serveChiHandler(handler http.HandlerFunc, method, path, routePattern string, body interface{}, setupCtx func(context.Context) context.Context, baseCtx ...context.Context) *httptest.ResponseRecorder {
	r := chi.NewRouter()
	switch method {
	case "GET":
		r.Get(routePattern, handler)
	case "POST":
		r.Post(routePattern, handler)
	case "PUT":
		r.Put(routePattern, handler)
	case "DELETE":
		r.Delete(routePattern, handler)
	}

	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes))
	} else {
		req = httptest.NewRequest(method, path, nil)
	}
	req.Header.Set("Content-Type", "application/json")

	base := req.Context()
	if len(baseCtx) > 0 {
		base = baseCtx[0]
	}
	if setupCtx != nil {
		ctx := setupCtx(base)
		req = req.WithContext(ctx)
	}

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

// serveAdminHandler is a convenience wrapper around serveChiHandler that sets up
// admin authentication context. Useful for testing admin endpoints with chi routing.
func serveAdminHandler(handler http.HandlerFunc, method, path, routePattern string, body interface{}, baseCtx ...context.Context) *httptest.ResponseRecorder {
	return serveChiHandler(handler, method, path, routePattern, body, setupAdminContext, baseCtx...)
}

// setupAdminContext adds admin JWT, admin role, and user ID to context.
func setupAdminContext(ctx context.Context) context.Context {
	adminToken := jwt.GenerateAdminToken()
	ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")
	if info := extractUserFromTestJWT(adminToken); info != nil {
		ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
	}
	return ctx
}

// setupUserContext adds user JWT to context.
func setupUserContext(ctx context.Context, token string) context.Context {
	if info := extractUserFromTestJWT(token); info != nil {
		ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
		ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
	}
	return ctx
}

// =============================================================================
// 1. User Creates Edit Request (RequestEditHandler)
// =============================================================================

// TestRequestEditHandler_TimeChange verifies that a user can request a time
// change for their confirmed booking and a booking_edit_request record is created.
func TestRequestEditHandler_TimeChange(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = token

	notes := "Please add gel polish to my appointment"
	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": notes,
	}
	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())
	}

	var (
		editReq BookingEditRequest
		err     error
	)
	if err = parseResponseBody(w, &editReq); err != nil {
		t.Fatalf("failed to parse edit request response: %v", err)
	}
	if editReq.Notes == nil {
		t.Fatal("expected notes to be set in response")
	}
	if *editReq.Notes != notes {
		t.Errorf("expected notes %q, got %q", notes, *editReq.Notes)
	}
	if editReq.NewStartTime != nil {
		t.Error("expected new_start_time to be nil for notes-only request")
	}

	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_blockers for notes-only request, got %d", blockerCount)
	}

	var dbNotes string
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(notes, '') FROM booking_edit_requests WHERE booking_id = $1", bookingID).Scan(&dbNotes)
	if err != nil {
		t.Fatalf("failed to query edit request: %v", err)
	}
	if dbNotes != notes {
		t.Errorf("expected DB notes %q, got %q", notes, dbNotes)
	}
}

// TestRequestEditHandler_AccessDenied verifies that a user cannot create an edit
// request for a booking they don't own.
func TestRequestEditHandler_AccessDenied(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, otherUserID, _, bookingID, _ := setupTwoUserEditRequestTest(t, ctx, tx)
	otherToken := jwt.GenerateUserToken(otherUserID)

	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"notes": "Trying to edit someone else's booking",
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, otherToken, ctx)

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

	// Verify no 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 != 0 {
		t.Errorf("expected 0 edit requests, got %d", erCount)
	}
}

// TestRequestEditHandler_CompletedBooking verifies that a user cannot request
// an edit for a completed booking.
func TestRequestEditHandler_CompletedBooking(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

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

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

	if w.Code != http.StatusForbidden {
		t.Errorf("expected status 403 for completed booking, got %d. body: %s", w.Code, w.Body.String())
	}

	if !strings.Contains(w.Body.String(), "completed") && !strings.Contains(w.Body.String(), "cancelled") {
		t.Errorf("expected error about completed/cancelled booking, got: %s", w.Body.String())
	}
}

// TestRequestEditHandler_CancelledBooking verifies that a user cannot request
// an edit for a cancelled booking.
func TestRequestEditHandler_CancelledBooking(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

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

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

	if w.Code != http.StatusForbidden {
		t.Errorf("expected status 403 for cancelled booking, got %d. body: %s", w.Code, w.Body.String())
	}

	if !strings.Contains(w.Body.String(), "cancelled") {
		t.Errorf("expected error about cancelled booking, got: %s", w.Body.String())
	}
}

// TestRequestEditHandler_EmptyBody verifies that requesting an edit with no
// changes (empty body) returns 400 Bad Request.
func TestRequestEditHandler_EmptyBody(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

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

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

// TestRequestEditHandler_UpsertBehavior verifies that creating a second edit
// request replaces the first (upsert), so only one row exists in the DB.
func TestRequestEditHandler_UpsertBehavior(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	// Create first edit request
	firstNotes := "First request: change time"
	handler := http.HandlerFunc(RequestEditHandler)
	reqBody1 := map[string]interface{}{
		"notes": firstNotes,
	}
	w1 := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody1, token, ctx)
	if w1.Code != http.StatusCreated {
		t.Fatalf("first edit request failed: %d. body: %s", w1.Code, w1.Body.String())
	}

	// Create second edit request (replaces first)
	secondNotes := "Second request: different notes"
	newStartTime := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	newStartTime = time.Date(newStartTime.Year(), newStartTime.Month(), newStartTime.Day(), 10, 0, 0, 0, newStartTime.Location())
	reqBody2 := map[string]interface{}{
		"notes":          secondNotes,
		"new_start_time": newStartTime.Format(time.RFC3339),
	}
	w2 := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody2, token, ctx)
	if w2.Code != http.StatusCreated {
		t.Fatalf("second edit request failed: %d. body: %s", w2.Code, w2.Body.String())
	}

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

	// Verify the second request's data is what's stored (the old one was replaced)
	var dbNotes string
	var dbNewTime *time.Time
	err = tx.QueryRow(ctx,
		"SELECT notes, new_start_time FROM booking_edit_requests WHERE booking_id = $1 AND requested_by = $2",
		bookingID, userID).Scan(&dbNotes, &dbNewTime)
	if err != nil {
		t.Fatalf("failed to query edit request: %v", err)
	}
	if dbNotes != secondNotes {
		t.Errorf("expected notes %q, got %q", secondNotes, dbNotes)
	}
	if dbNewTime == nil {
		t.Error("expected new_start_time to be set")
	} else if !dbNewTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected new_start_time %v, got %v", newStartTime, *dbNewTime)
	}
}

// TestRequestEditHandler_BookingNotFound verifies that requesting an edit for
// a non-existent booking returns 404.
func TestRequestEditHandler_BookingNotFound(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)
	}

	_, 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": "Some change",
	}
	w := makeRequest(handler, "POST", "/api/bookings/nonexistent-id/edit-request", reqBody, token, ctx)

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

// TestRequestEditHandler_WithServices verifies that a user can request a
// services change (new_services) along with a time change.
func TestRequestEditHandler_WithServices(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

	serviceID2, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create second service: %v", err)
	}
	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),
		"new_services":   []string{serviceID2},
	}
	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())
	}

	var editReq BookingEditRequest
	if err = parseResponseBody(w, &editReq); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if len(editReq.NewServices) != 1 || editReq.NewServices[0] != serviceID2 {
		t.Errorf("expected new_services [%s], got %v", serviceID2, editReq.NewServices)
	}

	// Verify time_blocker duration uses new service duration
	var blockerDuration int
	err = tx.QueryRow(ctx,
		"SELECT duration_minutes FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerDuration)
	if err != nil {
		t.Fatalf("failed to query time_blocker duration: %v", err)
	}
	if blockerDuration < 30 {
		t.Errorf("expected blocker duration >= 30 (service duration), got %d", blockerDuration)
	}
}

// TestRequestEditHandler_ServicesOnBookingWithOverrides verifies that a user
// cannot change services on a booking that has override prices/durations.
func TestRequestEditHandler_ServicesOnBookingWithOverrides(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)

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

	serviceID2, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create second service: %v", err)
	}
	handler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"new_services": []string{serviceID2},
	}
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)

	if w.Code != http.StatusForbidden {
		t.Errorf("expected status 403 for booking with overrides, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// 2. User Deletes Edit Request (DeleteEditRequestHandler)
// =============================================================================

// TestDeleteEditRequestHandler_Success verifies that a user can delete their
// pending edit request and the associated time_blocker + admin notification
// are removed.
func TestDeleteEditRequestHandler_Success(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	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())

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

	var blockerID string
	err := tx.QueryRow(ctx,
		"SELECT id FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerID)
	if err != nil {
		t.Fatalf("expected time_blocker to exist: %v", err)
	}

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

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

	// Verify edit request deleted from DB
	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 requests after delete, got %d", erCount)
	}

	// Verify time_blocker was deleted
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE id = $1", blockerID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected time_blocker to be deleted, but still exists")
	}

	// Verify admin notification was deleted
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM admin_notifications WHERE booking_id = $1 AND reason = 'edit_requested'",
		bookingID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query admin_notifications: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected admin_notification to be deleted, but still exists")
	}
}

// TestDeleteEditRequestHandler_NoEditRequest verifies that deleting a
// non-existent edit request returns 404.
func TestDeleteEditRequestHandler_NoEditRequest(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

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

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

// TestDeleteEditRequestHandler_AccessDenied verifies that one user cannot
// delete another user's edit request.
func TestDeleteEditRequestHandler_AccessDenied(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	ownerID, otherUserID, _, bookingID, ownerToken := setupTwoUserEditRequestTest(t, ctx, tx)
	_ = ownerID

	// Create edit request as owner
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"notes": "Owner's edit"}, ownerToken, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Other user tries to delete
	otherToken := jwt.GenerateUserToken(otherUserID)
	delHandler := http.HandlerFunc(DeleteEditRequestHandler)
	w = makeRequest(delHandler, "DELETE", "/api/bookings/"+bookingID+"/edit-request", nil, otherToken, ctx)

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

	// Verify edit request still 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 to remain, got %d", erCount)
	}
}

// TestDeleteEditRequestHandler_BookingNotFound verifies that deleting an edit
// request for a non-existent booking returns 404.
func TestDeleteEditRequestHandler_BookingNotFound(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

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

	_, 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)

	delHandler := http.HandlerFunc(DeleteEditRequestHandler)
	w := makeRequest(delHandler, "DELETE", "/api/bookings/nonexistent-id/edit-request", nil, token, ctx)

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

// =============================================================================
// 3. User Views Their Edit Request (GetMyEditRequestHandler)
// =============================================================================

// TestGetMyEditRequestHandler_Success verifies that a user can view their
// pending edit request for a specific booking.
func TestGetMyEditRequestHandler_Success(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	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())

	// Create edit request first
	createHandler := http.HandlerFunc(RequestEditHandler)
	reqBody := map[string]interface{}{
		"new_start_time": newStartTime.Format(time.RFC3339),
		"notes":          "View test notes",
	}
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request", reqBody, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Now view it via chi router (to handle URL params)
	viewHandler := http.HandlerFunc(GetMyEditRequestHandler)
	w = serveChiHandler(viewHandler, "GET", "/api/bookings/"+bookingID+"/edit-request",
		"/api/bookings/{id}/edit-request", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(token); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		EditRequest *EnrichedEditRequest `json:"edit_request"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp.EditRequest == nil {
		t.Fatal("expected edit_request in response")
	}
	if resp.EditRequest.BookingID != bookingID {
		t.Errorf("expected booking_id %s, got %s", bookingID, resp.EditRequest.BookingID)
	}
	if resp.EditRequest.Notes == nil || *resp.EditRequest.Notes != "View test notes" {
		t.Errorf("expected notes 'View test notes', got %v", resp.EditRequest.Notes)
	}
	if resp.EditRequest.Original == nil || resp.EditRequest.Proposed == nil {
		t.Error("expected original and proposed snapshots")
	}
	if resp.EditRequest.User == nil {
		t.Error("expected user summary in enriched response")
	}
}

// TestGetMyEditRequestHandler_NotFound verifies that viewing a non-existent
// edit request returns 404.
func TestGetMyEditRequestHandler_NotFound(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

	viewHandler := http.HandlerFunc(GetMyEditRequestHandler)
	w := serveChiHandler(viewHandler, "GET", "/api/bookings/"+bookingID+"/edit-request",
		"/api/bookings/{id}/edit-request", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(token); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

	if w.Code != http.StatusOK {
		t.Errorf("expected status 200 (graceful empty response), got %d. body: %s", w.Code, w.Body.String())
	}
	if !strings.Contains(w.Body.String(), `"edit_request":null`) && !strings.Contains(w.Body.String(), `"edit_request": null`) {
		t.Errorf("expected edit_request to be null in response, got: %s", w.Body.String())
	}
}

// TestGetMyEditRequestHandler_AccessDenied verifies that a user cannot view
// another user's edit request.
func TestGetMyEditRequestHandler_AccessDenied(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	ownerID, otherUserID, _, bookingID, ownerToken := setupTwoUserEditRequestTest(t, ctx, tx)
	_ = ownerID

	// Create edit request as owner
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"notes": "Owner's edit"}, ownerToken, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Other user tries to view
	otherToken := jwt.GenerateUserToken(otherUserID)
	viewHandler := http.HandlerFunc(GetMyEditRequestHandler)
	w = serveChiHandler(viewHandler, "GET", "/api/bookings/"+bookingID+"/edit-request",
		"/api/bookings/{id}/edit-request", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(otherToken); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

// =============================================================================
// 4. User Lists Their Edit Requests (GetMyEditRequestsHandler)
// =============================================================================

// TestGetMyEditRequestsHandler_Success verifies that a user can list all their
// pending edit requests across bookings.
func TestGetMyEditRequestsHandler_Success(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)

	// Create a second booking with edit request (within 48h to avoid auto-approval)
	booking2Time := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
	bookingID2, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, booking2Time)
	if err != nil {
		t.Fatalf("failed to create second booking: %v", err)
	}

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

	// Create edit requests for both bookings
	createHandler := http.HandlerFunc(RequestEditHandler)
	for _, bid := range []string{bookingID, bookingID2} {
		w := makeRequest(createHandler, "POST", "/api/bookings/"+bid+"/edit-request",
			map[string]interface{}{"notes": "Test edit for " + bid}, token, ctx)
		if w.Code != http.StatusCreated {
			t.Fatalf("failed to create edit request for %s: %d", bid, w.Code)
		}
	}

	// List all edit requests
	listHandler := http.HandlerFunc(GetMyEditRequestsHandler)
	w := serveChiHandler(listHandler, "GET", "/api/bookings/edit-requests",
		"/api/bookings/edit-requests", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(token); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		EditRequests []*EnrichedEditRequest `json:"edit_requests"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if len(resp.EditRequests) != 2 {
		t.Errorf("expected 2 edit requests, got %d", len(resp.EditRequests))
	}
}

// TestGetMyEditRequestsHandler_Empty verifies that listing edit requests for a
// user with no requests returns an empty list.
func TestGetMyEditRequestsHandler_Empty(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

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

	listHandler := http.HandlerFunc(GetMyEditRequestsHandler)
	w := serveChiHandler(listHandler, "GET", "/api/bookings/edit-requests",
		"/api/bookings/edit-requests", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(token); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		EditRequests []*EnrichedEditRequest `json:"edit_requests"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp.EditRequests == nil {
		t.Fatal("expected non-nil empty edit_requests list")
	}
	if len(resp.EditRequests) != 0 {
		t.Errorf("expected 0 edit requests, got %d", len(resp.EditRequests))
	}
}

// =============================================================================
// 5. Admin Lists All Edit Requests (AdminListEditRequestsHandler)
// =============================================================================

// TestAdminListEditRequestsHandler_Success verifies that the admin can list
// all edit requests with pagination metadata (requests + total).
func TestAdminListEditRequestsHandler_Success(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

	// Create an edit request
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"notes": "Admin list test"}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Admin lists all
	listHandler := http.HandlerFunc(AdminListEditRequestsHandler)
	adminToken := jwt.GenerateAdminToken()
	w = serveChiHandler(listHandler, "GET", "/api/admin/bookings/edit-requests",
		"/api/admin/bookings/edit-requests", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(adminToken); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		Requests []BookingEditRequest `json:"requests"`
		Total    int                  `json:"total"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp.Total < 1 {
		t.Errorf("expected total >= 1, got %d", resp.Total)
	}
	if len(resp.Requests) < 1 {
		t.Errorf("expected at least 1 request, got %d", len(resp.Requests))
	}
	// Verify joined booking and user info
	if resp.Requests[0].Booking == nil {
		t.Error("expected booking info to be joined")
	}
	if resp.Requests[0].User == nil {
		t.Error("expected user info to be joined")
	}
}

// =============================================================================
// 6. Admin Lists All Enriched Edit Requests (AdminListAllEditRequestsHandler)
// =============================================================================

// TestAdminListAllEditRequestsHandler_Success verifies AdminListAllEditRequestsHandler
// returns all edit requests as enriched objects.
func TestAdminListAllEditRequestsHandler_Success(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

	// Create an edit request
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"notes": "Admin enriched list test"}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Admin lists all
	listHandler := http.HandlerFunc(AdminListAllEditRequestsHandler)
	w = serveAdminHandler(listHandler, "GET", "/api/admin/bookings/edit-requests",
		"/api/admin/bookings/edit-requests", nil, ctx)

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

	var resp struct {
		EditRequests []*EnrichedEditRequest `json:"edit_requests"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if len(resp.EditRequests) < 1 {
		t.Errorf("expected at least 1 edit request, got %d", len(resp.EditRequests))
	}
	if resp.EditRequests[0].Original == nil || resp.EditRequests[0].Proposed == nil {
		t.Error("expected enriched edit request with original and proposed snapshots")
	}
}

// =============================================================================
// 7. Admin Gets Edit Request for Booking (AdminGetBookingEditRequestHandler)
// =============================================================================

// TestAdminGetBookingEditRequestHandler_Success verifies the admin can view
// the pending edit request for a specific booking.
func TestAdminGetBookingEditRequestHandler_Success(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID
	_ = 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())

	// Create edit request
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{
			"new_start_time": newStartTime.Format(time.RFC3339),
			"notes":          "Admin view test",
		}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Admin views it
	viewHandler := http.HandlerFunc(AdminGetBookingEditRequestHandler)
	w = serveAdminHandler(viewHandler, "GET", "/api/admin/bookings/"+bookingID+"/edit-request",
		"/api/admin/bookings/{id}/edit-request", nil, ctx)

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

	var resp struct {
		EditRequest *EnrichedEditRequest `json:"edit_request"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp.EditRequest == nil {
		t.Fatal("expected edit_request in response")
	}
	if resp.EditRequest.BookingID != bookingID {
		t.Errorf("expected booking_id %s, got %s", bookingID, resp.EditRequest.BookingID)
	}
}

// TestAdminGetBookingEditRequestHandler_NotFound verifies the admin gets 404
// when there is no edit request for the given booking.
func TestAdminGetBookingEditRequestHandler_NotFound(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, _ := setupEditRequestTest(t, ctx, tx)

	viewHandler := http.HandlerFunc(AdminGetBookingEditRequestHandler)
	w := serveAdminHandler(viewHandler, "GET", "/api/admin/bookings/"+bookingID+"/edit-request",
		"/api/admin/bookings/{id}/edit-request", nil, ctx)

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

// =============================================================================
// 8. Admin Approves Edit Request (AdminApproveEditRequestHandler)
// =============================================================================

// TestAdminApproveEditRequestHandler_TimeChange verifies that approving an
// edit request with a new start time updates the booking and cleans up
// the edit request + time_blocker.
func TestAdminApproveEditRequestHandler_TimeChange(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	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())

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

	// Get edit request ID
	editRequestID := getEditRequestIDFromDB(t, ctx, tx, bookingID)

	// Admin approves
	approveHandler := http.HandlerFunc(AdminApproveEditRequestHandler)
	w = serveAdminHandler(approveHandler, "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

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

	// Verify booking start_time was updated
	var dbStartTime time.Time
	err := tx.QueryRow(ctx,
		"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !dbStartTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected booking start_time %v, got %v", newStartTime, dbStartTime)
	}

	// 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 edit request to be deleted, got count %d", erCount)
	}

	// Verify time_blocker was deleted
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected time_blocker to be deleted, got count %d", erCount)
	}

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

// TestAdminApproveEditRequestHandler_WithServices verifies that approving an
// edit request with new_services replaces the booking's services.
func TestAdminApproveEditRequestHandler_WithServices(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)

	serviceID2, err := fixtures.CreateTestService(tx)
	if err != nil {
		t.Fatalf("failed to create second service: %v", err)
	}
	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())

	editRequestID := createEditRequestDirectly(t, ctx, tx, bookingID, userID, &newStartTime, []string{serviceID2}, nil)
	_ = serviceID
	_ = token

	var dbReqID string
	err = tx.QueryRow(ctx,
		"SELECT id FROM booking_edit_requests WHERE id = $1", editRequestID).Scan(&dbReqID)
	if err != nil {
		t.Fatalf("edit request should exist: %v", err)
	}

	approveHandler := http.HandlerFunc(AdminApproveEditRequestHandler)
	w := serveAdminHandler(approveHandler, "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

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

	var serviceCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM booking_services WHERE booking_id = $1", bookingID).Scan(&serviceCount)
	if err != nil {
		t.Fatalf("failed to query booking_services: %v", err)
	}
	if serviceCount != 1 {
		t.Errorf("expected 1 booking_service after approval, got %d", serviceCount)
	}

	var actualServiceID string
	err = tx.QueryRow(ctx,
		"SELECT service_id FROM booking_services WHERE booking_id = $1", bookingID).Scan(&actualServiceID)
	if err != nil {
		t.Fatalf("failed to get booking service: %v", err)
	}
	if actualServiceID != serviceID2 {
		t.Errorf("expected service_id %s, got %s", serviceID2, actualServiceID)
	}

	var dbStartTime time.Time
	err = tx.QueryRow(ctx,
		"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !dbStartTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected booking start_time %v, got %v", newStartTime, dbStartTime)
	}
}

// TestAdminApproveEditRequestHandler_NotFound verifies that approving a
// non-existent edit request returns 404.
func TestAdminApproveEditRequestHandler_NotFound(t *testing.T) {
	ctx, _ := testutils.SetupTestTx(t)

	approveHandler := http.HandlerFunc(AdminApproveEditRequestHandler)
	w := serveAdminHandler(approveHandler, "POST",
		"/api/admin/bookings/fake-booking-id/edit-requests/nonexistent-request-id/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

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

	if !strings.Contains(w.Body.String(), "Edit request not found") {
		t.Errorf("expected 'Edit request not found' error, got: %s", w.Body.String())
	}
}

// =============================================================================
// 9. Admin Rejects Edit Request (AdminRejectEditRequestHandler)
// =============================================================================

// TestAdminRejectEditRequestHandler_Success verifies that rejecting an edit
// request removes it, cleans up the time_blocker, and acknowledges the
// admin notification.
func TestAdminRejectEditRequestHandler_Success(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	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())

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

	editRequestID := getEditRequestIDFromDB(t, ctx, tx, bookingID)

	// Verify time_blocker exists
	var blockerID string
	err := tx.QueryRow(ctx,
		"SELECT id FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID)).Scan(&blockerID)
	if err != nil {
		t.Fatalf("expected time_blocker to exist before reject: %v", err)
	}

	// Admin rejects
	rejectHandler := http.HandlerFunc(AdminRejectEditRequestHandler)
	w = serveAdminHandler(rejectHandler, "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/deny",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/deny", nil, ctx)

	if w.Code != http.StatusNoContent {
		t.Fatalf("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 edit request to be deleted, got count %d", erCount)
	}

	// Verify time_blocker was deleted
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE id = $1", blockerID).Scan(&erCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected time_blocker to be deleted, got count %d", erCount)
	}

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

	// Verify booking start_time was NOT changed
	var dbStartTime time.Time
	var originalStartTime time.Time
	// Get original start time from before
	_ = tx.QueryRow(ctx,
		"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&originalStartTime)

	err = tx.QueryRow(ctx,
		"SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&dbStartTime)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if !dbStartTime.Equal(originalStartTime) {
		t.Errorf("expected booking start_time to remain unchanged, got %v", dbStartTime)
	}
}

// TestAdminRejectEditRequestHandler_NotFound verifies that rejecting a
// non-existent edit request returns 404.
func TestAdminRejectEditRequestHandler_NotFound(t *testing.T) {
	ctx, _ := testutils.SetupTestTx(t)

	rejectHandler := http.HandlerFunc(AdminRejectEditRequestHandler)
	w := serveAdminHandler(rejectHandler, "POST",
		"/api/admin/bookings/fake-booking-id/edit-requests/nonexistent-request-id/deny",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/deny", nil, ctx)

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

	if !strings.Contains(w.Body.String(), "Edit request not found") {
		t.Errorf("expected 'Edit request not found' error, got: %s", w.Body.String())
	}
}

// =============================================================================
// 10. Time-Blocker Lifecycle
// =============================================================================

// TestRequestEditHandler_TimeBlockerCreated verifies that creating an edit
// request with a time change creates a RESERVATION:edit_request time_blocker.
func TestRequestEditHandler_TimeBlockerCreated(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

	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())

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

	// Verify time_blocker exists with correct description
	var description string
	var blockerStartTime time.Time
	var durationMinutes int
	err := tx.QueryRow(ctx,
		"SELECT description, start_time, duration_minutes FROM time_blockers WHERE description = $1",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID),
	).Scan(&description, &blockerStartTime, &durationMinutes)
	if err != nil {
		t.Fatalf("expected time_blocker to exist: %v", err)
	}

	if !blockerStartTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected blocker start_time %v, got %v", newStartTime, blockerStartTime)
	}
	if durationMinutes <= 0 {
		t.Errorf("expected positive duration_minutes, got %d", durationMinutes)
	}
}

// TestRequestEditHandler_TimeBlockerNotCreatedForNotesOnly verifies that a
// notes-only edit request does NOT create a time_blocker.
func TestRequestEditHandler_TimeBlockerNotCreatedForNotesOnly(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"notes": "Just notes, no time change"}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

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

// TestRequestEditHandler_TimeBlockerReplacedOnUpsert verifies that when an
// edit request is replaced (upsert) with a different time, the old
// time_blocker is deleted and a new one is created.
func TestRequestEditHandler_TimeBlockerReplacedOnUpsert(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

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

	time2 := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	time2 = time.Date(time2.Year(), time2.Month(), time2.Day(), 15, 0, 0, 0, time2.Location())

	handler := http.HandlerFunc(RequestEditHandler)

	// Create first edit request
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": time1.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("first edit request failed: %d", w.Code)
	}

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

	// Replace with second time
	w = makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": time2.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("second edit request failed: %d", w.Code)
	}

	// Verify old time_blocker is gone
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1 AND start_time = $2",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID), time1).Scan(&count)
	if err != nil {
		t.Fatalf("failed to query old time_blockers: %v", err)
	}
	if count != 0 {
		t.Errorf("expected old time_blocker to be deleted, got count %d", count)
	}

	// Verify new time_blocker exists for time2
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = $1 AND start_time = $2",
		fmt.Sprintf("RESERVATION:edit_request:%s", bookingID), time2).Scan(&count)
	if err != nil {
		t.Fatalf("failed to query new time_blockers: %v", err)
	}
	if count != 1 {
		t.Errorf("expected 1 time_blocker for time2, got %d", count)
	}
}

// =============================================================================
// 11. Working Hours Validation on Approve
// =============================================================================

// TestAdminApproveEditRequestHandler_WithNotesOnly verifies that approving a
// notes-only edit request updates the booking notes.
func TestAdminApproveEditRequestHandler_WithNotesOnly(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID
	_ = userID

	notes := "Updated notes from edit request"

	// Create notes-only edit request
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"notes": notes}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	editRequestID := getEditRequestIDFromDB(t, ctx, tx, bookingID)

	// Admin approves
	approveHandler := http.HandlerFunc(AdminApproveEditRequestHandler)
	w = serveAdminHandler(approveHandler, "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

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

	// Verify booking notes were updated
	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: %v", err)
	}
	if dbNotes != notes {
		t.Errorf("expected booking notes %q, got %q", notes, dbNotes)
	}
}

// TestAdminApproveEditRequestHandler_OverlapWithBooking verifies that approving
// an edit request that would cause a time overlap returns 409.
func TestAdminApproveEditRequestHandler_OverlapWithBooking(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

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

	// Get service duration for overlap calculation
	var serviceDuration int
	err = tx.QueryRow(ctx,
		"SELECT duration_minutes FROM services WHERE id = $1", serviceID).Scan(&serviceDuration)
	if err != nil {
		t.Fatalf("failed to get service duration: %v", err)
	}

	token := jwt.GenerateUserToken(userID)

	// Create first booking at time T
	// Use a start time <48h away so auto-approval doesn't trigger at request-
	// creation time, allowing us to test the approval-time overlap check.
	baseTime := clock.Now().Add(40 * time.Hour).Truncate(time.Second)
	baseTime = time.Date(baseTime.Year(), baseTime.Month(), baseTime.Day(), 9, 0, 0, 0, baseTime.Location())

	booking1, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking1: %v", err)
	}
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET start_time = $1, status = 'confirmed' WHERE id = $2", baseTime, booking1)
	if err != nil {
		t.Fatalf("failed to update booking1: %v", err)
	}

	// Create second booking that overlaps with first
	booking2, err := fixtures.CreateTestBooking(tx, userID, serviceID)
	if err != nil {
		t.Fatalf("failed to create booking2: %v", err)
	}

	// Set booking2 start time during booking1's slot (overlap)
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET start_time = $1, status = 'confirmed' WHERE id = $2",
		baseTime.Add(time.Duration(serviceDuration/2)*time.Minute), booking2)
	if err != nil {
		t.Fatalf("failed to update booking2: %v", err)
	}

	// Create an edit request for booking2 proposing a move to a time that
	// overlaps booking1. The request is <48h away so auto-approval skips the
	// creation-time overlap check; the request is stored as pending.
	handler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(handler, "POST", "/api/bookings/"+booking2+"/edit-request",
		map[string]interface{}{
			"new_start_time": baseTime.Format(time.RFC3339), // move to time that overlaps booking1
		}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request on booking2: %d. body: %s", w.Code, w.Body.String())
	}

	editRequestID := getEditRequestIDFromDB(t, ctx, tx, booking2)

	// Admin tries to approve — should get overlap conflict
	approveHandler := http.HandlerFunc(AdminApproveEditRequestHandler)
	w = serveAdminHandler(approveHandler, "POST",
		"/api/admin/bookings/"+booking2+"/edit-requests/"+editRequestID+"/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

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

// TestAdminApproveEditRequestHandler_TimeBlockerDeletedOnApprove verifies
// that approving an edit request removes the associated time_blocker.
func TestAdminApproveEditRequestHandler_TimeBlockerDeletedOnApprove(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID
	_ = 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)
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": newStartTime.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	editRequestID := getEditRequestIDFromDB(t, ctx, tx, bookingID)

	approveHandler := http.HandlerFunc(AdminApproveEditRequestHandler)
	w = serveAdminHandler(approveHandler, "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

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

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

// TestAdminRejectEditRequestHandler_TimeBlockerDeletedOnReject verifies
// that rejecting an edit request removes the associated time_blocker.
func TestAdminRejectEditRequestHandler_TimeBlockerDeletedOnReject(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	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)
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": newStartTime.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	editRequestID := getEditRequestIDFromDB(t, ctx, tx, bookingID)

	rejectHandler := http.HandlerFunc(AdminRejectEditRequestHandler)
	w = serveAdminHandler(rejectHandler, "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/deny",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/deny", nil, ctx)

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

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

// =============================================================================
// 12. UserCancelBookingHandler cleans up edit requests
// =============================================================================

// TestUserCancelBookingHandler_CleansUpEditRequest verifies that when a user
// cancels a booking with a pending edit request, the edit request, associated
// time_blocker, and admin_notification are all deleted.
func TestUserCancelBookingHandler_CleansUpEditRequest(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	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())

	// Create edit request (creates time_blocker + admin_notification)
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": newStartTime.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Verify edit request 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.Fatalf("expected 1 edit request before cancel, got %d", erCount)
	}

	// Verify time_blocker exists
	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.Fatalf("expected 1 time_blocker before cancel, got %d", blockerCount)
	}

	// Verify admin_notification exists
	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 admin_notifications: %v", err)
	}
	if notifCount != 1 {
		t.Fatalf("expected 1 admin_notification before cancel, got %d", notifCount)
	}

	// Cancel the booking
	cancelHandler := http.HandlerFunc(UserCancelBookingHandler)
	w = makeRequest(cancelHandler, "DELETE", "/api/bookings/"+bookingID, nil, token, ctx)
	if w.Code != http.StatusNoContent {
		t.Fatalf("expected 204 on cancel, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify edit request was deleted
	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 after cancel: %v", err)
	}
	if erCount != 0 {
		t.Errorf("expected 0 edit requests after cancel, got %d", erCount)
	}

	// Verify time_blocker was deleted
	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 after cancel: %v", err)
	}
	if blockerCount != 0 {
		t.Errorf("expected 0 time_blockers after cancel, got %d", blockerCount)
	}

	// Verify admin_notification was deleted
	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 admin_notifications after cancel: %v", err)
	}
	if notifCount != 0 {
		t.Errorf("expected 0 admin_notifications after cancel, got %d", notifCount)
	}
}

// =============================================================================
// 13. Working Hours Validation — Exceptional Closed Hours Block on Approve
// =============================================================================

// TestAdminApproveEditRequestHandler_BlockedByExceptionalClosedHours verifies
// that approving an edit request whose proposed time falls during exceptional
// closed hours returns 409 Conflict.
func TestAdminApproveEditRequestHandler_BlockedByExceptionalClosedHours(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	// Create an exceptional closed hours group for a fixed date (Thursday)
	targetDate := time.Date(2026, 2, 26, 0, 0, 0, 0, time.UTC) // Thursday Feb 26, 2026
	var groupID int
	err := tx.QueryRow(ctx, `
		INSERT INTO exceptional_working_hours_groups (name, description)
		VALUES ($1, $2)
		RETURNING id
	`, "Holiday Closure", "Test holiday").Scan(&groupID)
	if err != nil {
		t.Fatalf("failed to create holiday group: %v", err)
	}
	defer tx.Exec(ctx, "DELETE FROM exceptional_working_hours_groups WHERE id = $1", groupID)

	// Add closed hours for targetDate (closed all day)
	dbWeekday := (int(targetDate.Weekday()) + 6) % 7
	_, err = tx.Exec(ctx, `
		INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
		VALUES ($1, $2, $3, $4, $5)
	`, groupID, dbWeekday, "00:00:00", "23:59:59", false)
	if err != nil {
		t.Fatalf("failed to create holiday hours: %v", err)
	}

	// Apply the group to the week containing targetDate
	daysToMonday := int(targetDate.Weekday())
	if daysToMonday == 0 {
		daysToMonday = 7
	}
	mondayOfWeek := targetDate.AddDate(0, 0, -daysToMonday+1)
	_, err = tx.Exec(ctx, `
		INSERT INTO exceptional_group_applications (group_id, week_start)
		VALUES ($1, $2)
	`, groupID, mondayOfWeek)
	if err != nil {
		t.Fatalf("failed to create holiday application: %v", err)
	}

	// Create edit request for a time during the closed period
	targetTime := targetDate.Add(14 * time.Hour).Truncate(time.Second) // 2 PM on targetDate
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": targetTime.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	editRequestID := getEditRequestIDFromDB(t, ctx, tx, bookingID)

	// Admin tries to approve — should be blocked
	approveHandler := http.HandlerFunc(AdminApproveEditRequestHandler)
	w = serveAdminHandler(approveHandler, "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

	if w.Code != http.StatusConflict {
		t.Fatalf("expected 409 Conflict for closed hours, got %d. body: %s", w.Code, w.Body.String())
	}

	if !strings.Contains(w.Body.String(), "closed") {
		t.Errorf("expected error about closed hours, got: %s", w.Body.String())
	}

	// Verify edit request still exists (not consumed by failed approve)
	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 != 1 {
		t.Errorf("expected edit request to still exist after failed approve, got count %d", erCount)
	}
}

// =============================================================================
// 14. Enriched Response — End-Time Calculation Correctness
// =============================================================================

// TestGetMyEditRequestHandler_EndTimeCalculation verifies that the enriched
// edit request response correctly calculates end_time from start_time +
// total service duration for both original and proposed snapshots.
func TestGetMyEditRequestHandler_EndTimeCalculation(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)

	// Get service duration
	var serviceDuration int
	err := tx.QueryRow(ctx,
		"SELECT duration_minutes FROM services WHERE id = $1", serviceID).Scan(&serviceDuration)
	if err != nil {
		t.Fatalf("failed to get service duration: %v", err)
	}

	// Create edit request with time change
	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())

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

	// View it
	viewHandler := http.HandlerFunc(GetMyEditRequestHandler)
	w = serveChiHandler(viewHandler, "GET", "/api/bookings/"+bookingID+"/edit-request",
		"/api/bookings/{id}/edit-request", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(token); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		EditRequest *EnrichedEditRequest `json:"edit_request"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	er := resp.EditRequest
	if er == nil || er.Original == nil || er.Proposed == nil {
		t.Fatal("expected enriched response with original and proposed snapshots")
	}

	// Original end_time should be original start_time + service duration
	origEndExpected := er.Original.StartTime.Add(time.Duration(serviceDuration) * time.Minute)
	if !er.Original.EndTime.Truncate(time.Second).Equal(origEndExpected.Truncate(time.Second)) {
		t.Errorf("original end_time: expected %v, got %v", origEndExpected, er.Original.EndTime)
	}

	// Proposed end_time should be proposed start_time + service duration
	propEndExpected := er.Proposed.StartTime.Add(time.Duration(serviceDuration) * time.Minute)
	if !er.Proposed.EndTime.Truncate(time.Second).Equal(propEndExpected.Truncate(time.Second)) {
		t.Errorf("proposed end_time: expected %v, got %v", propEndExpected, er.Proposed.EndTime)
	}
}

// =============================================================================
// 15. Cross-User Isolation for GetMyEditRequestsHandler
// =============================================================================

// TestGetMyEditRequestsHandler_CrossUserIsolation verifies that user A's
// edit requests do not appear in user B's list.
func TestGetMyEditRequestsHandler_CrossUserIsolation(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	ownerID, otherUserID, _, bookingID, ownerToken := setupTwoUserEditRequestTest(t, ctx, tx)
	_ = ownerID

	// Create edit request as owner
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"notes": "Owner's edit"}, ownerToken, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Other user lists their edit requests — should be empty
	otherToken := jwt.GenerateUserToken(otherUserID)
	listHandler := http.HandlerFunc(GetMyEditRequestsHandler)
	w = serveChiHandler(listHandler, "GET", "/api/bookings/edit-requests",
		"/api/bookings/edit-requests", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(otherToken); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		EditRequests []*EnrichedEditRequest `json:"edit_requests"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	if len(resp.EditRequests) != 0 {
		t.Errorf("expected 0 edit requests for other user, got %d", len(resp.EditRequests))
	}

	// Owner lists their edit requests — should see 1
	w = serveChiHandler(listHandler, "GET", "/api/bookings/edit-requests",
		"/api/bookings/edit-requests", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(ownerToken); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

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

	if len(resp.EditRequests) != 1 {
		t.Errorf("expected 1 edit request for owner, got %d", len(resp.EditRequests))
	}
}

// =============================================================================
// 16. AdminGetBookingEditRequestHandler returns enriched data
// =============================================================================

// TestAdminGetBookingEditRequestHandler_EnrichedData verifies the admin view
// returns full enriched data with original/proposed snapshots and user summary.
func TestAdminGetBookingEditRequestHandler_EnrichedData(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)
	_ = serviceID

	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())

	// Create edit request
	createHandler := http.HandlerFunc(RequestEditHandler)
	w := makeRequest(createHandler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{
			"new_start_time": newStartTime.Format(time.RFC3339),
			"notes":          "Admin enriched test",
		}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("failed to create edit request: %d", w.Code)
	}

	// Admin views it
	viewHandler := http.HandlerFunc(AdminGetBookingEditRequestHandler)
	w = serveAdminHandler(viewHandler, "GET", "/api/admin/bookings/"+bookingID+"/edit-request",
		"/api/admin/bookings/{id}/edit-request", nil, ctx)

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

	var resp struct {
		EditRequest *EnrichedEditRequest `json:"edit_request"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	er := resp.EditRequest
	if er == nil {
		t.Fatal("expected edit_request in response")
	}
	if er.BookingID != bookingID {
		t.Errorf("expected booking_id %s, got %s", bookingID, er.BookingID)
	}
	if er.Original == nil {
		t.Error("expected original snapshot to be populated")
	}
	if er.Proposed == nil {
		t.Error("expected proposed snapshot to be populated")
	}
	if er.User == nil {
		t.Error("expected user summary to be populated")
	}
	if er.User.FullName == "" {
		t.Error("expected user full_name to be non-empty")
	}
	if er.Notes == nil || *er.Notes != "Admin enriched test" {
		t.Errorf("expected notes 'Admin enriched test', got %v", er.Notes)
	}
	// Verify proposed start_time matches the requested time
	if er.Proposed.StartTime == nil {
		t.Fatal("expected proposed start_time to be set")
	}
	if !er.Proposed.StartTime.Truncate(time.Second).Equal(newStartTime) {
		t.Errorf("expected proposed start_time %v, got %v", newStartTime, er.Proposed.StartTime)
	}
}

// =============================================================================
// 17. buildEnrichedEditRequest — has_overrides=true uses original services
// =============================================================================

// TestGetMyEditRequestHandler_WithOverrides verifies that when a booking has
// override prices/durations, the enriched response uses original services for
// both original and proposed snapshots (has_overrides branch).
func TestGetMyEditRequestHandler_WithOverrides(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)

	// Add an override to the booking service
	_, err := tx.Exec(ctx,
		"UPDATE booking_services SET override_price = 75.00, override_duration_minutes = 90 WHERE booking_id = $1 AND service_id = $2",
		bookingID, serviceID)
	if err != nil {
		t.Fatalf("failed to set override: %v", err)
	}

	// Create edit request (time change only — services change blocked for overrides,
	// but time change is allowed)
	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())

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

	// View it
	viewHandler := http.HandlerFunc(GetMyEditRequestHandler)
	w = serveChiHandler(viewHandler, "GET", "/api/bookings/"+bookingID+"/edit-request",
		"/api/bookings/{id}/edit-request", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(token); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		EditRequest *EnrichedEditRequest `json:"edit_request"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	er := resp.EditRequest
	if er == nil || er.Original == nil || er.Proposed == nil {
		t.Fatal("expected enriched response")
	}

	// Both original and proposed should have services (from booking_services with overrides)
	if len(er.Original.Services) == 0 {
		t.Error("expected original services to be populated")
	}
	if len(er.Proposed.Services) == 0 {
		t.Error("expected proposed services to be populated")
	}

	// The override price should be reflected (75.00 instead of default)
	if er.Original.Services[0].Price != 75.00 {
		t.Errorf("expected original service price 75.00 (override), got %v", er.Original.Services[0].Price)
	}
	if er.Original.Services[0].DurationMinutes != 90 {
		t.Errorf("expected original service duration 90 (override), got %d", er.Original.Services[0].DurationMinutes)
	}
}

// =============================================================================
// 18. AdminListEditRequestsHandler pagination params
// =============================================================================

// TestAdminListEditRequestsHandler_Pagination verifies that the admin list
// endpoint returns correct total count for pagination metadata.
func TestAdminListEditRequestsHandler_Pagination(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, serviceID, bookingID, token := setupEditRequestTest(t, ctx, tx)

	// Create 4 additional bookings with edit requests (within 48h to avoid auto-approval)
	bookingIDs := []string{bookingID}
	for i := 0; i < 4; i++ {
		bookingTime := clock.Now().Add(36 * time.Hour).Truncate(time.Second)
		newBookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, bookingTime)
		if err != nil {
			t.Fatalf("failed to create booking %d: %v", i, err)
		}
		_, err = tx.Exec(ctx, "UPDATE bookings SET status = $1 WHERE id = $2", "confirmed", newBookingID)
		if err != nil {
			t.Fatalf("failed to confirm booking %d: %v", i, err)
		}
		bookingIDs = append(bookingIDs, newBookingID)
	}

	// Create 1 edit request per booking (5 total)
	createHandler := http.HandlerFunc(RequestEditHandler)
	for i, bid := range bookingIDs {
		w := makeRequest(createHandler, "POST", "/api/bookings/"+bid+"/edit-request",
			map[string]interface{}{"notes": fmt.Sprintf("Edit %d", i)}, token, ctx)
		if w.Code != http.StatusCreated {
			t.Fatalf("failed to create edit request %d: %d", i, w.Code)
		}
	}

	// Admin lists all — verifies total count is correct for frontend pagination
	listHandler := http.HandlerFunc(AdminListEditRequestsHandler)
	adminToken := jwt.GenerateAdminToken()
	w := serveChiHandler(listHandler, "GET", "/api/admin/bookings/"+bookingID+"/edit-requests",
		"/api/admin/bookings/{id}/edit-requests", nil, func(ctx context.Context) context.Context {
			if info := extractUserFromTestJWT(adminToken); info != nil {
				ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
				ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			}
			return ctx
		}, ctx)

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

	var resp struct {
		Requests []BookingEditRequest `json:"requests"`
		Total    int                  `json:"total"`
	}
	if err := parseResponseBody(w, &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

	// Total should reflect all 5 edit requests across bookings
	if resp.Total != 5 {
		t.Errorf("expected total 5, got %d", resp.Total)
	}
	if len(resp.Requests) != 5 {
		t.Errorf("expected 5 requests, got %d", len(resp.Requests))
	}
}

// =============================================================================
// 19. Notification Upsert on Edit Request Replace
// =============================================================================

// TestRequestEditHandler_NotificationUpsertOnReplace verifies that when a user
// submits a second edit request (upsert), the old edit_requested notification
// is deleted and a fresh one is created.
func TestRequestEditHandler_NotificationUpsertOnReplace(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	_, _, bookingID, token := setupEditRequestTest(t, ctx, tx)

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

	time2 := clock.Now().Add(72 * time.Hour).Truncate(time.Second)
	time2 = time.Date(time2.Year(), time2.Month(), time2.Day(), 15, 0, 0, 0, time2.Location())

	handler := http.HandlerFunc(RequestEditHandler)

	// Create first edit request
	w := makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": time1.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("first edit request failed: %d", w.Code)
	}

	// Capture first notification's created_at
	var firstCreatedAt time.Time
	err := tx.QueryRow(ctx,
		"SELECT created_at FROM admin_notifications WHERE booking_id = $1 AND reason = 'edit_requested'",
		bookingID).Scan(&firstCreatedAt)
	if err != nil {
		t.Fatalf("expected first notification to exist: %v", err)
	}

	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 count notifications: %v", err)
	}
	if notifCount != 1 {
		t.Fatalf("expected 1 notification after first request, got %d", notifCount)
	}

	// Wait a moment so timestamps differ
	time.Sleep(100 * time.Millisecond)

	// Create second edit request (upsert)
	w = makeRequest(handler, "POST", "/api/bookings/"+bookingID+"/edit-request",
		map[string]interface{}{"new_start_time": time2.Format(time.RFC3339)}, token, ctx)
	if w.Code != http.StatusCreated {
		t.Fatalf("second edit request failed: %d", w.Code)
	}

	// Verify only 1 notification exists (old deleted, new created)
	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 count notifications after upsert: %v", err)
	}
	if notifCount != 1 {
		t.Errorf("expected 1 notification after upsert, got %d", notifCount)
	}

	// Verify the notification has a fresh created_at (newer than original)
	var secondCreatedAt time.Time
	err = tx.QueryRow(ctx,
		"SELECT created_at FROM admin_notifications WHERE booking_id = $1 AND reason = 'edit_requested'",
		bookingID).Scan(&secondCreatedAt)
	if err != nil {
		t.Fatalf("expected notification to exist after upsert: %v", err)
	}
}

// TestAdminApproveEditRequest_ClosedExceptionalHours_Rejected verifies that admin cannot approve an edit request
// that lands in a closed period due to exceptional working hours.
func TestAdminApproveEditRequest_ClosedExceptionalHours_Rejected(t *testing.T) {
	ctx, tx := testutils.SetupTestTx(t)

	userID, _, bookingID, _ := setupEditRequestTest(t, ctx, tx)

	// Create exceptional holiday group for a date
	targetDate := time.Date(2026, 2, 26, 0, 0, 0, 0, time.UTC)
	var groupID int
	err := tx.QueryRow(ctx, `
		INSERT INTO exceptional_working_hours_groups (name, description)
		VALUES ('Holiday', 'Closed')
		RETURNING id
	`).Scan(&groupID)
	if err != nil {
		t.Fatalf("failed to create group: %v", err)
	}

	dbWeekday := (int(targetDate.Weekday()) + 6) % 7
	_, err = tx.Exec(ctx, `
		INSERT INTO exceptional_working_hours (group_id, weekday, start_time, end_time, is_open)
		VALUES ($1, $2, $3, $4, $5)
	`, groupID, dbWeekday, "00:00:00", "23:59:59", false)
	if err != nil {
		t.Fatalf("failed to create holiday hours: %v", err)
	}

	daysToMonday := int(targetDate.Weekday())
	if daysToMonday == 0 {
		daysToMonday = 7
	}
	mondayOfWeek := targetDate.AddDate(0, 0, -daysToMonday+1)
	_, err = tx.Exec(ctx, `
		INSERT INTO exceptional_group_applications (group_id, week_start)
		VALUES ($1, $2)
	`, groupID, mondayOfWeek)
	if err != nil {
		t.Fatalf("failed to create holiday application: %v", err)
	}

	// Create edit request for that date
	newTime := targetDate.Add(14 * time.Hour).Truncate(time.Minute)
	editRequestID := createEditRequestDirectly(t, ctx, tx, bookingID, userID, &newTime, nil, nil)

	// Admin approves
	w := serveAdminHandler(http.HandlerFunc(AdminApproveEditRequestHandler), "POST",
		"/api/admin/bookings/"+bookingID+"/edit-requests/"+editRequestID+"/approve",
		"/api/admin/bookings/{id}/edit-requests/{request_id}/approve", nil, ctx)

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