//go:build test
// +build test

package bookings

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

	"crussell/db"
	"crussell/handlers/scheduling"
	"crussell/mw"
	"crussell/testutils/fixtures"
	"crussell/testutils/jwt"
	"crussell/testutils/testdb"

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

func resetReserveTestData(t *testing.T) {
	t.Helper()
	testdb.TruncateTables(t, db.DB)
	seedDefaultWorkingHours(t)
}

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

	// Add chi middleware stack for proper routing context
	r := chi.NewRouter()
	r.Use(middleware.RequestID)
	r.Use(middleware.RealIP)

	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}

	// Set user context from token if present
	if token != "" {
		if info := extractUserFromTestJWT(token); info != nil {
			rctx := chi.NewRouteContext()
			ctx := context.WithValue(req.Context(), chi.RouteCtxKey, rctx)
			ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
			ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
			req = req.WithContext(ctx)
		}
	}

	w := httptest.NewRecorder()

	handler := http.HandlerFunc(ReserveSlotHandler)
	r.Post("/api/bookings/reserve", handler)
	r.ServeHTTP(w, req)
	return w
}

func reserveTestToken(t *testing.T, userID, role string) string {
	t.Helper()
	return jwt.GenerateTestToken(userID, role)
}

// TestReserveSlot_LoggedIn verifies logged-in users can reserve a slot.
func TestReserveSlot_LoggedIn(t *testing.T) {
	resetReserveTestData(t)

	userID, _ := fixtures.CreateTestUser(db.DB)
	token := reserveTestToken(t, userID, "verified_email")
	serviceID, err := fixtures.CreateTestService(db.DB)
	if err != nil {
		t.Fatalf("failed to create test service: %v", err)
	}
	startTime := time.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)

	reqBody := ReserveSlotRequest{
		StartTime:  startTime,
		ServiceIDs: []string{serviceID},
	}

	w := makeReserveRequest("POST", "/api/bookings/reserve", reqBody, token)

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

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

	if response.IsAnonymous {
		t.Error("expected is_anonymous=false for logged-in user")
	}
	if response.DurationMinutes == 0 {
		t.Error("expected non-zero duration")
	}
}

// TestReserveSlot_LoggedIn_ReplacesExisting verifies creating a second reservation
// for the same user deletes the first one (max 1 per user).
func TestReserveSlot_LoggedIn_ReplacesExisting(t *testing.T) {
	resetReserveTestData(t)

	userID, _ := fixtures.CreateTestUser(db.DB)
	token := reserveTestToken(t, userID, "verified_email")
	serviceID, err := fixtures.CreateTestService(db.DB)
	if err != nil {
		t.Fatalf("failed to create test service: %v", err)
	}
	serviceIDs := []string{serviceID}

	// First reservation
	startTime1 := time.Now().Add(48 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
	w1 := makeReserveRequest("POST", "/api/bookings/reserve", ReserveSlotRequest{
		StartTime:  startTime1,
		ServiceIDs: serviceIDs,
	}, token)
	if w1.Code != http.StatusCreated {
		t.Fatalf("first reservation failed: %d", w1.Code)
	}

	// Second reservation (should replace first)
	startTime2 := time.Now().Add(72 * time.Hour).Truncate(24 * time.Hour).Add(14 * time.Hour)
	w2 := makeReserveRequest("POST", "/api/bookings/reserve", ReserveSlotRequest{
		StartTime:  startTime2,
		ServiceIDs: serviceIDs,
	}, token)
	if w2.Code != http.StatusCreated {
		t.Fatalf("second reservation failed: %d", w2.Code)
	}

	// Verify only one user reservation exists
	var count int
	err = db.DB.QueryRow(context.Background(), `
		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 != 1 {
		t.Errorf("expected 1 reservation for user, got %d", count)
	}
}

// TestReserveSlot_Anonymous verifies anonymous users can reserve a slot.
func TestReserveSlot_Anonymous(t *testing.T) {
	resetReserveTestData(t)

	serviceID, err := fixtures.CreateTestService(db.DB)
	if err != nil {
		t.Fatalf("failed to create test service: %v", err)
	}
	serviceIDs := []string{serviceID}
	startTime := time.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)

	reqBody := ReserveSlotRequest{
		StartTime:  startTime,
		ServiceIDs: serviceIDs,
	}

	w := makeReserveRequest("POST", "/api/bookings/reserve", reqBody, "")

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

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

	if !response.IsAnonymous {
		t.Error("expected is_anonymous=true for anonymous user")
	}
}

// TestReserveSlot_ValidationErrors verifies that missing or invalid
// fields result in 400 Bad Request.
func TestReserveSlot_ValidationErrors(t *testing.T) {
	resetReserveTestData(t)

	serviceID, err := fixtures.CreateTestService(db.DB)
	if err != nil {
		t.Fatalf("failed to create test service: %v", err)
	}
	serviceIDs := []string{serviceID}

	// Missing start_time
	w := makeReserveRequest("POST", "/api/bookings/reserve", map[string]interface{}{
		"service_ids": serviceIDs,
	}, "")
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected 400 for missing start_time, got %d. body: %s", w.Code, w.Body.String())
	}

	// Missing service_ids
	startTime := time.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
	w = makeReserveRequest("POST", "/api/bookings/reserve", map[string]interface{}{
		"start_time": startTime,
	}, "")
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected 400 for missing service_ids, got %d. body: %s", w.Code, w.Body.String())
	}

	// Past start_time
	w = makeReserveRequest("POST", "/api/bookings/reserve", ReserveSlotRequest{
		StartTime:  time.Now().Add(-1 * time.Hour),
		ServiceIDs: serviceIDs,
	}, "")
	if w.Code != http.StatusBadRequest {
		t.Errorf("expected 400 for past start_time, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestReserveSlot_BlockedByExistingBooking verifies that reserving a slot
// that overlaps an existing booking returns 409 Conflict.
func TestReserveSlot_BlockedByExistingBooking(t *testing.T) {
	resetReserveTestData(t)

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

	// Create a fixture user and booking at the same time
	userID, _ := fixtures.CreateTestUser(db.DB)
	bookingStart := time.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
	_, err = db.DB.Exec(context.Background(), `
		INSERT INTO bookings (user_id, start_time, status, deposit_required)
		VALUES ($1, $2, 'confirmed', false)
	`, userID, bookingStart)
	if err != nil {
		t.Fatalf("failed to create booking: %v", err)
	}

	w := makeReserveRequest("POST", "/api/bookings/reserve", ReserveSlotRequest{
		StartTime:  bookingStart,
		ServiceIDs: []string{serviceID},
	}, "")
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for overlapping booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestReserveSlot_BlockedByTimeBlocker verifies that reserving a blocked
// time slot returns 409 Conflict.
func TestReserveSlot_BlockedByTimeBlocker(t *testing.T) {
	resetReserveTestData(t)

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

	blockerStart := time.Now().Add(24 * time.Hour).Truncate(24 * time.Hour).Add(10 * time.Hour)
	_, err = db.DB.Exec(context.Background(), `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
		VALUES ($1, 60, 'Admin Blocked', NULL)
	`, blockerStart)
	if err != nil {
		t.Fatalf("failed to create blocker: %v", err)
	}

	w := makeReserveRequest("POST", "/api/bookings/reserve", ReserveSlotRequest{
		StartTime:  blockerStart,
		ServiceIDs: []string{serviceID},
	}, "")
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for blocked slot, got %d. body: %s", w.Code, w.Body.String())
	}
}

// TestReserveSlot_DualCleanup verifies that CleanupOldReservations deletes
// anon reservations after 10 minutes and user reservations after 1 hour.
func TestReserveSlot_DualCleanup(t *testing.T) {
	resetReserveTestData(t)

	ctx := context.Background()

	// Create fixture users for user reservations
	user1ID, _ := fixtures.CreateTestUser(db.DB)
	user2ID, _ := fixtures.CreateTestUser(db.DB)

	// Create old anon reservation (15 min ago)
	_, err := db.DB.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at, created_by)
		VALUES ($1, 60, 'RESERVATION:anon:abc12345:1234', $2, NULL)
	`, time.Now().Add(24*time.Hour), time.Now().Add(-15*time.Minute))
	if err != nil {
		t.Fatalf("failed to create old anon reservation: %v", err)
	}

	// Create recent anon reservation (5 min ago) - should survive
	_, err = db.DB.Exec(ctx, `
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at, created_by)
		VALUES ($1, 60, 'RESERVATION:anon:def67890:1234', $2, NULL)
	`, time.Now().Add(48*time.Hour), time.Now().Add(-5*time.Minute))
	if err != nil {
		t.Fatalf("failed to create recent anon reservation: %v", err)
	}

	// Create old user reservation (45 min ago) - should survive (> 10min, < 1hr)
	_, err = db.DB.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:1234', $2, $3)
	`, user1ID), time.Now().Add(72*time.Hour), time.Now().Add(-45*time.Minute), user1ID)
	if err != nil {
		t.Fatalf("failed to create old user reservation: %v", err)
	}

	// Create very old user reservation (2 hours ago) - should be deleted
	_, err = db.DB.Exec(ctx, fmt.Sprintf(`
		INSERT INTO time_blockers (start_time, duration_minutes, description, created_at, created_by)
		VALUES ($1, 60, 'RESERVATION:user:%s:1234', $2, $3)
	`, user2ID), time.Now().Add(96*time.Hour), time.Now().Add(-2*time.Hour), user2ID)
	if err != nil {
		t.Fatalf("failed to create very old user reservation: %v", err)
	}

	// Run cleanup
	err = scheduling.CleanupOldReservations(ctx)
	if err != nil {
		t.Fatalf("CleanupOldReservations failed: %v", err)
	}

	// Verify old anon was deleted
	var anonOldCount int
	db.DB.QueryRow(ctx, `SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:anon:abc12345:%'`).Scan(&anonOldCount)
	if anonOldCount > 0 {
		t.Error("expected old anon reservation to be deleted")
	}

	// Verify recent anon survived
	var anonRecentCount int
	db.DB.QueryRow(ctx, `SELECT COUNT(*) FROM time_blockers WHERE description LIKE 'RESERVATION:anon:def67890:%'`).Scan(&anonRecentCount)
	if anonRecentCount != 1 {
		t.Error("expected recent anon reservation to survive")
	}

	// Verify 45-min user reservation survived
	var user45Count int
	db.DB.QueryRow(ctx, `SELECT COUNT(*) FROM time_blockers WHERE description LIKE $1`, fmt.Sprintf("RESERVATION:user:%s:%%", user1ID)).Scan(&user45Count)
	if user45Count != 1 {
		t.Error("expected 45-min user reservation to survive")
	}

	// Verify 2hr user reservation was deleted
	var user2hrCount int
	db.DB.QueryRow(ctx, `SELECT COUNT(*) FROM time_blockers WHERE description LIKE $1`, fmt.Sprintf("RESERVATION:user:%s:%%", user2ID)).Scan(&user2hrCount)
	if user2hrCount > 0 {
		t.Error("expected 2-hour user reservation to be deleted")
	}
}
