package ratelimits

import (
	"context"
	"math/rand/v2"
	"net"
	"net/netip"
	"testing"
	"time"

	"github.com/jmhodges/clock"

	"github.com/letsencrypt/boulder/config"
	berrors "github.com/letsencrypt/boulder/errors"
	blog "github.com/letsencrypt/boulder/log"
	"github.com/letsencrypt/boulder/metrics"
	"github.com/letsencrypt/boulder/test"
)

// overriddenIP is overridden in 'testdata/working_override.yml' to have higher
// burst and count values.
const overriddenIP = "64.112.117.1"

// newTestLimiter constructs a new limiter.
func newTestLimiter(t *testing.T, s Source, clk clock.FakeClock) *Limiter {
	l, err := NewLimiter(clk, s, metrics.NoopRegisterer)
	test.AssertNotError(t, err, "should not error")
	return l
}

// newTestTransactionBuilder constructs a new *TransactionBuilder with the
// following configuration:
//   - 'NewRegistrationsPerIPAddress' burst: 20 count: 20 period: 1s
//   - 'NewRegistrationsPerIPAddress:64.112.117.1' burst: 40 count: 40 period: 1s
func newTestTransactionBuilder(t *testing.T) *TransactionBuilder {
	c, err := NewTransactionBuilderFromFiles("testdata/working_default.yml", "testdata/working_override.yml", metrics.NoopRegisterer, blog.NewMock())
	test.AssertNotError(t, err, "should not error")
	err = c.loadOverrides(context.Background())
	test.AssertNotError(t, err, "loading overrides")

	return c
}

func setup(t *testing.T) (context.Context, map[string]*Limiter, *TransactionBuilder, clock.FakeClock, string) {
	testCtx := context.Background()
	clk := clock.NewFake()

	// Generate a random IP address to avoid collisions during and between test
	// runs.
	randIP := make(net.IP, 4)
	for i := range 4 {
		randIP[i] = byte(rand.IntN(256))
	}

	// Construct a limiter for each source.
	return testCtx, map[string]*Limiter{
		"inmem": newInmemTestLimiter(t, clk),
		"redis": newRedisTestLimiter(t, clk),
	}, newTestTransactionBuilder(t), clk, randIP.String()
}

func resetBucket(t *testing.T, l *Limiter, ctx context.Context, limit *Limit, bucketKey string) {
	t.Helper()
	txn, err := newResetTransaction(limit, bucketKey)
	test.AssertNotError(t, err, "txn should be valid")
	err = l.BatchReset(ctx, []Transaction{txn})
	test.AssertNotError(t, err, "should not error")
}

func TestLimiter_CheckWithLimitOverrides(t *testing.T) {
	t.Parallel()
	testCtx, limiters, txnBuilder, clk, testIP := setup(t)
	for name, l := range limiters {
		t.Run(name, func(t *testing.T) {
			overriddenBucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(overriddenIP))
			overriddenLimit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, overriddenBucketKey)
			test.AssertNotError(t, err, "should not error")

			// Attempt to spend all 40 requests, this should succeed.
			overriddenTxn40, err := newTransaction(overriddenLimit, overriddenBucketKey, 40)
			test.AssertNotError(t, err, "txn should be valid")
			d, err := l.Spend(testCtx, overriddenTxn40)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")

			// Attempting to spend 1 more, this should fail.
			overriddenTxn1, err := newTransaction(overriddenLimit, overriddenBucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.Spend(testCtx, overriddenTxn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, !d.allowed, "should not be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Verify our RetryIn is correct. 1 second == 1000 milliseconds and
			// 1000/40 = 25 milliseconds per request.
			test.AssertEquals(t, d.retryIn, time.Millisecond*25)

			// Wait 50 milliseconds and try again.
			clk.Add(d.retryIn)

			// We should be allowed to spend 1 more request.
			d, err = l.Spend(testCtx, overriddenTxn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Wait 1 second for a full bucket reset.
			clk.Add(d.resetIn)

			// Quickly spend 40 requests in a row.
			for i := range 40 {
				d, err = l.Spend(testCtx, overriddenTxn1)
				test.AssertNotError(t, err, "should not error")
				test.Assert(t, d.allowed, "should be allowed")
				test.AssertEquals(t, d.remaining, int64(39-i))
			}

			// Attempting to spend 1 more, this should fail.
			d, err = l.Spend(testCtx, overriddenTxn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, !d.allowed, "should not be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Wait 1 second for a full bucket reset.
			clk.Add(d.resetIn)

			normalBucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP))
			normalLimit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, normalBucketKey)
			test.AssertNotError(t, err, "should not error")

			// Spend the same bucket but in a batch with bucket subject to
			// default limits. This should succeed, but the decision should
			// reflect that of the default bucket.
			defaultTxn1, err := newTransaction(normalLimit, normalBucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultTxn1})
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(19))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)

			// Refund quota to both buckets. This should succeed, but the
			// decision should reflect that of the default bucket.
			d, err = l.BatchRefund(testCtx, []Transaction{overriddenTxn1, defaultTxn1})
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(20))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Duration(0))

			// Once more.
			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultTxn1})
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(19))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)

			// Reset between tests.
			resetBucket(t, l, testCtx, overriddenLimit, overriddenBucketKey)
			resetBucket(t, l, testCtx, normalLimit, normalBucketKey)

			// Spend the same bucket but in a batch with a Transaction that is
			// check-only. This should succeed, but the decision should reflect
			// that of the default bucket.
			defaultCheckOnlyTxn1, err := newCheckOnlyTransaction(normalLimit, normalBucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultCheckOnlyTxn1})
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(19))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)

			// Check the remaining quota of the overridden bucket.
			overriddenCheckOnlyTxn0, err := newCheckOnlyTransaction(overriddenLimit, overriddenBucketKey, 0)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.Check(testCtx, overriddenCheckOnlyTxn0)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(39))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*25)

			// Check the remaining quota of the default bucket.
			defaultTxn0, err := newTransaction(normalLimit, normalBucketKey, 0)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.Check(testCtx, defaultTxn0)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(20))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Duration(0))

			// Spend the same bucket but in a batch with a Transaction that is
			// spend-only. This should succeed, but the decision should reflect
			// that of the overridden bucket.
			defaultSpendOnlyTxn1, err := newSpendOnlyTransaction(normalLimit, normalBucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultSpendOnlyTxn1})
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(38))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)

			// Check the remaining quota of the overridden bucket.
			d, err = l.Check(testCtx, overriddenCheckOnlyTxn0)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(38))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)

			// Check the remaining quota of the default bucket.
			d, err = l.Check(testCtx, defaultTxn0)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(19))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)

			// Once more, but in now the spend-only Transaction will attempt to
			// spend 20 requests. The spend-only Transaction should fail, but
			// the decision should reflect that of the overridden bucket.
			defaultSpendOnlyTxn20, err := newSpendOnlyTransaction(normalLimit, normalBucketKey, 20)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.BatchSpend(testCtx, []Transaction{overriddenTxn1, defaultSpendOnlyTxn20})
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(37))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*75)

			// Check the remaining quota of the overridden bucket.
			d, err = l.Check(testCtx, overriddenCheckOnlyTxn0)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(37))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*75)

			// Check the remaining quota of the default bucket.
			d, err = l.Check(testCtx, defaultTxn0)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(19))
			test.AssertEquals(t, d.retryIn, time.Duration(0))
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)

			// Reset between tests.
			resetBucket(t, l, testCtx, overriddenLimit, overriddenBucketKey)
		})
	}
}

func TestLimiter_InitializationViaCheckAndSpend(t *testing.T) {
	t.Parallel()
	testCtx, limiters, txnBuilder, _, testIP := setup(t)
	for name, l := range limiters {
		t.Run(name, func(t *testing.T) {
			bucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP))
			limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey)
			test.AssertNotError(t, err, "should not error")

			// Check on an empty bucket should return the theoretical next state
			// of that bucket if the cost were spent.
			txn1, err := newTransaction(limit, bucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			d, err := l.Check(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(19))
			// Verify our ResetIn timing is correct. 1 second == 1000
			// milliseconds and 1000/20 = 50 milliseconds per request.
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
			test.AssertEquals(t, d.retryIn, time.Duration(0))

			// However, that cost should not be spent yet, a 0 cost check should
			// tell us that we actually have 20 remaining.
			txn0, err := newTransaction(limit, bucketKey, 0)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.Check(testCtx, txn0)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(20))
			test.AssertEquals(t, d.resetIn, time.Duration(0))
			test.AssertEquals(t, d.retryIn, time.Duration(0))

			// Reset our bucket.
			resetBucket(t, l, testCtx, limit, bucketKey)

			// Similar to above, but we'll use Spend() to actually initialize
			// the bucket. Spend should return the same result as Check.
			d, err = l.Spend(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(19))
			// Verify our ResetIn timing is correct. 1 second == 1000
			// milliseconds and 1000/20 = 50 milliseconds per request.
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
			test.AssertEquals(t, d.retryIn, time.Duration(0))

			// And that cost should have been spent; a 0 cost check should still
			// tell us that we have 19 remaining.
			d, err = l.Check(testCtx, txn0)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(19))
			// Verify our ResetIn is correct. 1 second == 1000 milliseconds and
			// 1000/20 = 50 milliseconds per request.
			test.AssertEquals(t, d.resetIn, time.Millisecond*50)
			test.AssertEquals(t, d.retryIn, time.Duration(0))
		})
	}
}

func TestLimiter_DefaultLimits(t *testing.T) {
	t.Parallel()
	testCtx, limiters, txnBuilder, clk, testIP := setup(t)
	for name, l := range limiters {
		t.Run(name, func(t *testing.T) {
			bucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP))
			limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey)
			test.AssertNotError(t, err, "should not error")

			// Attempt to spend all 20 requests, this should succeed.
			txn20, err := newTransaction(limit, bucketKey, 20)
			test.AssertNotError(t, err, "txn should be valid")
			d, err := l.Spend(testCtx, txn20)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Attempting to spend 1 more, this should fail.
			txn1, err := newTransaction(limit, bucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.Spend(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, !d.allowed, "should not be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Verify our ResetIn is correct. 1 second == 1000 milliseconds and
			// 1000/20 = 50 milliseconds per request.
			test.AssertEquals(t, d.retryIn, time.Millisecond*50)

			// Wait 50 milliseconds and try again.
			clk.Add(d.retryIn)

			// We should be allowed to spend 1 more request.
			d, err = l.Spend(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Wait 1 second for a full bucket reset.
			clk.Add(d.resetIn)

			// Quickly spend 20 requests in a row.
			for i := range 20 {
				d, err = l.Spend(testCtx, txn1)
				test.AssertNotError(t, err, "should not error")
				test.Assert(t, d.allowed, "should be allowed")
				test.AssertEquals(t, d.remaining, int64(19-i))
			}

			// Attempting to spend 1 more, this should fail.
			d, err = l.Spend(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, !d.allowed, "should not be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)
		})
	}
}

func TestLimiter_RefundAndReset(t *testing.T) {
	t.Parallel()
	testCtx, limiters, txnBuilder, clk, testIP := setup(t)
	for name, l := range limiters {
		t.Run(name, func(t *testing.T) {
			bucketKey := newIPAddressBucketKey(NewRegistrationsPerIPAddress, netip.MustParseAddr(testIP))
			limit, err := txnBuilder.getLimit(NewRegistrationsPerIPAddress, bucketKey)
			test.AssertNotError(t, err, "should not error")

			// Attempt to spend all 20 requests, this should succeed.
			txn20, err := newTransaction(limit, bucketKey, 20)
			test.AssertNotError(t, err, "txn should be valid")
			d, err := l.Spend(testCtx, txn20)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Refund 10 requests.
			txn10, err := newTransaction(limit, bucketKey, 10)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.Refund(testCtx, txn10)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, d.remaining, int64(10))

			// Spend 10 requests, this should succeed.
			d, err = l.Spend(testCtx, txn10)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			resetBucket(t, l, testCtx, limit, bucketKey)

			// Attempt to spend 20 more requests, this should succeed.
			d, err = l.Spend(testCtx, txn20)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, d.allowed, "should be allowed")
			test.AssertEquals(t, d.remaining, int64(0))
			test.AssertEquals(t, d.resetIn, time.Second)

			// Reset to full.
			clk.Add(d.resetIn)

			// Refund 1 requests above our limit, this should fail.
			txn1, err := newTransaction(limit, bucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			d, err = l.Refund(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")
			test.Assert(t, !d.allowed, "should not be allowed")
			test.AssertEquals(t, d.remaining, int64(20))

			// Spend so we can refund.
			_, err = l.Spend(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")

			// Refund a spendOnly Transaction, which should succeed.
			spendOnlyTxn1, err := newSpendOnlyTransaction(limit, bucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			_, err = l.Refund(testCtx, spendOnlyTxn1)
			test.AssertNotError(t, err, "should not error")

			// Spend so we can refund.
			expectedDecision, err := l.Spend(testCtx, txn1)
			test.AssertNotError(t, err, "should not error")

			// Refund a checkOnly Transaction, which shouldn't error but should
			// return the same TAT as the previous spend.
			checkOnlyTxn1, err := newCheckOnlyTransaction(limit, bucketKey, 1)
			test.AssertNotError(t, err, "txn should be valid")
			newDecision, err := l.Refund(testCtx, checkOnlyTxn1)
			test.AssertNotError(t, err, "should not error")
			test.AssertEquals(t, newDecision.newTAT, expectedDecision.newTAT)
		})
	}
}

func TestRateLimitError(t *testing.T) {
	t.Parallel()
	now := clock.NewFake().Now()

	testCases := []struct {
		name            string
		decision        *Decision
		expectedErr     string
		expectedErrType berrors.ErrorType
	}{
		{
			name: "Allowed decision",
			decision: &Decision{
				allowed: true,
			},
		},
		{
			name: "RegistrationsPerIP limit reached",
			decision: &Decision{
				allowed: false,
				retryIn: 5 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name:   NewRegistrationsPerIPAddress,
						Burst:  10,
						Period: config.Duration{Duration: time.Hour},
					},
				},
			},
			expectedErr:     "too many new registrations (10) from this IP address in the last 1h0m0s, retry after 1970-01-01 00:00:05 UTC: see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ip-address",
			expectedErrType: berrors.RateLimit,
		},
		{
			name: "RegistrationsPerIPv6Range limit reached",
			decision: &Decision{
				allowed: false,
				retryIn: 10 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name:   NewRegistrationsPerIPv6Range,
						Burst:  5,
						Period: config.Duration{Duration: time.Hour},
					},
				},
			},
			expectedErr:     "too many new registrations (5) from this /48 subnet of IPv6 addresses in the last 1h0m0s, retry after 1970-01-01 00:00:10 UTC: see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ipv6-range",
			expectedErrType: berrors.RateLimit,
		},
		{
			name: "NewOrdersPerAccount limit reached",
			decision: &Decision{
				allowed: false,
				retryIn: 10 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name:   NewOrdersPerAccount,
						Burst:  2,
						Period: config.Duration{Duration: time.Hour},
					},
				},
			},
			expectedErr:     "too many new orders (2) from this account in the last 1h0m0s, retry after 1970-01-01 00:00:10 UTC: see https://letsencrypt.org/docs/rate-limits/#new-orders-per-account",
			expectedErrType: berrors.RateLimit,
		},
		{
			name: "FailedAuthorizationsPerDomainPerAccount limit reached",
			decision: &Decision{
				allowed: false,
				retryIn: 15 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name:   FailedAuthorizationsPerDomainPerAccount,
						Burst:  7,
						Period: config.Duration{Duration: time.Hour},
					},
					bucketKey: "4:12345:example.com",
				},
			},
			expectedErr:     "too many failed authorizations (7) for \"example.com\" in the last 1h0m0s, retry after 1970-01-01 00:00:15 UTC: see https://letsencrypt.org/docs/rate-limits/#authorization-failures-per-identifier-per-account",
			expectedErrType: berrors.RateLimit,
		},
		{
			name: "CertificatesPerDomain limit reached",
			decision: &Decision{
				allowed: false,
				retryIn: 20 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name:   CertificatesPerDomain,
						Burst:  3,
						Period: config.Duration{Duration: time.Hour},
					},
					bucketKey: "5:example.org",
				},
			},
			expectedErr:     "too many certificates (3) already issued for \"example.org\" in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC: see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain",
			expectedErrType: berrors.RateLimit,
		},
		{
			name: "CertificatesPerDomainPerAccount limit reached",
			decision: &Decision{
				allowed: false,
				retryIn: 20 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name:   CertificatesPerDomainPerAccount,
						Burst:  3,
						Period: config.Duration{Duration: time.Hour},
					},
					bucketKey: "6:12345678:example.net",
				},
			},
			expectedErr:     "too many certificates (3) already issued for \"example.net\" in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC: see https://letsencrypt.org/docs/rate-limits/#new-certificates-per-registered-domain",
			expectedErrType: berrors.RateLimit,
		},
		{
			name: "LimitOverrideRequestsPerIPAddress limit reached",
			decision: &Decision{
				allowed: false,
				retryIn: 20 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name:   LimitOverrideRequestsPerIPAddress,
						Burst:  3,
						Period: config.Duration{Duration: time.Hour},
					},
				},
			},
			expectedErr:     "too many override request form submissions (3) from this IP address in the last 1h0m0s, retry after 1970-01-01 00:00:20 UTC: see https://letsencrypt.org/docs/rate-limits/#new-registrations-per-ip-address",
			expectedErrType: berrors.RateLimit,
		},
		{
			name: "Unknown rate limit name",
			decision: &Decision{
				allowed: false,
				retryIn: 30 * time.Second,
				transaction: Transaction{
					limit: &Limit{
						Name: 9999999,
					},
				},
			},
			expectedErr:     "cannot generate error for unknown rate limit",
			expectedErrType: berrors.InternalServer,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			err := tc.decision.Result(now)
			if tc.expectedErr == "" {
				test.AssertNotError(t, err, "expected no error")
			} else {
				test.AssertError(t, err, "expected an error")
				test.AssertEquals(t, err.Error(), tc.expectedErr)
				test.AssertErrorIs(t, err, tc.expectedErrType)
			}
		})
	}
}
