package kopia

import (
	"context"
	"math"
	"strings"
	"testing"
	"time"

	"github.com/alcionai/clues"
	"github.com/kopia/kopia/repo"
	"github.com/kopia/kopia/repo/blob"
	"github.com/kopia/kopia/repo/format"
	"github.com/kopia/kopia/snapshot"
	"github.com/kopia/kopia/snapshot/policy"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"golang.org/x/exp/maps"

	"github.com/alcionai/corso/src/internal/common/ptr"
	strTD "github.com/alcionai/corso/src/internal/common/str/testdata"
	"github.com/alcionai/corso/src/internal/tester"
	"github.com/alcionai/corso/src/pkg/control/repository"
	"github.com/alcionai/corso/src/pkg/fault"
	"github.com/alcionai/corso/src/pkg/storage"
	storeTD "github.com/alcionai/corso/src/pkg/storage/testdata"
)

func openLocalKopiaRepo(
	t tester.TestT,
	ctx context.Context, //revive:disable-line:context-as-argument
) (*conn, error) {
	st := storeTD.NewFilesystemStorage(t)
	repoNameHash := strTD.NewHashForRepoConfigName()

	k := NewConn(st)
	if err := k.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash); err != nil {
		return nil, err
	}

	return k, nil
}

func openKopiaRepo(
	t tester.TestT,
	ctx context.Context, //revive:disable-line:context-as-argument
) (*conn, error) {
	st := storeTD.NewPrefixedS3Storage(t)
	repoNameHash := strTD.NewHashForRepoConfigName()

	k := NewConn(st)
	if err := k.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash); err != nil {
		return nil, err
	}

	return k, nil
}

// ---------------
// unit tests
// ---------------
type WrapperUnitSuite struct {
	tester.Suite
}

func TestWrapperUnitSuite(t *testing.T) {
	suite.Run(t, &WrapperUnitSuite{Suite: tester.NewUnitSuite(t)})
}

func (suite *WrapperUnitSuite) TestCloseWithoutOpenDoesNotCrash() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	k := conn{}

	assert.NotPanics(t, func() {
		k.Close(ctx)
	})
}

// ---------------
// integration tests that use kopia
// ---------------
type WrapperIntegrationSuite struct {
	tester.Suite
}

func TestWrapperIntegrationSuite(t *testing.T) {
	suite.Run(t, &WrapperIntegrationSuite{
		Suite: tester.NewIntegrationSuite(
			t,
			[][]string{storeTD.AWSStorageCredEnvs}),
	})
}

func (suite *WrapperIntegrationSuite) TestInitialize_SamePassphrase() {
	t := suite.T()
	repoNameHash := strTD.NewHashForRepoConfigName()

	ctx, flush := tester.NewContext(t)
	defer flush()

	st := storeTD.NewFilesystemStorage(t)
	k := NewConn(st)

	err := k.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
	require.NoError(t, err, clues.ToCore(err))

	err = k.Close(ctx)
	require.NoError(t, err, clues.ToCore(err))

	err = k.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
	assert.NoError(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestInitialize_IncorrectPassphrase() {
	t := suite.T()
	repoNameHash := strTD.NewHashForRepoConfigName()

	ctx, flush := tester.NewContext(t)
	defer flush()

	st1 := storeTD.NewFilesystemStorage(t)
	k := NewConn(st1)

	err := k.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
	require.NoError(t, err, clues.ToCore(err))

	err = k.Close(ctx)
	require.NoError(t, err, clues.ToCore(err))

	// Hacky way to edit the existing passphrase for the repo so we can check that
	// we get a sensible error back.
	st2 := st1
	st2.Config = maps.Clone(st1.Config)

	var found bool

	for k, v := range st2.Config {
		if strings.Contains(strings.ToLower(k), "passphrase") {
			st2.Config[k] = v + "1"
			found = true

			break
		}
	}

	require.True(t, found, "unable to update passphrase for test")

	k = NewConn(st2)

	err = k.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
	assert.Error(t, err, clues.ToCore(err))
	assert.ErrorIs(t, err, ErrorRepoAlreadyExists)
}

func (suite *WrapperIntegrationSuite) TestBadProviderErrors() {
	t := suite.T()
	repoNameHash := strTD.NewHashForRepoConfigName()

	ctx, flush := tester.NewContext(t)
	defer flush()

	st := storeTD.NewFilesystemStorage(t)
	st.Provider = storage.ProviderUnknown
	k := NewConn(st)

	err := k.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
	assert.Error(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestConnectWithoutInitErrors() {
	t := suite.T()
	repoNameHash := strTD.NewHashForRepoConfigName()

	ctx, flush := tester.NewContext(t)
	defer flush()

	st := storeTD.NewFilesystemStorage(t)
	k := NewConn(st)

	err := k.Connect(ctx, repository.Options{}, repoNameHash)
	assert.Error(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestCloseTwiceDoesNotCrash() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	k, err := openKopiaRepo(t, ctx)
	require.NoError(t, err, clues.ToCore(err))

	err = k.Close(ctx)
	assert.NoError(t, err, clues.ToCore(err))
	assert.Nil(t, k.Repository)

	err = k.Close(ctx)
	assert.NoError(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestCloseAfterWrap() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	k, err := openKopiaRepo(t, ctx)
	require.NoError(t, err, clues.ToCore(err))

	err = k.wrap()
	require.NoError(t, err, clues.ToCore(err))

	assert.Equal(t, 2, k.refCount)

	err = k.Close(ctx)
	require.NoError(t, err, clues.ToCore(err))
	assert.NotNil(t, k.Repository)
	assert.Equal(t, 1, k.refCount)

	err = k.Close(ctx)
	require.NoError(t, err, clues.ToCore(err))
	assert.Nil(t, k.Repository)
	assert.Equal(t, 0, k.refCount)
}

func (suite *WrapperIntegrationSuite) TestOpenAfterClose() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	k, err := openKopiaRepo(t, ctx)
	require.NoError(t, err, clues.ToCore(err))

	err = k.Close(ctx)
	assert.NoError(t, err, clues.ToCore(err))

	err = k.wrap()
	assert.Error(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestBadCompressorType() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	k, err := openKopiaRepo(t, ctx)
	require.NoError(t, err, clues.ToCore(err))

	defer func() {
		err := k.Close(ctx)
		assert.NoError(t, err, clues.ToCore(err))
	}()

	err = k.Compression(ctx, "not-a-compressor")
	assert.Error(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestGetPolicyOrDefault_GetsDefault() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	k, err := openKopiaRepo(t, ctx)
	require.NoError(t, err, clues.ToCore(err))

	defer func() {
		err := k.Close(ctx)
		assert.NoError(t, err, clues.ToCore(err))
	}()

	si := snapshot.SourceInfo{
		Host:     "exchangeemail",
		UserName: "tenantID-resourceID",
		Path:     "test-path-root",
	}

	p, err := k.getPolicyOrEmpty(ctx, si)
	require.NoError(t, err, clues.ToCore(err))
	assert.Equal(t, policy.Policy{}, *p)
}

func (suite *WrapperIntegrationSuite) TestSetCompressor() {
	t := suite.T()

	ctx, flush := tester.NewContext(t)
	defer flush()

	compressor := "pgzip"

	k, err := openKopiaRepo(t, ctx)
	require.NoError(t, err, clues.ToCore(err))

	defer func() {
		err := k.Close(ctx)
		assert.NoError(t, err, clues.ToCore(err))
	}()

	err = k.Compression(ctx, compressor)
	assert.NoError(t, err, clues.ToCore(err))

	// Check the policy was actually created and has the right compressor.
	p, err := k.getPolicyOrEmpty(ctx, policy.GlobalPolicySourceInfo)
	require.NoError(t, err, clues.ToCore(err))
	assert.Equal(t, compressor, string(p.CompressionPolicy.CompressorName))

	// Check the global policy will be the effective policy in future snapshots
	// for some source info.
	si := snapshot.SourceInfo{
		Host:     "exchangeemail",
		UserName: "tenantID-resourceID",
		Path:     "test-path-root",
	}

	policyTree, err := policy.TreeForSource(ctx, k, si)
	require.NoError(t, err, clues.ToCore(err))
	assert.Equal(
		t,
		compressor,
		string(policyTree.EffectivePolicy().CompressionPolicy.CompressorName))
}

func (suite *WrapperIntegrationSuite) TestConfigPersistentConfigOnInitAndNotOnConnect() {
	repoNameHash := strTD.NewHashForRepoConfigName()

	table := []struct {
		name         string
		mutateParams repository.PersistentConfig
		checkFunc    func(
			t *testing.T,
			wanted repository.PersistentConfig,
			mutableParams format.MutableParameters,
			blobConfig format.BlobStorageConfiguration,
		)
	}{
		{
			name: "MinEpochDuration",
			mutateParams: repository.PersistentConfig{
				MinEpochDuration: ptr.To(defaultMinEpochDuration + time.Minute),
			},
			checkFunc: func(
				t *testing.T,
				wanted repository.PersistentConfig,
				mutableParams format.MutableParameters,
				blobConfig format.BlobStorageConfiguration,
			) {
				assert.Equal(t, *wanted.MinEpochDuration, mutableParams.EpochParameters.MinEpochDuration)
			},
		},
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctx, flush := tester.NewContext(t)
			defer flush()

			k, err := openLocalKopiaRepo(t, ctx)
			require.NoError(t, err, clues.ToCore(err))

			// Close is safe to call even if the repo is already closed.
			t.Cleanup(func() {
				k.Close(ctx)
			})

			// Need to disconnect and connect again because in-memory state in kopia
			// isn't updated immediately.
			err = k.Close(ctx)
			require.NoError(t, err, clues.ToCore(err))

			err = k.Connect(ctx, repository.Options{}, repoNameHash)
			require.NoError(t, err, clues.ToCore(err))

			mutable, blob, err := k.getPersistentConfig(ctx)
			require.NoError(t, err, clues.ToCore(err))

			defaultParams := repository.PersistentConfig{
				MinEpochDuration: ptr.To(defaultMinEpochDuration),
			}

			test.checkFunc(t, defaultParams, mutable, blob)

			err = k.updatePersistentConfig(ctx, test.mutateParams)
			require.NoError(t, err, clues.ToCore(err))

			err = k.Close(ctx)
			require.NoError(t, err, clues.ToCore(err))

			err = k.Connect(ctx, repository.Options{}, repoNameHash)
			require.NoError(t, err, clues.ToCore(err))

			mutable, blob, err = k.getPersistentConfig(ctx)
			require.NoError(t, err, clues.ToCore(err))
			test.checkFunc(t, test.mutateParams, mutable, blob)

			err = k.Close(ctx)
			require.NoError(t, err, clues.ToCore(err))
		})
	}
}

func (suite *WrapperIntegrationSuite) TestConfigPolicyDefaultsSetOnInitAndNotOnConnect() {
	newCompressor := "pgzip"
	newRetentionDaily := policy.OptionalInt(42)
	newRetention := policy.RetentionPolicy{KeepDaily: &newRetentionDaily}
	newSchedInterval := time.Second * 42
	repoNameHash := strTD.NewHashForRepoConfigName()

	table := []struct {
		name          string
		checkInitFunc func(*testing.T, *policy.Policy)
		checkFunc     func(*testing.T, *policy.Policy)
		mutator       func(context.Context, *policy.Policy) error
	}{
		{
			name: "Compression",
			checkInitFunc: func(t *testing.T, p *policy.Policy) {
				t.Helper()
				require.Equal(t, defaultCompressor, string(p.CompressionPolicy.CompressorName))
			},
			checkFunc: func(t *testing.T, p *policy.Policy) {
				t.Helper()
				require.Equal(t, newCompressor, string(p.CompressionPolicy.CompressorName))
			},
			mutator: func(innerCtx context.Context, p *policy.Policy) error {
				_, res := updateCompressionOnPolicy(newCompressor, p)
				return res
			},
		},
		{
			name: "Retention",
			checkInitFunc: func(t *testing.T, p *policy.Policy) {
				t.Helper()
				require.Equal(
					t,
					defaultRetention,
					p.RetentionPolicy)
				assert.Equal(
					t,
					math.MaxInt,
					p.RetentionPolicy.EffectiveKeepLatest().OrDefault(42))
			},
			checkFunc: func(t *testing.T, p *policy.Policy) {
				t.Helper()
				require.Equal(
					t,
					newRetention,
					p.RetentionPolicy)
				assert.Equal(
					t,
					42,
					p.RetentionPolicy.EffectiveKeepLatest().OrDefault(42))
			},
			mutator: func(innerCtx context.Context, p *policy.Policy) error {
				updateRetentionOnPolicy(newRetention, p)

				return nil
			},
		},
		{
			name: "Scheduling",
			checkInitFunc: func(t *testing.T, p *policy.Policy) {
				t.Helper()
				require.Equal(t, defaultSchedulingInterval, p.SchedulingPolicy.Interval())
			},
			checkFunc: func(t *testing.T, p *policy.Policy) {
				t.Helper()
				require.Equal(t, newSchedInterval, p.SchedulingPolicy.Interval())
			},
			mutator: func(innerCtx context.Context, p *policy.Policy) error {
				updateSchedulingOnPolicy(newSchedInterval, p)

				return nil
			},
		},
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctx, flush := tester.NewContext(t)
			defer flush()

			k, err := openKopiaRepo(t, ctx)
			require.NoError(t, err, clues.ToCore(err))

			p, err := k.getPolicyOrEmpty(ctx, policy.GlobalPolicySourceInfo)
			require.NoError(t, err, clues.ToCore(err))

			test.checkInitFunc(t, p)

			err = test.mutator(ctx, p)
			require.NoError(t, err, clues.ToCore(err))

			err = k.writeGlobalPolicy(ctx, "TestDefaultPolicyConfigSet", p)
			require.NoError(t, err, clues.ToCore(err))

			err = k.Close(ctx)
			require.NoError(t, err, clues.ToCore(err))

			err = k.Connect(ctx, repository.Options{}, repoNameHash)
			require.NoError(t, err, clues.ToCore(err))

			defer func() {
				err := k.Close(ctx)
				assert.NoError(t, err, clues.ToCore(err))
			}()

			p, err = k.getPolicyOrEmpty(ctx, policy.GlobalPolicySourceInfo)
			require.NoError(t, err, clues.ToCore(err))
			test.checkFunc(t, p)
		})
	}
}

func (suite *WrapperIntegrationSuite) TestInitAndConnWithTempDirectory() {
	t := suite.T()
	repoNameHash := strTD.NewHashForRepoConfigName()

	ctx, flush := tester.NewContext(t)
	defer flush()

	k, err := openKopiaRepo(t, ctx)
	require.NoError(t, err, clues.ToCore(err))

	err = k.Close(ctx)
	require.NoError(t, err, clues.ToCore(err))

	// Re-open with Connect.
	err = k.Connect(ctx, repository.Options{}, repoNameHash)
	require.NoError(t, err, clues.ToCore(err))

	err = k.Close(ctx)
	assert.NoError(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestSetUserAndHost() {
	t := suite.T()
	repoNameHash := strTD.NewHashForRepoConfigName()

	ctx, flush := tester.NewContext(t)
	defer flush()

	opts := repository.Options{
		User: "foo",
		Host: "bar",
	}

	st := storeTD.NewFilesystemStorage(t)
	k := NewConn(st)

	err := k.Initialize(ctx, opts, repository.Retention{}, repoNameHash)
	require.NoError(t, err, clues.ToCore(err))

	kopiaOpts := k.ClientOptions()
	require.Equal(t, opts.User, kopiaOpts.Username)
	require.Equal(t, opts.Host, kopiaOpts.Hostname)

	err = k.Close(ctx)
	require.NoError(t, err, clues.ToCore(err))

	// Re-open with Connect and a different user/hostname.
	opts.User = "hello"
	opts.Host = "world"

	err = k.Connect(ctx, opts, repoNameHash)
	require.NoError(t, err, clues.ToCore(err))

	kopiaOpts = k.ClientOptions()
	require.Equal(t, opts.User, kopiaOpts.Username)
	require.Equal(t, opts.Host, kopiaOpts.Hostname)

	err = k.Close(ctx)
	require.NoError(t, err, clues.ToCore(err))

	// Make sure not setting the values uses the kopia defaults.
	opts.User = ""
	opts.Host = ""

	err = k.Connect(ctx, opts, repoNameHash)
	require.NoError(t, err, clues.ToCore(err))

	kopiaOpts = k.ClientOptions()
	assert.NotEmpty(t, kopiaOpts.Username)
	assert.NotEqual(t, "hello", kopiaOpts.Username)
	assert.NotEmpty(t, kopiaOpts.Hostname)
	assert.NotEqual(t, "world", kopiaOpts.Hostname)

	err = k.Close(ctx)
	assert.NoError(t, err, clues.ToCore(err))
}

func (suite *WrapperIntegrationSuite) TestUpdatePersistentConfig() {
	table := []struct {
		name string
		opts func(
			startParams format.MutableParameters,
			startBlobConfig format.BlobStorageConfiguration,
		) repository.PersistentConfig
		expectErr    assert.ErrorAssertionFunc
		expectConfig func(
			startParams format.MutableParameters,
			startBlobConfig format.BlobStorageConfiguration,
		) (format.MutableParameters, format.BlobStorageConfiguration)
	}{
		{
			name: "NoOptionsSet NoChange",
			opts: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) repository.PersistentConfig {
				return repository.PersistentConfig{}
			},
			expectErr: assert.NoError,
			expectConfig: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) (format.MutableParameters, format.BlobStorageConfiguration) {
				return startParams, startBlobConfig
			},
		},
		{
			name: "NoValueChange NoChange",
			opts: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) repository.PersistentConfig {
				return repository.PersistentConfig{
					MinEpochDuration: ptr.To(startParams.EpochParameters.MinEpochDuration),
				}
			},
			expectErr: assert.NoError,
			expectConfig: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) (format.MutableParameters, format.BlobStorageConfiguration) {
				return startParams, startBlobConfig
			},
		},
		{
			name: "MinEpochLessThanLowerBound Errors",
			opts: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) repository.PersistentConfig {
				return repository.PersistentConfig{
					MinEpochDuration: ptr.To(minEpochDurationLowerBound - time.Second),
				}
			},
			expectErr: assert.Error,
			expectConfig: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) (format.MutableParameters, format.BlobStorageConfiguration) {
				return startParams, startBlobConfig
			},
		},
		{
			name: "MinEpochGreaterThanUpperBound Errors",
			opts: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) repository.PersistentConfig {
				return repository.PersistentConfig{
					MinEpochDuration: ptr.To(minEpochDurationUpperBound + time.Second),
				}
			},
			expectErr: assert.Error,
			expectConfig: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) (format.MutableParameters, format.BlobStorageConfiguration) {
				return startParams, startBlobConfig
			},
		},
		{
			name: "UpdateMinEpoch Succeeds",
			opts: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) repository.PersistentConfig {
				return repository.PersistentConfig{
					MinEpochDuration: ptr.To(minEpochDurationLowerBound),
				}
			},
			expectErr: assert.NoError,
			expectConfig: func(
				startParams format.MutableParameters,
				startBlobConfig format.BlobStorageConfiguration,
			) (format.MutableParameters, format.BlobStorageConfiguration) {
				startParams.EpochParameters.MinEpochDuration = minEpochDurationLowerBound
				return startParams, startBlobConfig
			},
		},
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()
			repoNameHash := strTD.NewHashForRepoConfigName()

			ctx, flush := tester.NewContext(t)
			defer flush()

			st1 := storeTD.NewPrefixedS3Storage(t)

			connection := NewConn(st1)
			err := connection.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
			require.NoError(t, err, "initializing repo: %v", clues.ToCore(err))

			// Need to close and reopen the repo due to kopia caching of values.
			err = connection.Close(ctx)
			require.NoError(t, err, clues.ToCore(err))

			err = connection.Connect(ctx, repository.Options{}, repoNameHash)
			require.NoError(t, err, clues.ToCore(err))

			startParams, startBlobConfig, err := connection.getPersistentConfig(ctx)
			require.NoError(t, err, clues.ToCore(err))

			opts := test.opts(startParams, startBlobConfig)

			err = connection.updatePersistentConfig(ctx, opts)
			test.expectErr(t, err, clues.ToCore(err))

			// Need to close and reopen the repo since the format manager will cache
			// the old value for some amount of time.
			err = connection.Close(ctx)
			require.NoError(t, err, clues.ToCore(err))

			// Open another connection to the repo to verify params are as expected
			// across repo connect calls.
			err = connection.Connect(ctx, repository.Options{}, repoNameHash)
			require.NoError(t, err, clues.ToCore(err))

			gotParams, gotBlobConfig, err := connection.getPersistentConfig(ctx)
			require.NoError(t, err, clues.ToCore(err))

			expectParams, expectBlobConfig := test.expectConfig(startParams, startBlobConfig)

			assert.Equal(t, expectParams, gotParams)
			assert.Equal(t, expectBlobConfig, gotBlobConfig)
		})
	}
}

// ---------------
// integration tests that require object locking to be enabled on the bucket.
// ---------------
type ConnRetentionIntegrationSuite struct {
	tester.Suite
}

func TestConnRetentionIntegrationSuite(t *testing.T) {
	suite.Run(t, &ConnRetentionIntegrationSuite{
		Suite: tester.NewRetentionSuite(
			t,
			[][]string{storeTD.AWSStorageCredEnvs}),
	})
}

// Test that providing retention doesn't change anything but retention values
// from the default values that kopia uses.
func (suite *ConnRetentionIntegrationSuite) TestInitWithAndWithoutRetention() {
	t := suite.T()
	repoNameHash := strTD.NewHashForRepoConfigName()

	ctx, flush := tester.NewContext(t)
	defer flush()

	st1 := storeTD.NewPrefixedS3Storage(t)

	k1 := NewConn(st1)
	err := k1.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
	require.NoError(t, err, "initializing repo 1: %v", clues.ToCore(err))

	st2 := storeTD.NewPrefixedS3Storage(t)

	k2 := NewConn(st2)
	err = k2.Initialize(
		ctx,
		repository.Options{},
		repository.Retention{
			Mode:     ptr.To(repository.GovernanceRetention),
			Duration: ptr.To(time.Hour * 48),
			Extend:   ptr.To(true),
		},
		repoNameHash)
	require.NoError(t, err, "initializing repo 2: %v", clues.ToCore(err))

	dr1, ok := k1.Repository.(repo.DirectRepository)
	require.True(t, ok, "getting direct repo 1")

	dr2, ok := k2.Repository.(repo.DirectRepository)
	require.True(t, ok, "getting direct repo 2")

	format1 := dr1.FormatManager().ScrubbedContentFormat()
	format2 := dr2.FormatManager().ScrubbedContentFormat()

	assert.Equal(t, format1, format2)

	blobCfg1, err := dr1.FormatManager().BlobCfgBlob()
	require.NoError(t, err, "getting blob config 1: %v", clues.ToCore(err))

	blobCfg2, err := dr2.FormatManager().BlobCfgBlob()
	require.NoError(t, err, "getting retention config 2: %v", clues.ToCore(err))

	assert.NotEqual(t, blobCfg1, blobCfg2)

	// Check to make sure retention not enabled unexpectedly.
	checkRetentionParams(t, ctx, k1, blob.RetentionMode(""), 0, assert.False)

	// Some checks to make sure retention was fully initialized as expected.
	checkRetentionParams(t, ctx, k2, blob.Governance, time.Hour*48, assert.True)
}

// TestVerifyDefaultConfigOptions checks that if the repo has misconfigured
// values an error is returned. This is easiest to do in a test suite that
// allows object locking because some of the configured values that are checked
// relate to object locking.
func (suite *ConnRetentionIntegrationSuite) TestVerifyDefaultConfigOptions() {
	nonzeroOpt := policy.OptionalInt(42)

	table := []struct {
		name         string
		setupRepo    func(context.Context, *testing.T, *conn)
		expectAlerts int
	}{
		{
			name:      "ValidConfigs NoRetention",
			setupRepo: func(context.Context, *testing.T, *conn) {},
		},
		{
			name: "ValidConfigs Retention",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				err := con.setRetentionParameters(
					ctx,
					repository.Retention{
						Mode:     ptr.To(repository.GovernanceRetention),
						Duration: ptr.To(48 * time.Hour),
						Extend:   ptr.To(true),
					})
				require.NoError(t, err, clues.ToCore(err))
			},
		},
		{
			name: "ValidRetentionButNotExtending",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				err := con.setRetentionParameters(
					ctx,
					repository.Retention{
						Mode:     ptr.To(repository.GovernanceRetention),
						Duration: ptr.To(48 * time.Hour),
						Extend:   ptr.To(false),
					})
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "ExtendingRetentionButNotConfigured",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				err := con.setRetentionParameters(
					ctx,
					repository.Retention{
						Extend: ptr.To(true),
					})
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "NonZeroScheduleInterval",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateSchedulingOnPolicy(time.Hour, pol)

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "OldValidCompressor",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				_, err = updateCompressionOnPolicy("s2-default", pol)
				require.NoError(t, err, clues.ToCore(err))

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 0,
		},
		{
			name: "NonDefaultCompression",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				_, err = updateCompressionOnPolicy("pgzip-best-speed", pol)
				require.NoError(t, err, clues.ToCore(err))

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "NonZeroSnapshotRetentionLatest",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				retention := policy.RetentionPolicy{
					KeepLatest:  &nonzeroOpt,
					KeepHourly:  &zeroOpt,
					KeepWeekly:  &zeroOpt,
					KeepDaily:   &zeroOpt,
					KeepMonthly: &zeroOpt,
					KeepAnnual:  &zeroOpt,
				}
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateRetentionOnPolicy(retention, pol)

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "NonZeroSnapshotRetentionHourly",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				retention := policy.RetentionPolicy{
					KeepLatest:  &zeroOpt,
					KeepHourly:  &nonzeroOpt,
					KeepWeekly:  &zeroOpt,
					KeepDaily:   &zeroOpt,
					KeepMonthly: &zeroOpt,
					KeepAnnual:  &zeroOpt,
				}
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateRetentionOnPolicy(retention, pol)

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "NonZeroSnapshotRetentionWeekly",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				retention := policy.RetentionPolicy{
					KeepLatest:  &zeroOpt,
					KeepHourly:  &zeroOpt,
					KeepWeekly:  &nonzeroOpt,
					KeepDaily:   &zeroOpt,
					KeepMonthly: &zeroOpt,
					KeepAnnual:  &zeroOpt,
				}
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateRetentionOnPolicy(retention, pol)

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "NonZeroSnapshotRetentionDaily",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				retention := policy.RetentionPolicy{
					KeepLatest:  &zeroOpt,
					KeepHourly:  &zeroOpt,
					KeepWeekly:  &zeroOpt,
					KeepDaily:   &nonzeroOpt,
					KeepMonthly: &zeroOpt,
					KeepAnnual:  &zeroOpt,
				}
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateRetentionOnPolicy(retention, pol)

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "NonZeroSnapshotRetentionMonthly",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				retention := policy.RetentionPolicy{
					KeepLatest:  &zeroOpt,
					KeepHourly:  &zeroOpt,
					KeepWeekly:  &zeroOpt,
					KeepDaily:   &zeroOpt,
					KeepMonthly: &nonzeroOpt,
					KeepAnnual:  &zeroOpt,
				}
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateRetentionOnPolicy(retention, pol)

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "NonZeroSnapshotRetentionAnnual",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				retention := policy.RetentionPolicy{
					KeepLatest:  &zeroOpt,
					KeepHourly:  &zeroOpt,
					KeepWeekly:  &zeroOpt,
					KeepDaily:   &zeroOpt,
					KeepMonthly: &zeroOpt,
					KeepAnnual:  &nonzeroOpt,
				}
				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateRetentionOnPolicy(retention, pol)

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 1,
		},
		{
			name: "MultipleAlerts",
			setupRepo: func(ctx context.Context, t *testing.T, con *conn) {
				err := con.setRetentionParameters(
					ctx,
					repository.Retention{
						Mode:     ptr.To(repository.GovernanceRetention),
						Duration: ptr.To(48 * time.Hour),
						Extend:   ptr.To(false),
					})
				require.NoError(t, err, clues.ToCore(err))

				pol, err := con.getGlobalPolicyOrEmpty(ctx)
				require.NoError(t, err, clues.ToCore(err))

				updateSchedulingOnPolicy(time.Hour, pol)

				_, err = updateCompressionOnPolicy("pgzip-best-speed", pol)
				require.NoError(t, err, clues.ToCore(err))

				err = con.writeGlobalPolicy(ctx, "test", pol)
				require.NoError(t, err, clues.ToCore(err))
			},
			expectAlerts: 3,
		},
	}

	for _, test := range table {
		suite.Run(test.name, func() {
			t := suite.T()

			ctx, flush := tester.NewContext(t)
			t.Cleanup(flush)

			repoNameHash := strTD.NewHashForRepoConfigName()
			st1 := storeTD.NewPrefixedS3Storage(t)

			con := NewConn(st1)
			err := con.Initialize(ctx, repository.Options{}, repository.Retention{}, repoNameHash)
			require.NoError(t, err, clues.ToCore(err))

			t.Cleanup(func() { con.Close(ctx) })

			test.setupRepo(ctx, t, con)

			errs := fault.New(true)
			con.verifyDefaultConfigOptions(ctx, errs)

			// There shouldn't be any reported failures because this is just to check
			// if things are alright.
			assert.NoError(t, errs.Failure(), clues.ToCore(errs.Failure()))
			assert.Len(t, errs.Alerts(), test.expectAlerts)
		})
	}
}
