//go:build test && dev

package payments

import (
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

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

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

// =============================================================================
// Unit tests — IsValidBookingStatusForPayment (pure function, no DB)
// =============================================================================

func TestIsValidBookingStatusForPayment_Confirmed(t *testing.T) {
	t.Parallel()
	if !IsValidBookingStatusForPayment("confirmed") {
		t.Error("expected 'confirmed' to be valid for payment")
	}
}

func TestIsValidBookingStatusForPayment_Pending(t *testing.T) {
	t.Parallel()
	if !IsValidBookingStatusForPayment("pending") {
		t.Error("expected 'pending' to be valid for payment")
	}
}

func TestIsValidBookingStatusForPayment_PendingRelease(t *testing.T) {
	t.Parallel()
	if !IsValidBookingStatusForPayment("pending_release") {
		t.Error("expected 'pending_release' to be valid for payment")
	}
}

func TestIsValidBookingStatusForPayment_InProgress(t *testing.T) {
	t.Parallel()
	if !IsValidBookingStatusForPayment("in_progress") {
		t.Error("expected 'in_progress' to be valid for payment")
	}
}

func TestIsValidBookingStatusForPayment_RejectsDepositLapsed(t *testing.T) {
	t.Parallel()
	if IsValidBookingStatusForPayment("deposit_lapsed") {
		t.Error("expected 'deposit_lapsed' to be rejected for payment")
	}
}

func TestIsValidBookingStatusForPayment_RejectsClientCancelled(t *testing.T) {
	t.Parallel()
	if IsValidBookingStatusForPayment("client_cancelled") {
		t.Error("expected 'client_cancelled' to be rejected for payment")
	}
}

func TestIsValidBookingStatusForPayment_RejectsWeCancelled(t *testing.T) {
	t.Parallel()
	if IsValidBookingStatusForPayment("we_cancelled") {
		t.Error("expected 'we_cancelled' to be rejected for payment")
	}
}

func TestIsValidBookingStatusForPayment_RejectsCompleted(t *testing.T) {
	t.Parallel()
	if IsValidBookingStatusForPayment("completed") {
		t.Error("expected 'completed' to be rejected for payment")
	}
}

func TestIsValidBookingStatusForPayment_RejectsNoShow(t *testing.T) {
	t.Parallel()
	if IsValidBookingStatusForPayment("no_show") {
		t.Error("expected 'no_show' to be rejected for payment")
	}
}

// =============================================================================
// Integration tests — CreateBookingPayment status guard
// =============================================================================

// setupPaymentStatusTest creates a user, service, and booking with the given
// status, returning the userID, bookingID, and user token.
func setupPaymentStatusTest(t *testing.T, ctx context.Context, q db.Querier, status string) (string, string, string) {
	t.Helper()

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

	// Use a far-future date so the booking is never in the cleanup window.
	bookingID, err := fixtures.CreateTestBookingAtTime(q, userID, serviceID, time.Date(2099, 12, 31, 10, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("failed to create test booking: %v", err)
	}

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

	userToken := jwt.GenerateUserToken(userID)
	return userID, bookingID, userToken
}

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

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

	cardToken := "cnon:test-card-nonce"
	req := CreateBookingPaymentRequest{
		Amount:         2500,
		PaymentType:    "deposit",
		NewCardToken:   &cardToken,
		SaveCard:       true,
		IdempotencyKey: "deposit-pending-release-" + bookingID,
	}

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

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

	// Verify the booking was promoted back to confirmed.
	var status string
	err := tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking status: %v", err)
	}
	if status != "confirmed" {
		t.Errorf("expected booking promoted from pending_release to 'confirmed', got %q", status)
	}
}

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

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

	// Pay 15% (£7.50 on a £50 booking) — below 20% threshold.
	cardToken := "cnon:test-card-nonce"
	req := CreateBookingPaymentRequest{
		Amount:         750, // £7.50 = 15% of £50
		PaymentType:    "partial",
		NewCardToken:   &cardToken,
		SaveCard:       false,
		IdempotencyKey: "below-threshold-" + bookingID,
	}

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

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

	// Payment below 20% threshold — booking should remain pending_release.
	var status string
	err := tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if status != "pending_release" {
		t.Errorf("expected booking to remain pending_release, got %q", status)
	}
}

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

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

	// Pay £25 via "balance" type — still should meet the 20% threshold.
	cardToken := "cnon:test-card-nonce"
	req := CreateBookingPaymentRequest{
		Amount:         2500, // £25.00 = 50% of £50
		PaymentType:    "balance",
		NewCardToken:   &cardToken,
		SaveCard:       false,
		IdempotencyKey: "balance-promotes-" + bookingID,
	}

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

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

	var status string
	err := tx.QueryRow(ctx,
		"SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&status)
	if err != nil {
		t.Fatalf("failed to query booking: %v", err)
	}
	if status != "confirmed" {
		t.Errorf("expected booking promoted to 'confirmed', got %q", status)
	}
}

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

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

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

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

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

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

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

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

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

	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409 for pending booking (not yet confirmed), got %d. body: %s", w.Code, w.Body.String())
	}
}

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

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

	cardToken := "cnon:test-card-nonce"
	req := CreateBookingPaymentRequest{
		Amount:         2500,
		PaymentType:    "deposit",
		NewCardToken:   &cardToken,
		SaveCard:       false,
		IdempotencyKey: "reject-lapsed-" + bookingID,
	}

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

	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409 for deposit_lapsed booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

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

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

	cardToken := "cnon:test-card-nonce"
	req := CreateBookingPaymentRequest{
		Amount:         2500,
		PaymentType:    "deposit",
		NewCardToken:   &cardToken,
		SaveCard:       false,
		IdempotencyKey: "reject-cancelled-" + bookingID,
	}

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

	if w.Code != http.StatusConflict {
		t.Errorf("expected status 409 for client_cancelled booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// Payment Lock Tests
// =============================================================================

func paymentLockRequest(method, path string, token string, baseCtx ...context.Context) *httptest.ResponseRecorder {
	w := httptest.NewRecorder()
	req := httptest.NewRequest(method, path, nil)
	req.Header.Set("Authorization", "Bearer "+token)

	rctx := chi.NewRouteContext()
	bookingID, _ := extractPaymentIDFromPath(path)
	rctx.URLParams.Add("id", bookingID)

	ctx := context.Background()
	if len(baseCtx) > 0 {
		ctx = baseCtx[0]
	}
	ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)

	if token != "" {
		if info := extractUserFromTestJWT(token); info != nil {
			ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
			ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
		}
	}

	AcquirePaymentLock(w, req.WithContext(ctx))
	return w
}

func TestAcquirePaymentLock_Confirmed(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "confirmed")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for confirmed booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_InProgress(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "in_progress")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for in_progress booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_PendingRelease(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "pending_release")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusOK {
		t.Errorf("expected 200 for pending_release booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_RejectsDepositLapsed(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "deposit_lapsed")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for deposit_lapsed booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_RejectsClientCancelled(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "client_cancelled")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for client_cancelled booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_RejectsWeCancelled(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "we_cancelled")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for we_cancelled booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_RejectsNoShow(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "no_show")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for no_show booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_RejectsPending(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "pending")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for pending booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestAcquirePaymentLock_RejectsCompleted(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "completed")
	w := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusConflict {
		t.Errorf("expected 409 for completed booking, got %d. body: %s", w.Code, w.Body.String())
	}
}

// =============================================================================
// ReleasePaymentLock — DELETE /api/bookings/{id}/payment-lock
// =============================================================================

func releasePaymentLockRequest(path string, token string, baseCtx ...context.Context) *httptest.ResponseRecorder {
	w := httptest.NewRecorder()
	req := httptest.NewRequest("DELETE", path, nil)
	req.Header.Set("Authorization", "Bearer "+token)

	rctx := chi.NewRouteContext()
	bookingID, _ := extractPaymentIDFromPath(path)
	rctx.URLParams.Add("id", bookingID)

	ctx := context.Background()
	if len(baseCtx) > 0 {
		ctx = baseCtx[0]
	}
	ctx = context.WithValue(ctx, chi.RouteCtxKey, rctx)

	if token != "" {
		if info := extractUserFromTestJWT(token); info != nil {
			ctx = context.WithValue(ctx, mw.UserIDKey, info.userID)
			ctx = context.WithValue(ctx, mw.UserRoleKey, info.role)
		}
	}

	ReleasePaymentLock(w, req.WithContext(ctx))
	return w
}

func TestReleasePaymentLock_HappyPath(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "confirmed")

	// First, acquire the lock to create a PAYMENT_IN_FLIGHT time_blocker
	lockW := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if lockW.Code != http.StatusOK {
		t.Fatalf("expected 200 when acquiring lock, got %d", lockW.Code)
	}

	// Verify the lock exists in the DB
	var lockCount int
	err := tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = 'PAYMENT_IN_FLIGHT:' || $1", bookingID).Scan(&lockCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers: %v", err)
	}
	if lockCount != 1 {
		t.Fatalf("expected 1 time_blocker before release, got %d", lockCount)
	}

	// Now release the lock
	w := releasePaymentLockRequest("/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusNoContent {
		t.Errorf("expected 204 No Content, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify the lock was removed from the DB
	err = tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = 'PAYMENT_IN_FLIGHT:' || $1", bookingID).Scan(&lockCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers after release: %v", err)
	}
	if lockCount != 0 {
		t.Errorf("expected 0 time_blockers after release, got %d", lockCount)
	}
}

func TestReleasePaymentLock_NoExistingLock(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "confirmed")

	// Release without acquiring first — should be idempotent (204)
	w := releasePaymentLockRequest("/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusNoContent {
		t.Errorf("expected 204 No Content for idempotent release, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestReleasePaymentLock_InvalidBookingID(t *testing.T) {
	w := releasePaymentLockRequest("/api/bookings/invalid/payment-lock", "")
	if w.Code != http.StatusNotFound {
		t.Errorf("expected 404 for invalid booking ID, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestReleasePaymentLock_EmptyBookingID(t *testing.T) {
	w := releasePaymentLockRequest("/api/bookings//payment-lock", "")
	if w.Code != http.StatusNotFound {
		t.Errorf("expected 404 for empty booking ID, got %d. body: %s", w.Code, w.Body.String())
	}
}

func TestReleasePaymentLock_AfterMultipleAcquires(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	_, bookingID, userToken := setupPaymentStatusTest(t, ctx, tx, "confirmed")

	// Acquire the lock twice — AcquirePaymentLock should be idempotent
	lockW := paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if lockW.Code != http.StatusOK {
		t.Fatalf("expected 200 on first acquire, got %d", lockW.Code)
	}
	lockW = paymentLockRequest("POST", "/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if lockW.Code != http.StatusOK {
		t.Fatalf("expected 200 on second acquire, got %d", lockW.Code)
	}

	// Release should still succeed
	w := releasePaymentLockRequest("/api/bookings/"+bookingID+"/payment-lock", userToken, ctx)
	if w.Code != http.StatusNoContent {
		t.Errorf("expected 204 No Content after multiple acquires, got %d. body: %s", w.Code, w.Body.String())
	}

	// Verify all locks are gone
	var lockCount int
	err := tx.QueryRow(ctx,
		"SELECT COUNT(*) FROM time_blockers WHERE description = 'PAYMENT_IN_FLIGHT:' || $1", bookingID).Scan(&lockCount)
	if err != nil {
		t.Fatalf("failed to query time_blockers: %v", err)
	}
	if lockCount != 0 {
		t.Errorf("expected 0 time_blockers after release, got %d", lockCount)
	}
}
