package ratelimits

import (
	"context"
	"errors"
	"fmt"
	"net/netip"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	io_prometheus_client "github.com/prometheus/client_model/go"

	"github.com/letsencrypt/boulder/config"
	"github.com/letsencrypt/boulder/core"
	"github.com/letsencrypt/boulder/identifier"
	blog "github.com/letsencrypt/boulder/log"
	"github.com/letsencrypt/boulder/metrics"
	"github.com/letsencrypt/boulder/test"
)

// loadAndParseDefaultLimits is a helper that calls both loadDefaults and
// parseDefaultLimits to handle a YAML file.
//
// TODO(#7901): Update the tests to test these functions individually.
func loadAndParseDefaultLimits(path string) (Limits, error) {
	fromFile, err := loadDefaultsFromFile(path)
	if err != nil {
		return nil, err
	}

	return parseDefaultLimits(fromFile)
}

// loadAndParseOverrideLimitsFromFile is a helper that calls both
// loadOverridesFromFile and parseOverrideLimits to handle a YAML file.
//
// TODO(#7901): Update the tests to test these functions individually.
func loadAndParseOverrideLimitsFromFile(path string) (Limits, error) {
	fromFile, err := loadOverridesFromFile(path)
	if err != nil {
		return nil, err
	}

	return parseOverrideLimits(fromFile)
}

func TestParseOverrideNameId(t *testing.T) {
	// 'enum:ipv4'
	// Valid IPv4 address.
	name, id, err := parseOverrideNameId(NewRegistrationsPerIPAddress.String() + ":10.0.0.1")
	test.AssertNotError(t, err, "should not error")
	test.AssertEquals(t, name, NewRegistrationsPerIPAddress)
	test.AssertEquals(t, id, "10.0.0.1")

	// 'enum:ipv6range'
	// Valid IPv6 address range.
	name, id, err = parseOverrideNameId(NewRegistrationsPerIPv6Range.String() + ":2602:80a:6000::/48")
	test.AssertNotError(t, err, "should not error")
	test.AssertEquals(t, name, NewRegistrationsPerIPv6Range)
	test.AssertEquals(t, id, "2602:80a:6000::/48")

	// Missing colon (this should never happen but we should avoid panicking).
	_, _, err = parseOverrideNameId(NewRegistrationsPerIPAddress.String() + "10.0.0.1")
	test.AssertError(t, err, "missing colon")

	// Empty string.
	_, _, err = parseOverrideNameId("")
	test.AssertError(t, err, "empty string")

	// Only a colon.
	_, _, err = parseOverrideNameId(NewRegistrationsPerIPAddress.String() + ":")
	test.AssertError(t, err, "only a colon")

	// Invalid enum.
	_, _, err = parseOverrideNameId("lol:noexist")
	test.AssertError(t, err, "invalid enum")
}

func TestParseOverrideNameEnumId(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name        string
		input       string
		wantLimit   Name
		wantId      string
		expectError bool
	}{
		{
			name:        "valid IPv4 address",
			input:       NewRegistrationsPerIPAddress.EnumString() + ":10.0.0.1",
			wantLimit:   NewRegistrationsPerIPAddress,
			wantId:      "10.0.0.1",
			expectError: false,
		},
		{
			name:        "valid IPv6 address range",
			input:       NewRegistrationsPerIPv6Range.EnumString() + ":2001:0db8:0000::/48",
			wantLimit:   NewRegistrationsPerIPv6Range,
			wantId:      "2001:0db8:0000::/48",
			expectError: false,
		},
		{
			name:        "missing colon",
			input:       NewRegistrationsPerIPAddress.EnumString() + "10.0.0.1",
			expectError: true,
		},
		{
			name:        "empty string",
			input:       "",
			expectError: true,
		},
		{
			name:        "only a colon",
			input:       NewRegistrationsPerIPAddress.EnumString() + ":",
			expectError: true,
		},
		{
			name:        "invalid enum",
			input:       "lol:noexist",
			expectError: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			limit, id, err := parseOverrideNameEnumId(tc.input)
			if tc.expectError {
				if err == nil {
					t.Errorf("expected error for input %q, but got none", tc.input)
				}
			} else {
				test.AssertNotError(t, err, tc.name)
				test.AssertEquals(t, limit, tc.wantLimit)
				test.AssertEquals(t, id, tc.wantId)
			}
		})
	}
}

func TestValidateLimit(t *testing.T) {
	err := ValidateLimit(&Limit{Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}})
	test.AssertNotError(t, err, "valid limit")

	// All of the following are invalid.
	for _, l := range []*Limit{
		{Burst: 0, Count: 1, Period: config.Duration{Duration: time.Second}},
		{Burst: 1, Count: 0, Period: config.Duration{Duration: time.Second}},
		{Burst: 1, Count: 1, Period: config.Duration{Duration: 0}},
	} {
		err = ValidateLimit(l)
		test.AssertError(t, err, "limit should be invalid")
	}
}

func TestLoadAndParseOverrideLimitsFromFile(t *testing.T) {
	// Load a single valid override limit with Id formatted as 'enum:RegId'.
	l, err := loadAndParseOverrideLimitsFromFile("testdata/working_override.yml")
	test.AssertNotError(t, err, "valid single override limit")
	expectKey := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1")
	test.AssertEquals(t, l[expectKey].Burst, int64(40))
	test.AssertEquals(t, l[expectKey].Count, int64(40))
	test.AssertEquals(t, l[expectKey].Period.Duration, time.Second)

	// Load single valid override limit with a 'domainOrCIDR' Id.
	l, err = loadAndParseOverrideLimitsFromFile("testdata/working_override_regid_domainorcidr.yml")
	test.AssertNotError(t, err, "valid single override limit with Id of regId:domainOrCIDR")
	expectKey = joinWithColon(CertificatesPerDomain.EnumString(), "example.com")
	test.AssertEquals(t, l[expectKey].Burst, int64(40))
	test.AssertEquals(t, l[expectKey].Count, int64(40))
	test.AssertEquals(t, l[expectKey].Period.Duration, time.Second)

	// Load multiple valid override limits with 'regId' Ids.
	l, err = loadAndParseOverrideLimitsFromFile("testdata/working_overrides.yml")
	test.AssertNotError(t, err, "multiple valid override limits")
	expectKey1 := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1")
	test.AssertEquals(t, l[expectKey1].Burst, int64(40))
	test.AssertEquals(t, l[expectKey1].Count, int64(40))
	test.AssertEquals(t, l[expectKey1].Period.Duration, time.Second)
	expectKey2 := joinWithColon(NewRegistrationsPerIPv6Range.EnumString(), "2602:80a:6000::/48")
	test.AssertEquals(t, l[expectKey2].Burst, int64(50))
	test.AssertEquals(t, l[expectKey2].Count, int64(50))
	test.AssertEquals(t, l[expectKey2].Period.Duration, time.Second*2)

	// Load multiple valid override limits with 'fqdnSet' Ids, as follows:
	//   - CertificatesPerFQDNSet:example.com
	//   - CertificatesPerFQDNSet:example.com,example.net
	//   - CertificatesPerFQDNSet:example.com,example.net,example.org
	entryKey1 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com"}))
	entryKey2 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net"}))
	entryKey3 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"}))
	entryKey4 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.ACMEIdentifiers{
		identifier.NewIP(netip.MustParseAddr("2602:80a:6000::1")),
		identifier.NewIP(netip.MustParseAddr("9.9.9.9")),
		identifier.NewDNS("example.com"),
	})

	l, err = loadAndParseOverrideLimitsFromFile("testdata/working_overrides_regid_fqdnset.yml")
	test.AssertNotError(t, err, "multiple valid override limits with 'fqdnSet' Ids")
	test.AssertEquals(t, l[entryKey1].Burst, int64(40))
	test.AssertEquals(t, l[entryKey1].Count, int64(40))
	test.AssertEquals(t, l[entryKey1].Period.Duration, time.Second)
	test.AssertEquals(t, l[entryKey2].Burst, int64(50))
	test.AssertEquals(t, l[entryKey2].Count, int64(50))
	test.AssertEquals(t, l[entryKey2].Period.Duration, time.Second*2)
	test.AssertEquals(t, l[entryKey3].Burst, int64(60))
	test.AssertEquals(t, l[entryKey3].Count, int64(60))
	test.AssertEquals(t, l[entryKey3].Period.Duration, time.Second*3)
	test.AssertEquals(t, l[entryKey4].Burst, int64(60))
	test.AssertEquals(t, l[entryKey4].Count, int64(60))
	test.AssertEquals(t, l[entryKey4].Period.Duration, time.Second*4)

	// Path is empty string.
	_, err = loadAndParseOverrideLimitsFromFile("")
	test.AssertError(t, err, "path is empty string")
	test.Assert(t, os.IsNotExist(err), "path is empty string")

	// Path to file which does not exist.
	_, err = loadAndParseOverrideLimitsFromFile("testdata/file_does_not_exist.yml")
	test.AssertError(t, err, "a file that does not exist ")
	test.Assert(t, os.IsNotExist(err), "test file should not exist")

	// Burst cannot be 0.
	_, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_burst_0.yml")
	test.AssertError(t, err, "single override limit with burst=0")
	test.AssertContains(t, err.Error(), "invalid burst")

	// Id cannot be empty.
	_, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_empty_id.yml")
	test.AssertError(t, err, "single override limit with empty id")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")

	// Name cannot be empty.
	_, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_empty_name.yml")
	test.AssertError(t, err, "single override limit with empty name")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")

	// Name must be a string representation of a valid Name enumeration.
	_, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_invalid_name.yml")
	test.AssertError(t, err, "single override limit with invalid name")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")

	// Multiple entries, second entry has a bad name.
	_, err = loadAndParseOverrideLimitsFromFile("testdata/busted_overrides_second_entry_bad_name.yml")
	test.AssertError(t, err, "multiple override limits, second entry is bad")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")

	// Multiple entries, third entry has id of "lol", instead of an IPv4 address.
	_, err = loadAndParseOverrideLimitsFromFile("testdata/busted_overrides_third_entry_bad_id.yml")
	test.AssertError(t, err, "multiple override limits, third entry has bad Id value")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")
}

func TestLoadOverrides(t *testing.T) {
	mockLog := blog.NewMock()

	tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "../test/config-next/ratelimit-overrides.yml", metrics.NoopRegisterer, mockLog)
	test.AssertNotError(t, err, "creating TransactionBuilder")
	err = tb.loadOverrides(context.Background())
	test.AssertNotError(t, err, "loading overrides in TransactionBuilder")
	overridesData, err := loadOverridesFromFile("../test/config-next/ratelimit-overrides.yml")
	test.AssertNotError(t, err, "loading overrides from file")
	testOverrides, err := parseOverrideLimits(overridesData)
	test.AssertNotError(t, err, "parsing overrides")

	newOverridesPerLimit := make(map[Name]float64)
	for _, override := range testOverrides {
		override.precompute()
		newOverridesPerLimit[override.Name]++
	}

	test.AssertDeepEquals(t, tb.limitRegistry.overrides, testOverrides)

	var iom io_prometheus_client.Metric

	for rlName, rlString := range nameToString {
		err = tb.limitRegistry.overridesPerLimit.WithLabelValues(rlString).Write(&iom)
		test.AssertNotError(t, err, fmt.Sprintf("encoding overridesPerLimit metric with label %q", rlString))
		test.AssertEquals(t, iom.Gauge.GetValue(), newOverridesPerLimit[rlName])
	}

	err = tb.limitRegistry.overridesTimestamp.Write(&iom)
	test.AssertNotError(t, err, "encoding overridesTimestamp metric")
	test.Assert(t, int64(iom.Gauge.GetValue()) >= time.Now().Unix()-5, "overridesTimestamp too old")

	// A failure loading overrides should log and return an error, and not
	// overwrite existing overrides.
	mockLog.Clear()
	tb.limitRegistry.refreshOverrides = func(context.Context, prometheus.Gauge, blog.Logger) (Limits, error) {
		return nil, errors.New("mock failure")
	}
	err = tb.limitRegistry.loadOverrides(context.Background())
	test.AssertError(t, err, "fail to load overrides")
	test.AssertDeepEquals(t, tb.limitRegistry.overrides, testOverrides)

	// An empty set of overrides should log a warning, return nil, and not
	// overwrite existing overrides.
	mockLog.Clear()
	tb.limitRegistry.refreshOverrides = func(context.Context, prometheus.Gauge, blog.Logger) (Limits, error) {
		return Limits{}, nil
	}
	err = tb.limitRegistry.loadOverrides(context.Background())
	test.AssertEquals(t, mockLog.GetAll()[0], "WARNING: loading overrides: no valid overrides")
	test.AssertNotError(t, err, "load empty overrides")
	test.AssertDeepEquals(t, tb.limitRegistry.overrides, testOverrides)
}

func TestNewRefresher(t *testing.T) {
	mockLog := blog.NewMock()

	reg := &limitRegistry{
		refreshOverrides: func(_ context.Context, _ prometheus.Gauge, logger blog.Logger) (Limits, error) {
			logger.Info("refreshed")
			return nil, nil
		},
		logger: mockLog,
	}

	// Create and simultaneously cancel a refresher.
	reg.NewRefresher(time.Millisecond * 2)()
	time.Sleep(time.Millisecond * 20)
	// The refresher should have run once, but then been cancelled before the
	// first tick.
	test.AssertDeepEquals(t, mockLog.GetAll(), []string{"INFO: refreshed", "WARNING: loading overrides: no valid overrides"})

	reg.NewRefresher(time.Nanosecond)
	retries := 0
	for retries < 5 {
		if slices.Contains(mockLog.GetAll(), "INFO: refreshed") {
			break
		}
		retries++
		time.Sleep(core.RetryBackoff(retries, time.Millisecond*2, time.Millisecond*50, 2))
	}
	test.AssertSliceContains(t, mockLog.GetAll(), "INFO: refreshed")
	test.Assert(t, len(mockLog.GetAll()) > 1, "refresher didn't run more than once")
}

func TestHydrateOverrideLimit(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name            string
		bucketKey       string
		limit           Limit
		expectBucketKey string
		expectError     string
	}{
		{
			name:            "bad limit name",
			bucketKey:       "",
			limit:           Limit{Name: 37},
			expectBucketKey: "",
			expectError:     "unrecognized limit name 37",
		},
		{
			name:      "CertificatesPerDomain with bad FQDN, should fail validateIdForName",
			bucketKey: "VelociousVacherin",
			limit: Limit{
				Name:   StringToName["CertificatesPerDomain"],
				Burst:  1,
				Count:  1,
				Period: config.Duration{Duration: time.Second},
			},
			expectBucketKey: "",
			expectError:     "\"VelociousVacherin\" is neither a domain (Domain name needs at least one dot) nor an IP address (ParseAddr(\"VelociousVacherin\"): unable to parse IP)",
		},
		{
			name:      "CertificatesPerDomain with IPv4 address",
			bucketKey: "64.112.117.1",
			limit: Limit{
				Name:   StringToName["CertificatesPerDomain"],
				Burst:  1,
				Count:  1,
				Period: config.Duration{Duration: time.Second},
			},
			expectBucketKey: "64.112.117.1/32",
			expectError:     "",
		},
		{
			name:      "CertificatesPerDomain with IPv6 address",
			bucketKey: "2602:80a:6000:666::",
			limit: Limit{
				Name:   StringToName["CertificatesPerDomain"],
				Burst:  1,
				Count:  1,
				Period: config.Duration{Duration: time.Second},
			},
			expectBucketKey: "2602:80a:6000:666::/64",
			expectError:     "",
		},
		{
			name:      "CertificatesPerFQDNSet",
			bucketKey: "example.com,example.net,example.org",
			limit: Limit{
				Name:   StringToName["CertificatesPerFQDNSet"],
				Burst:  1,
				Count:  1,
				Period: config.Duration{Duration: time.Second},
			},
			expectBucketKey: "394e82811f52e2da38b970afdb21c9bc9af81060939c690183c00fce37408738",
			expectError:     "",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			bk, err := hydrateOverrideLimit(tc.bucketKey, tc.limit.Name)
			if tc.expectError != "" {
				if err == nil {
					t.Errorf("expected error for test %q but got none", tc.name)
				}
				test.AssertContains(t, err.Error(), tc.expectError)
			} else {
				test.AssertNotError(t, err, tc.name)
				test.AssertEquals(t, bk, tc.expectBucketKey)
			}
		})
	}
}

func TestLoadAndParseDefaultLimits(t *testing.T) {
	// Load a single valid default limit.
	l, err := loadAndParseDefaultLimits("testdata/working_default.yml")
	test.AssertNotError(t, err, "valid single default limit")
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20))
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20))
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second)

	// Load multiple valid default limits.
	l, err = loadAndParseDefaultLimits("testdata/working_defaults.yml")
	test.AssertNotError(t, err, "multiple valid default limits")
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20))
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20))
	test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second)
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Burst, int64(30))
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Count, int64(30))
	test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Period.Duration, time.Second*2)

	// Path is empty string.
	_, err = loadAndParseDefaultLimits("")
	test.AssertError(t, err, "path is empty string")
	test.Assert(t, os.IsNotExist(err), "path is empty string")

	// Path to file which does not exist.
	_, err = loadAndParseDefaultLimits("testdata/file_does_not_exist.yml")
	test.AssertError(t, err, "a file that does not exist")
	test.Assert(t, os.IsNotExist(err), "test file should not exist")

	// Burst cannot be 0.
	_, err = loadAndParseDefaultLimits("testdata/busted_default_burst_0.yml")
	test.AssertError(t, err, "single default limit with burst=0")
	test.AssertContains(t, err.Error(), "invalid burst")

	// Name cannot be empty.
	_, err = loadAndParseDefaultLimits("testdata/busted_default_empty_name.yml")
	test.AssertError(t, err, "single default limit with empty name")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")

	// Name must be a string representation of a valid Name enumeration.
	_, err = loadAndParseDefaultLimits("testdata/busted_default_invalid_name.yml")
	test.AssertError(t, err, "single default limit with invalid name")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")

	// Multiple entries, second entry has a bad name.
	_, err = loadAndParseDefaultLimits("testdata/busted_defaults_second_entry_bad_name.yml")
	test.AssertError(t, err, "multiple default limits, one is bad")
	test.Assert(t, !os.IsNotExist(err), "test file should exist")
}

func TestLoadAndDumpOverrides(t *testing.T) {
	t.Parallel()

	input := `
- CertificatesPerDomain:
    burst: 5000
    count: 5000
    period: 168h0m0s
    ids:
        - id: example.com
          comment: IN-10057
        - id: example.net
          comment: IN-10057
- CertificatesPerDomain:
    burst: 300
    count: 300
    period: 168h0m0s
    ids:
        - id: example.org
          comment: IN-10057
- CertificatesPerDomainPerAccount:
    burst: 12000
    count: 12000
    period: 168h0m0s
    ids:
        - id: "123456789"
          comment: Affluent (IN-8322)
- CertificatesPerDomainPerAccount:
    burst: 6000
    count: 6000
    period: 168h0m0s
    ids:
        - id: "543219876"
          comment: Affluent (IN-8322)
        - id: "987654321"
          comment: Affluent (IN-8322)
- CertificatesPerFQDNSet:
    burst: 50
    count: 50
    period: 168h0m0s
    ids:
        - id: example.co.uk,example.cn
          comment: IN-6843
- CertificatesPerFQDNSet:
    burst: 24
    count: 24
    period: 168h0m0s
    ids:
        - id: example.org,example.com,example.net
          comment: IN-6006
- FailedAuthorizationsPerDomainPerAccount:
    burst: 250
    count: 250
    period: 1h0m0s
    ids:
        - id: "123456789"
          comment: Digital Lake (IN-6736)
- FailedAuthorizationsPerDomainPerAccount:
    burst: 50
    count: 50
    period: 1h0m0s
    ids:
        - id: "987654321"
          comment: Digital Lake (IN-6856)
- FailedAuthorizationsPerDomainPerAccount:
    burst: 10
    count: 10
    period: 1h0m0s
    ids:
        - id: "543219876"
          comment: Big Mart (IN-6949)
- NewOrdersPerAccount:
    burst: 3000
    count: 3000
    period: 3h0m0s
    ids:
        - id: "123456789"
          comment: Galaxy Hoster (IN-8180)
- NewOrdersPerAccount:
    burst: 1000
    count: 1000
    period: 3h0m0s
    ids:
        - id: "543219876"
          comment: Big Mart (IN-8180)
        - id: "987654321"
          comment: Buy More (IN-10057)
- NewRegistrationsPerIPAddress:
    burst: 100000
    count: 100000
    period: 3h0m0s
    ids:
        - id: 2600:1f1c:5e0:e702:ca06:d2a3:c7ce:a02e
          comment: example.org IN-2395
        - id: 55.66.77.88
          comment: example.org IN-2395
- NewRegistrationsPerIPAddress:
    burst: 200
    count: 200
    period: 3h0m0s
    ids:
        - id: 11.22.33.44
          comment: example.net (IN-1583)`

	expectCSV := `
name,id,count,burst,period,comment
CertificatesPerDomain,example.com,5000,5000,168h0m0s,IN-10057
CertificatesPerDomain,example.net,5000,5000,168h0m0s,IN-10057
CertificatesPerDomain,example.org,300,300,168h0m0s,IN-10057
CertificatesPerDomainPerAccount,123456789,12000,12000,168h0m0s,Affluent (IN-8322)
CertificatesPerDomainPerAccount,543219876,6000,6000,168h0m0s,Affluent (IN-8322)
CertificatesPerDomainPerAccount,987654321,6000,6000,168h0m0s,Affluent (IN-8322)
CertificatesPerFQDNSet,7c956936126b492845ddb48f4d220034509e7c0ad54ed2c1ba2650406846d9c3,50,50,168h0m0s,IN-6843
CertificatesPerFQDNSet,394e82811f52e2da38b970afdb21c9bc9af81060939c690183c00fce37408738,24,24,168h0m0s,IN-6006
FailedAuthorizationsPerDomainPerAccount,123456789,250,250,1h0m0s,Digital Lake (IN-6736)
FailedAuthorizationsPerDomainPerAccount,987654321,50,50,1h0m0s,Digital Lake (IN-6856)
FailedAuthorizationsPerDomainPerAccount,543219876,10,10,1h0m0s,Big Mart (IN-6949)
NewOrdersPerAccount,123456789,3000,3000,3h0m0s,Galaxy Hoster (IN-8180)
NewOrdersPerAccount,543219876,1000,1000,3h0m0s,Big Mart (IN-8180)
NewOrdersPerAccount,987654321,1000,1000,3h0m0s,Buy More (IN-10057)
NewRegistrationsPerIPAddress,2600:1f1c:5e0:e702:ca06:d2a3:c7ce:a02e,100000,100000,3h0m0s,example.org IN-2395
NewRegistrationsPerIPAddress,55.66.77.88,100000,100000,3h0m0s,example.org IN-2395
NewRegistrationsPerIPAddress,11.22.33.44,200,200,3h0m0s,example.net (IN-1583)
`
	tempDir := t.TempDir()
	tempFile := filepath.Join(tempDir, "overrides.yaml")

	err := os.WriteFile(tempFile, []byte(input), 0644)
	test.AssertNotError(t, err, "writing temp overrides.yaml")

	original, err := LoadOverridesByBucketKey(tempFile)
	test.AssertNotError(t, err, "loading overrides")
	test.Assert(t, len(original) > 0, "expected at least one override loaded")

	dumpFile := filepath.Join(tempDir, "dumped.yaml")
	err = DumpOverrides(dumpFile, original)
	test.AssertNotError(t, err, "dumping overrides")

	dumped, err := os.ReadFile(dumpFile)
	test.AssertNotError(t, err, "reading dumped overrides file")
	test.AssertEquals(t, strings.TrimLeft(string(dumped), "\n"), strings.TrimLeft(expectCSV, "\n"))
}
