//go:build test && dev

package bookings

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

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

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

// cancelReservationRequest creates and serves a DELETE /api/bookings/reserve request.
// The ctx carries the test transaction from SetupTestTx so the handler's
// db.Conn.Exec call routes through the same transaction.
// If userID is non-empty, it sets up the auth context (simulating RequireAuth).
func cancelReservationRequest(ctx context.Context, userID, role, token string) *httptest.ResponseRecorder {
	req := httptest.NewRequest("DELETE", "/api/bookings/reserve", nil)
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	// Start with the test transaction context so db.Conn.Exec routes through it
	rctx := chi.NewRouteContext()
	reqCtx := context.WithValue(ctx, chi.RouteCtxKey, rctx)
	if userID != "" {
		reqCtx = context.WithValue(reqCtx, mw.UserIDKey, userID)
		reqCtx = context.WithValue(reqCtx, mw.UserRoleKey, role)
	}
	req = req.WithContext(reqCtx)

	w := httptest.NewRecorder()
	http.HandlerFunc(CancelReservationHandler).ServeHTTP(w, req)
	return w
}

// TestCancelReservation_Success creates a reservation for the user, cancels it,
// and verifies the time_blocker is deleted from the database.
func TestCancelReservation_Success(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	token := jwt.GenerateUserToken(userID)
	startTime := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)

	// Create a reservation for this user
	_, err = tx.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:12345', $2)
	`, userID), startTime, userID)
	if err != nil {
		t.Fatalf("failed to create reservation: %v", err)
	}

	// Verify reservation exists before cancel
	var countBefore int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		userID,
	).Scan(&countBefore)
	if err != nil {
		t.Fatalf("failed to count reservations before: %v", err)
	}
	if countBefore != 1 {
		t.Fatalf("expected 1 reservation before cancel, got %d", countBefore)
	}

	// Call CancelReservationHandler
	w := cancelReservationRequest(ctx, userID, "verified_email", token)

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

	// Parse response
	var resp map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to parse response: %v", err)
	}
	if resp["status"] != "reservation cancelled" {
		t.Errorf("expected status 'reservation cancelled', got %q", resp["status"])
	}

	// Verify reservation was deleted
	var countAfter int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		userID,
	).Scan(&countAfter)
	if err != nil {
		t.Fatalf("failed to count reservations after: %v", err)
	}
	if countAfter != 0 {
		t.Errorf("expected 0 reservations after cancel, got %d", countAfter)
	}
}

// TestCancelReservation_NoActiveReservation verifies that calling cancel
// without an active reservation returns 200 (idempotent, no error).
func TestCancelReservation_NoActiveReservation(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	token := jwt.GenerateUserToken(userID)

	// Verify no reservations exist
	var countBefore int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		userID,
	).Scan(&countBefore)
	if err != nil {
		t.Fatalf("failed to count reservations before: %v", err)
	}
	if countBefore != 0 {
		t.Fatalf("expected 0 reservations before cancel, got %d", countBefore)
	}

	// Call CancelReservationHandler
	w := cancelReservationRequest(ctx, userID, "verified_email", token)

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

	// Verify still no reservations
	var countAfter int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		userID,
	).Scan(&countAfter)
	if err != nil {
		t.Fatalf("failed to count reservations after: %v", err)
	}
	if countAfter != 0 {
		t.Errorf("expected 0 reservations after cancel, got %d", countAfter)
	}
}

// TestCancelReservation_Unauthenticated verifies that calling the handler
// without a valid user in context returns 401 Unauthorized.
func TestCancelReservation_Unauthenticated(t *testing.T) {
	t.Parallel()

	w := cancelReservationRequest(context.Background(), "", "", "")

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

// TestCancelReservation_EmptyUserIDInContext verifies that having a context
// with an empty userID string is treated as unauthenticated.
func TestCancelReservation_EmptyUserIDInContext(t *testing.T) {
	t.Parallel()

	w := cancelReservationRequest(context.Background(), "", "verified_email", "")

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

// TestCancelReservation_DeletesOnlyOwnUserReservation verifies that cancelling
// only deletes the requesting user's reservation, not another user's.
func TestCancelReservation_DeletesOnlyOwnUserReservation(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	user1ID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create user1: %v", err)
	}
	user2ID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create user2: %v", err)
	}

	token1 := jwt.GenerateUserToken(user1ID)
	startTime := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)

	// Create reservation for user1
	_, err = tx.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:11111', $2)
	`, user1ID), startTime, user1ID)
	if err != nil {
		t.Fatalf("failed to create user1 reservation: %v", err)
	}

	// Create reservation for user2
	_, err = tx.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:22222', $2)
	`, user2ID), startTime, user2ID)
	if err != nil {
		t.Fatalf("failed to create user2 reservation: %v", err)
	}

	// Call CancelReservationHandler as user1
	w := cancelReservationRequest(ctx, user1ID, "verified_email", token1)

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

	// Verify user1's reservation is gone
	var user1Count int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		user1ID,
	).Scan(&user1Count)
	if err != nil {
		t.Fatalf("failed to count user1 reservations: %v", err)
	}
	if user1Count != 0 {
		t.Errorf("expected user1 reservations to be deleted, got %d", user1Count)
	}

	// Verify user2's reservation still exists
	var user2Count int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		user2ID,
	).Scan(&user2Count)
	if err != nil {
		t.Fatalf("failed to count user2 reservations: %v", err)
	}
	if user2Count != 1 {
		t.Errorf("expected user2 reservation to survive, got %d", user2Count)
	}
}

// TestCancelReservation_CleanIdempotent verifies calling cancel twice
// is safe (second call also succeeds, no error).
func TestCancelReservation_CleanIdempotent(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	token := jwt.GenerateUserToken(userID)
	startTime := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)

	// Create a reservation for this user
	_, err = tx.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:99999', $2)
	`, userID), startTime, userID)
	if err != nil {
		t.Fatalf("failed to create reservation: %v", err)
	}

	// First cancel
	w1 := cancelReservationRequest(ctx, userID, "verified_email", token)
	if w1.Code != http.StatusOK {
		t.Fatalf("first cancel: expected 200, got %d. body: %s", w1.Code, w1.Body.String())
	}

	// Second cancel (no reservation left)
	w2 := cancelReservationRequest(ctx, userID, "verified_email", token)
	if w2.Code != http.StatusOK {
		t.Errorf("second cancel (idempotent): expected 200, got %d. body: %s", w2.Code, w2.Body.String())
	}

	// Verify no reservations remain
	var count int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		userID,
	).Scan(&count)
	if err != nil {
		t.Fatalf("failed to count reservations: %v", err)
	}
	if count != 0 {
		t.Errorf("expected 0 reservations after two cancels, got %d", count)
	}
}

// TestCancelReservation_DoesNotTouchAnonReservations verifies that the user
// cancel handler only targets RESERVATION:user:% — it must NEVER touch
// RESERVATION:anon:% entries (which belong to the pre-auth reservation flow
// with a hashed-IP created_by = NULL).
//
// Anon reservations are cleaned up by the next reserve attempt via the
// pre-overlap DELETE in reserve.go. A separate endpoint (or TTL) handles
// them — user cancel must not interfere.
func TestCancelReservation_DoesNotTouchAnonReservations(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	token := jwt.GenerateUserToken(userID)
	userStart := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
	anonStart := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(14 * time.Hour)

	// Create a user reservation
	_, err = tx.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:11111', $2)
	`, userID), userStart, userID)
	if err != nil {
		t.Fatalf("failed to create user reservation: %v", err)
	}

	// Create an anon reservation (created_by = NULL, description RESERVATION:anon:%)
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:anon:abc12345:22222', NULL)
	`, anonStart)
	if err != nil {
		t.Fatalf("failed to create anon reservation: %v", err)
	}

	// Call user cancel
	w := cancelReservationRequest(ctx, userID, "verified_email", token)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify user reservation is gone
	var userCount int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		userID,
	).Scan(&userCount)
	if err != nil {
		t.Fatalf("failed to count user reservations: %v", err)
	}
	if userCount != 0 {
		t.Errorf("expected user reservation to be deleted, got %d", userCount)
	}

	// Verify anon reservation is untouched
	var anonCount int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:anon:%'`,
	).Scan(&anonCount)
	if err != nil {
		t.Fatalf("failed to count anon reservations: %v", err)
	}
	if anonCount != 1 {
		t.Errorf("expected anon reservation to survive user cancel (separate flow), got %d", anonCount)
	}
}

// TestCancelReservation_DoesNotTouchAdminReservations verifies that the user
// cancel handler only targets RESERVATION:user:% — it must NEVER touch
// RESERVATION:admin:% entries (which belong to the admin walk-in/call-in
// reservation flow with the admin's user ID as created_by).
//
// This is the inverse guarantee of TestAdminCancelReservation_DoesNotDeleteUserReservations
// and verifies the two endpoints are properly partitioned by the WHERE clause.
func TestCancelReservation_DoesNotTouchAdminReservations(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

	token := jwt.GenerateUserToken(userID)
	userStart := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
	adminStart := clock.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(14 * time.Hour)

	// Create a user reservation
	_, err = tx.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:33333', $2)
	`, userID), userStart, userID)
	if err != nil {
		t.Fatalf("failed to create user reservation: %v", err)
	}

	// Create an admin reservation
	_, err = tx.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'RESERVATION:admin:walkin:guest:44444', $2)
	`, adminStart, adminID)
	if err != nil {
		t.Fatalf("failed to create admin reservation: %v", err)
	}

	// Call user cancel as the user (NOT as the admin)
	w := cancelReservationRequest(ctx, userID, "verified_email", token)
	if w.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify user reservation is gone
	var userCount int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:user:%' AND created_by = $1`,
		userID,
	).Scan(&userCount)
	if err != nil {
		t.Fatalf("failed to count user reservations: %v", err)
	}
	if userCount != 0 {
		t.Errorf("expected user reservation to be deleted, got %d", userCount)
	}

	// Verify admin reservation is untouched
	var adminCount int
	err = tx.QueryRow(ctx,
		`SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:admin:%' AND created_by = $1`,
		adminID,
	).Scan(&adminCount)
	if err != nil {
		t.Fatalf("failed to count admin reservations: %v", err)
	}
	if adminCount != 1 {
		t.Errorf("expected admin reservation to survive user cancel (different endpoint), got %d", adminCount)
	}
}
