//go:build test && dev

package bookings

import (
	"context"
	"testing"
	"time"

	"crussell/clock"
	"crussell/db"
	"crussell/testutils"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// =============================================================================
// ProgressBookingHandler — Dedup guards for early-payment discounts
// =============================================================================

func setupDedupTest(t *testing.T, tx db.Querier, ctx context.Context) (string, string, string) {
	t.Helper()

	userID := createTestUser(t, 0, tx, ctx)
	serviceID := createTestService(t, 50.00, tx, ctx)

	now := clock.Now()
	startTime := now.Add(72 * time.Hour)
	bookingID := createPendingBooking(t, userID, serviceID, startTime, tx, ctx)

	return userID, serviceID, bookingID
}

func insertPaymentForBooking(t *testing.T, bookingID, userID string, amount int, tx db.Querier, ctx context.Context) {
	t.Helper()
	_, err := tx.Exec(ctx, `
		INSERT INTO payments (booking_id, payment_type, payment_method, amount, status, created_at, updated_at)
		VALUES ($1, 'full', 'cash', $2, 'completed', NOW(), NOW())
	`, bookingID, amount)
	require.NoError(t, err)
}

func TestProgressBooking_LoyaltyDedup(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	userID, _, bookingID := setupDedupTest(t, tx, ctx)

	// Pre-apply loyalty discount (simulating early-payment)
	_, err := tx.Exec(ctx, `
		INSERT INTO booking_discounts (booking_id, user_id, discount_source, discount_percent, original_total, discount_amount)
		VALUES ($1, $2, 'loyalty', 10, 5000, 500)
	`, bookingID, userID)
	require.NoError(t, err)

	insertPaymentForBooking(t, bookingID, userID, 5000, tx, ctx)

	// Complete the booking — should not double-apply loyalty
	completeBooking(t, bookingID, ctx)

	count := getDiscountRowCount(t, bookingID, tx, ctx)
	assert.Equal(t, 1, count, "Loyalty should not be double-applied at completion")
}

func TestProgressBooking_TimeBasedCampaignDedup(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	userID, _, bookingID := setupDedupTest(t, tx, ctx)

	// Create a time-based campaign
	campaignName := "Time Dedup Test"
	campaign := createTestCampaign(t, campaignName, "time_based", 10.0, nil, nil, nil, nil, tx, ctx)

	// Pre-apply the campaign (simulating early-payment auto-apply)
	_, err := tx.Exec(ctx, `
		INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, discount_percent, original_total, discount_amount)
		VALUES ($1, $2, 'campaign', $3, 'time_based', 10, 5000, 500)
	`, bookingID, userID, campaign)
	require.NoError(t, err)

	// Increment campaign's times_redeemed as if payment-time applied it
	_, err = tx.Exec(ctx, `
		UPDATE discount_campaigns SET times_redeemed = 1 WHERE id = $1
	`, campaign)
	require.NoError(t, err)

	insertPaymentForBooking(t, bookingID, userID, 5000, tx, ctx)

	// Complete the booking
	completeBooking(t, bookingID, ctx)

	count := getDiscountRowCount(t, bookingID, tx, ctx)
	assert.Equal(t, 1, count, "Time-based campaign should not be double-applied at completion")
}

func TestProgressBooking_UserMilestoneDedup(t *testing.T) {
	t.Parallel()
	ctx, tx := testutils.SetupTestTx(t)
	userID, _, bookingID := setupDedupTest(t, tx, ctx)

	// Give user 5 completed bookings
	svcID := createTestService(t, 30.00, tx, ctx)
	for i := 0; i < 5; i++ {
		bid := createPendingBooking(t, userID, svcID, clock.Now().Add(-time.Duration(30-i)*24*time.Hour), tx, ctx)
		insertPaymentForBooking(t, bid, userID, 3000, tx, ctx)
		completeBooking(t, bid, ctx)
	}

	val := 5
	maxRed := 1
	campaign := createTestCampaign(t, "5th Visit", "milestone", 15.0, strPtr("per_user_booking_count"), strPtr("bookings"), &val, &maxRed, tx, ctx)

	// Pre-apply the milestone (simulating early-payment)
	_, err := tx.Exec(ctx, `
		INSERT INTO booking_discounts (booking_id, user_id, discount_source, source_id, campaign_type, discount_percent, original_total, discount_amount)
		VALUES ($1, $2, 'campaign', $3, 'milestone', 15, 5000, 750)
	`, bookingID, userID, campaign)
	require.NoError(t, err)

	// Mark the campaign as already having 1 redemption
	_, err = tx.Exec(ctx, `
		UPDATE discount_campaigns SET times_redeemed = 1 WHERE id = $1
	`, campaign)
	require.NoError(t, err)

	insertPaymentForBooking(t, bookingID, userID, 5000, tx, ctx)
	completeBooking(t, bookingID, ctx)

	count := getDiscountRowCount(t, bookingID, tx, ctx)
	assert.Equal(t, 1, count, "User milestone should not be double-applied at completion")
}

// =============================================================================
// No-Show Tracking — ApplyDepositsIfNeeded at cancellation time
// =============================================================================

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

	userID := createTestUser(t, 0, tx, ctx)
	serviceID := createTestService(t, 50.00, tx, ctx)

	// Create 2 no-show bookings (confirmed bookings cancelled <24h before start)
	now := clock.Now()
	for i := 0; i < 2; i++ {
		bid := createPendingBooking(t, userID, serviceID, now.Add(-time.Duration(i)*time.Hour), tx, ctx)
		insertPaymentForBooking(t, bid, userID, 5000, tx, ctx)

		_, err := tx.Exec(ctx, `
			UPDATE bookings SET status = 'no_show' WHERE id = $1
		`, bid)
		require.NoError(t, err)
	}

	// Run ApplyDepositsIfNeeded
	applied, err := ApplyDepositsIfNeeded(ctx, tx, userID)
	require.NoError(t, err)
	assert.True(t, applied, "Should have applied deposits_required = 3 after 2 no-shows")

	var depositsRequired int
	tx.QueryRow(ctx,
		"SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&depositsRequired)
	assert.Equal(t, 3, depositsRequired, "Expected deposits_required = 3 after 2 no-shows in 6 months")
}

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

	userID := createTestUser(t, 0, tx, ctx)
	serviceID := createTestService(t, 50.00, tx, ctx)

	bid := createPendingBooking(t, userID, serviceID, clock.Now().Add(-time.Hour), tx, ctx)
	insertPaymentForBooking(t, bid, userID, 5000, tx, ctx)

	_, err := tx.Exec(ctx, "UPDATE bookings SET status = 'no_show' WHERE id = $1", bid)
	require.NoError(t, err)

	applied, err := ApplyDepositsIfNeeded(ctx, tx, userID)
	require.NoError(t, err)
	assert.False(t, applied, "Single no-show should not trigger deposits_required")

	var depositsRequired int
	tx.QueryRow(ctx,
		"SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&depositsRequired)
	assert.Equal(t, 0, depositsRequired, "Expected deposits_required = 0 with only 1 no-show")
}

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

	userID := createTestUser(t, 0, tx, ctx)
	serviceID := createTestService(t, 50.00, tx, ctx)

	// Create a no-show more than 6 months ago — should not count
	oldBid := createPendingBooking(t, userID, serviceID, clock.Now().Add(-200*24*time.Hour), tx, ctx)
	insertPaymentForBooking(t, oldBid, userID, 5000, tx, ctx)

	_, err := tx.Exec(ctx, "UPDATE bookings SET status = 'no_show' WHERE id = $1", oldBid)
	require.NoError(t, err)

	// Create a recent no-show (within 6 months)
	recentBid := createPendingBooking(t, userID, serviceID, clock.Now().Add(-time.Hour), tx, ctx)
	insertPaymentForBooking(t, recentBid, userID, 5000, tx, ctx)

	_, err = tx.Exec(ctx, "UPDATE bookings SET status = 'no_show' WHERE id = $1", recentBid)
	require.NoError(t, err)

	// Should only count 1 recent no-show, not trigger
	applied, err := ApplyDepositsIfNeeded(ctx, tx, userID)
	require.NoError(t, err)
	assert.False(t, applied, "1 old + 1 recent = 2 total but only 1 in 6-month window")
}

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

	userID := createTestUser(t, 0, tx, ctx)
	serviceID := createTestService(t, 50.00, tx, ctx)

	for i := 0; i < 2; i++ {
		bid := createPendingBooking(t, userID, serviceID, clock.Now().Add(-time.Duration(i)*time.Hour), tx, ctx)
		insertPaymentForBooking(t, bid, userID, 5000, tx, ctx)
		_, err := tx.Exec(ctx, "UPDATE bookings SET status = 'no_show' WHERE id = $1", bid)
		require.NoError(t, err)

		// Forgive the first one
		if i == 0 {
			_, err = tx.Exec(ctx, "INSERT INTO forgiven_no_shows (booking_id) VALUES ($1)", bid)
			require.NoError(t, err)
		}
	}

	applied, err := ApplyDepositsIfNeeded(ctx, tx, userID)
	require.NoError(t, err)
	assert.False(t, applied, "1 forgiven + 1 unforgiven = should not trigger")
}

// =============================================================================
// Discount + 3 paid bookings → no-show records cleared
// =============================================================================

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

	userID := createTestUser(t, 0, tx, ctx)
	serviceID := createTestService(t, 50.00, tx, ctx)

	// Set deposits_required to 3 (simulating after 2 no-shows triggered it)
	_, err := tx.Exec(ctx,
		"UPDATE users SET deposits_required = 3 WHERE id = $1", userID)
	require.NoError(t, err)

	// Create 2 no-show records
	for i := 0; i < 2; i++ {
		bid := createPendingBooking(t, userID, serviceID, clock.Now().Add(-time.Duration(i+1)*time.Hour), tx, ctx)
		insertPaymentForBooking(t, bid, userID, 5000, tx, ctx)
		_, err := tx.Exec(ctx, "UPDATE bookings SET status = 'no_show' WHERE id = $1", bid)
		require.NoError(t, err)
	}

	// Complete 3 paid bookings — each should decrement deposits_required
	for i := 0; i < 3; i++ {
		bid := createPendingBooking(t, userID, serviceID, clock.Now().Add(time.Duration(i+1)*time.Hour), tx, ctx)
		insertPaymentForBooking(t, bid, userID, 5000, tx, ctx)
		completeBooking(t, bid, ctx)
	}

	// After 3 completions, deposits_required should be 0
	var depositsRequired int
	tx.QueryRow(ctx,
		"SELECT deposits_required FROM users WHERE id = $1", userID).Scan(&depositsRequired)
	assert.Equal(t, 0, depositsRequired, "Expected 0 after 3 paid bookings from 3")

	// No-show records should be forgiven (inserted into forgiven_no_shows)
	var forgivenCount int
	tx.QueryRow(ctx, `
		SELECT COUNT(*) FROM forgiven_no_shows fns
		JOIN bookings b ON b.id = fns.booking_id
		WHERE b.user_id = $1
	`, userID).Scan(&forgivenCount)

	// Try again — should NOT trigger deposits_required again since no-shows are forgiven
	applied, _ := ApplyDepositsIfNeeded(ctx, tx, userID)
	assert.False(t, applied, "Should not trigger: no-shows were forgiven after 3 paid bookings")
}
