package mysql

import (
	"context"
	"testing"
	"time"

	"github.com/DATA-DOG/go-sqlmock"
	"hyapp/pkg/appcode"
)

func TestClaimPendingWalletOutboxPrioritizesRecoveryOverPendingAtBatchOne(t *testing.T) {
	tests := []struct {
		name             string
		eventID          string
		recoveryStatus   string
		expectEmptyRetry bool
	}{
		{name: "due retry", eventID: "wallet-retry", recoveryStatus: outboxStatusRetryable},
		{name: "expired delivery", eventID: "wallet-expired", recoveryStatus: outboxStatusDelivering, expectEmptyRetry: true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			db, mock, err := sqlmock.New()
			if err != nil {
				t.Fatalf("create sqlmock: %v", err)
			}
			defer db.Close()

			mock.ExpectBegin()
			if tt.expectEmptyRetry {
				mock.ExpectQuery(walletOutboxRetryClaimPattern()).
					WithArgs("lalu", outboxStatusRetryable, sqlmock.AnyArg(), 1).
					WillReturnRows(walletOutboxClaimRows())
			}
			claimPattern := walletOutboxRetryClaimPattern()
			if tt.recoveryStatus == outboxStatusDelivering {
				claimPattern = walletOutboxExpiredClaimPattern()
			}
			mock.ExpectQuery(claimPattern).
				WithArgs("lalu", tt.recoveryStatus, sqlmock.AnyArg(), 1).
				WillReturnRows(walletOutboxClaimRows().AddRow(
					"lalu", tt.eventID, "WalletBalanceChanged", "tx-1", "cmd-1", int64(1001),
					"COIN", int64(1), int64(0), `{}`, int64(200), 0, "", "old-worker", int64(100),
				))
			lockUntilMS := time.Now().UTC().Add(time.Minute).UnixMilli()
			mock.ExpectExec(`(?s)UPDATE wallet_outbox\s+SET status = \?, worker_id = \?, lock_until_ms = \?, updated_at_ms = \?\s+WHERE app_code = \? AND event_id = \?`).
				WithArgs(outboxStatusDelivering, "worker-1", lockUntilMS, sqlmock.AnyArg(), "lalu", tt.eventID).
				WillReturnResult(sqlmock.NewResult(0, 1))
			mock.ExpectCommit()

			records, err := (&Repository{db: db}).ClaimPendingWalletOutbox(
				appcode.WithContext(context.Background(), "lalu"), "worker-1", 1, lockUntilMS,
			)
			if err != nil {
				t.Fatalf("claim wallet recovery row: %v", err)
			}
			if len(records) != 1 || records[0].EventID != tt.eventID {
				t.Fatalf("batch=1 must claim %s before an older pending row: %+v", tt.recoveryStatus, records)
			}
			// No pending query is registered: issuing one after the recovery row filled
			// the batch makes sqlmock fail, protecting the starvation boundary directly.
			if err := mock.ExpectationsWereMet(); err != nil {
				t.Fatalf("wallet claim priority mismatch: %v", err)
			}
		})
	}
}

func walletOutboxClaimRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"app_code", "event_id", "event_type", "transaction_id", "command_id", "user_id", "asset_type",
		"available_delta", "frozen_delta", "payload", "created_at_ms", "retry_count", "last_error", "worker_id", "lock_until_ms",
	})
}

func walletOutboxRetryClaimPattern() string {
	return `(?s)FROM wallet_outbox FORCE INDEX \(idx_wallet_outbox_pending\).*WHERE app_code = \? AND status = \?\s+AND next_retry_at_ms <= \?\s+AND lock_until_ms IS NULL.*FOR UPDATE SKIP LOCKED`
}

func walletOutboxExpiredClaimPattern() string {
	return `(?s)FROM wallet_outbox FORCE INDEX \(idx_wallet_outbox_claim\).*WHERE app_code = \? AND status = \?\s+AND lock_until_ms <= \?.*FOR UPDATE SKIP LOCKED`
}
