package mysql

import (
	"context"
	"testing"

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

func TestClaimPendingUserOutboxPrioritizesRecoveryOverPendingAtBatchOne(t *testing.T) {
	tests := []struct {
		name             string
		eventID          string
		recoveryStatus   string
		expectEmptyRetry bool
	}{
		{name: "due retry", eventID: "user-retry", recoveryStatus: "retryable"},
		{name: "expired run", eventID: "user-expired", recoveryStatus: "running", 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(userOutboxRetryClaimPattern()).
					WithArgs("lalu", sqlmock.AnyArg(), 1).
					WillReturnRows(userOutboxClaimRows())
			}
			claimPattern := userOutboxRetryClaimPattern()
			if tt.recoveryStatus == "running" {
				claimPattern = userOutboxExpiredClaimPattern()
			}
			mock.ExpectQuery(claimPattern).
				WithArgs("lalu", sqlmock.AnyArg(), 1).
				WillReturnRows(userOutboxClaimRows().AddRow(
					"lalu", tt.eventID, "UserRegistered", "user", int64(1001), `{}`, int64(200),
				))
			mock.ExpectExec(`(?s)UPDATE user_outbox\s+SET status = 'running', worker_id = \?, lock_until_ms = \?, updated_at_ms = \?\s+WHERE app_code = \? AND event_id = \?`).
				WithArgs("worker-1", sqlmock.AnyArg(), sqlmock.AnyArg(), "lalu", tt.eventID).
				WillReturnResult(sqlmock.NewResult(0, 1))
			mock.ExpectCommit()

			records, err := (&Repository{db: db}).ClaimPendingUserOutbox(
				appcode.WithContext(context.Background(), "lalu"), "worker-1", 1,
			)
			if err != nil {
				t.Fatalf("claim user 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("user claim priority mismatch: %v", err)
			}
		})
	}
}

func userOutboxClaimRows() *sqlmock.Rows {
	return sqlmock.NewRows([]string{
		"app_code", "event_id", "event_type", "aggregate_type", "aggregate_id", "payload_json", "created_at_ms",
	})
}

func userOutboxRetryClaimPattern() string {
	return `(?s)FROM user_outbox FORCE INDEX \(idx_user_outbox_status_created\).*WHERE app_code = \? AND status = 'retryable' AND next_retry_at_ms <= \? AND lock_until_ms = 0.*FOR UPDATE SKIP LOCKED`
}

func userOutboxExpiredClaimPattern() string {
	return `(?s)FROM user_outbox FORCE INDEX \(idx_user_outbox_lock\).*WHERE app_code = \? AND status = 'running' AND lock_until_ms <= \?.*FOR UPDATE SKIP LOCKED`
}
