//go:build test && dev

package bookings

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

	"crussell/clock"
	"crussell/db"
	"crussell/handlers/payments"
	"crussell/mw"
	"crussell/testutils"
	"crussell/testutils/fixtures"
	"crussell/testutils/jwt"
)

func boolPtr(b bool) *bool {
	return &b
}

// =============================================================================
// populateDepositFields - DepositProtectedAmount
// =============================================================================

func TestPopulateDepositFields_ProtectedAmountCappedAt50Pct(t *testing.T) {

	b := &Booking{
		TotalAmount: 200,
		StartTime:   time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC),
	}

	// Paid 150 on a 200 total — protected deposit = min(150, 100) = 100
	populateDepositFields(b, false, 150)

	if b.DepositProtectedAmount != 100 {
		t.Errorf("expected protected deposit 100 (50%% of 200), got %.2f", b.DepositProtectedAmount)
	}
	// Deposit not required, so DepositPaid should be false
	if b.DepositPaid {
		t.Error("expected DepositPaid false when deposit not required")
	}
}

func TestPopulateDepositFields_ProtectedAmountEqualsPaidWhenUnder50Pct(t *testing.T) {

	b := &Booking{
		TotalAmount: 200,
		StartTime:   time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC),
	}

	// Paid 30 on a 200 total — protected deposit = min(30, 100) = 30
	populateDepositFields(b, true, 30)

	if b.DepositProtectedAmount != 30 {
		t.Errorf("expected protected deposit 30, got %.2f", b.DepositProtectedAmount)
	}
	// 30 < 40 (20% of 200), so deposit is NOT paid
	if b.DepositPaid {
		t.Error("expected DepositPaid false when pre-payments < 20% deposit")
	}
}

func TestPopulateDepositFields_DepositPaidWhenMet(t *testing.T) {

	b := &Booking{
		TotalAmount: 200,
		StartTime:   time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC),
	}

	// Paid 50 on a 200 total — 50 >= 40 (20%), so deposit paid
	populateDepositFields(b, true, 50)

	if !b.DepositPaid {
		t.Error("expected DepositPaid true when pre-payments >= 20%")
	}
	if b.DepositProtectedAmount != 50 {
		t.Errorf("expected protected deposit 50, got %.2f", b.DepositProtectedAmount)
	}
}

func TestPopulateDepositFields_NoDepositRequired(t *testing.T) {

	b := &Booking{
		TotalAmount: 100,
		StartTime:   time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC),
	}

	// Deposit not required, paid 80 — protected = min(80, 50) = 50
	populateDepositFields(b, false, 80)

	if b.DepositPaid {
		t.Error("expected DepositPaid false when deposit_required is false")
	}
	if b.DepositProtectedAmount != 50 {
		t.Errorf("expected protected deposit 50, got %.2f", b.DepositProtectedAmount)
	}
}

func TestPopulateDepositFields_DeadlineSet(t *testing.T) {

	b := &Booking{
		TotalAmount: 100,
		StartTime:   time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC),
	}

	populateDepositFields(b, true, 0)

	if b.DepositDeadline == nil {
		t.Fatal("expected non-nil DepositDeadline")
	}
	deadline, err := time.Parse(time.RFC3339, *b.DepositDeadline)
	if err != nil {
		t.Fatalf("failed to parse deadline: %v", err)
	}
	expectedDeadline := b.StartTime.Add(-payments.DepositDeadlineWindow)
	if !deadline.Equal(expectedDeadline) {
		t.Errorf("expected deadline %v, got %v", expectedDeadline, deadline)
	}
}

// =============================================================================
// RequestEditHandler - Notice period validation
// =============================================================================

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

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

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

	// Booking starting in 2 hours (<48h)
	soon := clock.Now().Add(2 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, soon)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

	// Add a payment so hasPayments = true
	_, err = fixtures.CreateTestPayment(tx, bookingID, 50, "online_square", "deposit", "completed")
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	token := jwt.GenerateUserToken(userID)
	newTime := soon.Add(48 * time.Hour)

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

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for <48h with payments, got %d: %s", w.Code, w.Body.String())
	}
}

func TestRequestEditHandler_NoticePeriod_BlocksNoPaymentUnder24h(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)
	}

	// Booking starting in 2 hours (<24h, no payments)
	soon := clock.Now().Add(2 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, soon)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)
	newTime := soon.Add(48 * time.Hour)

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

	if w.Code != http.StatusForbidden {
		t.Errorf("expected 403 for <24h without payments, got %d: %s", w.Code, w.Body.String())
	}
}

func TestAdminCreateBookingForUser_EvictsPendingReleaseOnOverlap(t *testing.T) {
	// AdminCreateBookingForUserHandler evicts any pending_release booking that
	// overlaps the new booking's time slot before creating the booking.
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

	// Create a booking that will end up in pending_release (deposit not paid).
	future := clock.Now().Add(48 * time.Hour)
	existingBookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, future)
	if err != nil {
		t.Fatalf("failed to create existing booking: %v", err)
	}

	// Set it to pending_release (as if deposit deadline passed).
	_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'pending_release' WHERE id = $1", existingBookingID)
	if err != nil {
		t.Fatalf("failed to set status to pending_release: %v", err)
	}

	// Admin creates a new booking at the same time — pending_release should be evicted
	// to deposit_lapsed, and the new booking should succeed.
	body := AdminCreateBookingForUserRequest{
		UserID:     userID,
		StartTime:  future,
		ServiceIDs: []string{serviceID},
	}

	handler := AdminCreateBookingForUserHandler
	w := serveChiHandler(handler, "POST", "/", "/", body, func(baseCtx context.Context) context.Context {
		baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
		baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
		return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
	})

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

	// Verify the existing booking was evicted to deposit_lapsed.
	var newStatus string
	err = tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", existingBookingID).Scan(&newStatus)
	if err != nil {
		t.Fatalf("failed to query existing booking: %v", err)
	}
	if newStatus != "deposit_lapsed" {
		t.Errorf("expected existing booking to be evicted to 'deposit_lapsed', got %q", newStatus)
	}
}

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

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

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

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

	// Create a confirmed booking far enough away that the reschedule is valid.
	future := clock.Now().Add(96 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, future)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

	// Reschedule to a different slot with forgive_noshow=true.
	body := map[string]interface{}{
		"start_time":     future.Add(48 * time.Hour).Format(time.RFC3339),
		"forgive_noshow": true,
		"forgive_fees":   true,
	}

	w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", body, func(baseCtx context.Context) context.Context {
		baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
		baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
		return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
	})

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

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

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

func TestRequestEditHandler_NoticePeriod_SetsNoShowWarningHeader(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)
	}

	// Booking starting in 48 hours (24-72h window, no payments).
	midRange := clock.Now().Add(48 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, midRange)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)
	newTime := midRange.Add(48 * time.Hour)

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

	// With 48h remaining and no payments: allowed (no 403), but warning header set.
	if w.Code != http.StatusCreated && w.Code != http.StatusOK {
		t.Fatalf("expected 200/201 for 48h notice without payments, got %d: %s", w.Code, w.Body.String())
	}
	header := w.Header().Get("X-No-Show-Warning")
	if header == "" {
		t.Error("expected X-No-Show-Warning header to be set for 24-72h reschedule")
		return
	}
	if !strings.Contains(header, "no-show") {
		t.Errorf("expected X-No-Show-Warning header to mention no-show, got: %s", header)
	}
}

func TestRequestEditHandler_NoticePeriod_AllowsWhenEnoughNotice(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)
	}

	// Booking starting in 36 hours (within 48h threshold so auto-approval 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 booking: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)
	newTime := bookingTime.Add(48 * time.Hour)

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

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

// =============================================================================
// DeleteBookingHandler - Refund calculation in cancellation response
// =============================================================================

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

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

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

	// Booking in the far future (full refund expected)
	farFuture := clock.Now().Add(200 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, farFuture)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(DeleteBookingHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID+"/cancel", map[string]interface{}{
		"reason": "client_cancelled",
	}, token, ctx)

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

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

	if resp["message"] != "Booking cancelled successfully" {
		t.Errorf("expected cancellation message, got: %v", resp["message"])
	}
	if resp["status"] != "client_cancelled" {
		t.Errorf("expected status 'client_cancelled', got: %v", resp["status"])
	}
	if id, ok := resp["id"].(string); !ok || id != bookingID {
		t.Errorf("expected booking id %q, got: %v", bookingID, resp["id"])
	}
}

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

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

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

	// Booking starting in 1 hour (<24h)
	soon := clock.Now().Add(1 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, soon)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := http.HandlerFunc(DeleteBookingHandler)
	w := makeRequest(handler, "DELETE", "/api/bookings/"+bookingID+"/cancel", map[string]interface{}{
		"reason": "client_cancelled",
	}, token, ctx)

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

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

	if resp["message"] != "Booking cancelled successfully" {
		t.Errorf("expected cancellation message, got: %v", resp["message"])
	}
	if resp["status"] != "client_cancelled" {
		t.Errorf("expected status 'client_cancelled', got: %v", resp["status"])
	}
	if id, ok := resp["id"].(string); !ok || id != bookingID {
		t.Errorf("expected booking id %q, got: %v", bookingID, resp["id"])
	}
}

// =============================================================================
// AdminCancelBookingHandler - Forgive fees and no-show
// =============================================================================

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

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

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

	// Booking starting in 1 hour (<24h, normally no refund)
	soon := clock.Now().Add(1 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, soon)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

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

	w := serveChiHandler(AdminCancelBookingHandler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", "/api/admin/bookings/{id}/cancel", map[string]interface{}{
		"forgive_fees": true,
	}, func(baseCtx context.Context) context.Context {
		baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
		adminToken := jwt.GenerateAdminToken()
		if info := extractUserFromTestJWT(adminToken); info != nil {
			baseCtx = context.WithValue(baseCtx, mw.UserIDKey, info.userID)
		}
		return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
	})

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

	var resp map[string]interface{}
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	refundCalc, ok := resp["refund_calculation"].(map[string]interface{})
	if !ok {
		t.Fatalf("refund_calculation missing, response: %s", w.Body.String())
	}
	if refundCalc["tier"] != "admin_full_refund" {
		t.Errorf("expected tier 'admin_full_refund', got %v", refundCalc["tier"])
	}
	if refundCalc["refundable_amount"] != 100.0 {
		t.Errorf("expected refundable_amount 100, got %v", refundCalc["refundable_amount"])
	}
}

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

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

	// Create a real admin user so the refund record FK on created_by is satisfied.
	adminID, err := fixtures.CreateTestAdminUser(tx)
	if err != nil {
		t.Fatalf("failed to create admin user: %v", err)
	}

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

	// Booking starting far in the future (>72h — full refund tier without forgiveness).
	farFuture := clock.Now().Add(200 * time.Hour)
	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, farFuture)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

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

	// Cancel WITHOUT forgive_fees — ProcessCancellationRefund should fire.
	// Use a chi router with a real admin token to satisfy the refund FK.
	w := serveChiHandler(AdminCancelBookingHandler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", "/api/admin/bookings/{id}/cancel", map[string]interface{}{}, func(baseCtx context.Context) context.Context {
		baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
		baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
		return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
	})

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

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

	refundCalc, ok := resp["refund_calculation"].(map[string]interface{})
	if !ok {
		t.Fatalf("refund_calculation missing, response: %s", w.Body.String())
	}
	if refundCalc["tier"] != "full_refund_72h" {
		t.Errorf("expected tier 'full_refund_72h', got %v", refundCalc["tier"])
	}
	if refundCalc["refundable_amount"] != 80.0 {
		t.Errorf("expected refundable_amount 80, got %v", refundCalc["refundable_amount"])
	}

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

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

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

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

	bookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID,
		time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

	w := serveChiHandler(AdminCancelBookingHandler, "POST", "/api/admin/bookings/"+bookingID+"/cancel", "/api/admin/bookings/{id}/cancel", map[string]interface{}{
		"forgive_noshow": true,
	}, func(baseCtx context.Context) context.Context {
		baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
		adminToken := jwt.GenerateAdminToken()
		if info := extractUserFromTestJWT(adminToken); info != nil {
			baseCtx = context.WithValue(baseCtx, mw.UserIDKey, info.userID)
		}
		return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
	})

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

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

// =============================================================================
// AdminRescheduleBookingHandler - Notice period warning header
// =============================================================================

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

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

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

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

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

	adminToken := jwt.GenerateAdminToken()

	handler := http.HandlerFunc(AdminRescheduleBookingHandler)
	newTime := clock.Now().Add(200 * time.Hour)

	w := makeRequest(handler, "PUT", "/api/admin/bookings/"+bookingID+"/reschedule", map[string]interface{}{
		"start_time": newTime.Format(time.RFC3339),
	}, adminToken, ctx)

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

func TestAdminRescheduleBookingHandler_ForgiveFees_Succeeds(t *testing.T) {
	// forgive_fees on a reschedule records an audit log but should not create
	// an admin_notification (admins know what they did). This test verifies
	// the reschedule succeeds with the flag present.
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

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

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

	newTime := future.Add(48 * time.Hour)
	w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", map[string]interface{}{
		"start_time":   newTime.Format(time.RFC3339),
		"forgive_fees": true,
	}, func(baseCtx context.Context) context.Context {
		baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
		baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
		return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
	})

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

	// Verify the booking was rescheduled to the new time (DB truncates to seconds).
	var actualStart time.Time
	tx.QueryRow(ctx, "SELECT start_time FROM bookings WHERE id = $1", bookingID).Scan(&actualStart)
	expectedStart := newTime.Truncate(time.Second)
	if !actualStart.Equal(expectedStart) {
		t.Errorf("expected start_time %v, got %v", expectedStart, actualStart)
	}

	// No admin_notification should be created for the admin's own action.
	var notifCount int
	tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM admin_notifications WHERE booking_id = $1", bookingID).Scan(&notifCount)
	if notifCount != 0 {
		t.Errorf("expected 0 admin_notifications (admin action), got %d", notifCount)
	}
}

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

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

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

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

	// Send request WITHOUT admin auth context.
	w := serveChiHandler(AdminRescheduleBookingHandler, "PUT", "/"+bookingID+"/reschedule", "/{id}/reschedule", map[string]interface{}{
		"start_time": clock.Now().Add(200 * time.Hour).Format(time.RFC3339),
	}, func(baseCtx context.Context) context.Context {
		return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
	})

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

// TestPopulateDepositFields_NegativeAmount_Safeguarded ensures that a negative
// preStartAmountPaid value is clamped to zero to prevent a negative DepositProtectedAmount.
func TestPopulateDepositFields_NegativeAmount_Safeguarded(t *testing.T) {
	b := &Booking{
		TotalAmount: 100,
		StartTime:   clock.Now(),
	}

	// Negative amount should be clamped to 0.
	populateDepositFields(b, true, -50)
	if b.DepositProtectedAmount < 0 {
		t.Errorf("expected non-negative DepositProtectedAmount, got %.2f", b.DepositProtectedAmount)
	}
	if b.DepositProtectedAmount != 0 {
		t.Errorf("expected DepositProtectedAmount 0 (clamped from -50), got %.2f", b.DepositProtectedAmount)
	}
}

// =============================================================================
// CreateBookingHandler — deposit advance window (payments.DepositAdvanceWindow)
// =============================================================================

func TestCreateBooking_DepositAdvanceWindow_BlocksUnder36h(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 = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(CreateBookingHandler)

	// Use a time 10 hours from now — well within the 36h deposit advance window
	// but past the 1h minimum advance check.
	nearTime := clock.Now().Add(10 * time.Hour).Truncate(time.Second)
	req := CreateBookingRequest{
		StartTime:  nearTime,
		ServiceIDs: []string{serviceID},
	}

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

	if w.Code != http.StatusBadRequest {
		t.Fatalf("expected 400 for booking within deposit advance window, got %d. body: %s", w.Code, w.Body.String())
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("deposit")) && !bytes.Contains(w.Body.Bytes(), []byte("hours in advance")) {
		t.Errorf("expected error about deposit advance window, got: %s", w.Body.String())
	}
}

func TestCreateBooking_DepositAdvanceWindow_AllowsOver36h(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 = 3 WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set deposits_required: %v", err)
	}

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

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(CreateBookingHandler)

	// Use next weekday >48h from now during working hours (well past 36h deposit window).
	// Use 13:00 (1pm) to ensure the 36h deposit window from ANY overnight test time
	// is always cleared — 36h from midnight UTC Wednesday = 12:00 UTC Thursday.
	farTime := nextWeekday(time.Thursday).Add(13 * time.Hour)
	req := CreateBookingRequest{
		StartTime:  farTime,
		ServiceIDs: []string{serviceID},
	}

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

	if w.Code != http.StatusCreated {
		t.Fatalf("expected 201 for booking outside deposit advance window, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestCreateBooking_DepositAdvanceWindow_SkipsWhenNoDepositRequired(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)
	}

	// deposits_required = 0 — advance window should not apply
	_, 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)
	}

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(CreateBookingHandler)

	// Booking within 36h but with no deposits required — should be allowed.
	// Use a time within working hours (midday on tomorrow or next weekday).
	midday := clock.Now().Truncate(24 * time.Hour).Add(29 * time.Hour)
	if midday.Hour() < 8 || midday.Hour() >= 20 {
		midday = nextWeekday(clock.Now().Weekday()).Add(12 * time.Hour)
	}
	nearTime := midday.Truncate(time.Second)
	req := CreateBookingRequest{
		StartTime:  nearTime,
		ServiceIDs: []string{serviceID},
	}

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

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

// =============================================================================
// CreateBookingHandler — pending_release eviction on slot overlap
// =============================================================================

func TestCreateBooking_EvictsPendingReleaseOnOverlap(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)
	}

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

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(CreateBookingHandler)

	// Create a booking far in the future that we'll mark as pending_release.
	farTime := nextWeekday(time.Monday).Add(10 * time.Hour)
	existingBookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, farTime)
	if err != nil {
		t.Fatalf("failed to create existing booking: %v", err)
	}

	// Mark the existing booking as pending_release (deposit deadline passed).
	_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'pending_release' WHERE id = $1", existingBookingID)
	if err != nil {
		t.Fatalf("failed to set existing booking to pending_release: %v", err)
	}

	// Now create a new booking at the same time slot — should evict the pending_release one.
	req := CreateBookingRequest{
		StartTime:  farTime,
		ServiceIDs: []string{serviceID},
	}

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

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

	// Verify the old booking was evicted to deposit_lapsed.
	var newStatus string
	err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", existingBookingID).Scan(&newStatus)
	if err != nil {
		t.Fatalf("failed to query existing booking status: %v", err)
	}
	if newStatus != "deposit_lapsed" {
		t.Errorf("expected existing booking status 'deposit_lapsed', got %q", newStatus)
	}

	// TODO: notify the affected user that their slot was released — not yet implemented.
}

func TestCreateBooking_LeftUnchangedWhenNoOverlapWithPendingRelease(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)
	}

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

	token := jwt.GenerateUserToken(userID)
	handler := http.HandlerFunc(CreateBookingHandler)

	// Create a pending_release booking at time A.
	timeA := nextWeekday(time.Monday).Add(10 * time.Hour)
	existingBookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, timeA)
	if err != nil {
		t.Fatalf("failed to create existing booking: %v", err)
	}

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

	// Create a new booking at a different time B that does NOT overlap.
	timeB := nextWeekday(time.Tuesday).Add(10 * time.Hour)
	req := CreateBookingRequest{
		StartTime:  timeB,
		ServiceIDs: []string{serviceID},
	}

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

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

	// Verify the old pending_release booking was NOT evicted.
	var newStatus string
	err = tx.QueryRow(ctx, "SELECT status FROM bookings WHERE id = $1", existingBookingID).Scan(&newStatus)
	if err != nil {
		t.Fatalf("failed to query existing booking status: %v", err)
	}
	if newStatus != "pending_release" {
		t.Errorf("expected existing booking to remain 'pending_release', got %q", newStatus)
	}
}

// =============================================================================
// EvictPendingReleaseOverlapping — direct function tests
// =============================================================================

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

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

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

	// Create a booking far in the future.
	future := clock.Now().Add(72 * time.Hour)
	existingBookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, future)
	if err != nil {
		t.Fatalf("failed to create existing booking: %v", err)
	}

	// Mark it as pending_release.
	_, err = tx.Exec(ctx,
		"UPDATE bookings SET status = 'pending_release' WHERE id = $1", existingBookingID)
	if err != nil {
		t.Fatalf("failed to set pending_release: %v", err)
	}

	// Call the shared eviction function with an overlapping slot.
	// Service is 60 min, so [future, future+60min] is the existing slot.
	// New slot [future+30min, future+90min] overlaps -> should evict.
	evicted, err := EvictPendingReleaseOverlapping(ctx, db.TxFromContext(ctx), future.Add(30*time.Minute), future.Add(90*time.Minute))
	if err != nil {
		t.Fatalf("EvictPendingReleaseOverlapping failed: %v", err)
	}

	if len(evicted) != 1 {
		t.Fatalf("expected 1 evicted booking, got %d", len(evicted))
	}
	if evicted[0].ID != existingBookingID {
		t.Errorf("expected evicted booking ID %s, got %s", existingBookingID, evicted[0].ID)
	}
	if evicted[0].UserID != userID {
		t.Errorf("expected evicted user ID %s, got %s", userID, evicted[0].UserID)
	}

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

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

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

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

	future := clock.Now().Add(72 * time.Hour)
	existingBookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, future)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

	// Insert a PAYMENT_IN_FLIGHT time_blocker for this booking — should
	// prevent eviction even though the slot overlaps.
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES (NOW(), 5, $1, $2)
	`, "PAYMENT_IN_FLIGHT:"+existingBookingID, userID)
	if err != nil {
		t.Fatalf("failed to create PAYMENT_IN_FLIGHT blocker: %v", err)
	}

	evicted, err := EvictPendingReleaseOverlapping(ctx, db.TxFromContext(ctx), future.Add(30*time.Minute), future.Add(90*time.Minute))
	if err != nil {
		t.Fatalf("EvictPendingReleaseOverlapping failed: %v", err)
	}

	if len(evicted) != 0 {
		t.Errorf("expected 0 evicted bookings (PAYMENT_IN_FLIGHT guard), got %d", len(evicted))
	}

	// Verify the booking was NOT evicted.
	var newStatus string
	err = tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", existingBookingID).Scan(&newStatus)
	if err != nil {
		t.Fatalf("failed to query booking status: %v", err)
	}
	if newStatus != "pending_release" {
		t.Errorf("expected booking to remain 'pending_release' (guarded), got %q", newStatus)
	}
}

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

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

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

	future := clock.Now().Add(72 * time.Hour)
	existingBookingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, future)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

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

	// Call eviction with a slot that does NOT overlap [future, future+60min].
	evicted, err := EvictPendingReleaseOverlapping(ctx, db.TxFromContext(ctx), future.Add(120*time.Minute), future.Add(180*time.Minute))
	if err != nil {
		t.Fatalf("EvictPendingReleaseOverlapping failed: %v", err)
	}

	if len(evicted) != 0 {
		t.Errorf("expected 0 evicted bookings (no overlap), got %d", len(evicted))
	}

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

// =============================================================================
// Handler-level eviction tests — verify eviction happens via the handler path
// =============================================================================

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

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

	adminID, err := fixtures.CreateTestAdminUser(tx)
	if err != nil {
		t.Fatalf("failed to create admin: %v", err)
	}

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

	// Create a pending_release booking at a far-future time slot.
	future := clock.Now().Add(72 * time.Hour)
	pendingID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, future)
	if err != nil {
		t.Fatalf("failed to create pending booking: %v", err)
	}

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

	// Create a second booking at the same time slot that we'll confirm.
	confirmedID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, future)
	if err != nil {
		t.Fatalf("failed to create confirmable booking: %v", err)
	}

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

	// Confirm the second booking — should evict the pending_release one.
	handler := ConfirmBookingHandler
	w := serveChiHandler(handler, "POST", "/"+confirmedID+"/confirm", "/{id}/confirm", map[string]interface{}{},
		func(baseCtx context.Context) context.Context {
			baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
			baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
			return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
		})

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

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

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

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

	adminID, err := fixtures.CreateTestAdminUser(tx)
	if err != nil {
		t.Fatalf("failed to create admin: %v", err)
	}

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

	// Create a pending_release booking at a specific time slot.
	slotTime := nextWeekday(time.Wednesday).Add(10 * time.Hour)

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

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

	// Create a confirmed booking to reschedule INTO the pending_release slot.
	rescheduleID, err := fixtures.CreateTestBookingAtTime(tx, userID, serviceID, slotTime.Add(-48*time.Hour))
	if err != nil {
		t.Fatalf("failed to create reschedule booking: %v", err)
	}

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

	// Reschedule the confirmed booking INTO the pending_release slot.
	body := map[string]interface{}{
		"start_time": slotTime.Format(time.RFC3339),
	}

	handler := AdminRescheduleBookingHandler
	w := serveChiHandler(handler, "PUT", "/"+rescheduleID+"/reschedule", "/{id}/reschedule", body,
		func(baseCtx context.Context) context.Context {
			baseCtx = context.WithValue(baseCtx, mw.UserRoleKey, "admin")
			baseCtx = context.WithValue(baseCtx, mw.UserIDKey, adminID)
			return db.ContextWithTx(baseCtx, db.TxFromContext(ctx))
		})

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

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