//go:build test && dev

package bookings

// Package bookings contains tests for admin booking reservation endpoints.
//
// Test Coverage:
//   - AdminReserveSlotHandler: POST /api/admin/bookings/reserve - Admin slot reservation (walk-in or call-in)
//
// Tests cover walk-in and call-in reservation types, validation, slot overlaps, and replacement behavior.
import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

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

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

// makeAdminReserveRequest creates a request with admin context for admin reserve slot handler
// It sets mw.UserIDKey and mw.UserRoleKey to "admin" in the context
func makeAdminReserveRequest(handler http.Handler, body interface{}, adminID string, requestCtx ...context.Context) *httptest.ResponseRecorder {
	var req *http.Request
	if body != nil {
		bodyBytes, _ := json.Marshal(body)
		req = httptest.NewRequest("POST", "/api/admin/bookings/reserve", bytes.NewReader(bodyBytes))
		req.Header.Set("Content-Type", "application/json")
	} else {
		req = httptest.NewRequest("POST", "/api/admin/bookings/reserve", nil)
	}

	baseCtx := req.Context()
	if len(requestCtx) > 0 {
		baseCtx = requestCtx[0]
	}

	rctx := chi.NewRouteContext()
	ctx := context.WithValue(baseCtx, chi.RouteCtxKey, rctx)

	ctx = context.WithValue(ctx, mw.UserIDKey, adminID)
	ctx = context.WithValue(ctx, mw.UserRoleKey, "admin")

	req = req.WithContext(ctx)

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

// =============================================================================
// Walk-in Reservation Tests
// =============================================================================

// TestAdminReserveSlot_WalkIn_Success tests that an admin can successfully
// create a walk-in reservation with a valid duration. The test verifies
// the reservation is created in the database with the correct duration.
func TestAdminReserveSlot_WalkIn_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	tomorrow := clock.Now().Add(24 * time.Hour)
	now := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 12, 0, 0, 0, tomorrow.Location())
	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       now,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

	// Parse response and verify duration
	var resp AdminReserveSlotResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

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

	// Verify time_blocker was created with correct description pattern
	var desc string
	err = tx.QueryRow(ctx,
		"SELECT description FROM time_blockers WHERE description LIKE 'RESERVATION:admin:walkin:%'",
	).Scan(&desc)
	if err != nil {
		t.Errorf("failed to query time_blocker: %v", err)
	}

	if !strings.HasPrefix(desc, "RESERVATION:admin:walkin:") {
		t.Errorf("expected description to start with 'RESERVATION:admin:walkin:', got %s", desc)
	}
}

// =============================================================================
// Call-in Reservation Tests
// =============================================================================

// TestAdminReserveSlot_CallIn_Success tests that an admin can successfully
// create a call-in reservation with valid service IDs. The test verifies
// the reservation duration matches the service duration.
func TestAdminReserveSlot_CallIn_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

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

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

	req := AdminReserveSlotRequest{
		UserID:          &userID,
		ReservationType: "callin",
		StartTime:       tomorrow,
		ServiceIDs:      []string{serviceID},
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

	// Parse response and verify duration matches service (30 min)
	var resp AdminReserveSlotResponse
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}

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

	// Verify time_blocker was created with correct description pattern
	var desc string
	err = tx.QueryRow(ctx,
		"SELECT description FROM time_blockers WHERE description LIKE 'RESERVATION:admin:callin:%'",
	).Scan(&desc)
	if err != nil {
		t.Errorf("failed to query time_blocker: %v", err)
	}

	if !strings.HasPrefix(desc, "RESERVATION:admin:callin:") {
		t.Errorf("expected description to start with 'RESERVATION:admin:callin:', got %s", desc)
	}
}

// =============================================================================
// Validation Tests - Missing Duration
// =============================================================================

// TestAdminReserveSlot_WalkIn_MissingDuration tests that walk-in reservations
// fail with HTTP 400 when duration_minutes is missing or zero.
func TestAdminReserveSlot_WalkIn_MissingDuration(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	now := clock.Now()
	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       now,
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

	// Verify error message mentions duration_minutes
	body := w.Body.String()
	if !strings.Contains(body, "duration_minutes") {
		t.Errorf("expected body to contain 'duration_minutes', got %s", body)
	}
}

// =============================================================================
// Validation Tests - Missing Services
// =============================================================================

// TestAdminReserveSlot_CallIn_MissingServices tests that call-in reservations
// fail with HTTP 400 when service_ids is empty.
func TestAdminReserveSlot_CallIn_MissingServices(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	req := AdminReserveSlotRequest{
		ReservationType: "callin",
		StartTime:       tomorrow,
		ServiceIDs:      []string{},
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

	// Verify error message mentions service
	body := w.Body.String()
	if !strings.Contains(body, "service") {
		t.Errorf("expected body to contain 'service', got %s", body)
	}
}

// =============================================================================
// Validation Tests - Invalid Reservation Type
// =============================================================================

// TestAdminReserveSlot_InvalidReservationType tests that reservations
// fail with HTTP 400 when reservation_type is invalid.
func TestAdminReserveSlot_InvalidReservationType(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	req := AdminReserveSlotRequest{
		ReservationType: "invalid",
		StartTime:       clock.Now(),
		DurationMinutes: 30,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

	// Verify error message mentions valid types
	body := w.Body.String()
	if !strings.Contains(body, "walkin") && !strings.Contains(body, "callin") {
		t.Errorf("expected body to contain 'walkin' or 'callin', got %s", body)
	}
}

// =============================================================================
// Slot Overlap Tests
// =============================================================================

// TestAdminReserveSlot_SlotOverlap tests that a reservation fails
// with HTTP 409 when the slot overlaps with an existing booking.
func TestAdminReserveSlot_SlotOverlap(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	// Create test admin user (for the booking)
	adminID, err := fixtures.CreateTestAdminUser(tx)
	if err != nil {
		t.Fatalf("failed to create admin: %v", err)
	}
	defer fixtures.DeleteUser(tx, adminID)

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

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

	// Set deposits_required=0 for test user
	_, 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)
	}

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

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

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

	// Now try to reserve a call-in that overlaps (tomorrow 10:15 - 15 min after start)
	overlapTime := tomorrow.Add(15 * time.Minute)

	req := AdminReserveSlotRequest{
		ReservationType: "callin",
		StartTime:       overlapTime,
		ServiceIDs:      []string{serviceID},
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

// =============================================================================
// Reservation Replacement Tests
// =============================================================================

// TestAdminReserveSlot_ReplacesExisting tests that reserving twice
// on the same admin replaces the previous reservation.
func TestAdminReserveSlot_ReplacesExisting(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	tomorrow := clock.Now().Add(24 * time.Hour)
	now := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 12, 0, 0, 0, tomorrow.Location())

	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       now,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}

	// First reservation
	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

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

	// Count reservations before second request
	var countBefore int
	var qerr error
	qerr = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:admin:%'",
	).Scan(&countBefore)
	if qerr != nil {
		t.Fatalf("failed to count reservations: %v", qerr)
	}

	// Second reservation (same admin, new time)
	laterTime := now.Add(1 * time.Hour)
	req2 := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       laterTime,
		DurationMinutes: 45,
		TTLMinutes:      15,
	}

	w = makeAdminReserveRequest(handler, req2, adminID, ctx)

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

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

	// Count reservations after second request - should still be 1
	var countAfter int
	qerr = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:admin:%'",
	).Scan(&countAfter)
	if qerr != nil {
		t.Fatalf("failed to count reservations: %v", qerr)
	}

	// Old one should be deleted, new one exists (id should be different)
	if countAfter != 1 {
		t.Errorf("expected 1 reservation after replacement, got %d", countAfter)
	}

	if firstResp.ID == secondResp.ID {
		t.Errorf("expected new reservation ID to be different from old one")
	}
}

// =============================================================================
// Past Start Time Tests
// =============================================================================

// TestAdminReserveSlot_WalkIn_PastStart tests that walk-in reservations
func TestAdminReserveSlot_WalkIn_PastStart(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	pastTime := clock.Now().Add(-5 * time.Minute)
	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       pastTime,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

	// Verify error message mentions the past time limit
	body := w.Body.String()
	if !strings.Contains(body, "past") {
		t.Errorf("expected body to contain 'past', got %s", body)
	}
}

// TestAdminReserveSlot_OutOfHours_CallIn_Success verifies that an admin can
// successfully create a call-in reservation with out_of_hours=true, bypassing
// the closing hours check even when booking outside normal hours.
func TestAdminReserveSlot_OutOfHours_CallIn_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	// Baseline test DB has 08:00-20:00 hours. Book 19:30 + 60min = 20:30 (> 20:00 closing)
	// Without out_of_hours this would fail; with out_of_hours=true it should succeed.
	tomorrow := clock.Now().Add(24 * time.Hour)
	lateBooking := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 19, 30, 0, 0, tomorrow.Location())

	req := AdminReserveSlotRequest{
		ReservationType: "callin",
		StartTime:       lateBooking,
		ServiceIDs:      []string{serviceID},
		TTLMinutes:      15,
		OutOfHours:      true,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

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

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

	if resp.StartTime.Format("15:04") != "19:30" {
		t.Errorf("expected start time 19:30, got %s", resp.StartTime.Format("15:04"))
	}
}

// TestAdminReserveSlot_OutOfHours_WithoutFlag_Fails verifies that an admin
// attempting to book a slot that ends after closing hours WITHOUT the
// out_of_hours flag is rejected, ensuring the flag is required.
func TestAdminReserveSlot_OutOfHours_WithoutFlag_Fails(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	// Baseline test DB has 08:00-20:00 hours. Book 19:30 + 60min = 20:30 (> 20:00 closing)
	// Without out_of_hours this should be rejected.
	tomorrow := clock.Now().Add(24 * time.Hour)
	lateBooking := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 19, 30, 0, 0, tomorrow.Location())

	req := AdminReserveSlotRequest{
		ReservationType: "callin",
		StartTime:       lateBooking,
		ServiceIDs:      []string{serviceID},
		TTLMinutes:      15,
		// OutOfHours intentionally NOT set (defaults to false)
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

	if w.Code != http.StatusBadRequest {
		t.Errorf("expected status 400 when booking beyond closing hours without out_of_hours flag, got %d. body: %s",
			w.Code, w.Body.String())
	}
}

// TestAdminReserveSlot_OutOfHours_WalkIn_Success verifies that an admin can
// successfully create a walk-in reservation with out_of_hours=true, bypassing
// the closing hours check and using explicit duration.
func TestAdminReserveSlot_OutOfHours_WalkIn_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Baseline test DB has 08:00-20:00 hours. Book walk-in 19:30 + 60min = 20:30 (> 20:00 closing)
	// Without out_of_hours this would fail; with out_of_hours=true it should succeed.
	tomorrow := clock.Now().Add(24 * time.Hour)
	lateBooking := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 19, 30, 0, 0, tomorrow.Location())

	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       lateBooking,
		DurationMinutes: 60,
		TTLMinutes:      15,
		OutOfHours:      true,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

	if w.Code != http.StatusCreated {
		t.Errorf("expected status 201 for out-of-hours walk-in reservation, got %d. body: %s",
			w.Code, w.Body.String())
	}

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

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

	// Verify reservation time_blocker was created
	var desc string
	err = tx.QueryRow(ctx,
		"SELECT description FROM time_blockers WHERE description LIKE 'RESERVATION:admin:walkin:%'",
	).Scan(&desc)
	if err != nil {
		t.Errorf("failed to query time_blocker: %v", err)
	}
	if !strings.HasPrefix(desc, "RESERVATION:admin:walkin:") {
		t.Errorf("expected RESERVATION:admin:walkin: prefix, got %s", desc)
	}
}

// TestAdminReserveSlot_OutOfHours_TimeBlockerBlocks verifies that even when
// out_of_hours=true is set, a time blocker at the requested time still causes
// the reservation to be rejected with 409 Conflict.
func TestAdminReserveSlot_OutOfHours_TimeBlockerBlocks(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Create a time blocker at a specific future time
	tomorrow := clock.Now().Add(24 * time.Hour)
	blockerStart := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 14, 0, 0, 0, tomorrow.Location())
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Admin Blocked Time', NULL)
	`, blockerStart)
	if err != nil {
		t.Fatalf("failed to create time blocker: %v", err)
	}

	// Try reserving during the blocked time with out_of_hours=true
	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       blockerStart,
		DurationMinutes: 30,
		TTLMinutes:      15,
		OutOfHours:      true,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 Conflict when blocker overlaps with out_of_hours reservation, got %d. body: %s",
			w.Code, w.Body.String())
	}
}

// TestAdminReserveSlot_BST_ClosingBoundary verifies that during BST (UTC+1),
// the closing-time check uses London local time, not UTC. A booking ending at
// 20:01 BST (19:01 UTC) should be rejected when closing is 20:00 BST, even
// though the UTC hour (19) is before the closing hour (20). Seed data has
// working hours 08:00-20:00 for all days.
func TestAdminReserveSlot_BST_ClosingBoundary(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Use a Monday in BST (2099-06-15 is a Monday in BST). Seed working hours
	// for Monday are 08:00-20:00.
	// Start at 18:31 UTC (= 19:31 BST), 30 min duration → ends at 19:01 UTC (= 20:01 BST).
	// This should be rejected because 20:01 BST > 20:00 BST closing.
	// Without .In(londonLocation), UTC hour 19 < closing 20, so this would
	// incorrectly pass — the fix catches it.
	bstDay := time.Date(2099, 6, 15, 18, 31, 0, 0, time.UTC)
	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       bstDay,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

	// Must be rejected (exceeds closing hours in London time).
	// Without .In(londonLocation), UTC hour 19 would be < closing 20 and pass.
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected 400 BadRequest (exceeds closing hours in BST), got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify the error message mentions closing hours.
	if !strings.Contains(w.Body.String(), "closing hours") && !strings.Contains(w.Body.String(), "closing") {
		t.Errorf("expected error about closing hours, got: %s", w.Body.String())
	}

	// Now test a booking that ends within closing hours (19:30 BST < 20:00 BST).
	// Start at 18:00 UTC (= 19:00 BST), 30 min duration → ends at 18:30 UTC (= 19:30 BST).
	bstDayOK := time.Date(2099, 6, 15, 18, 0, 0, 0, time.UTC)
	req2 := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       bstDayOK,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}

	w2 := makeAdminReserveRequest(handler, req2, adminID, ctx)
	if w2.Code != http.StatusCreated {
		t.Errorf("expected 201 Created (within closing hours in BST), got %d. body: %s", w2.Code, w2.Body.String())
	}
}

// =============================================================================
// Autumn DST (BST→GMT Transition) Tests
// =============================================================================

// TestAdminReserveSlot_AutumnDST_ClosingBoundary verifies that during autumn DST
// (BST→GMT transition on Oct 25, 2026), the closing-time check works correctly
// when London is in GMT (UTC+0). After the transition at 02:00 BST (→ 01:00 GMT),
// local time equals UTC. This test sets Sunday's closing to 17:00 and verifies:
//  1. A slot ending exactly at 17:00 GMT (= 17:00 UTC) is allowed (end == closing)
//  2. A slot ending 1 minute after 17:00 GMT (= 17:01 UTC) is rejected
//
// Use of londonLocation for time.Date construction ensures the test time is
// interpreted in the local timezone context of the autumn DST transition day.
func TestAdminReserveSlot_AutumnDST_ClosingBoundary(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Oct 25, 2026 is a Sunday (DB weekday 6). BST ends at 02:00 BST (→ 01:00 GMT),
	// so the entire working day is in GMT. Override Sunday's closing to 17:00 to test
	// the closing boundary on the autumn DST transition day.
	_, err = tx.Exec(ctx, `
		INSERT INTO working_hours (weekday, start_time, end_time, is_open)
		VALUES (6, '08:00', '17:00', true)
		ON CONFLICT (weekday) DO UPDATE SET start_time = '08:00', end_time = '17:00', is_open = true
	`)
	if err != nil {
		t.Fatalf("failed to set Sunday working hours: %v", err)
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)

	// Test 1: End exactly at 17:00 GMT (= 17:00 UTC, since GMT = UTC+0).
	// Start 16:30 GMT + 30 min → ends 17:00 GMT → end == closing, allowed.
	closingSlot := time.Date(2026, 10, 25, 16, 30, 0, 0, londonLocation)
	req1 := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       closingSlot,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}
	w1 := makeAdminReserveRequest(handler, req1, adminID, ctx)
	if w1.Code != http.StatusCreated {
		t.Errorf("ending exactly at 17:00 GMT: expected 201 (end == closing allowed), got %d. body: %s",
			w1.Code, w1.Body.String())
	}

	// Parse response and verify duration
	var resp1 AdminReserveSlotResponse
	if err := json.Unmarshal(w1.Body.Bytes(), &resp1); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp1.DurationMinutes != 30 {
		t.Errorf("expected duration 30, got %d", resp1.DurationMinutes)
	}

	// Test 2: End 1 min after 17:00 GMT (= 17:01 UTC).
	// Start 16:31 GMT + 30 min → ends 17:01 GMT → rejected (past closing).
	pastClose := time.Date(2026, 10, 25, 16, 31, 0, 0, londonLocation)
	req2 := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       pastClose,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}
	w2 := makeAdminReserveRequest(handler, req2, adminID, ctx)
	if w2.Code != http.StatusBadRequest {
		t.Errorf("ending 1 min after 17:00 GMT: expected 400, got %d. body: %s",
			w2.Code, w2.Body.String())
	}

	// Verify error message mentions closing hours
	if !strings.Contains(w2.Body.String(), "closing hours") && !strings.Contains(w2.Body.String(), "closing") {
		t.Errorf("expected error about closing hours, got: %s", w2.Body.String())
	}
}

// TestAdminReserveSlot_BST_WeekdayLookup verifies that the weekday used for
// working-hours lookup uses London time, not UTC. At 23:30 UTC on a Sunday
// in BST (= 00:30 BST Monday), UTC says Sunday (DB weekday 6) but London says
// Monday (DB weekday 0). Deleting Sunday's row should cause failure WITHOUT
// the fix, but succeed WITH the fix (London-time weekday=Monday, row exists).
func TestAdminReserveSlot_BST_WeekdayLookup(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Delete Sunday's (DB weekday 6) working hours row.
	_, err = tx.Exec(ctx, "DELETE FROM working_hours WHERE weekday = 6")
	if err != nil {
		t.Fatalf("failed to delete Sunday hours: %v", err)
	}

	// Also seed Monday (weekday 0) explicitly so the test doesn't depend on fixture defaults.
	_, err = tx.Exec(ctx, `
	    INSERT INTO working_hours (weekday, start_time, end_time, is_open)
	    VALUES (0, '09:00', '17:00', true)
	    ON CONFLICT (weekday) DO UPDATE SET start_time = '09:00', end_time = '17:00', is_open = true
	`)
	if err != nil {
		t.Fatalf("failed to seed Monday hours: %v", err)
	}

	// Book at 23:30 UTC on a Sunday in BST (2099-06-14 is Sunday, 2099-06-15 is Monday).
	// 23:30 UTC Sunday = 00:30 BST Monday. London time = Monday (DB weekday 0, exists).
	// UTC time = Sunday (DB weekday 6, deleted).
	sunday2330UTC := time.Date(2099, 6, 14, 23, 30, 0, 0, time.UTC)
	req := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       sunday2330UTC,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}

	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

	if w.Code != http.StatusCreated {
		t.Errorf("expected 201 Created (London weekday=Monday, row exists), got %d. body: %s — UTC weekday=Sunday (deleted), London weekday=Monday (exists)", w.Code, w.Body.String())
	}
}

// TestAdminReserveSlot_ClosingComparison_EdgeCases verifies that the closing-time
// string comparison correctly handles edge cases at the boundary (BUG 5 fix).
// Tests: ending exactly at closing, 1 min before, and 1 min after.
func TestAdminReserveSlot_ClosingComparison_EdgeCases(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Seed data has all days open 08:00-20:00. Test various closing edges.
	// Use a BST Monday (2099-06-15, BST period so UTC != London).
	// 20:00 BST = 19:00 UTC. A service ending at 19:59 UTC = 20:59 BST (after closing).
	// Actually seed data is 08:00-20:00 BST, so 20:00 BST closing = 19:00 UTC.

	// Test 1: End exactly at closing (20:00 BST = 19:00 UTC) — should be allowed
	// (end == closing is not "beyond" closing — the check is strict greater-than).
	endAtClose := time.Date(2099, 6, 15, 18, 30, 0, 0, time.UTC) // start 18:30 UTC
	req1 := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       endAtClose,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}
	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w1 := makeAdminReserveRequest(handler, req1, adminID, ctx)
	if w1.Code != http.StatusCreated {
		t.Errorf("closing exactly at 20:00 BST: expected 201 (end == closing is allowed), got %d", w1.Code)
	}

	// Test 2: End 1 minute after closing (20:01 BST = 19:01 UTC) — should reject
	oneMinAfter := time.Date(2099, 6, 15, 18, 31, 0, 0, time.UTC)
	req2 := AdminReserveSlotRequest{
		ReservationType: "walkin",
		StartTime:       oneMinAfter,
		DurationMinutes: 30,
		TTLMinutes:      15,
	}
	w2 := makeAdminReserveRequest(handler, req2, adminID, ctx)
	if w2.Code != http.StatusBadRequest {
		t.Errorf("closing 1 min after 20:00 BST: expected 400, got %d", w2.Code)
	}
}

// TestAdminReserveSlot_PendingRelease_DoesNotBlock verifies that a
// pending_release booking does NOT block the admin reserve endpoint.
// Like the user-facing reserve, admin reserves are pre-checks — eviction
// happens when the actual booking is created.
func TestAdminReserveSlot_PendingRelease_DoesNotBlock(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

	future := clock.Now().Add(7 * 24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)

	// Create a pending_release booking at this time slot
	_, err = tx.Exec(ctx, `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, $2, 'pending_release', false)
	`, userID, future)
	if err != nil {
		t.Fatalf("failed to create pending_release booking: %v", err)
	}

	// Admin reserves the same slot — should succeed
	req := AdminReserveSlotRequest{
		ReservationType: "callin",
		StartTime:       future,
		ServiceIDs:      []string{serviceID},
		TTLMinutes:      15,
	}
	handler := http.HandlerFunc(AdminReserveSlotHandler)
	w := makeAdminReserveRequest(handler, req, adminID, ctx)

	if w.Code == http.StatusConflict {
		t.Errorf("pending_release should NOT block admin reserve – it is evictable, got 409")
	}
	if w.Code != http.StatusCreated {
		t.Errorf("expected 201 for admin reserve with only pending_release overlap, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestAdminReserveSlot_SlotOverlap tests that a reservation fails when the
// requested time slot overlaps with an existing booking.
