//go:build test && dev

package payments

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

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

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

func makePaymentRequest(handler http.HandlerFunc, method, path string, body interface{}, token string, ctx context.Context) *httptest.ResponseRecorder {
	return makePaymentAuthRequest(handler, method, path, body, token, "", ctx)
}

func makePaymentAuthRequest(handler http.HandlerFunc, method, path string, body interface{}, token, userIDOverride string, ctx context.Context) *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)
	}
	if token != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}

	rctx := chi.NewRouteContext()
	if id, paramName := extractPaymentIDFromPath(path); id != "" {
		rctx.URLParams.Add(paramName, id)
	}
	ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)

	var userID, userRole string
	if userIDOverride != "" {
		userID = userIDOverride
		userRole = "verified_email"
	} else if token != "" {
		if info := extractUserFromTestJWT(token); info != nil {
			userID = info.userID
			userRole = info.role
		}
	}

	if userID != "" {
		ctx = context.WithValue(ctx, mw.UserIDKey, userID)
		ctx = context.WithValue(ctx, mw.UserRoleKey, userRole)
	}

	req = req.WithContext(ctx)

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

type paymentUserInfo struct {
	userID string
	role   string
}

func extractUserFromTestJWT(token string) *paymentUserInfo {
	parts := splitToken(token)
	if len(parts) != 3 {
		return nil
	}

	decoded, err := base64URLDecode(parts[1])
	if err != nil {
		return nil
	}

	var claims map[string]interface{}
	if err := json.Unmarshal(decoded, &claims); err != nil {
		return nil
	}

	userID, _ := claims["user_id"].(string)
	role, _ := claims["role"].(string)

	if userID == "" {
		return nil
	}

	return &paymentUserInfo{userID: userID, role: role}
}

func splitToken(token string) []string {
	var result []string
	var current []byte
	for _, c := range token {
		if c == '.' {
			result = append(result, string(current))
			current = nil
		} else {
			current = append(current, byte(c))
		}
	}
	if len(current) > 0 {
		result = append(result, string(current))
	}
	return result
}

func base64URLDecode(s string) ([]byte, error) {
	return base64.RawURLEncoding.DecodeString(s)
}

func extractPaymentIDFromPath(path string) (string, string) {
	patterns := []struct {
		prefix    string
		paramName string
	}{
		{"/api/admin/payments/", "payment_id"},
		{"/api/admin/bookings/", "id"},
		{"/api/bookings/", "id"},
		{"/api/user/payment-methods/", "id"},
	}
	for _, p := range patterns {
		if idx := findPaymentLastSegment(path, p.prefix); idx >= 0 {
			endIdx := len(path)
			for i := idx; i < len(path); i++ {
				if path[i] == '/' {
					endIdx = i
					break
				}
			}
			return path[idx:endIdx], p.paramName
		}
	}
	return "", ""
}

func findPaymentLastSegment(path, prefix string) int {
	for i := len(path) - 1; i >= len(prefix); i-- {
		if len(path) > i && path[i-len(prefix):i] == prefix {
			return i
		}
	}
	return -1
}

func parsePaymentResponseBody(w *httptest.ResponseRecorder, dest interface{}) error {
	return json.Unmarshal(w.Body.Bytes(), dest)
}

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

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

	adminToken := jwt.GenerateAdminToken()

	req := CreateTerminalPaymentRequest{
		Amount:      5000,
		PaymentType: "full",
		TipEnabled:  true,
	}

	handler := CreateTerminalPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/payment", req, adminToken, ctx)

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

	var resp CheckoutResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.CheckoutID == "" {
		t.Error("expected checkout ID to be set")
	}

	if resp.Status != "PENDING" {
		t.Errorf("expected status PENDING, got %s", resp.Status)
	}

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

func setupTestData(t *testing.T, ctx context.Context, q db.Querier) (string, string, string) {
	return setupTestDataAtTime(t, ctx, q, time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC))
}

// setupTestDataPast creates a booking with start_time in the past (1 hour ago)
// to prevent payment-split logic from triggering. Used by tests that verify
// payment sequencing or idempotency rather than deposit allocation.
func setupTestDataPast(t *testing.T, ctx context.Context, q db.Querier) (string, string, string) {
	return setupTestDataAtTime(t, ctx, q, clock.Now().Add(-1*time.Hour))
}

func setupTestDataAtTime(t *testing.T, ctx context.Context, q db.Querier, startTime time.Time) (string, string, string) {
	userID, err := fixtures.CreateTestUser(q)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

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

	bookingID, err := fixtures.CreateTestBookingAtTime(q, userID, serviceID, startTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}

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

	return userID, bookingID, serviceID
}

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

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

	adminToken := jwt.GenerateAdminToken()

	overrideAmount := int64(3000)
	req := CreateTerminalPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		OverrideAmount: &overrideAmount,
		TipEnabled:     false,
	}

	handler := CreateTerminalPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/payment", req, adminToken, ctx)

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

	var resp CheckoutResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}
}

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

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

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

	adminToken := jwt.GenerateAdminToken()

	req := CreateTerminalPaymentRequest{
		Amount:      5000,
		PaymentType: "full",
	}

	handler := CreateTerminalPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/bookings/"+bookingID+"/payment", req, adminToken, ctx)

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

	_ = serviceID
}

func TestTerminalPayment_BookingNotFound(t *testing.T) {
	t.Parallel()
	ctx, _ := testutils.SetupTestTx(t)

	adminToken := jwt.GenerateAdminToken()

	req := CreateTerminalPaymentRequest{
		Amount:      5000,
		PaymentType: "full",
	}

	handler := CreateTerminalPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/bookings/non-existent/payment", req, adminToken, ctx)

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

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

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

	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:test-card-nonce"
	req := CreateBookingPaymentRequest{
		Amount:         2500,
		PaymentType:    "deposit",
		NewCardToken:   &cardToken,
		SaveCard:       true,
		IdempotencyKey: "deposit-key-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

	var resp PaymentResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.ID == "" {
		t.Error("expected payment ID to be set")
	}

	if resp.Status != "completed" {
		t.Errorf("expected status completed, got %s", resp.Status)
	}

	if resp.Amount != 2500 {
		t.Errorf("expected amount 2500, got %d", resp.Amount)
	}

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

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

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

	cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "cfa_mock_card_123", "VISA", "4242")
	if err != nil {
		t.Fatalf("failed to create payment method: %v", err)
	}

	userToken := jwt.GenerateUserToken(userID)

	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		CardID:         &cardID,
		IdempotencyKey: "saved-card-key-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

	var resp PaymentResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.Status != "completed" {
		t.Errorf("expected status completed, got %s", resp.Status)
	}
}

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

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

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

	userToken := jwt.GenerateUserToken(otherUserID)

	cardToken := "cnon:test-card-nonce"
	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "not-owned-key-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

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

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

	_, err = fixtures.CreateTestPaymentMethod(tx, userID, "cfa_card_1", "VISA", "1111")
	if err != nil {
		t.Fatalf("failed to create payment method 1: %v", err)
	}

	_, err = fixtures.CreateTestPaymentMethod(tx, userID, "cfa_card_2", "MASTERCARD", "2222")
	if err != nil {
		t.Fatalf("failed to create payment method 2: %v", err)
	}

	userToken := jwt.GenerateUserToken(userID)

	handler := GetUserPaymentMethods
	w := makePaymentRequest(handler, "GET", "/api/user/payment-methods", nil, userToken, ctx)

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

	var cards []SavedCard
	if err := parsePaymentResponseBody(w, &cards); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if len(cards) != 2 {
		t.Errorf("expected 2 cards, got %d", len(cards))
	}
}

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

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

	cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "cfa_card_delete", "VISA", "9999")
	if err != nil {
		t.Fatalf("failed to create payment method: %v", err)
	}

	userToken := jwt.GenerateUserToken(userID)

	handler := DeletePaymentMethod
	w := makePaymentRequest(handler, "DELETE", "/api/user/payment-methods/"+cardID, nil, userToken, ctx)

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

	var resp map[string]string
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp["status"] != "deleted" {
		t.Errorf("expected status deleted, got %s", resp["status"])
	}
}

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

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

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

	adminToken := jwt.GenerateTestToken(adminID, "admin")

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

	squarePaymentID := "sqp_test_123"
	_, err = tx.Exec(ctx, "UPDATE payments SET square_payment_id = $1 WHERE id = $2", squarePaymentID, paymentID)
	if err != nil {
		t.Fatalf("failed to update payment: %v", err)
	}

	req := RefundRequest{
		Amount: 5000,
		Reason: "customer request",
	}

	handler := RefundPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/payments/"+paymentID+"/refund", req, adminToken, ctx)

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

	var resp RefundResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.Amount != 5000 {
		t.Errorf("expected amount 5000, got %d", resp.Amount)
	}

	if resp.Status != "completed" {
		t.Errorf("expected status completed, got %s", resp.Status)
	}
}

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

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

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

	adminToken := jwt.GenerateTestToken(adminID, "admin")

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

	squarePaymentID := "sqp_test_456"
	_, err = tx.Exec(ctx, "UPDATE payments SET square_payment_id = $1 WHERE id = $2", squarePaymentID, paymentID)
	if err != nil {
		t.Fatalf("failed to update payment: %v", err)
	}

	req := RefundRequest{
		Amount: 2500,
		Reason: "partial refund",
	}

	handler := RefundPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/payments/"+paymentID+"/refund", req, adminToken, ctx)

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

	var resp RefundResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.Amount != 2500 {
		t.Errorf("expected amount 2500, got %d", resp.Amount)
	}
}

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

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

	adminToken := jwt.GenerateAdminToken()

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

	squarePaymentID := "sqp_test_789"
	_, err = tx.Exec(ctx, "UPDATE payments SET square_payment_id = $1 WHERE id = $2", squarePaymentID, paymentID)
	if err != nil {
		t.Fatalf("failed to update payment: %v", err)
	}

	req := RefundRequest{
		Amount: 6000,
		Reason: "over refund attempt",
	}

	handler := RefundPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/payments/"+paymentID+"/refund", req, adminToken, ctx)

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

func TestRefund_PaymentNotFound(t *testing.T) {
	t.Parallel()
	ctx, _ := testutils.SetupTestTx(t)

	adminToken := jwt.GenerateAdminToken()

	req := RefundRequest{
		Amount: 1000,
		Reason: "test",
	}

	handler := RefundPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/payments/non-existent/refund", req, adminToken, ctx)

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

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

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

	adminToken := jwt.GenerateAdminToken()

	paymentID, err := fixtures.CreateTestPayment(tx, bookingID, 5000.00, "in_person_card", "full", "pending")
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	req := RefundRequest{
		Amount: 5000,
		Reason: "test",
	}

	handler := RefundPayment
	w := makePaymentRequest(handler, "POST", "/api/admin/payments/"+paymentID+"/refund", req, adminToken, ctx)

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

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

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

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

	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:tip-card"
	req := CreateTipPaymentRequest{
		Amount:    500,
		CardToken: cardToken,
	}

	handler := CreateTipPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/tip", req, userToken, ctx)

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

	var resp PaymentResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.PaymentType != "tip" {
		t.Errorf("expected payment type tip, got %s", resp.PaymentType)
	}

	if resp.Amount != 500 {
		t.Errorf("expected amount 500, got %d", resp.Amount)
	}
}

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

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

	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:tip-card"
	req := CreateTipPaymentRequest{
		Amount:    500,
		CardToken: cardToken,
	}

	handler := CreateTipPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/tip", req, userToken, ctx)

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

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

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

	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:idempotent-card"
	idempotencyKey := "idempotent-same-key"

	req1 := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: idempotencyKey,
	}

	handler := CreateBookingPayment
	w1 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req1, userToken, ctx)

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

	var resp1 PaymentResponse
	if err := parsePaymentResponseBody(w1, &resp1); err != nil {
		t.Errorf("failed to parse first response: %v", err)
	}

	req2 := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: idempotencyKey,
	}

	w2 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req2, userToken, ctx)

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

	var resp2 PaymentResponse
	if err := parsePaymentResponseBody(w2, &resp2); err != nil {
		t.Errorf("failed to parse second response: %v", err)
	}

	if resp1.ID != resp2.ID {
		t.Errorf("expected same payment ID, got %s and %s", resp1.ID, resp2.ID)
	}

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

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

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

	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:different-key-card"

	req1 := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "key-1",
	}

	handler := CreateBookingPayment
	w1 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req1, userToken, ctx)

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

	req2 := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "key-2",
	}

	w2 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req2, userToken, ctx)

	// The second request is blocked because only one "full" payment is
	// allowed per booking (the payment-type duplicate guard prevents the
	// two-tab double-payment race even when idempotency keys differ).
	if w2.Code != http.StatusConflict {
		t.Errorf("second request expected status 409, got %d. body: %s", w2.Code, w2.Body.String())
	}

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

// =============================================================================
// Payment-type duplicate guard — serialization lock prevents double payments
// =============================================================================

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

	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "pending_release")

	// First: a deposit payment should succeed.
	cardToken := "cnon:diff-type-card"
	depositReq := CreateBookingPaymentRequest{
		Amount:         2000,
		PaymentType:    "deposit",
		NewCardToken:   &cardToken,
		IdempotencyKey: "diff-type-deposit-" + bookingID,
	}

	handler := CreateBookingPayment
	w1 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", depositReq, userToken, ctx)
	if w1.Code != http.StatusOK {
		t.Fatalf("deposit payment expected 200, got %d. body: %s", w1.Code, w1.Body.String())
	}

	// Second: a balance payment uses a different payment_type — should also succeed.
	balanceReq := CreateBookingPaymentRequest{
		Amount:         3000,
		PaymentType:    "balance",
		NewCardToken:   &cardToken,
		IdempotencyKey: "diff-type-balance-" + bookingID,
	}

	w2 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", balanceReq, userToken, ctx)
	if w2.Code != http.StatusOK {
		t.Errorf("balance payment expected 200 (different type allowed), got %d. body: %s", w2.Code, w2.Body.String())
	}

	// Verify at least one payment of each type exists. buildSplitRecords may
	// create extra records (e.g. a 'balance' portion alongside 'deposit'), so
	// we check DISTINCT types rather than a raw row count.
	var distinctTypes []string
	rows, err := tx.Query(ctx,
		"SELECT DISTINCT payment_type FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house') ORDER BY payment_type",
		bookingID)
	if err != nil {
		t.Fatalf("failed to query payments: %v", err)
	}
	defer rows.Close()
	for rows.Next() {
		var pt string
		if err := rows.Scan(&pt); err == nil {
			distinctTypes = append(distinctTypes, pt)
		}
	}
	if len(distinctTypes) < 2 {
		t.Errorf("expected at least 2 distinct payment types, got %d: %v", len(distinctTypes), distinctTypes)
	}
}

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

	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "pending_release")

	cardToken := "cnon:dup-type-card"

	// First 'full' payment succeeds.
	req1 := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "dup-type-first-" + bookingID,
	}

	handler := CreateBookingPayment
	w1 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req1, userToken, ctx)
	if w1.Code != http.StatusOK {
		t.Fatalf("first payment expected 200, got %d. body: %s", w1.Code, w1.Body.String())
	}

	// Second 'full' payment with a different idempotency key should be blocked
	// by the payment-type duplicate guard.
	req2 := CreateBookingPaymentRequest{
		Amount:         2000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "dup-type-second-" + bookingID,
	}

	w2 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req2, userToken, ctx)
	if w2.Code != http.StatusConflict {
		t.Errorf("duplicate 'full' payment expected 409, got %d. body: %s", w2.Code, w2.Body.String())
	}

	// Verify only one real payment was created. buildSplitRecords converts the
	// first 'full' payment into 'deposit' + 'balance', so we count deposit records
	// rather than 'full' — the exact guard above confirmed the 409 rejection.
	var depositCount int
	err := tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_type = 'deposit' AND payment_method NOT IN ('discount', 'on_the_house')",
		bookingID).Scan(&depositCount)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if depositCount != 1 {
		t.Errorf("expected 1 deposit record (split from first 'full' payment), got %d", depositCount)
	}
}

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

	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "confirmed")

	cardToken := "cnon:partial-card"

	// First partial payment.
	req1 := CreateBookingPaymentRequest{
		Amount:         1000,
		PaymentType:    "partial",
		NewCardToken:   &cardToken,
		IdempotencyKey: "partial-first-" + bookingID,
	}

	handler := CreateBookingPayment
	w1 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req1, userToken, ctx)
	if w1.Code != http.StatusOK {
		t.Fatalf("first partial expected 200, got %d. body: %s", w1.Code, w1.Body.String())
	}

	// Second partial payment (different key, same type) — allowed because
	// the duplicate guard explicitly exempts 'partial'.
	req2 := CreateBookingPaymentRequest{
		Amount:         1500,
		PaymentType:    "partial",
		NewCardToken:   &cardToken,
		IdempotencyKey: "partial-second-" + bookingID,
	}

	w2 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req2, userToken, ctx)
	if w2.Code != http.StatusOK {
		t.Errorf("second partial expected 200, got %d. body: %s", w2.Code, w2.Body.String())
	}

	// Count all real payments (buildSplitRecords converts partials to deposit
	// when within the 50% deposit cap). Both should have been created.
	var total int
	err := tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house')",
		bookingID).Scan(&total)
	if err != nil {
		t.Fatalf("failed to count payments: %v", err)
	}
	if total != 2 {
		t.Errorf("expected 2 payments (both created), got %d", total)
	}
}

func TestSquareWebhook_DevMode_NoSignature(t *testing.T) {
	t.Parallel()
	t.Skip("webhook handler tested in webhooks package")
}

// ============================================================
// User Booking Payment Tests — deposit, full, partial, balance
// ============================================================

func setupDepositBooking(t *testing.T, ctx context.Context, q db.Querier) (string, string) {
	return setupDepositBookingAtTime(t, ctx, q, time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC))
}

// setupDepositBookingPast creates a confirmed booking with start_time in the past
// (1 hour ago). This prevents the payment-split logic from triggering, which is
// useful for tests that verify payment sequencing rather than deposit splitting.
func setupDepositBookingPast(t *testing.T, ctx context.Context, q db.Querier) (string, string) {
	return setupDepositBookingAtTime(t, ctx, q, clock.Now().Add(-1*time.Hour))
}

func setupDepositBookingAtTime(t *testing.T, ctx context.Context, q db.Querier, startTime time.Time) (string, string) {
	userID, err := fixtures.CreateTestUser(q)
	if err != nil {
		t.Fatalf("failed to create test user: %v", err)
	}

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

	bookingID, err := fixtures.CreateTestBookingAtTime(q, userID, serviceID, startTime)
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}

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

	return userID, bookingID
}

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:deposit-card"
	req := CreateBookingPaymentRequest{
		Amount:         2500,
		PaymentType:    "deposit",
		NewCardToken:   &cardToken,
		SaveCard:       false,
		IdempotencyKey: "deposit-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

	var resp PaymentResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.PaymentType != "deposit" {
		t.Errorf("expected payment type deposit, got %s", resp.PaymentType)
	}

	if resp.Amount != 2500 {
		t.Errorf("expected amount 2500, got %d", resp.Amount)
	}

	if resp.Status != "completed" {
		t.Errorf("expected status completed, got %s", resp.Status)
	}

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

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:full-card"
	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "full-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

	var resp PaymentResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.PaymentType != "full" {
		t.Errorf("expected payment type full, got %s", resp.PaymentType)
	}

	if resp.Amount != 5000 {
		t.Errorf("expected amount 5000, got %d", resp.Amount)
	}
}

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:partial-card"
	req := CreateBookingPaymentRequest{
		Amount:         1500,
		PaymentType:    "partial",
		NewCardToken:   &cardToken,
		IdempotencyKey: "partial-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

	var resp PaymentResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.PaymentType != "partial" {
		t.Errorf("expected payment type partial, got %s", resp.PaymentType)
	}

	if resp.Amount != 1500 {
		t.Errorf("expected amount 1500, got %d", resp.Amount)
	}
}

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:balance-card"
	req := CreateBookingPaymentRequest{
		Amount:         3500,
		PaymentType:    "balance",
		NewCardToken:   &cardToken,
		IdempotencyKey: "balance-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

	var resp PaymentResponse
	if err := parsePaymentResponseBody(w, &resp); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if resp.PaymentType != "balance" {
		t.Errorf("expected payment type balance, got %s", resp.PaymentType)
	}
}

// ---------------------------------------------------------------------------
// Payment-split tests — verify that a single Square charge is recorded as
// multiple payment rows when paid before the booking start time, and that
// both records share the same square_payment_id.
// ---------------------------------------------------------------------------

func TestBookingPayment_FullPayment_SplitsIntoDepositAndBalance(t *testing.T) {
	// A full payment of £50 on a £50 booking (future-dated) should be split:
	//   record 1: payment_type='deposit', amount=25.00
	//   record 2: payment_type='balance', amount=25.00
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:split-full-card"
	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "split-full-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

	// Should be exactly 2 payment records.
	rows, err := tx.Query(ctx,
		`SELECT payment_type, amount, square_payment_id
		 FROM payments WHERE booking_id = $1 ORDER BY amount DESC`, bookingID)
	if err != nil {
		t.Fatalf("failed to query payments: %v", err)
	}
	defer rows.Close()

	var records []struct {
		ptype           string
		amount          float64
		squarePaymentID *string
	}
	for rows.Next() {
		var r struct {
			ptype           string
			amount          float64
			squarePaymentID *string
		}
		if err := rows.Scan(&r.ptype, &r.amount, &r.squarePaymentID); err != nil {
			t.Fatalf("failed to scan row: %v", err)
		}
		records = append(records, r)
	}

	if len(records) != 2 {
		t.Fatalf("expected 2 split records, got %d", len(records))
	}

	// First record should be the deposit portion (larger or equal — deposit is 25, balance is 25).
	if records[0].ptype != "deposit" {
		t.Errorf("expected first record to be 'deposit', got %q", records[0].ptype)
	}
	// Second record should be balance.
	if records[1].ptype != "balance" {
		t.Errorf("expected second record to be 'balance', got %q", records[1].ptype)
	}

	// Both records must share the same square_payment_id.
	if records[0].squarePaymentID == nil || records[1].squarePaymentID == nil {
		t.Error("both records should have a square_payment_id")
	} else if *records[0].squarePaymentID != *records[1].squarePaymentID {
		t.Errorf("expected same square_payment_id, got %q and %q",
			*records[0].squarePaymentID, *records[1].squarePaymentID)
	}
}

func TestBookingPayment_FullPayment_PastBooking_DoesNotSplit(t *testing.T) {
	// A full payment on a PAST booking should NOT split (single record).
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

	userID, bookingID := setupDepositBookingPast(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:nosplit-card"
	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "nosplit-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

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

func TestBookingPayment_TransactionAtomicity_SplitRollsBackOnError(t *testing.T) {
	// Verify that when the split-record insert fails, the entire group rolls
	// back atomically. We simulate a failure by causing the second INSERT to
	// violate a NOT NULL constraint (passing an invalid record).
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

	// Use a nil idempotency key on the split record — this works fine for both.
	// Instead we rely on the fact that the handler wraps both inserts in a
	// single transaction: if either fails, neither survives.
	//
	// Because we can't easily inject a DB error through the handler, we verify
	// the architecture at the service level instead:

	innerTx, err := db.Conn.Begin(ctx)
	if err != nil {
		t.Fatalf("failed to begin tx: %v", err)
	}
	defer innerTx.Rollback(ctx)

	svc := NewPaymentService()

	now := clock.Now()
	// First record — valid.
	pid1, err := svc.CreatePaymentRecordTx(ctx, innerTx, PaymentRecord{
		BookingID:     bookingID,
		PaymentType:   "deposit",
		PaymentMethod: "cash",
		Status:        "completed",
		Amount:        25.00,
		CreatedAt:     now,
		UpdatedAt:     now,
	}, nil)
	if err != nil {
		t.Fatalf("failed to create first payment record: %v", err)
	}
	if pid1 == "" {
		t.Fatal("expected non-empty payment id")
	}

	// Second record — also valid.
	pid2, err := svc.CreatePaymentRecordTx(ctx, innerTx, PaymentRecord{
		BookingID:     bookingID,
		PaymentType:   "balance",
		PaymentMethod: "cash",
		Status:        "completed",
		Amount:        25.00,
		CreatedAt:     now,
		UpdatedAt:     now,
	}, nil)
	if err != nil {
		t.Fatalf("failed to create second payment record: %v", err)
	}
	if pid2 == "" {
		t.Fatal("expected non-empty payment id")
	}

	if err := innerTx.Commit(ctx); err != nil {
		t.Fatalf("failed to commit tx: %v", err)
	}

	// Both records should exist.
	var count int
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1 OR id = $2", pid1, pid2).Scan(&count)
	if count != 2 {
		t.Errorf("expected 2 committed records, got %d", count)
	}

	// Now test rollback: start a new inner tx, insert, then rollback.
	tx2, err := db.Conn.Begin(ctx)
	if err != nil {
		t.Fatalf("failed to begin tx2: %v", err)
	}

	pid3, err := svc.CreatePaymentRecordTx(ctx, tx2, PaymentRecord{
		BookingID:     bookingID,
		PaymentType:   "deposit",
		PaymentMethod: "cash",
		Status:        "completed",
		Amount:        10.00,
		CreatedAt:     now,
		UpdatedAt:     now,
	}, nil)
	if err != nil {
		t.Fatalf("failed to create rolled-back record: %v", err)
	}

	tx2.Rollback(ctx)

	// Rolled-back record should NOT exist.
	var rollbackCount int
	tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE id = $1", pid3).Scan(&rollbackCount)
	if rollbackCount != 0 {
		t.Errorf("expected 0 records after rollback, got %d", rollbackCount)
	}
}

// ---------------------------------------------------------------------------
// nonDepositPaymentType unit tests — pure function, no DB needed.
// ---------------------------------------------------------------------------

func TestNonDepositPaymentType_FirstPaymentFull(t *testing.T) {
	// First-ever payment, paying full amount → "full"
	result := nonDepositPaymentType("full", 100, 100, 100)
	if result != "full" {
		t.Errorf("expected 'full', got %q", result)
	}
}

func TestNonDepositPaymentType_BalanceWhenPriorExists(t *testing.T) {
	// Total paid after this = 100, portion = 50, prior = 50 → "balance"
	result := nonDepositPaymentType("balance", 100, 50, 100)
	if result != "balance" {
		t.Errorf("expected 'balance', got %q", result)
	}
}

func TestNonDepositPaymentType_PartialWhenUnderTotal(t *testing.T) {
	// Paying 30 on a 100 total → "partial"
	result := nonDepositPaymentType("partial", 30, 30, 100)
	if result != "partial" {
		t.Errorf("expected 'partial', got %q", result)
	}

	// Same result when request type is "full" but amount doesn't cover total
	result = nonDepositPaymentType("full", 80, 80, 100)
	if result != "partial" {
		t.Errorf("expected 'partial' when full doesn't cover total, got %q", result)
	}
}

func TestNonDepositPaymentType_FullWhenFirstPaymentFullyCovers(t *testing.T) {
	// First payment ever, exactly covers total → "full"
	result := nonDepositPaymentType("full", 100, 100, 100)
	if result != "full" {
		t.Errorf("expected 'full', got %q", result)
	}
}

func TestNonDepositPaymentType_DepositReqTypeBecomesPartial(t *testing.T) {
	// Request type is "deposit" but amount doesn't fully cover → "partial"
	result := nonDepositPaymentType("deposit", 30, 30, 100)
	if result != "partial" {
		t.Errorf("expected 'partial' for deposit type under total, got %q", result)
	}
}

// ---------------------------------------------------------------------------
// buildSplitRecords unit tests — pure function, no DB needed.
// ---------------------------------------------------------------------------

func makeTestRecord(bookingID, ptype string, amount float64) PaymentRecord {
	now := clock.Now()
	key := "test-key"
	return PaymentRecord{
		BookingID:       bookingID,
		PaymentType:     ptype,
		PaymentMethod:   "online_square",
		Status:          "completed",
		Amount:          amount,
		SquarePaymentID: strPtr("sq_test"),
		IdempotencyKey:  &key,
		Fees:            1.50,
		CreatedAt:       now,
		UpdatedAt:       now,
	}
}

func strPtr(s string) *string { return &s }

func TestBuildSplitRecords_FutureBooking_FullPayment_Splits(t *testing.T) {
	// £50 payment on a £50 future booking → splits into deposit £25 + balance £25
	record := makeTestRecord("b1", "full", 50)
	info := &BookingPaymentInfo{
		StartTime:   clock.Now().Add(48 * time.Hour),
		TotalAmount: 50,
		TotalPaid:   0,
	}
	records := buildSplitRecords(record, "full", info, 50)

	if len(records) != 2 {
		t.Fatalf("expected 2 records, got %d", len(records))
	}
	if records[0].PaymentType != "deposit" {
		t.Errorf("expected first record 'deposit', got %q", records[0].PaymentType)
	}
	if records[0].Amount != 25 {
		t.Errorf("expected first record amount 25, got %.2f", records[0].Amount)
	}
	if records[1].PaymentType != "balance" {
		t.Errorf("expected second record 'balance', got %q", records[1].PaymentType)
	}
	if records[1].Amount != 25 {
		t.Errorf("expected second record amount 25, got %.2f", records[1].Amount)
	}
	// Both share the same SquarePaymentID.
	if *records[0].SquarePaymentID != *records[1].SquarePaymentID {
		t.Error("split records must share square_payment_id")
	}
	// Split record has separate idempotency key.
	if *records[1].IdempotencyKey != *records[0].IdempotencyKey+"-split-1" {
		t.Errorf("split key should be derived, got %q", *records[1].IdempotencyKey)
	}
	// Split record has zero fees (all on primary).
	if records[1].Fees != 0 {
		t.Errorf("expected split fees=0, got %.2f", records[1].Fees)
	}
}

func TestBuildSplitRecords_PastBooking_NoSplit(t *testing.T) {
	// Same amount on a PAST booking → single record
	record := makeTestRecord("b2", "full", 50)
	info := &BookingPaymentInfo{
		StartTime:   clock.Now().Add(-2 * time.Hour),
		TotalAmount: 50,
		TotalPaid:   0,
	}
	records := buildSplitRecords(record, "full", info, 50)

	if len(records) != 1 {
		t.Fatalf("expected 1 record (no split), got %d", len(records))
	}
	if records[0].PaymentType != "full" {
		t.Errorf("expected 'full', got %q", records[0].PaymentType)
	}
}

func TestBuildSplitRecords_DepositWithinCap_NoSplit(t *testing.T) {
	// £20 deposit on a £50 total (40% < 50% cap) → single deposit record
	record := makeTestRecord("b3", "deposit", 20)
	info := &BookingPaymentInfo{
		StartTime:   clock.Now().Add(48 * time.Hour),
		TotalAmount: 50,
		TotalPaid:   0,
	}
	records := buildSplitRecords(record, "deposit", info, 20)

	if len(records) != 1 {
		t.Fatalf("expected 1 record (within cap), got %d", len(records))
	}
	if records[0].PaymentType != "deposit" {
		t.Errorf("expected 'deposit', got %q", records[0].PaymentType)
	}
}

func TestBuildSplitRecords_PaymentLessThanDepositMax_NoSplit(t *testing.T) {
	// £25 on a £100 total (25% < 50% cap) → single deposit record
	record := makeTestRecord("b4", "deposit", 25)
	info := &BookingPaymentInfo{
		StartTime:   clock.Now().Add(48 * time.Hour),
		TotalAmount: 100,
		TotalPaid:   0,
	}
	records := buildSplitRecords(record, "deposit", info, 25)

	if len(records) != 1 {
		t.Fatalf("expected 1 record (under 50%%), got %d", len(records))
	}
}

// ---------------------------------------------------------------------------
// Handler-level atomicity — verify the full handler succeeds with split.
// ---------------------------------------------------------------------------

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:atomic-card"
	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "atomic-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

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

	if resp.ID == "" {
		t.Fatal("expected non-empty payment ID")
	}
	if resp.Amount != 5000 {
		t.Errorf("expected amount 5000, got %d", resp.Amount)
	}
	if resp.PaymentType != "full" {
		t.Errorf("expected payment type 'full' in response, got %q", resp.PaymentType)
	}

	// Verify both split records exist and the total paid is correct.
	var recordCount int
	tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND status = 'completed'", bookingID).Scan(&recordCount)
	if recordCount != 2 {
		t.Errorf("expected 2 completed payment records from split, got %d", recordCount)
	}

	var totalPaid float64
	tx.QueryRow(ctx,
		"SELECT COALESCE(SUM(amount), 0) FROM payments WHERE booking_id = $1 AND status = 'completed'", bookingID).Scan(&totalPaid)
	if totalPaid != 50.00 {
		t.Errorf("expected total paid £50.00, got £%.2f", totalPaid)
	}

	// Deposit threshold should have been met — verify booking promoted from pending_release.
	var status string
	tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if status == "pending_release" {
		t.Error("expected booking to be promoted from pending_release after payment meets 20% threshold")
	}
}

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:zero-card"
	req := CreateBookingPaymentRequest{
		Amount:         0,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "zero-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:neg-card"
	req := CreateBookingPaymentRequest{
		Amount:         -100,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "neg-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:invalid-type-card"
	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "invalid_type",
		NewCardToken:   &cardToken,
		IdempotencyKey: "invalid-type-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, userToken, ctx)

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

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

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

	cardToken := "cnon:no-auth-card"
	req := CreateBookingPaymentRequest{
		Amount:         5000,
		PaymentType:    "full",
		NewCardToken:   &cardToken,
		IdempotencyKey: "no-auth-test-1",
	}

	handler := CreateBookingPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req, "", ctx)

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

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

	userID, bookingID := setupDepositBooking(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:deposit-balance-card"

	req1 := CreateBookingPaymentRequest{
		Amount:         2500,
		PaymentType:    "deposit",
		NewCardToken:   &cardToken,
		IdempotencyKey: "deposit-balance-1",
	}

	handler := CreateBookingPayment
	w1 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req1, userToken, ctx)

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

	req2 := CreateBookingPaymentRequest{
		Amount:         2500,
		PaymentType:    "balance",
		NewCardToken:   &cardToken,
		IdempotencyKey: "deposit-balance-2",
	}

	w2 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req2, userToken, ctx)

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

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

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

	// Past booking to avoid payment-split; we're testing sequence not deposit allocation.
	userID, bookingID := setupDepositBookingPast(t, ctx, tx)
	userToken := jwt.GenerateUserToken(userID)

	cardToken := "cnon:partial-balance-card"

	req1 := CreateBookingPaymentRequest{
		Amount:         1000,
		PaymentType:    "partial",
		NewCardToken:   &cardToken,
		IdempotencyKey: "partial-balance-1",
	}

	handler := CreateBookingPayment
	w1 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req1, userToken, ctx)

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

	req2 := CreateBookingPaymentRequest{
		Amount:         4000,
		PaymentType:    "balance",
		NewCardToken:   &cardToken,
		IdempotencyKey: "partial-balance-2",
	}

	w2 := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/payment", req2, userToken, ctx)

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

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

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

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

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

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

	otherToken := jwt.GenerateUserToken(otherUserID)

	req := CreateTipPaymentRequest{
		Amount:    500,
		CardToken: "cnon:wrong-owner-tip",
	}

	handler := CreateTipPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/tip", req, otherToken, ctx)

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

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

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

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

	userToken := jwt.GenerateUserToken(userID)

	for i := 0; i < 3; i++ {
		req := CreateTipPaymentRequest{
			Amount:    int64(200 + i*100),
			CardToken: fmt.Sprintf("cnon:multi-tip-%d", i),
		}

		handler := CreateTipPayment
		w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/tip", req, userToken, ctx)

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

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

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

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

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

	userToken := jwt.GenerateUserToken(userID)

	cancelCtx, cancel := context.WithCancel(ctx)
	cancel()

	req := CreateTipPaymentRequest{
		Amount:    500,
		CardToken: "cnon:tip-card",
	}

	handler := CreateTipPayment
	w := makePaymentRequest(handler, "POST", "/api/bookings/"+bookingID+"/tip", req, userToken, cancelCtx)

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

	var completedTipCount int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM payments WHERE booking_id = $1 AND payment_type = 'tip' AND status = 'completed'", bookingID).Scan(&completedTipCount)
	if err != nil {
		t.Errorf("failed to query completed tip payments: %v", err)
	}
	if completedTipCount != 0 {
		t.Errorf("expected 0 completed tip payments, got %d", completedTipCount)
	}
}

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

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

	userToken := jwt.GenerateUserToken(userID)

	handler := GetUserPaymentMethods
	w := makePaymentRequest(handler, "GET", "/api/user/payment-methods", nil, userToken, ctx)

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

	var cards []SavedCard
	if err := parsePaymentResponseBody(w, &cards); err != nil {
		t.Errorf("failed to parse response: %v", err)
	}

	if len(cards) != 0 {
		t.Errorf("expected 0 cards, got %d", len(cards))
	}
}

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

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

	cardID, err := fixtures.CreateTestPaymentMethod(tx, userID, "cfa_wrong_owner", "VISA", "0000")
	if err != nil {
		t.Fatalf("failed to create payment method: %v", err)
	}

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

	otherToken := jwt.GenerateUserToken(otherUserID)

	handler := DeletePaymentMethod
	w := makePaymentRequest(handler, "DELETE", "/api/user/payment-methods/"+cardID, nil, otherToken, ctx)

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

	var count int
	err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM user_saved_cards WHERE id = $1 AND deleted_at IS NULL", cardID).Scan(&count)
	if err != nil {
		t.Errorf("failed to query card: %v", err)
	}
	if count != 1 {
		t.Error("expected card to still exist (not deleted by wrong owner)")
	}
}

func TestValidatePartialAmount(t *testing.T) {
	tests := []struct {
		name           string
		amountCents    int64
		remainingCents int64
		expectErr      bool
	}{
		{"valid partial", 500, 1000, false},
		{"exact remaining", 1000, 1000, false},
		{"exceeds remaining", 1500, 1000, true},
		{"zero amount", 0, 1000, true},
		{"negative amount", -100, 1000, true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := ValidatePartialAmount(tt.amountCents, tt.remainingCents)
			if tt.expectErr && err == nil {
				t.Error("expected error, got nil")
			}
			if !tt.expectErr && err != nil {
				t.Errorf("expected no error, got %v", err)
			}
		})
	}
}

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

	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 service: %v", err)
	}

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

	service := NewPaymentService()

	initialRemaining, err := service.GetBookingRemainingBalanceCents(ctx, bookingID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if initialRemaining <= 0 {
		t.Fatalf("expected positive remaining balance, got %d", initialRemaining)
	}

	_, err = service.CreatePaymentRecord(ctx, PaymentRecord{
		BookingID:     bookingID,
		PaymentType:   "partial",
		PaymentMethod: "cash",
		Status:        "completed",
		Amount:        20.00,
	}, nil)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	afterPartial, err := service.GetBookingRemainingBalanceCents(ctx, bookingID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if afterPartial != initialRemaining-2000 {
		t.Errorf("expected %d cents remaining after £20 payment, got %d", initialRemaining-2000, afterPartial)
	}

	_, err = service.CreatePaymentRecord(ctx, PaymentRecord{
		BookingID:     bookingID,
		PaymentType:   "balance",
		PaymentMethod: "cash",
		Status:        "completed",
		Amount:        float64(afterPartial) / 100.0,
	}, nil)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	afterFull, err := service.GetBookingRemainingBalanceCents(ctx, bookingID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if afterFull != 0 {
		t.Errorf("expected 0 cents remaining after full payment, got %d", afterFull)
	}
}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := CreatePaymentMethod
	reqBody := CreatePaymentMethodRequest{
		CardNumber: "4111111111111111",
		Expiry:     "12/30",
		CVC:        "123",
	}

	w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody, token, ctx)

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

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

	if card.Brand != "VISA" {
		t.Errorf("expected brand VISA, got %s", card.Brand)
	}
	if card.Last4 != "1111" {
		t.Errorf("expected last4 1111, got %s", card.Last4)
	}
	if !card.IsDefault {
		t.Error("expected first card to be default")
	}
}

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

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

	token := jwt.GenerateUserToken(userID)

	handler := CreatePaymentMethod
	reqBody := CreatePaymentMethodRequest{
		CardNumber: "4111111111111111",
		Expiry:     "01/20",
		CVC:        "123",
	}

	w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody, token, ctx)

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

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

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

	token := jwt.GenerateUserToken(userID)

	tests := []struct {
		name   string
		expiry string
	}{
		{"bad format", "12-30"},
		{"bad month", "13/30"},
		{"bad year", "12/abc"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := CreatePaymentMethod
			reqBody := CreatePaymentMethodRequest{
				CardNumber: "4111111111111111",
				Expiry:     tt.expiry,
				CVC:        "123",
			}

			w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody, token, ctx)

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

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

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

	token := jwt.GenerateUserToken(userID)

	tests := []struct {
		name string
		body CreatePaymentMethodRequest
	}{
		{"no card number", CreatePaymentMethodRequest{Expiry: "12/30", CVC: "123"}},
		{"no expiry", CreatePaymentMethodRequest{CardNumber: "4111111111111111", CVC: "123"}},
		{"no cvc", CreatePaymentMethodRequest{CardNumber: "4111111111111111", Expiry: "12/30"}},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			handler := CreatePaymentMethod
			w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", tt.body, token, ctx)

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

func TestCreatePaymentMethod_NoAuthRejected(t *testing.T) {
	t.Parallel()
	ctx, _ := testutils.SetupTestTx(t)

	handler := CreatePaymentMethod
	w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", CreatePaymentMethodRequest{
		CardNumber: "4111111111111111",
		Expiry:     "12/30",
		CVC:        "123",
	}, "", ctx)

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

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

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

	token := jwt.GenerateUserToken(userID)

	// Create first card
	handler := CreatePaymentMethod
	reqBody := CreatePaymentMethodRequest{
		CardNumber: "4111111111111111",
		Expiry:     "12/30",
		CVC:        "123",
	}

	w := makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody, token, ctx)
	if w.Code != http.StatusOK {
		t.Fatalf("failed to create first card: %d. body: %s", w.Code, w.Body.String())
	}

	reqBody2 := CreatePaymentMethodRequest{
		CardNumber: "5500000000000004",
		Expiry:     "06/30",
		CVC:        "456",
	}

	w = makePaymentRequest(handler, "POST", "/api/user/payment-methods", reqBody2, token, ctx)
	if w.Code != http.StatusOK {
		t.Fatalf("failed to create second card: %d. body: %s", w.Code, w.Body.String())
	}

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

	if card.Brand != "MASTERCARD" {
		t.Errorf("expected brand MASTERCARD, got %s", card.Brand)
	}
	if card.IsDefault {
		t.Error("expected second card to NOT be default")
	}
}
