//go:build test && dev

package payments

import (
	"context"
	"testing"
	"time"

	"crussell/clock"
	"crussell/db"
	"crussell/testutils"
	"crussell/testutils/fixtures"
)

// =============================================================================
// CalculateRefundForCancellation - Pure function tests
// =============================================================================

func TestCalculateRefundForCancellation_FullRefund_Over72h(t *testing.T) {
	t.Parallel()
	now := time.Date(2099, 12, 28, 8, 0, 0, 0, time.UTC)
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC) // >72h away

	result := CalculateRefundForCancellation(100, 50, now, start)

	if result.Tier != "full_refund_72h" {
		t.Errorf("expected tier 'full_refund_72h', got %q", result.Tier)
	}
	if result.RefundableAmount != 50 {
		t.Errorf("expected refundable 50, got %.2f", result.RefundableAmount)
	}
	if result.KeptAmount != 0 {
		t.Errorf("expected kept 0, got %.2f", result.KeptAmount)
	}
	if result.ProtectedDeposit != 50 {
		t.Errorf("expected protected deposit 50, got %.2f", result.ProtectedDeposit)
	}
}

func TestCalculateRefundForCancellation_PartialRefund_24to72h(t *testing.T) {
	now := time.Date(2099, 12, 30, 8, 0, 0, 0, time.UTC) // ~50h before
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	result := CalculateRefundForCancellation(100, 80, now, start)

	if result.Tier != "partial_refund_24h_72h" {
		t.Errorf("expected tier 'partial_refund_24h_72h', got %q", result.Tier)
	}
	// Protected deposit: min(80, 50) = 50
	// Refundable: 80 - 50 = 30
	if result.ProtectedDeposit != 50 {
		t.Errorf("expected protected deposit 50, got %.2f", result.ProtectedDeposit)
	}
	if result.RefundableAmount != 30 {
		t.Errorf("expected refundable 30, got %.2f", result.RefundableAmount)
	}
	if result.KeptAmount != 50 {
		t.Errorf("expected kept 50, got %.2f", result.KeptAmount)
	}
}

func TestCalculateRefundForCancellation_NoRefund_Under24h(t *testing.T) {
	now := time.Date(2099, 12, 31, 9, 0, 0, 0, time.UTC) // 1h before
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	result := CalculateRefundForCancellation(100, 100, now, start)

	if result.Tier != "no_refund_under_24h" {
		t.Errorf("expected tier 'no_refund_under_24h', got %q", result.Tier)
	}
	if result.RefundableAmount != 0 {
		t.Errorf("expected refundable 0, got %.2f", result.RefundableAmount)
	}
	if result.KeptAmount != 100 {
		t.Errorf("expected kept 100, got %.2f", result.KeptAmount)
	}
}

func TestCalculateRefundForCancellation_NoShow_KeptAll(t *testing.T) {
	now := time.Date(2099, 12, 31, 12, 0, 0, 0, time.UTC) // past start
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	result := CalculateRefundForCancellation(100, 50, now, start)

	if result.Tier != "no_refund_under_24h" {
		t.Errorf("expected tier 'no_refund_under_24h', got %q", result.Tier)
	}
	if result.RefundableAmount != 0 {
		t.Errorf("expected refundable 0 for no-show, got %.2f", result.RefundableAmount)
	}
}

func TestCalculateRefundForCancellation_ProtectedDepositCappedAt50Pct(t *testing.T) {
	now := time.Date(2099, 12, 30, 8, 0, 0, 0, time.UTC)
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	// Paid 200 on a 300 total — protected deposit caps at 150 (50% of 300)
	result := CalculateRefundForCancellation(300, 200, now, start)

	if result.ProtectedDeposit != 150 {
		t.Errorf("expected protected deposit 150 (50%% of 300), got %.2f", result.ProtectedDeposit)
	}
	if result.RefundableAmount != 50 {
		t.Errorf("expected refundable 50 (200-150), got %.2f", result.RefundableAmount)
	}
}

func TestCalculateRefundForCancellation_PaidLessThan50Pct(t *testing.T) {
	now := time.Date(2099, 12, 30, 8, 0, 0, 0, time.UTC)
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	// Paid 30 on a 200 total — protected deposit = min(30, 100) = 30
	result := CalculateRefundForCancellation(200, 30, now, start)

	if result.ProtectedDeposit != 30 {
		t.Errorf("expected protected deposit 30, got %.2f", result.ProtectedDeposit)
	}
	if result.RefundableAmount != 0 {
		t.Errorf("expected refundable 0 (30-30), got %.2f", result.RefundableAmount)
	}
}

func TestCalculateRefundForCancellation_Exact72hBoundary(t *testing.T) {
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	now := start.Add(-72 * time.Hour) // exactly 72h before (not >72)

	result := CalculateRefundForCancellation(100, 100, now, start)

	// Exactly 72h is NOT >72 — falls into partial refund tier
	if result.Tier != "partial_refund_24h_72h" {
		t.Errorf("expected partial refund at exactly 72h, got %q", result.Tier)
	}
}

func TestCalculateRefundForCancellation_Exact24hBoundary(t *testing.T) {
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	now := start.Add(-24 * time.Hour) // exactly 24h before

	result := CalculateRefundForCancellation(100, 100, now, start)

	// Exactly 24h should be >=24 — partial refund
	if result.Tier != "partial_refund_24h_72h" {
		t.Errorf("expected partial refund at exactly 24h, got %q", result.Tier)
	}
}

// =============================================================================
// ProcessCancellationRefund - Integration tests
// =============================================================================

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

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

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

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

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

	// Cancel >72h before — full refund expected
	now := time.Date(2099, 12, 28, 8, 0, 0, 0, time.UTC)
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	result, err := ProcessCancellationRefund(ctx, bookingID, 50, 50, start, now, "client_cancelled", &userID)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result == nil {
		t.Fatal("expected non-nil result")
	}
	if result.RefundableAmount != 50 {
		t.Errorf("expected refundable 50, got %.2f", result.RefundableAmount)
	}

	// Check refund record was created
	var refundCount int
	tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM refunds WHERE booking_id = $1", bookingID).Scan(&refundCount)
	if refundCount != 1 {
		t.Errorf("expected 1 refund record, got %d", refundCount)
	}
}

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

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

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

	// Cancel <24h before — refundable should be 0
	now := time.Date(2099, 12, 31, 9, 0, 0, 0, time.UTC)
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	result, err := ProcessCancellationRefund(ctx, bookingID, 100, 0, start, now, "no_show", &userID)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result == nil {
		t.Fatal("expected non-nil result")
	}
	if result.RefundableAmount != 0 {
		t.Errorf("expected refundable 0, got %.2f", result.RefundableAmount)
	}
}

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

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

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

	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	now := time.Date(2099, 12, 28, 8, 0, 0, 0, time.UTC)

	result, err := ProcessCancellationRefund(ctx, bookingID, 100, 0, start, now, "client_cancelled", &userID)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 0 {
		t.Errorf("expected refundable 0 when nothing paid, got %.2f", result.RefundableAmount)
	}

	var refundCount int
	tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM refunds WHERE booking_id = $1", bookingID).Scan(&refundCount)
	if refundCount != 0 {
		t.Errorf("expected 0 refund records, got %d", refundCount)
	}
}

// =============================================================================
// ProcessCancellationRefund — gift card refund routing
// =============================================================================

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

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

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

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

	var giftCardID string
	if err := tx.QueryRow(ctx, `
		INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, expiry_date, last_used_at)
		VALUES (100, 40, $1, false, NULL, NOW())
		RETURNING id
	`, userID).Scan(&giftCardID); err != nil {
		t.Fatalf("failed to create gift card: %v", err)
	}

	var paymentID string
	if err := tx.QueryRow(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, status, amount, gift_card_id, created_at, updated_at)
		VALUES ($1, 'full', 'giftcard', 'completed', 60, $2, NOW(), NOW())
		RETURNING id
	`, bookingID, giftCardID).Scan(&paymentID); err != nil {
		t.Fatalf("failed to create giftcard payment: %v", err)
	}

	// Booking is far in the future — full refund.
	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 60,
		farFuture, clock.Now(), "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 60 {
		t.Errorf("expected refundable 60 (full refund >72h), got %.2f", result.RefundableAmount)
	}

	var amountRemaining float64
	err = tx.QueryRow(ctx,
		"SELECT amount_remaining FROM gift_cards WHERE id = $1", giftCardID).Scan(&amountRemaining)
	if err != nil {
		t.Fatalf("failed to query gift card balance: %v", err)
	}
	if amountRemaining != 100 {
		t.Errorf("expected gift card amount_remaining 100 (40 + 60), got %.2f", amountRemaining)
	}

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

	var txCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM gift_card_transactions WHERE gift_card_id = $1 AND transaction_type = 'refund'", giftCardID).Scan(&txCount)
	if err != nil {
		t.Fatalf("failed to query gift card transactions: %v", err)
	}
	if txCount != 1 {
		t.Errorf("expected 1 gift card refund transaction, got %d", txCount)
	}
}

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

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

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

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

	// Create a cash payment of 30.
	_, err = fixtures.CreateTestPayment(tx, bookingID, 30, "cash", "deposit", "completed")
	if err != nil {
		t.Fatalf("failed to create cash payment: %v", err)
	}

	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 30,
		farFuture, clock.Now(), "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 30 {
		t.Errorf("expected refundable 30, got %.2f", result.RefundableAmount)
	}

	// Verify user balance was credited.
	var balance float64
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(balance, 0) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		t.Fatalf("failed to query balance: %v", err)
	}
	if balance != 30 {
		t.Errorf("expected user balance 30, got %.2f", balance)
	}
}

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

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

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

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

	// Create an online_square payment — this will be handled by Square mock.
	_, err = fixtures.CreateTestPayment(tx, bookingID, 100, "online_square", "full", "completed")
	if err != nil {
		t.Fatalf("failed to create card payment: %v", err)
	}

	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 100,
		farFuture, clock.Now(), "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 100 {
		t.Errorf("expected refundable 100, got %.2f", result.RefundableAmount)
	}

	// Square API refund is processed AFTER the transaction commits (see
	// ProcessPendingSquareRefunds). In dev/test the payment has no
	// square_payment_id, so the pending refund is marked "completed" without
	// a Square API call. No balance credit is generated — the refund record
	// existence is the authoritative record of the refund.
	var status string
	err = tx.QueryRow(ctx, "SELECT status FROM refunds WHERE booking_id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query refund status: %v", err)
	}
	if status != "completed" {
		t.Errorf("expected refund status 'completed', got %q", status)
	}

	// No balance credit should have been created (Square payment method uses
	// post-commit refund processing, not balance credits).
	var balance float64
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(balance, 0) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		// No row = no balance credit — this is the expected outcome.
		// The refund was processed as a direct record, not a balance credit.
		t.Logf("no balance row (expected): %v", err)
	} else if balance > 0 {
		t.Errorf("expected no balance credit for Square payment, got %.2f", balance)
	}
}

// =============================================================================
// ProcessCancellationRefund — non-money payment methods (discount, on_the_house)
// =============================================================================

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

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

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

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

	// Create a discount payment (no real money exchanged).
	_, err = fixtures.CreateTestPayment(tx, bookingID, 20, "discount", "partial", "completed")
	if err != nil {
		t.Fatalf("failed to create discount payment: %v", err)
	}

	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 20,
		farFuture, clock.Now(), "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 20 {
		t.Errorf("expected refundable 20 (full refund >72h), got %.2f", result.RefundableAmount)
	}

	// Discount payments should NOT create a balance credit.
	var balance float64
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(balance, 0) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		balance = 0
	}
	if balance != 0 {
		t.Errorf("expected no balance credit for discount payment, got %.2f", balance)
	}
}

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

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

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

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

	// Create an on_the_house payment (no real money exchanged).
	_, err = fixtures.CreateTestPayment(tx, bookingID, 100, "on_the_house", "full", "completed")
	if err != nil {
		t.Fatalf("failed to create on_the_house payment: %v", err)
	}

	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 100,
		farFuture, clock.Now(), "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 100 {
		t.Errorf("expected refundable 100 (full refund >72h), got %.2f", result.RefundableAmount)
	}

	// on_the_house payments should NOT create a balance credit.
	var balance float64
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(balance, 0) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		balance = 0
	}
	if balance != 0 {
		t.Errorf("expected no balance credit for on_the_house payment, got %.2f", balance)
	}
}

// =============================================================================
// ProcessCancellationRefund — missing user_id edge case
// =============================================================================

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

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

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

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

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

	// Set user_id to NULL on the booking to simulate a purged guest account.
	_, err = tx.Exec(ctx, "UPDATE bookings SET user_id = NULL WHERE id = $1", bookingID)
	if err != nil {
		t.Fatalf("failed to nullify booking user_id: %v", err)
	}

	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 50,
		farFuture, clock.Now(), "client_cancelled", nil,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 50 {
		t.Errorf("expected refundable 50, got %.2f", result.RefundableAmount)
	}

	// Refund record should still be created even without user_id.
	var refundCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM refunds WHERE booking_id = $1", bookingID).Scan(&refundCount)
	if err != nil {
		t.Fatalf("failed to query refunds: %v", err)
	}
	if refundCount != 1 {
		t.Errorf("expected 1 refund record (user_id-less), got %d", refundCount)
	}
}

// =============================================================================
// ProcessCancellationRefund — guest users must NOT get balance credits
// =============================================================================

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

	// Create a user and promote them to guest role.
	userID, err := fixtures.CreateTestUser(tx)
	if err != nil {
		t.Fatalf("failed to create user: %v", err)
	}
	_, err = tx.Exec(ctx, "UPDATE users SET account_role = 'guest' WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set guest role: %v", err)
	}

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

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

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

	// Create a gift card payment.
	_, err = fixtures.CreateTestPayment(tx, bookingID, 50, "giftcard", "deposit", "completed")
	if err != nil {
		t.Fatalf("failed to create giftcard payment: %v", err)
	}

	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 50,
		farFuture, clock.Now(), "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 50 {
		t.Errorf("expected refundable 50, got %.2f", result.RefundableAmount)
	}

	// Guest must NOT have a balance credit.
	var balance float64
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(balance, 0) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		// No row means balance is 0 — this is the expected outcome.
		balance = 0
	}
	if balance != 0 {
		t.Errorf("expected guest balance 0 (guests do not receive balance credits), got %.2f", balance)
	}

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

func TestProcessCancellationRefund_GuestCashDoesNotCreditBalance(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)
	}
	_, err = tx.Exec(ctx, "UPDATE users SET account_role = 'guest' WHERE id = $1", userID)
	if err != nil {
		t.Fatalf("failed to set guest role: %v", err)
	}

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

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

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

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

	farFuture := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)
	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 30,
		farFuture, clock.Now(), "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 30 {
		t.Errorf("expected refundable 30, got %.2f", result.RefundableAmount)
	}

	// Guest must NOT have a balance credit.
	var balance float64
	err = tx.QueryRow(ctx,
		"SELECT COALESCE(balance, 0) FROM user_giftcard_balances WHERE user_id = $1", userID).Scan(&balance)
	if err != nil {
		balance = 0
	}
	if balance != 0 {
		t.Errorf("expected guest balance 0 (guests do not receive balance credits), got %.2f", balance)
	}
}

// =============================================================================
// Refund with split payments — verify dedup when 2 records share square_payment_id
// =============================================================================

func TestProcessCancellationRefund_SplitPayment_DeduplicatesSquareRefund(t *testing.T) {
	// When a single Square charge is split into 2 DB payment records (deposit + balance)
	// sharing the same square_payment_id, the refund loop must only call Square once.
	// The second record should be credited to the user balance instead.
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)

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

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

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

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

	sameSquareID := "sqp_split_dedup_test"
	now := clock.Now()

	// Create 2 payment records sharing the same square_payment_id — simulating a
	// split charge where one Square payment was recorded as deposit + balance.
	svc := NewPaymentService()

	_, err = svc.CreatePaymentRecord(ctx, PaymentRecord{
		BookingID:       bookingID,
		PaymentType:     "deposit",
		PaymentMethod:   "online_square",
		Status:          "completed",
		Amount:          25.00,
		SquarePaymentID: &sameSquareID,
		CreatedAt:       now,
		UpdatedAt:       now,
	}, nil)
	if err != nil {
		t.Fatalf("failed to create deposit record: %v", err)
	}

	_, err = svc.CreatePaymentRecord(ctx, PaymentRecord{
		BookingID:       bookingID,
		PaymentType:     "balance",
		PaymentMethod:   "online_square",
		Status:          "completed",
		Amount:          25.00,
		SquarePaymentID: &sameSquareID,
		CreatedAt:       now,
		UpdatedAt:       now,
	}, nil)
	if err != nil {
		t.Fatalf("failed to create balance record: %v", err)
	}

	// Cancel 72+ hours before → full refund of £50.
	farFuture := time.Date(2099, 12, 28, 8, 0, 0, 0, time.UTC)
	start := time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC)

	result, err := ProcessCancellationRefund(
		ctx, bookingID, 100, 50,
		start, farFuture, "client_cancelled", &userID,
	)
	if err != nil {
		t.Fatalf("ProcessCancellationRefund failed: %v", err)
	}
	if result.RefundableAmount != 50 {
		t.Errorf("expected refundable 50, got %.2f", result.RefundableAmount)
	}

	// Should have created 1 Square refund (for the deposit record) and credited
	// the balance portion via user balance.
	var refundCount int
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM refunds WHERE booking_id = $1", bookingID).Scan(&refundCount)
	if err != nil {
		t.Fatalf("failed to query refunds: %v", err)
	}
	if refundCount != 2 {
		t.Errorf("expected 2 refund records (1 Square + 1 balance credit), got %d", refundCount)
	}

}

// =============================================================================
// ProcessPendingSquareRefunds — post-commit Square refund processing
// =============================================================================

// TestProcessPendingSquareRefunds_ProcessesPendingRecords verifies that
// ProcessPendingSquareRefunds queries for "pending" Square refund records
// without square_refund_id and marks them "completed" (no actual Square
// API call when square_payment_id is NULL in mock/dev).
func TestProcessPendingSquareRefunds_ProcessesPendingRecords(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)
	}

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

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

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

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

	// Manually insert a "pending" refund record (simulating what ProcessCancellationRefundTx creates)
	_, err = tx.Exec(ctx, `
		INSERT INTO refunds (payment_id, booking_id, amount, status, reason, created_at)
		SELECT id, $1, amount, 'pending', 'client_cancelled', NOW()
		FROM payments WHERE booking_id = $1 AND payment_method = 'online_square'
	`, bookingID)
	if err != nil {
		t.Fatalf("failed to insert pending refund: %v", err)
	}

	// Commit the test transaction so the refund records are persisted.
	pgxTx := db.TxFromContext(ctx)
	if pgxTx == nil {
		t.Fatal("no transaction in context")
	}
	if err := pgxTx.Commit(ctx); err != nil {
		t.Fatalf("failed to commit test tx: %v", err)
	}

	// Use a fresh context (no closed transaction) so db.Conn falls through to pool.
	freshCtx := context.Background()

	// Now call the post-commit function
	ProcessPendingSquareRefunds(freshCtx, bookingID, "client_cancelled")

	// Verify the refund record was marked completed
	var status string
	err = db.Conn.QueryRow(freshCtx, `SELECT status FROM refunds WHERE booking_id = $1`, bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query refund status: %v", err)
	}
	if status != "completed" {
		t.Errorf("expected refund status 'completed', got %q", status)
	}
}

// TestProcessPendingSquareRefunds_SkipsCompletedRecords verifies that
// ProcessPendingSquareRefunds does not modify already-completed refunds.
func TestProcessPendingSquareRefunds_SkipsCompletedRecords(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)
	}

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

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

	// Create a payment first
	var paymentID string
	err = tx.QueryRow(ctx, `
		INSERT INTO payments (booking_id, amount, payment_method, payment_type, status)
		VALUES ($1, 50, 'cash', 'full', 'completed') RETURNING id
	`, bookingID).Scan(&paymentID)
	if err != nil {
		t.Fatalf("failed to create payment: %v", err)
	}

	// Insert a "completed" refund directly (simulating non-Square refund path)
	_, err = tx.Exec(ctx, `
		INSERT INTO refunds (payment_id, booking_id, amount, status, reason, created_at)
		VALUES ($1, $2, 50, 'completed', 'cash_refund', NOW())
	`, paymentID, bookingID)
	if err != nil {
		t.Fatalf("failed to insert completed refund: %v", err)
	}

	pgxTx := db.TxFromContext(ctx)
	if pgxTx == nil {
		t.Fatal("no transaction in context")
	}
	if err := pgxTx.Commit(ctx); err != nil {
		t.Fatalf("failed to commit test tx: %v", err)
	}

	freshCtx := context.Background()
	ProcessPendingSquareRefunds(freshCtx, bookingID, "client_cancelled")

	// Verify the completed refund was left untouched
	var status string
	err = db.Conn.QueryRow(freshCtx, `SELECT status FROM refunds WHERE booking_id = $1`, bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query refund status: %v", err)
	}
	if status != "completed" {
		t.Errorf("expected existing status 'completed', got %q", status)
	}
}
