// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package s3

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"maps"
	"net/http"
	"net/url"
	"os"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
	"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
	dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
	"github.com/aws/smithy-go/middleware"
	smithyhttp "github.com/aws/smithy-go/transport/http"
	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/aws-sdk-go-base/v2/mockdata"
	"github.com/hashicorp/aws-sdk-go-base/v2/servicemocks"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hcldec"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/backend"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/configs/hcl2shim"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/states/remote"
	"github.com/hashicorp/terraform/internal/states/statemgr"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

var (
	mockStsGetCallerIdentityRequestBody = url.Values{
		"Action":  []string{"GetCallerIdentity"},
		"Version": []string{"2011-06-15"},
	}.Encode()
)

// verify that we are doing ACC tests or the S3 tests specifically
func testACC(t *testing.T) {
	skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_S3_TEST") == ""
	if skip {
		t.Log("s3 backend tests require setting TF_ACC or TF_S3_TEST")
		t.Skip()
	}
	if os.Getenv("AWS_DEFAULT_REGION") == "" {
		os.Setenv("AWS_DEFAULT_REGION", "us-west-2")
	}
}

func TestBackend_impl(t *testing.T) {
	var _ backend.Backend = new(Backend)
}

func TestBackend_InternalValidate(t *testing.T) {
	b := New()

	schema := b.ConfigSchema()
	if err := schema.InternalValidate(); err != nil {
		t.Fatalf("failed InternalValidate: %s", err)
	}
}

func TestBackendConfig_original(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	region := "us-west-1"

	config := map[string]interface{}{
		"region":         region,
		"bucket":         "tf-test",
		"key":            "state",
		"encrypt":        true,
		"dynamodb_table": "dynamoTable",
	}

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)

	if b.awsConfig.Region != region {
		t.Fatalf("Incorrect region was populated")
	}
	if b.awsConfig.RetryMaxAttempts != 5 {
		t.Fatalf("Default max_retries was not set")
	}
	if b.bucketName != "tf-test" {
		t.Fatalf("Incorrect bucketName was populated")
	}
	if b.keyName != "state" {
		t.Fatalf("Incorrect keyName was populated")
	}

	credentials, err := b.awsConfig.Credentials.Retrieve(ctx)
	if err != nil {
		t.Fatalf("Error when requesting credentials")
	}
	if credentials.AccessKeyID == "" {
		t.Fatalf("No Access Key Id was populated")
	}
	if credentials.SecretAccessKey == "" {
		t.Fatalf("No Secret Access Key was populated")
	}

	// Check S3 Endpoint
	expectedS3Endpoint := defaultEndpointS3(region)
	var s3Endpoint string
	_, err = b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{},
		func(opts *s3.Options) {
			opts.APIOptions = append(opts.APIOptions,
				addRetrieveEndpointURLMiddleware(t, &s3Endpoint),
				addCancelRequestMiddleware(),
			)
		},
	)
	if err == nil {
		t.Fatal("Checking S3 Endpoint: Expected an error, got none")
	} else if !errors.Is(err, errCancelOperation) {
		t.Fatalf("Checking S3 Endpoint: Unexpected error: %s", err)
	}

	if s3Endpoint != expectedS3Endpoint {
		t.Errorf("Checking S3 Endpoint: expected endpoint %q, got %q", expectedS3Endpoint, s3Endpoint)
	}

	// Check DynamoDB Endpoint
	expectedDynamoDBEndpoint := defaultEndpointDynamo(region)
	var dynamoDBEndpoint string
	_, err = b.dynClient.ListTables(ctx, &dynamodb.ListTablesInput{},
		func(opts *dynamodb.Options) {
			opts.APIOptions = append(opts.APIOptions,
				addRetrieveEndpointURLMiddleware(t, &dynamoDBEndpoint),
				addCancelRequestMiddleware(),
			)
		},
	)
	if err == nil {
		t.Fatal("Checking DynamoDB Endpoint: Expected an error, got none")
	} else if !errors.Is(err, errCancelOperation) {
		t.Fatalf("Checking DynamoDB Endpoint: Unexpected error: %s", err)
	}

	if dynamoDBEndpoint != expectedDynamoDBEndpoint {
		t.Errorf("Checking DynamoDB Endpoint: expected endpoint %q, got %q", expectedDynamoDBEndpoint, dynamoDBEndpoint)
	}
}

func TestBackendConfig_withLockfile(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	region := "us-west-1"

	config := map[string]interface{}{
		"region":       region,
		"bucket":       "tf-test",
		"key":          "state",
		"encrypt":      true,
		"use_lockfile": true,
	}

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)

	if b.awsConfig.Region != region {
		t.Fatalf("Incorrect region was populated")
	}
	if b.awsConfig.RetryMaxAttempts != 5 {
		t.Fatalf("Default max_retries was not set")
	}
	if b.bucketName != "tf-test" {
		t.Fatalf("Incorrect bucketName was populated")
	}
	if b.keyName != "state" {
		t.Fatalf("Incorrect keyName was populated")
	}

	if b.useLockFile != true {
		t.Fatalf("Expected useLockFile to be true")
	}

	credentials, err := b.awsConfig.Credentials.Retrieve(ctx)
	if err != nil {
		t.Fatalf("Error when requesting credentials")
	}
	if credentials.AccessKeyID == "" {
		t.Fatalf("No Access Key Id was populated")
	}
	if credentials.SecretAccessKey == "" {
		t.Fatalf("No Secret Access Key was populated")
	}

	// Check S3 Endpoint
	expectedS3Endpoint := defaultEndpointS3(region)
	var s3Endpoint string
	_, err = b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{},
		func(opts *s3.Options) {
			opts.APIOptions = append(opts.APIOptions,
				addRetrieveEndpointURLMiddleware(t, &s3Endpoint),
				addCancelRequestMiddleware(),
			)
		},
	)
	if err == nil {
		t.Fatal("Checking S3 Endpoint: Expected an error, got none")
	} else if !errors.Is(err, errCancelOperation) {
		t.Fatalf("Checking S3 Endpoint: Unexpected error: %s", err)
	}

	if s3Endpoint != expectedS3Endpoint {
		t.Errorf("Checking S3 Endpoint: expected endpoint %q, got %q", expectedS3Endpoint, s3Endpoint)
	}
}

func TestBackendConfig_multiLock(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	region := "us-west-1"

	config := map[string]interface{}{
		"region":         region,
		"bucket":         "tf-test",
		"key":            "state",
		"encrypt":        true,
		"use_lockfile":   true,
		"dynamodb_table": "dynamoTable",
	}

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)

	if b.awsConfig.Region != region {
		t.Fatalf("Incorrect region was populated")
	}
	if b.awsConfig.RetryMaxAttempts != 5 {
		t.Fatalf("Default max_retries was not set")
	}
	if b.bucketName != "tf-test" {
		t.Fatalf("Incorrect bucketName was populated")
	}
	if b.keyName != "state" {
		t.Fatalf("Incorrect keyName was populated")
	}

	if b.useLockFile != true {
		t.Fatalf("Expected useLockFile to be true")
	}

	credentials, err := b.awsConfig.Credentials.Retrieve(ctx)
	if err != nil {
		t.Fatalf("Error when requesting credentials")
	}
	if credentials.AccessKeyID == "" {
		t.Fatalf("No Access Key Id was populated")
	}
	if credentials.SecretAccessKey == "" {
		t.Fatalf("No Secret Access Key was populated")
	}

	// Check S3 Endpoint
	expectedS3Endpoint := defaultEndpointS3(region)
	var s3Endpoint string
	_, err = b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{},
		func(opts *s3.Options) {
			opts.APIOptions = append(opts.APIOptions,
				addRetrieveEndpointURLMiddleware(t, &s3Endpoint),
				addCancelRequestMiddleware(),
			)
		},
	)
	if err == nil {
		t.Fatal("Checking S3 Endpoint: Expected an error, got none")
	} else if !errors.Is(err, errCancelOperation) {
		t.Fatalf("Checking S3 Endpoint: Unexpected error: %s", err)
	}

	if s3Endpoint != expectedS3Endpoint {
		t.Errorf("Checking S3 Endpoint: expected endpoint %q, got %q", expectedS3Endpoint, s3Endpoint)
	}

	// Check DynamoDB Endpoint
	expectedDynamoDBEndpoint := defaultEndpointDynamo(region)
	var dynamoDBEndpoint string
	_, err = b.dynClient.ListTables(ctx, &dynamodb.ListTablesInput{},
		func(opts *dynamodb.Options) {
			opts.APIOptions = append(opts.APIOptions,
				addRetrieveEndpointURLMiddleware(t, &dynamoDBEndpoint),
				addCancelRequestMiddleware(),
			)
		},
	)
	if err == nil {
		t.Fatal("Checking DynamoDB Endpoint: Expected an error, got none")
	} else if !errors.Is(err, errCancelOperation) {
		t.Fatalf("Checking DynamoDB Endpoint: Unexpected error: %s", err)
	}

	if dynamoDBEndpoint != expectedDynamoDBEndpoint {
		t.Errorf("Checking DynamoDB Endpoint: expected endpoint %q, got %q", expectedDynamoDBEndpoint, dynamoDBEndpoint)
	}
}

func TestBackendConfig_InvalidRegion(t *testing.T) {
	testACC(t)

	cases := map[string]struct {
		config        map[string]any
		expectedDiags tfdiags.Diagnostics
	}{
		"with region validation": {
			config: map[string]interface{}{
				"region":                      "nonesuch",
				"bucket":                      "tf-test",
				"key":                         "state",
				"skip_credentials_validation": true,
			},
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.AttributeValue(
					tfdiags.Error,
					"Invalid region value",
					`Invalid AWS Region: nonesuch`,
					cty.GetAttrPath("region"),
				),
			},
		},
		"skip region validation": {
			config: map[string]interface{}{
				"region":                      "nonesuch",
				"bucket":                      "tf-test",
				"key":                         "state",
				"skip_region_validation":      true,
				"skip_credentials_validation": true,
			},
			expectedDiags: nil,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			b := New()
			configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(tc.config))

			configSchema, diags := b.PrepareConfig(configSchema)
			if len(diags) > 0 {
				t.Fatal(diags.ErrWithWarnings())
			}

			confDiags := b.Configure(configSchema)
			diags = diags.Append(confDiags)

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestBackendConfig_RegionEnvVar(t *testing.T) {
	testACC(t)
	config := map[string]interface{}{
		"bucket": "tf-test",
		"key":    "state",
	}

	cases := map[string]struct {
		vars map[string]string
	}{
		"AWS_REGION": {
			vars: map[string]string{
				"AWS_REGION": "us-west-1",
			},
		},

		"AWS_DEFAULT_REGION": {
			vars: map[string]string{
				"AWS_DEFAULT_REGION": "us-west-1",
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			for k, v := range tc.vars {
				os.Setenv(k, v)
			}
			t.Cleanup(func() {
				for k := range tc.vars {
					os.Unsetenv(k)
				}
			})

			b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend)

			if b.awsConfig.Region != "us-west-1" {
				t.Fatalf("Incorrect region was populated")
			}
		})
	}
}

func TestBackendConfig_DynamoDBEndpoint(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	region := "us-west-1"

	cases := map[string]struct {
		config           map[string]any
		vars             map[string]string
		expectedEndpoint string
		expectedDiags    tfdiags.Diagnostics
	}{
		"none": {
			expectedEndpoint: defaultEndpointDynamo(region),
		},
		"config URL": {
			config: map[string]any{
				"endpoints": map[string]any{
					"dynamodb": "https://dynamo.test",
				},
			},
			expectedEndpoint: "https://dynamo.test/",
		},
		"config hostname": {
			config: map[string]any{
				"endpoints": map[string]any{
					"dynamodb": "dynamo.test",
				},
			},
			expectedEndpoint: "dynamo.test/",
			expectedDiags: tfdiags.Diagnostics{
				legacyIncompleteURLDiag("dynamo.test", cty.GetAttrPath("endpoints").GetAttr("dynamodb")),
			},
		},
		"deprecated config URL": {
			config: map[string]any{
				"dynamodb_endpoint": "https://dynamo.test",
			},
			expectedEndpoint: "https://dynamo.test/",
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("dynamodb_endpoint"), cty.GetAttrPath("endpoints").GetAttr("dynamodb")),
			},
		},
		"deprecated config hostname": {
			config: map[string]any{
				"dynamodb_endpoint": "dynamo.test",
			},
			expectedEndpoint: "dynamo.test/",
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("dynamodb_endpoint"), cty.GetAttrPath("endpoints").GetAttr("dynamodb")),
				legacyIncompleteURLDiag("dynamo.test", cty.GetAttrPath("dynamodb_endpoint")),
			},
		},
		"config conflict": {
			config: map[string]any{
				"dynamodb_endpoint": "https://dynamo.test",
				"endpoints": map[string]any{
					"dynamodb": "https://dynamo.test",
				},
			},
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("dynamodb_endpoint"), cty.GetAttrPath("endpoints").GetAttr("dynamodb")),
				wholeBodyErrDiag(
					"Conflicting Parameters",
					fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`,
						pathString(cty.GetAttrPath("dynamodb_endpoint")),
						pathString(cty.GetAttrPath("endpoints").GetAttr("dynamodb")),
					),
				)},
		},
		"envvar": {
			vars: map[string]string{
				"AWS_ENDPOINT_URL_DYNAMODB": "https://dynamo.test",
			},
			expectedEndpoint: "https://dynamo.test/",
		},
		"deprecated envvar": {
			vars: map[string]string{
				"AWS_DYNAMODB_ENDPOINT": "https://dynamo.test",
			},
			expectedEndpoint: "https://dynamo.test/",
			expectedDiags: tfdiags.Diagnostics{
				deprecatedEnvVarDiag("AWS_DYNAMODB_ENDPOINT", "AWS_ENDPOINT_URL_DYNAMODB"),
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			config := map[string]interface{}{
				"region": region,
				"bucket": "tf-test",
				"key":    "state",
			}

			if tc.vars != nil {
				for k, v := range tc.vars {
					os.Setenv(k, v)
				}
				t.Cleanup(func() {
					for k := range tc.vars {
						os.Unsetenv(k)
					}
				})
			}

			if tc.config != nil {
				for k, v := range tc.config {
					config[k] = v
				}
			}

			raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config))
			b := raw.(*Backend)

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}

			if !diags.HasErrors() {
				var dynamoDBEndpoint string
				_, err := b.dynClient.ListTables(ctx, &dynamodb.ListTablesInput{},
					func(opts *dynamodb.Options) {
						opts.APIOptions = append(opts.APIOptions,
							addRetrieveEndpointURLMiddleware(t, &dynamoDBEndpoint),
							addCancelRequestMiddleware(),
						)
					},
				)
				if err == nil {
					t.Fatal("Expected an error, got none")
				} else if !errors.Is(err, errCancelOperation) {
					t.Fatalf("Unexpected error: %s", err)
				}

				if dynamoDBEndpoint != tc.expectedEndpoint {
					t.Errorf("expected endpoint %q, got %q", tc.expectedEndpoint, dynamoDBEndpoint)
				}
			}
		})
	}
}

func TestBackendConfig_IAMEndpoint(t *testing.T) {
	testACC(t)

	// Doesn't test for expected endpoint, since the IAM endpoint is used internally to `aws-sdk-go-base`
	// The mocked tests won't work if the config parameter doesn't work
	cases := map[string]struct {
		config        map[string]any
		vars          map[string]string
		expectedDiags tfdiags.Diagnostics
	}{
		"none": {},
		"config URL": {
			config: map[string]any{
				"endpoints": map[string]any{
					"iam": "https://iam.test",
				},
			},
		},
		"config hostname": {
			config: map[string]any{
				"endpoints": map[string]any{
					"iam": "iam.test",
				},
			},
			expectedDiags: tfdiags.Diagnostics{
				legacyIncompleteURLDiag("iam.test", cty.GetAttrPath("endpoints").GetAttr("iam")),
			},
		},
		"deprecated config URL": {
			config: map[string]any{
				"iam_endpoint": "https://iam.test",
			},
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("iam_endpoint"), cty.GetAttrPath("endpoints").GetAttr("iam")),
			},
		},
		"deprecated config hostname": {
			config: map[string]any{
				"iam_endpoint": "iam.test",
			},
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("iam_endpoint"), cty.GetAttrPath("endpoints").GetAttr("iam")),
				legacyIncompleteURLDiag("iam.test", cty.GetAttrPath("iam_endpoint")),
			},
		},
		"config conflict": {
			config: map[string]any{
				"iam_endpoint": "https://iam.test",
				"endpoints": map[string]any{
					"iam": "https://iam.test",
				},
			},
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("iam_endpoint"), cty.GetAttrPath("endpoints").GetAttr("iam")),
				wholeBodyErrDiag(
					"Conflicting Parameters",
					fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`,
						pathString(cty.GetAttrPath("iam_endpoint")),
						pathString(cty.GetAttrPath("endpoints").GetAttr("iam")),
					),
				),
			},
		},
		"envvar": {
			vars: map[string]string{
				"AWS_ENDPOINT_URL_IAM": "https://iam.test",
			},
		},
		"deprecated envvar": {
			vars: map[string]string{
				"AWS_IAM_ENDPOINT": "https://iam.test",
			},
			expectedDiags: tfdiags.Diagnostics{
				deprecatedEnvVarDiag("AWS_IAM_ENDPOINT", "AWS_ENDPOINT_URL_IAM"),
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			config := map[string]interface{}{
				"region": "us-west-1",
				"bucket": "tf-test",
				"key":    "state",
			}

			if tc.vars != nil {
				for k, v := range tc.vars {
					os.Setenv(k, v)
				}
				t.Cleanup(func() {
					for k := range tc.vars {
						os.Unsetenv(k)
					}
				})
			}

			if tc.config != nil {
				for k, v := range tc.config {
					config[k] = v
				}
			}

			_, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config))

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestBackendConfig_S3Endpoint(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	region := "us-west-1"

	cases := map[string]struct {
		config           map[string]any
		vars             map[string]string
		expectedEndpoint string
		expectedDiags    tfdiags.Diagnostics
	}{
		"none": {
			expectedEndpoint: defaultEndpointS3(region),
		},
		"config URL": {
			config: map[string]any{
				"endpoints": map[string]any{
					"s3": "https://s3.test",
				},
			},
			expectedEndpoint: "https://s3.test/",
		},
		"config hostname": {
			config: map[string]any{
				"endpoints": map[string]any{
					"s3": "s3.test",
				},
			},
			expectedEndpoint: "/s3.test",
			expectedDiags: tfdiags.Diagnostics{
				legacyIncompleteURLDiag("s3.test", cty.GetAttrPath("endpoints").GetAttr("s3")),
			},
		},
		"deprecated config URL": {
			config: map[string]any{
				"endpoint": "https://s3.test",
			},
			expectedEndpoint: "https://s3.test/",
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("endpoint"), cty.GetAttrPath("endpoints").GetAttr("s3")),
			},
		},
		"deprecated config hostname": {
			config: map[string]any{
				"endpoint": "s3.test",
			},
			expectedEndpoint: "/s3.test",
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("endpoint"), cty.GetAttrPath("endpoints").GetAttr("s3")),
				legacyIncompleteURLDiag("s3.test", cty.GetAttrPath("endpoint")),
			},
		},
		"config conflict": {
			config: map[string]any{
				"endpoint": "https://s3.test",
				"endpoints": map[string]any{
					"s3": "https://s3.test",
				},
			},
			expectedDiags: tfdiags.Diagnostics{
				deprecatedAttrDiag(cty.GetAttrPath("endpoint"), cty.GetAttrPath("endpoints").GetAttr("s3")),
				wholeBodyErrDiag(
					"Conflicting Parameters",
					fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`,
						pathString(cty.GetAttrPath("endpoint")),
						pathString(cty.GetAttrPath("endpoints").GetAttr("s3")),
					),
				),
			},
		},
		"envvar": {
			vars: map[string]string{
				"AWS_ENDPOINT_URL_S3": "https://s3.test",
			},
			expectedEndpoint: "https://s3.test/",
		},
		"deprecated envvar": {
			vars: map[string]string{
				"AWS_S3_ENDPOINT": "https://s3.test",
			},
			expectedEndpoint: "https://s3.test/",
			expectedDiags: tfdiags.Diagnostics{
				deprecatedEnvVarDiag("AWS_S3_ENDPOINT", "AWS_ENDPOINT_URL_S3"),
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			config := map[string]interface{}{
				"region": region,
				"bucket": "tf-test",
				"key":    "state",
			}

			if tc.vars != nil {
				for k, v := range tc.vars {
					os.Setenv(k, v)
				}
				t.Cleanup(func() {
					for k := range tc.vars {
						os.Unsetenv(k)
					}
				})
			}

			if tc.config != nil {
				for k, v := range tc.config {
					config[k] = v
				}
			}

			raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config))
			b := raw.(*Backend)

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}

			if !diags.HasErrors() {
				var s3Endpoint string
				_, err := b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{},
					func(opts *s3.Options) {
						opts.APIOptions = append(opts.APIOptions,
							addRetrieveEndpointURLMiddleware(t, &s3Endpoint),
							addCancelRequestMiddleware(),
						)
					},
				)
				if err == nil {
					t.Fatal("Expected an error, got none")
				} else if !errors.Is(err, errCancelOperation) {
					t.Fatalf("Unexpected error: %s", err)
				}

				if s3Endpoint != tc.expectedEndpoint {
					t.Errorf("expected endpoint %q, got %q", tc.expectedEndpoint, s3Endpoint)
				}
			}
		})
	}
}

func TestBackendConfig_EC2MetadataEndpoint(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	cases := map[string]struct {
		config           map[string]any
		vars             map[string]string
		expectedEndpoint string
		expectedDiags    tfdiags.Diagnostics
	}{
		"none": {
			expectedEndpoint: "http://169.254.169.254/latest/meta-data",
		},
		"config URL": {
			config: map[string]any{
				"ec2_metadata_service_endpoint": "https://ec2.test",
			},
			expectedEndpoint: "https://ec2.test/latest/meta-data",
		},
		"config hostname": {
			config: map[string]any{
				"ec2_metadata_service_endpoint": "ec2.test",
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must be a valid URL containing at least a scheme and hostname. Had "ec2.test"`,
					cty.GetAttrPath("ec2_metadata_service_endpoint"),
				),
			},
		},
		"config IPv4 mode": {
			config: map[string]any{
				"ec2_metadata_service_endpoint":      "https://ec2.test",
				"ec2_metadata_service_endpoint_mode": "IPv4",
			},
			expectedEndpoint: "https://ec2.test/latest/meta-data",
		},
		"config IPv6 mode": {
			config: map[string]any{
				"ec2_metadata_service_endpoint":      "https://ec2.test",
				"ec2_metadata_service_endpoint_mode": "IPv6",
			},
			expectedEndpoint: "https://ec2.test/latest/meta-data",
		},
		"config invalid mode": {
			config: map[string]any{
				"ec2_metadata_service_endpoint":      "https://ec2.test",
				"ec2_metadata_service_endpoint_mode": "invalid",
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					"Value must be one of [IPv4, IPv6]",
					cty.GetAttrPath("ec2_metadata_service_endpoint_mode"),
				),
			},
		},
		"envvar": {
			vars: map[string]string{
				"AWS_EC2_METADATA_SERVICE_ENDPOINT": "https://ec2.test",
			},
			expectedEndpoint: "https://ec2.test/latest/meta-data",
		},
		"envvar IPv4 mode": {
			vars: map[string]string{
				"AWS_EC2_METADATA_SERVICE_ENDPOINT":      "https://ec2.test",
				"AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv4",
			},
			expectedEndpoint: "https://ec2.test/latest/meta-data",
		},
		"envvar IPv6 mode": {
			vars: map[string]string{
				"AWS_EC2_METADATA_SERVICE_ENDPOINT":      "https://ec2.test",
				"AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv6",
			},
			expectedEndpoint: "https://ec2.test/latest/meta-data",
		},
		"envvar invalid mode": {
			vars: map[string]string{
				"AWS_EC2_METADATA_SERVICE_ENDPOINT":      "https://ec2.test",
				"AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "invalid",
			},
			// expectedEndpoint: "ec2.test",
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Error,
					"unknown EC2 IMDS endpoint mode, must be either IPv6 or IPv4",
					"",
				),
			},
		},
		"deprecated envvar": {
			vars: map[string]string{
				"AWS_METADATA_URL": "https://ec2.test",
			},
			expectedEndpoint: "https://ec2.test/latest/meta-data",
			expectedDiags: tfdiags.Diagnostics{
				deprecatedEnvVarDiag("AWS_METADATA_URL", "AWS_EC2_METADATA_SERVICE_ENDPOINT"),
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			config := map[string]interface{}{
				"region": "us-west-1",
				"bucket": "tf-test",
				"key":    "state",
			}

			if tc.vars != nil {
				for k, v := range tc.vars {
					os.Setenv(k, v)
				}
				t.Cleanup(func() {
					for k := range tc.vars {
						os.Unsetenv(k)
					}
				})
			}

			if tc.config != nil {
				for k, v := range tc.config {
					config[k] = v
				}
			}

			raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config))
			b := raw.(*Backend)

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}

			if !diags.HasErrors() {
				var imdsEndpoint string
				imdsClient := imds.NewFromConfig(b.awsConfig)
				_, err := imdsClient.GetMetadata(ctx, &imds.GetMetadataInput{},
					func(opts *imds.Options) {
						opts.APIOptions = append(opts.APIOptions,
							addRetrieveEndpointURLMiddleware(t, &imdsEndpoint),
							addCancelRequestMiddleware(),
						)
					},
				)
				if err == nil {
					t.Fatal("Expected an error, got none")
				} else if !errors.Is(err, errCancelOperation) {
					t.Fatalf("Unexpected error: %s", err)
				}

				if imdsEndpoint != tc.expectedEndpoint {
					t.Errorf("expected endpoint %q, got %q", tc.expectedEndpoint, imdsEndpoint)
				}
			}
		})
	}
}

func TestBackendConfig_AssumeRole(t *testing.T) {
	testACC(t)

	testCases := map[string]struct {
		Config           map[string]interface{}
		MockStsEndpoints []*servicemocks.MockEndpoint
	}{
		"role_arn": {
			Config: map[string]interface{}{
				"bucket":       "tf-test",
				"key":          "state",
				"region":       "us-west-1",
				"role_arn":     servicemocks.MockStsAssumeRoleArn,
				"session_name": servicemocks.MockStsAssumeRoleSessionName,
			},
			MockStsEndpoints: []*servicemocks.MockEndpoint{
				{
					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
						"Action":          []string{"AssumeRole"},
						"DurationSeconds": []string{"900"},
						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
						"Version":         []string{"2011-06-15"},
					}.Encode()},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
				},
				{
					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
				},
			},
		},
		"assume_role_duration_seconds": {
			Config: map[string]interface{}{
				"assume_role_duration_seconds": 3600,
				"bucket":                       "tf-test",
				"key":                          "state",
				"region":                       "us-west-1",
				"role_arn":                     servicemocks.MockStsAssumeRoleArn,
				"session_name":                 servicemocks.MockStsAssumeRoleSessionName,
			},
			MockStsEndpoints: []*servicemocks.MockEndpoint{
				{
					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
						"Action":          []string{"AssumeRole"},
						"DurationSeconds": []string{"3600"},
						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
						"Version":         []string{"2011-06-15"},
					}.Encode()},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
				},
				{
					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
				},
			},
		},
		"external_id": {
			Config: map[string]interface{}{
				"bucket":       "tf-test",
				"external_id":  servicemocks.MockStsAssumeRoleExternalId,
				"key":          "state",
				"region":       "us-west-1",
				"role_arn":     servicemocks.MockStsAssumeRoleArn,
				"session_name": servicemocks.MockStsAssumeRoleSessionName,
			},
			MockStsEndpoints: []*servicemocks.MockEndpoint{
				{
					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
						"Action":          []string{"AssumeRole"},
						"DurationSeconds": []string{"900"},
						"ExternalId":      []string{servicemocks.MockStsAssumeRoleExternalId},
						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
						"Version":         []string{"2011-06-15"},
					}.Encode()},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
				},
				{
					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
				},
			},
		},
		"assume_role_policy": {
			Config: map[string]interface{}{
				"assume_role_policy": servicemocks.MockStsAssumeRolePolicy,
				"bucket":             "tf-test",
				"key":                "state",
				"region":             "us-west-1",
				"role_arn":           servicemocks.MockStsAssumeRoleArn,
				"session_name":       servicemocks.MockStsAssumeRoleSessionName,
			},
			MockStsEndpoints: []*servicemocks.MockEndpoint{
				{
					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
						"Action":          []string{"AssumeRole"},
						"DurationSeconds": []string{"900"},
						"Policy":          []string{servicemocks.MockStsAssumeRolePolicy},
						"RoleArn":         []string{servicemocks.MockStsAssumeRoleArn},
						"RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName},
						"Version":         []string{"2011-06-15"},
					}.Encode()},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
				},
				{
					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
				},
			},
		},
		"assume_role_policy_arns": {
			Config: map[string]interface{}{
				"assume_role_policy_arns": []interface{}{servicemocks.MockStsAssumeRolePolicyArn},
				"bucket":                  "tf-test",
				"key":                     "state",
				"region":                  "us-west-1",
				"role_arn":                servicemocks.MockStsAssumeRoleArn,
				"session_name":            servicemocks.MockStsAssumeRoleSessionName,
			},
			MockStsEndpoints: []*servicemocks.MockEndpoint{
				{
					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
						"Action":                  []string{"AssumeRole"},
						"DurationSeconds":         []string{"900"},
						"PolicyArns.member.1.arn": []string{servicemocks.MockStsAssumeRolePolicyArn},
						"RoleArn":                 []string{servicemocks.MockStsAssumeRoleArn},
						"RoleSessionName":         []string{servicemocks.MockStsAssumeRoleSessionName},
						"Version":                 []string{"2011-06-15"},
					}.Encode()},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
				},
				{
					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
				},
			},
		},
		"assume_role_tags": {
			Config: map[string]interface{}{
				"assume_role_tags": map[string]interface{}{
					servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue,
				},
				"bucket":       "tf-test",
				"key":          "state",
				"region":       "us-west-1",
				"role_arn":     servicemocks.MockStsAssumeRoleArn,
				"session_name": servicemocks.MockStsAssumeRoleSessionName,
			},
			MockStsEndpoints: []*servicemocks.MockEndpoint{
				{
					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
						"Action":              []string{"AssumeRole"},
						"DurationSeconds":     []string{"900"},
						"RoleArn":             []string{servicemocks.MockStsAssumeRoleArn},
						"RoleSessionName":     []string{servicemocks.MockStsAssumeRoleSessionName},
						"Tags.member.1.Key":   []string{servicemocks.MockStsAssumeRoleTagKey},
						"Tags.member.1.Value": []string{servicemocks.MockStsAssumeRoleTagValue},
						"Version":             []string{"2011-06-15"},
					}.Encode()},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
				},
				{
					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
				},
			},
		},
		"assume_role_transitive_tag_keys": {
			Config: map[string]interface{}{
				"assume_role_tags": map[string]interface{}{
					servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue,
				},
				"assume_role_transitive_tag_keys": []interface{}{servicemocks.MockStsAssumeRoleTagKey},
				"bucket":                          "tf-test",
				"key":                             "state",
				"region":                          "us-west-1",
				"role_arn":                        servicemocks.MockStsAssumeRoleArn,
				"session_name":                    servicemocks.MockStsAssumeRoleSessionName,
			},
			MockStsEndpoints: []*servicemocks.MockEndpoint{
				{
					Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{
						"Action":                     []string{"AssumeRole"},
						"DurationSeconds":            []string{"900"},
						"RoleArn":                    []string{servicemocks.MockStsAssumeRoleArn},
						"RoleSessionName":            []string{servicemocks.MockStsAssumeRoleSessionName},
						"Tags.member.1.Key":          []string{servicemocks.MockStsAssumeRoleTagKey},
						"Tags.member.1.Value":        []string{servicemocks.MockStsAssumeRoleTagValue},
						"TransitiveTagKeys.member.1": []string{servicemocks.MockStsAssumeRoleTagKey},
						"Version":                    []string{"2011-06-15"},
					}.Encode()},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"},
				},
				{
					Request:  &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody},
					Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"},
				},
			},
		},
	}

	for testName, testCase := range testCases {
		testCase := testCase

		t.Run(testName, func(t *testing.T) {
			closeSts, _, stsEndpoint := mockdata.GetMockedAwsApiSession("STS", testCase.MockStsEndpoints)
			defer closeSts()

			testCase.Config["sts_endpoint"] = stsEndpoint

			b := New()
			diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(testCase.Config)))

			if diags.HasErrors() {
				for _, diag := range diags {
					t.Errorf("unexpected error: %s", diag.Description().Summary)
				}
			}
		})
	}
}

func TestBackendConfig_PrepareConfigValidation(t *testing.T) {
	cases := map[string]struct {
		config        cty.Value
		expectedDiags tfdiags.Diagnostics
	}{
		"null bucket": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.NullVal(cty.String),
				"key":    cty.StringVal("test"),
				"region": cty.StringVal("us-west-2"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(cty.GetAttrPath("bucket")),
			},
		},
		"empty bucket": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal(""),
				"key":    cty.StringVal("test"),
				"region": cty.StringVal("us-west-2"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					"The value cannot be empty or all whitespace",
					cty.GetAttrPath("bucket"),
				),
			},
		},

		"null key": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.NullVal(cty.String),
				"region": cty.StringVal("us-west-2"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(cty.GetAttrPath("key")),
			},
		},
		"empty key": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal(""),
				"region": cty.StringVal("us-west-2"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					"The value cannot be empty or all whitespace",
					cty.GetAttrPath("key"),
				),
			},
		},
		"key with leading slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("/leading-slash"),
				"region": cty.StringVal("us-west-2"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start or end with "/"`,
					cty.GetAttrPath("key"),
				),
			},
		},
		"key with trailing slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("trailing-slash/"),
				"region": cty.StringVal("us-west-2"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start or end with "/"`,
					cty.GetAttrPath("key"),
				),
			},
		},
		"key with double slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test/with/double//slash"),
				"region": cty.StringVal("us-west-2"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`Value must not contain "//"`,
					cty.GetAttrPath("key"),
				),
			},
		},

		"null region": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test"),
				"region": cty.NullVal(cty.String),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Missing region value",
					`The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`,
					cty.GetAttrPath("region"),
				),
			},
		},
		"empty region": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test"),
				"region": cty.StringVal(""),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Missing region value",
					`The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`,
					cty.GetAttrPath("region"),
				),
			},
		},

		"workspace_key_prefix with leading slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket":               cty.StringVal("test"),
				"key":                  cty.StringVal("test"),
				"region":               cty.StringVal("us-west-2"),
				"workspace_key_prefix": cty.StringVal("/env"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start or end with "/"`,
					cty.GetAttrPath("workspace_key_prefix"),
				),
			},
		},
		"workspace_key_prefix with trailing slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket":               cty.StringVal("test"),
				"key":                  cty.StringVal("test"),
				"region":               cty.StringVal("us-west-2"),
				"workspace_key_prefix": cty.StringVal("env/"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start or end with "/"`,
					cty.GetAttrPath("workspace_key_prefix"),
				),
			},
		},

		"encyrption key conflict": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket":               cty.StringVal("test"),
				"key":                  cty.StringVal("test"),
				"region":               cty.StringVal("us-west-2"),
				"workspace_key_prefix": cty.StringVal("env"),
				"sse_customer_key":     cty.StringVal("1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o="),
				"kms_key_id":           cty.StringVal("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Attribute Combination",
					`Only one of kms_key_id, sse_customer_key can be set.`,
					cty.Path{},
				),
			},
		},

		"shared credentials file conflict": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket":                   cty.StringVal("test"),
				"key":                      cty.StringVal("test"),
				"region":                   cty.StringVal("us-west-2"),
				"shared_credentials_file":  cty.StringVal("test"),
				"shared_credentials_files": cty.SetVal([]cty.Value{cty.StringVal("test2")}),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Attribute Combination",
					`Only one of shared_credentials_file, shared_credentials_files can be set.`,
					cty.Path{},
				),
				attributeWarningDiag(
					"Deprecated Parameter",
					`The parameter "shared_credentials_file" is deprecated. Use parameter "shared_credentials_files" instead.`,
					cty.GetAttrPath("shared_credentials_file"),
				),
			},
		},

		"allowed forbidden account ids conflict": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket":                cty.StringVal("test"),
				"key":                   cty.StringVal("test"),
				"region":                cty.StringVal("us-west-2"),
				"allowed_account_ids":   cty.SetVal([]cty.Value{cty.StringVal("012345678901")}),
				"forbidden_account_ids": cty.SetVal([]cty.Value{cty.StringVal("012345678901")}),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Attribute Combination",
					`Only one of allowed_account_ids, forbidden_account_ids can be set.`,
					cty.Path{},
				),
			},
		},

		"dynamodb_table deprecation": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket":         cty.StringVal("test"),
				"key":            cty.StringVal("test"),
				"region":         cty.StringVal("us-west-2"),
				"dynamodb_table": cty.StringVal("test"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeWarningDiag(
					"Deprecated Parameter",
					`The parameter "dynamodb_table" is deprecated. Use parameter "use_lockfile" instead.`,
					cty.GetAttrPath("dynamodb_table"),
				),
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			servicemocks.StashEnv(t)

			b := New()

			_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))

			if diff := cmp.Diff(valDiags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

func TestBackendConfig_PrepareConfigWithEnvVars(t *testing.T) {
	cases := map[string]struct {
		config      cty.Value
		vars        map[string]string
		expectedErr string
	}{
		"region env var AWS_REGION": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test"),
				"region": cty.NullVal(cty.String),
			}),
			vars: map[string]string{
				"AWS_REGION": "us-west-1",
			},
		},
		"region env var AWS_DEFAULT_REGION": {
			config: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test"),
				"region": cty.NullVal(cty.String),
			}),
			vars: map[string]string{
				"AWS_DEFAULT_REGION": "us-west-1",
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			servicemocks.StashEnv(t)

			b := New()

			for k, v := range tc.vars {
				os.Setenv(k, v)
			}

			_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))
			if tc.expectedErr != "" {
				if valDiags.Err() != nil {
					actualErr := valDiags.Err().Error()
					if !strings.Contains(actualErr, tc.expectedErr) {
						t.Fatalf("unexpected validation result: %v", valDiags.Err())
					}
				} else {
					t.Fatal("expected an error, got none")
				}
			} else if valDiags.Err() != nil {
				t.Fatalf("expected no error, got %s", valDiags.Err())
			}
		})
	}
}

type proxyCase struct {
	url           string
	expectedProxy string
}

func TestBackendConfig_Proxy(t *testing.T) {
	cases := map[string]struct {
		config               map[string]any
		environmentVariables map[string]string
		expectedDiags        tfdiags.Diagnostics
		urls                 []proxyCase
	}{
		"no config": {
			config: map[string]any{},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "",
				},
			},
		},

		"http_proxy empty string": {
			config: map[string]any{
				"http_proxy": "",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "",
				},
			},
		},

		"http_proxy config": {
			config: map[string]any{
				"http_proxy": "http://http-proxy.test:1234",
			},
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Missing HTTPS Proxy",
					fmt.Sprintf(
						"An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+
							"To specify no proxy for HTTPS, set the HTTPS to an empty string.",
						"http://http-proxy.test:1234"),
				),
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
			},
		},

		"https_proxy config": {
			config: map[string]any{
				"https_proxy": "http://https-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
			},
		},

		"http_proxy config https_proxy config": {
			config: map[string]any{
				"http_proxy":  "http://http-proxy.test:1234",
				"https_proxy": "http://https-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
			},
		},

		"http_proxy config https_proxy config empty string": {
			config: map[string]any{
				"http_proxy":  "http://http-proxy.test:1234",
				"https_proxy": "",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "",
				},
			},
		},

		"https_proxy config http_proxy config empty string": {
			config: map[string]any{
				"http_proxy":  "",
				"https_proxy": "http://https-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
			},
		},

		"http_proxy config https_proxy config no_proxy config": {
			config: map[string]any{
				"http_proxy":  "http://http-proxy.test:1234",
				"https_proxy": "http://https-proxy.test:1234",
				"no_proxy":    "dont-proxy.test",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "http://dont-proxy.test",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
				{
					url:           "https://dont-proxy.test",
					expectedProxy: "",
				},
			},
		},

		"HTTP_PROXY envvar": {
			config: map[string]any{},
			environmentVariables: map[string]string{
				"HTTP_PROXY": "http://http-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "",
				},
			},
		},

		"http_proxy envvar": {
			config: map[string]any{},
			environmentVariables: map[string]string{
				"http_proxy": "http://http-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "",
				},
			},
		},

		"HTTPS_PROXY envvar": {
			config: map[string]any{},
			environmentVariables: map[string]string{
				"HTTPS_PROXY": "http://https-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
			},
		},

		"https_proxy envvar": {
			config: map[string]any{},
			environmentVariables: map[string]string{
				"https_proxy": "http://https-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
			},
		},

		"http_proxy config HTTPS_PROXY envvar": {
			config: map[string]any{
				"http_proxy": "http://http-proxy.test:1234",
			},
			environmentVariables: map[string]string{
				"HTTPS_PROXY": "http://https-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
			},
		},

		"http_proxy config https_proxy envvar": {
			config: map[string]any{
				"http_proxy": "http://http-proxy.test:1234",
			},
			environmentVariables: map[string]string{
				"https_proxy": "http://https-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
			},
		},

		"http_proxy config NO_PROXY envvar": {
			config: map[string]any{
				"http_proxy": "http://http-proxy.test:1234",
			},
			environmentVariables: map[string]string{
				"NO_PROXY": "dont-proxy.test",
			},
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Missing HTTPS Proxy",
					fmt.Sprintf(
						"An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+
							"To specify no proxy for HTTPS, set the HTTPS to an empty string.",
						"http://http-proxy.test:1234"),
				),
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "http://dont-proxy.test",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://dont-proxy.test",
					expectedProxy: "",
				},
			},
		},

		"http_proxy config no_proxy envvar": {
			config: map[string]any{
				"http_proxy": "http://http-proxy.test:1234",
			},
			environmentVariables: map[string]string{
				"no_proxy": "dont-proxy.test",
			},
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Missing HTTPS Proxy",
					fmt.Sprintf(
						"An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+
							"To specify no proxy for HTTPS, set the HTTPS to an empty string.",
						"http://http-proxy.test:1234"),
				),
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "http://dont-proxy.test",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "https://dont-proxy.test",
					expectedProxy: "",
				},
			},
		},

		"HTTP_PROXY envvar HTTPS_PROXY envvar NO_PROXY envvar": {
			config: map[string]any{},
			environmentVariables: map[string]string{
				"HTTP_PROXY":  "http://http-proxy.test:1234",
				"HTTPS_PROXY": "http://https-proxy.test:1234",
				"NO_PROXY":    "dont-proxy.test",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://http-proxy.test:1234",
				},
				{
					url:           "http://dont-proxy.test",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://https-proxy.test:1234",
				},
				{
					url:           "https://dont-proxy.test",
					expectedProxy: "",
				},
			},
		},

		"http_proxy config overrides HTTP_PROXY envvar": {
			config: map[string]any{
				"http_proxy": "http://config-proxy.test:1234",
			},
			environmentVariables: map[string]string{
				"HTTP_PROXY": "http://envvar-proxy.test:1234",
			},
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.Sourceless(
					tfdiags.Warning,
					"Missing HTTPS Proxy",
					fmt.Sprintf(
						"An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+
							"To specify no proxy for HTTPS, set the HTTPS to an empty string.",
						"http://config-proxy.test:1234"),
				),
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "http://config-proxy.test:1234",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://config-proxy.test:1234",
				},
			},
		},

		"https_proxy config overrides HTTPS_PROXY envvar": {
			config: map[string]any{
				"https_proxy": "http://config-proxy.test:1234",
			},
			environmentVariables: map[string]string{
				"HTTPS_PROXY": "http://envvar-proxy.test:1234",
			},
			urls: []proxyCase{
				{
					url:           "http://example.com",
					expectedProxy: "",
				},
				{
					url:           "https://example.com",
					expectedProxy: "http://config-proxy.test:1234",
				},
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			config := map[string]any{
				"region":                      "us-west-2",
				"bucket":                      "tf-test",
				"key":                         "state",
				"skip_credentials_validation": true,
				"skip_requesting_account_id":  true,
				"access_key":                  servicemocks.MockStaticAccessKey,
				"secret_key":                  servicemocks.MockStaticSecretKey,
			}

			for k, v := range tc.environmentVariables {
				t.Setenv(k, v)
			}

			maps.Copy(config, tc.config)

			raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config))
			b := raw.(*Backend)

			tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectedDiags)

			client := b.awsConfig.HTTPClient
			bClient, ok := client.(*awshttp.BuildableClient)
			if !ok {
				t.Fatalf("expected awshttp.BuildableClient, got %T", client)
			}
			transport := bClient.GetTransport()
			proxyF := transport.Proxy

			for _, url := range tc.urls {
				req, _ := http.NewRequest("GET", url.url, nil)
				pUrl, err := proxyF(req)
				if err != nil {
					t.Fatalf("unexpected error: %s", err)
				}
				if url.expectedProxy != "" {
					if pUrl == nil {
						t.Errorf("expected proxy for %q, got none", url.url)
					} else if pUrl.String() != url.expectedProxy {
						t.Errorf("expected proxy %q for %q, got %q", url.expectedProxy, url.url, pUrl.String())
					}
				} else {
					if pUrl != nil {
						t.Errorf("expected no proxy for %q, got %q", url.url, pUrl.String())
					}
				}
			}
		})
	}
}

func TestBackendBasic(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "testState"

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":  bucketName,
		"key":     keyName,
		"encrypt": true,
		"region":  "us-west-1",
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

	backend.TestBackendStates(t, b)
}

func TestBackendLocked(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"dynamodb_table": bucketName,
		"region":         "us-west-1",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"dynamodb_table": bucketName,
		"region":         "us-west-1",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedWithFile(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedWithFile_ObjectLock_Compliance(t *testing.T) {
	testACC(t)
	objectLockPreCheck(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
		s3BucketWithVersioning,
		s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance),
	)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedWithFile_ObjectLock_Governance(t *testing.T) {
	testACC(t)
	objectLockPreCheck(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
		s3BucketWithVersioning,
		s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance),
	)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedWithFileAndDynamoDB(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   true,
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   true,
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendLockedMixedFileAndDynamoDB(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   true,
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackend_LockFileCleanupOnDynamoDBLock(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   false, // Only use DynamoDB
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   true, // Use both DynamoDB and lockfile
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

	backend.TestBackendStateLocks(t, b1, b2)

	// Attempt to retrieve the lock file from S3.
	_, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(b1.bucketName),
		Key:    aws.String(b1.keyName + ".tflock"),
	})
	// We expect an error here, indicating that the lock file does not exist.
	// The absence of the lock file is expected, as it should have been
	// cleaned up following a failed lock acquisition due to `b1` already
	// acquiring a DynamoDB lock.
	if err != nil {
		if !IsA[*s3types.NoSuchKey](err) {
			t.Fatalf("unexpected error: %s", err)
		}
	} else {
		t.Fatalf("expected error, got none")
	}
}

func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Compliance(t *testing.T) {
	testACC(t)
	objectLockPreCheck(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   false, // Only use DynamoDB
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   true, // Use both DynamoDB and lockfile
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
		s3BucketWithVersioning,
		s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance),
	)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

	backend.TestBackendStateLocks(t, b1, b2)

	// Attempt to retrieve the lock file from S3.
	_, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(b1.bucketName),
		Key:    aws.String(b1.keyName + ".tflock"),
	})
	// We expect an error here, indicating that the lock file does not exist.
	// The absence of the lock file is expected, as it should have been
	// cleaned up following a failed lock acquisition due to `b1` already
	// acquiring a DynamoDB lock.
	if err != nil {
		if !IsA[*s3types.NoSuchKey](err) {
			t.Fatalf("unexpected error: %s", err)
		}
	} else {
		t.Fatalf("expected error, got none")
	}
}

func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Governance(t *testing.T) {
	testACC(t)
	objectLockPreCheck(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   false, // Only use DynamoDB
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   true, // Use both DynamoDB and lockfile
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region,
		s3BucketWithVersioning,
		s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance),
	)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

	backend.TestBackendStateLocks(t, b1, b2)

	// Attempt to retrieve the lock file from S3.
	_, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(b1.bucketName),
		Key:    aws.String(b1.keyName + ".tflock"),
	})
	// We expect an error here, indicating that the lock file does not exist.
	// The absence of the lock file is expected, as it should have been
	// cleaned up following a failed lock acquisition due to `b1` already
	// acquiring a DynamoDB lock.
	if err != nil {
		if !IsA[*s3types.NoSuchKey](err) {
			t.Fatalf("unexpected error: %s", err)
		}
	} else {
		t.Fatalf("expected error, got none")
	}
}

func TestBackend_LockDeletedOutOfBand(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":         bucketName,
		"key":            keyName,
		"encrypt":        true,
		"use_lockfile":   true,
		"dynamodb_table": bucketName,
		"region":         "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	createDynamoDBTable(ctx, t, b1.dynClient, bucketName)
	defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName)

	testBackendStateLockDeletedOutOfBand(ctx, t, b1)
}

func TestBackend_KmsKeyId(t *testing.T) {
	testACC(t)
	kmsKeyID := os.Getenv("TF_S3_TEST_KMS_KEY_ID")
	if kmsKeyID == "" {
		t.Skip("TF_S3_KMS_KEY_ID is empty. Set this variable to an existing KMS key ID to run this test.")
	}

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"kms_key_id":   kmsKeyID,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"kms_key_id":   kmsKeyID,
		"use_lockfile": true,
		"region":       "us-west-2",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackend_ACL(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state"

	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
		"acl":          "bucket-owner-full-control",
	})).(*Backend)

	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":       bucketName,
		"key":          keyName,
		"encrypt":      true,
		"use_lockfile": true,
		"region":       "us-west-2",
		"acl":          "bucket-owner-full-control",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region)

	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateForceUnlock(t, b1, b2)
}

func TestBackendConfigKmsKeyId(t *testing.T) {
	testACC(t)

	testCases := map[string]struct {
		config        map[string]any
		expectedKeyId string
		expectedDiags tfdiags.Diagnostics
	}{
		"valid": {
			config: map[string]any{
				"kms_key_id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab",
			},
			expectedKeyId: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab",
		},

		"invalid": {
			config: map[string]any{
				"kms_key_id": "not-an-arn",
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid KMS Key ID",
					`Value must be a valid KMS Key ID, got "not-an-arn"`,
					cty.GetAttrPath("kms_key_id"),
				),
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
			config := map[string]any{
				"bucket":  bucketName,
				"encrypt": true,
				"key":     "test-SSE-KMS",
				"region":  "us-west-1",
			}
			maps.Copy(config, tc.config)

			b := New().(*Backend)
			configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config))

			configSchema, diags := b.PrepareConfig(configSchema)

			if !diags.HasErrors() {
				confDiags := b.Configure(configSchema)
				diags = diags.Append(confDiags)
			}

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Fatalf("unexpected diagnostics difference: %s", diff)
			}

			if tc.expectedKeyId != "" {
				if string(b.kmsKeyID) != tc.expectedKeyId {
					t.Fatal("unexpected value for KMS key Id")
				}
			}
		})
	}
}

func TestBackendSSECustomerKey(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	testCases := map[string]struct {
		config               map[string]any
		environmentVariables map[string]string
		expectedKey          string
		expectedDiags        tfdiags.Diagnostics
	}{
		// config
		"config valid": {
			config: map[string]any{
				"sse_customer_key": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
			},
			expectedKey: string(must(base64.StdEncoding.DecodeString("4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk="))),
		},
		"config invalid length": {
			config: map[string]any{
				"sse_customer_key": "test",
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid sse_customer_key value",
					"sse_customer_key must be 44 characters in length",
					cty.GetAttrPath("sse_customer_key"),
				),
			},
		},
		"config invalid encoding": {
			config: map[string]any{
				"sse_customer_key": "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka",
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid sse_customer_key value",
					"sse_customer_key must be base64 encoded: illegal base64 data at input byte 0",
					cty.GetAttrPath("sse_customer_key"),
				),
			},
		},

		// env var
		"envvar valid": {
			environmentVariables: map[string]string{
				"AWS_SSE_CUSTOMER_KEY": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
			},
			expectedKey: string(must(base64.StdEncoding.DecodeString("4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk="))),
		},
		"envvar invalid length": {
			environmentVariables: map[string]string{
				"AWS_SSE_CUSTOMER_KEY": "test",
			},
			expectedDiags: tfdiags.Diagnostics{
				wholeBodyErrDiag(
					"Invalid AWS_SSE_CUSTOMER_KEY value",
					`The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`,
				),
			},
		},
		"envvar invalid encoding": {
			environmentVariables: map[string]string{
				"AWS_SSE_CUSTOMER_KEY": "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka",
			},
			expectedDiags: tfdiags.Diagnostics{
				wholeBodyErrDiag(
					"Invalid AWS_SSE_CUSTOMER_KEY value",
					`The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded: illegal base64 data at input byte 0`,
				),
			},
		},

		// conflict
		"config kms_key_id and envvar AWS_SSE_CUSTOMER_KEY": {
			config: map[string]any{
				"kms_key_id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab",
			},
			environmentVariables: map[string]string{
				"AWS_SSE_CUSTOMER_KEY": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=",
			},
			expectedDiags: tfdiags.Diagnostics{
				wholeBodyErrDiag(
					"Invalid encryption configuration",
					encryptionKeyConflictEnvVarError,
				),
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
			config := map[string]any{
				"bucket":  bucketName,
				"encrypt": true,
				"key":     "test-SSE-C",
				"region":  "us-west-1",
			}
			maps.Copy(config, tc.config)

			oldEnv := os.Environ() // For now, save without clearing
			defer servicemocks.PopEnv(oldEnv)
			for k, v := range tc.environmentVariables {
				os.Setenv(k, v)
			}

			b := New().(*Backend)
			configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config))

			configSchema, diags := b.PrepareConfig(configSchema)

			if !diags.HasErrors() {
				confDiags := b.Configure(configSchema)
				diags = diags.Append(confDiags)
			}

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Fatalf("unexpected diagnostics difference: %s", diff)
			}

			if tc.expectedKey != "" {
				if string(b.customerEncryptionKey) != tc.expectedKey {
					t.Fatal("unexpected value for customer encryption key")
				}
			}

			if !diags.HasErrors() {
				createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
				defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

				backend.TestBackendStates(t, b)
			}
		})
	}
}

// add some extra junk in S3 to try and confuse the env listing.
func TestBackendExtraPaths(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "test/state/tfstate"

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":  bucketName,
		"key":     keyName,
		"encrypt": true,
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

	// put multiple states in old env paths.
	s1 := states.NewState()
	s2 := states.NewState()

	// RemoteClient to Put things in various paths
	client := &RemoteClient{
		s3Client:             b.s3Client,
		dynClient:            b.dynClient,
		bucketName:           b.bucketName,
		path:                 b.path("s1"),
		serverSideEncryption: b.serverSideEncryption,
		acl:                  b.acl,
		kmsKeyID:             b.kmsKeyID,
		ddbTable:             b.ddbTable,
	}

	// Write the first state
	stateMgr := &remote.State{Client: client}
	if err := stateMgr.WriteState(s1); err != nil {
		t.Fatal(err)
	}
	if err := stateMgr.PersistState(nil); err != nil {
		t.Fatal(err)
	}

	// Write the second state
	// Note a new state manager - otherwise, because these
	// states are equal, the state will not Put to the remote
	client.path = b.path("s2")
	stateMgr2 := &remote.State{Client: client}
	if err := stateMgr2.WriteState(s2); err != nil {
		t.Fatal(err)
	}
	if err := stateMgr2.PersistState(nil); err != nil {
		t.Fatal(err)
	}

	s2Lineage := stateMgr2.StateSnapshotMeta().Lineage

	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
		t.Fatal(err)
	}

	// put a state in an env directory name
	client.path = b.workspaceKeyPrefix + "/error"
	if err := stateMgr.WriteState(states.NewState()); err != nil {
		t.Fatal(err)
	}
	if err := stateMgr.PersistState(nil); err != nil {
		t.Fatal(err)
	}
	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
		t.Fatal(err)
	}

	// add state with the wrong key for an existing env
	client.path = b.workspaceKeyPrefix + "/s2/notTestState"
	if err := stateMgr.WriteState(states.NewState()); err != nil {
		t.Fatal(err)
	}
	if err := stateMgr.PersistState(nil); err != nil {
		t.Fatal(err)
	}
	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
		t.Fatal(err)
	}

	// remove the state with extra subkey
	if diags := client.Delete(); diags.HasErrors() {
		t.Fatal(diags.Err())
	}

	// delete the real workspace
	if diags := b.DeleteWorkspace("s2", true); diags.HasErrors() {
		t.Fatal(diags)
	}

	if err := checkStateList(b, []string{"default", "s1"}); err != nil {
		t.Fatal(err)
	}

	// fetch that state again, which should produce a new lineage
	s2Mgr, sDiags := b.StateMgr("s2")
	if sDiags.HasErrors() {
		t.Fatal(sDiags)
	}
	if err := s2Mgr.RefreshState(); err != nil {
		t.Fatal(err)
	}

	if s2Mgr.(*remote.State).StateSnapshotMeta().Lineage == s2Lineage {
		t.Fatal("state s2 was not deleted")
	}
	_ = s2Mgr.State() // We need the side-effect
	s2Lineage = stateMgr.StateSnapshotMeta().Lineage

	// add a state with a key that matches an existing environment dir name
	client.path = b.workspaceKeyPrefix + "/s2/"
	if err := stateMgr.WriteState(states.NewState()); err != nil {
		t.Fatal(err)
	}
	if err := stateMgr.PersistState(nil); err != nil {
		t.Fatal(err)
	}

	// make sure s2 is OK
	s2Mgr, sDiags = b.StateMgr("s2")
	if sDiags.HasErrors() {
		t.Fatal(sDiags)
	}
	if err := s2Mgr.RefreshState(); err != nil {
		t.Fatal(err)
	}

	if stateMgr.StateSnapshotMeta().Lineage != s2Lineage {
		t.Fatal("we got the wrong state for s2")
	}

	if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil {
		t.Fatal(err)
	}
}

// ensure we can separate the workspace prefix when it also matches the prefix
// of the workspace name itself.
func TestBackendPrefixInWorkspace(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":               bucketName,
		"key":                  "test-env.tfstate",
		"workspace_key_prefix": "env",
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

	// get a state that contains the prefix as a substring
	sMgr, sDiags := b.StateMgr("env-1")
	if sDiags.HasErrors() {
		t.Fatal(sDiags)
	}
	if err := sMgr.RefreshState(); err != nil {
		t.Fatal(err)
	}

	if err := checkStateList(b, []string{"default", "env-1"}); err != nil {
		t.Fatal(err)
	}
}

// ensure that we create the lock file in the correct location when using a
// workspace prefix.
func TestBackendLockFileWithPrefix(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())

	workspacePrefix := "prefix"
	key := "test/test-env.tfstate"

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":               bucketName,
		"use_lockfile":         true,
		"key":                  key,
		"workspace_key_prefix": workspacePrefix,
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, s3BucketWithVersioning)
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

	// get a state that contains the prefix as a substring
	sMgr, sDiags := b.StateMgr("env-1")
	if sDiags.HasErrors() {
		t.Fatal(sDiags)
	}
	if err := sMgr.RefreshState(); err != nil {
		t.Fatal(err)
	}

	if err := checkStateList(b, []string{"default", "env-1"}); err != nil {
		t.Fatal(err)
	}

	// Check if the lock file is created in the correct location
	//
	// If created and cleaned up correctly, a delete marker should
	// be present at the lock file key location.
	lockFileKey := fmt.Sprintf("%s/env-1/%s.tflock", workspacePrefix, key)
	out, err := b.s3Client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{
		Bucket: aws.String(bucketName),
	})
	if err != nil {
		t.Fatal(err)
	}

	found := false
	for _, item := range out.DeleteMarkers {
		if aws.ToString(item.Key) == lockFileKey {
			found = true
		}
	}
	if !found {
		t.Fatalf("lock file %q not found in expected location", lockFileKey)
	}
}

func TestBackendRestrictedRoot_Default(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	workspacePrefix := defaultWorkspaceKeyPrefix

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket": bucketName,
		"key":    "test/test-env.tfstate",
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, s3BucketWithPolicy(fmt.Sprintf(`{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "Statement1",
			"Effect": "Deny",
			"Principal": "*",
			"Action": "s3:ListBucket",
			"Resource": "arn:aws:s3:::%[1]s",
			"Condition": {
				"StringLike": {
					"s3:prefix": "%[2]s/*"
				}
			}
		}
	]
}`, bucketName, workspacePrefix)))
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

	sMgr, sDiags := b.StateMgr(backend.DefaultStateName)
	if sDiags.HasErrors() {
		t.Fatal(sDiags)
	}
	if err := sMgr.RefreshState(); err != nil {
		t.Fatal(err)
	}

	if err := checkStateList(b, []string{"default"}); err != nil {
		t.Fatal(err)
	}
}

func TestBackendRestrictedRoot_NamedPrefix(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	workspacePrefix := "prefix"

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":               bucketName,
		"key":                  "test/test-env.tfstate",
		"workspace_key_prefix": workspacePrefix,
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, s3BucketWithPolicy(fmt.Sprintf(`{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "Statement1",
			"Effect": "Deny",
			"Principal": "*",
			"Action": "s3:ListBucket",
			"Resource": "arn:aws:s3:::%[1]s",
			"Condition": {
				"StringLike": {
					"s3:prefix": "%[2]s/*"
				}
			}
		}
	]
}`, bucketName, workspacePrefix)))
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

	_, sDiags := b.StateMgr(backend.DefaultStateName)
	if !sDiags.HasErrors() {
		t.Fatal("expected AccessDenied error, got none")
	}
	if s := sDiags.Err().Error(); !strings.Contains(s, fmt.Sprintf("Unable to list objects in S3 bucket %q with prefix %q:", bucketName, workspacePrefix+"/")) {
		t.Fatalf("expected AccessDenied error, got: %s", s)
	}
}

func TestBackendWrongRegion(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "testState"

	bucketRegion := "us-west-1"
	backendRegion := "us-east-1"
	if backendRegion == bucketRegion {
		t.Fatalf("bucket region and backend region must not be the same")
	}

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":  bucketName,
		"key":     keyName,
		"encrypt": true,
		"region":  backendRegion,
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, bucketRegion)
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, bucketRegion)

	if _, sDiags := b.StateMgr(backend.DefaultStateName); !sDiags.HasErrors() {
		t.Fatal("expected error, got none")
	} else {
		var comparableErr error
		if errValue, isDiag := sDiags.Err().(tfdiags.DiagnosticsAsError); isDiag {
			// To use `As` below we need to extract the error that's wrapped
			// in a diagnostic.
			comparableErr = errValue.WrappedErrors()[0]
		}
		if regionErr, ok := As[bucketRegionError](comparableErr); ok {
			if a, e := regionErr.bucketRegion, bucketRegion; a != e {
				t.Errorf("expected bucket region %q, got %q", e, a)
			}
			if a, e := regionErr.requestRegion, backendRegion; a != e {
				t.Errorf("expected request region %q, got %q", e, a)
			}
		} else {
			t.Fatalf("expected bucket region error, got: %v", sDiags.Err())
		}
	}
}

func TestBackendS3ObjectLock(t *testing.T) {
	testACC(t)
	objectLockPreCheck(t)

	ctx := context.TODO()

	bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix())
	keyName := "testState"

	b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":  bucketName,
		"key":     keyName,
		"encrypt": true,
		"region":  "us-west-1",
	})).(*Backend)

	createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region,
		s3BucketWithVersioning,
		s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance),
	)
	defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region)

	backend.TestBackendStates(t, b)
}

func TestKeyEnv(t *testing.T) {
	testACC(t)

	ctx := context.TODO()

	keyName := "some/paths/tfstate"

	bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix())
	b0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":               bucket0Name,
		"key":                  keyName,
		"encrypt":              true,
		"workspace_key_prefix": "",
	})).(*Backend)

	createS3Bucket(ctx, t, b0.s3Client, bucket0Name, b0.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b0.s3Client, bucket0Name, b0.awsConfig.Region)

	bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix())
	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":               bucket1Name,
		"key":                  keyName,
		"encrypt":              true,
		"workspace_key_prefix": "project/env:",
	})).(*Backend)

	createS3Bucket(ctx, t, b1.s3Client, bucket1Name, b1.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b1.s3Client, bucket1Name, b1.awsConfig.Region)

	bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix())
	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":  bucket2Name,
		"key":     keyName,
		"encrypt": true,
	})).(*Backend)

	createS3Bucket(ctx, t, b2.s3Client, bucket2Name, b2.awsConfig.Region)
	defer deleteS3Bucket(ctx, t, b2.s3Client, bucket2Name, b2.awsConfig.Region)

	if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil {
		t.Fatal(err)
	}

	if err := testGetWorkspaceForKey(b0, "ws1/some/paths/tfstate", "ws1"); err != nil {
		t.Fatal(err)
	}

	if err := testGetWorkspaceForKey(b1, "project/env:/ws1/some/paths/tfstate", "ws1"); err != nil {
		t.Fatal(err)
	}

	if err := testGetWorkspaceForKey(b1, "project/env:/ws2/some/paths/tfstate", "ws2"); err != nil {
		t.Fatal(err)
	}

	if err := testGetWorkspaceForKey(b2, "env:/ws3/some/paths/tfstate", "ws3"); err != nil {
		t.Fatal(err)
	}

	backend.TestBackendStates(t, b0)
	backend.TestBackendStates(t, b1)
	backend.TestBackendStates(t, b2)
}

func TestAssumeRole_PrepareConfigValidation(t *testing.T) {
	path := cty.GetAttrPath("field")

	cases := map[string]struct {
		config        map[string]cty.Value
		expectedDiags tfdiags.Diagnostics
	}{
		"basic": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
			},
		},

		"invalid ARN": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("not an arn"),
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid ARN",
					`The value "not an arn" cannot be parsed as an ARN: arn: invalid prefix`,
					path.GetAttr("role_arn"),
				),
			},
		},

		"no role_arn": {
			config: map[string]cty.Value{},
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(path.GetAttr("role_arn")),
			},
		},

		"nil role_arn": {
			config: map[string]cty.Value{},
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(path.GetAttr("role_arn")),
			},
		},

		"empty role_arn": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal(""),
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					"The value cannot be empty or all whitespace",
					path.GetAttr("role_arn"),
				),
			},
		},

		"with duration": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"duration": cty.StringVal("2h"),
			},
		},

		"invalid duration": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"duration": cty.StringVal("two hours"),
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Duration",
					`The value "two hours" cannot be parsed as a duration: time: invalid duration "two hours"`,
					path.GetAttr("duration"),
				),
			},
		},

		"with external_id": {
			config: map[string]cty.Value{
				"role_arn":    cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"external_id": cty.StringVal("external-id"),
			},
		},

		"empty external_id": {
			config: map[string]cty.Value{
				"role_arn":    cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"external_id": cty.StringVal(""),
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value Length",
					`Length must be between 2 and 1224, had 0`,
					path.GetAttr("external_id"),
				),
			},
		},

		"with policy": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"policy":   cty.StringVal("{}"),
			},
		},

		"invalid policy": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"policy":   cty.StringVal(""),
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value cannot be empty or all whitespace`,
					path.GetAttr("policy"),
				),
			},
		},

		"with policy_arns": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"policy_arns": cty.SetVal([]cty.Value{
					cty.StringVal("arn:aws:iam::123456789012:policy/testpolicy"),
				}),
			},
		},

		"invalid policy_arns": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"policy_arns": cty.SetVal([]cty.Value{
					cty.StringVal("not an arn"),
				}),
			},
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid ARN",
					`The value "not an arn" cannot be parsed as an ARN: arn: invalid prefix`,
					path.GetAttr("policy_arns").IndexString("not an arn"),
				),
			},
		},

		"with session_name": {
			config: map[string]cty.Value{
				"role_arn":     cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"session_name": cty.StringVal("session-name"),
			},
		},

		"source_identity": {
			config: map[string]cty.Value{
				"role_arn":        cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"source_identity": cty.StringVal("source-identity"),
			},
		},

		"with tags": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"tags": cty.MapVal(map[string]cty.Value{
					"tag-key": cty.StringVal("tag-value"),
				}),
			},
		},

		"with transitive_tag_keys": {
			config: map[string]cty.Value{
				"role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"),
				"transitive_tag_keys": cty.SetVal([]cty.Value{
					cty.StringVal("tag-key"),
				}),
			},
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			schema := assumeRoleSchema.Attributes
			vals := make(map[string]cty.Value, len(schema))
			for name, attrSchema := range schema {
				if val, ok := tc.config[name]; ok {
					vals[name] = val
				} else {
					vals[name] = cty.NullVal(attrSchema.SchemaAttribute().Type)
				}
			}
			config := cty.ObjectVal(vals)

			var diags tfdiags.Diagnostics
			validateNestedAttribute(assumeRoleSchema, config, path, &diags)

			if diff := cmp.Diff(diags, tc.expectedDiags, diagnosticComparer); diff != "" {
				t.Errorf("unexpected diagnostics difference: %s", diff)
			}
		})
	}
}

// TestBackend_CoerceValue verifies a cty.Object can be coerced into
// an s3 backend Block
//
// This serves as a smoke test for use of the terraform_remote_state
// data source with the s3 backend, replicating the process that
// data source uses. The returned value is ignored as the object is
// large (representing the entire s3 backend schema) and the focus of
// this test is early detection of coercion failures.
func TestBackend_CoerceValue(t *testing.T) {
	testCases := map[string]struct {
		Input   cty.Value
		WantErr string
	}{
		"basic": {
			Input: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test"),
			}),
		},
		"missing bucket": {
			Input: cty.ObjectVal(map[string]cty.Value{
				"key": cty.StringVal("test"),
			}),
			WantErr: `attribute "bucket" is required`,
		},
		"missing key": {
			Input: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
			}),
			WantErr: `attribute "key" is required`,
		},
		"assume_role": {
			Input: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test"),
				"assume_role": cty.ObjectVal(map[string]cty.Value{
					"role_arn": cty.StringVal("test"),
				}),
			}),
		},
		"assume_role missing role_arn": {
			Input: cty.ObjectVal(map[string]cty.Value{
				"bucket":      cty.StringVal("test"),
				"key":         cty.StringVal("test"),
				"assume_role": cty.ObjectVal(map[string]cty.Value{}),
			}),
			WantErr: `.assume_role: attribute "role_arn" is required`,
		},
		"assume_role_with_web_identity": {
			Input: cty.ObjectVal(map[string]cty.Value{
				"bucket": cty.StringVal("test"),
				"key":    cty.StringVal("test"),
				"assume_role_with_web_identity": cty.ObjectVal(map[string]cty.Value{
					"role_arn": cty.StringVal("test"),
				}),
			}),
		},
		"assume_role_with_web_identity missing role_arn": {
			Input: cty.ObjectVal(map[string]cty.Value{
				"bucket":                        cty.StringVal("test"),
				"key":                           cty.StringVal("test"),
				"assume_role_with_web_identity": cty.ObjectVal(map[string]cty.Value{}),
			}),
			WantErr: `.assume_role_with_web_identity: attribute "role_arn" is required`,
		},
	}

	for name, test := range testCases {
		t.Run(name, func(t *testing.T) {
			b := Backend{}
			// Skip checking the returned cty.Value as this object will be large.
			_, gotErrObj := b.ConfigSchema().CoerceValue(test.Input)

			if gotErrObj == nil {
				if test.WantErr != "" {
					t.Fatalf("coersion succeeded; want error: %q", test.WantErr)
				}
			} else {
				gotErr := tfdiags.FormatError(gotErrObj)
				if gotErr != test.WantErr {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", gotErr, test.WantErr)
				}
			}
		})
	}
}

func testBackendStateLockDeletedOutOfBand(ctx context.Context, t *testing.T, b1 *Backend) {
	t.Helper()

	tableName := b1.ddbTable
	bucketName := b1.bucketName
	s3StateKey := b1.keyName
	s3LockKey := s3StateKey + lockFileSuffix
	// The dynamoDB LockID value is the full statfile path (not the generated UUID)
	ddbLockID := fmt.Sprintf("%s/%s", bucketName, s3StateKey)

	// Get the default state
	b1StateMgr, sDiags := b1.StateMgr(backend.DefaultStateName)
	if sDiags.HasErrors() {
		t.Fatalf("error: %s", sDiags.Err())
	}
	if err := b1StateMgr.RefreshState(); err != nil {
		t.Fatalf("bad: %s", err)
	}

	// Fast exit if this doesn't support locking at all
	if _, ok := b1StateMgr.(statemgr.Locker); !ok {
		t.Logf("TestBackend: backend %T doesn't support state locking, not testing", b1)
		return
	}

	t.Logf("testing deletion of a dynamoDB state lock out of band")

	// Reassign so its obvious whats happening
	locker := b1StateMgr.(statemgr.Locker)

	info := statemgr.NewLockInfo()
	info.Operation = "test"
	info.Who = "clientA"

	lockID, err := locker.Lock(info)
	if err != nil {
		t.Fatal("unable to get initial lock:", err)
	}

	getInput := &s3.GetObjectInput{
		Bucket: &bucketName,
		Key:    &s3LockKey,
	}

	// Verify the s3 lock file exists
	if _, err = b1.s3Client.GetObject(ctx, getInput); err != nil {
		t.Fatal("failed to get s3 lock file:", err)
	}

	deleteInput := &dynamodb.DeleteItemInput{
		Key: map[string]dynamodbtypes.AttributeValue{
			"LockID": &dynamodbtypes.AttributeValueMemberS{
				Value: ddbLockID,
			},
		},
		TableName: aws.String(tableName),
	}

	// Delete the DynamoDB lock out of band
	if _, err = b1.dynClient.DeleteItem(ctx, deleteInput); err != nil {
		t.Fatal("failed to delete dynamodb item:", err)
	}

	if err = locker.Unlock(lockID); err == nil {
		t.Fatal("expected unlock failure, no error returned")
	}

	// Verify the s3 lock file was still cleaned up by Unlock
	_, err = b1.s3Client.GetObject(ctx, getInput)
	if err != nil {
		if !IsA[*s3types.NoSuchKey](err) {
			t.Fatalf("unexpected error getting s3 lock file: %s", err)
		}
	} else {
		t.Fatalf("expected error getting s3 lock file, got none")
	}
}

func testGetWorkspaceForKey(b *Backend, key string, expected string) error {
	if actual := b.keyEnv(key); actual != expected {
		return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual)
	}
	return nil
}

func checkStateList(b backend.Backend, expected []string) error {
	states, diags := b.Workspaces()
	if diags.HasErrors() {
		return diags.Err()
	}

	if !reflect.DeepEqual(states, expected) {
		return fmt.Errorf("incorrect states listed: %q", states)
	}
	return nil
}

type createS3BucketOptions struct {
	versioning     bool
	objectLockMode s3types.ObjectLockRetentionMode
	policy         string
}

type createS3BucketOptionsFunc func(*createS3BucketOptions)

func createS3Bucket(ctx context.Context, t *testing.T, s3Client *s3.Client, bucketName, region string, optFns ...createS3BucketOptionsFunc) {
	t.Helper()

	var opts createS3BucketOptions
	for _, f := range optFns {
		f(&opts)
	}

	createBucketReq := &s3.CreateBucketInput{
		Bucket: aws.String(bucketName),
	}
	if region != "us-east-1" {
		createBucketReq.CreateBucketConfiguration = &s3types.CreateBucketConfiguration{
			LocationConstraint: s3types.BucketLocationConstraint(region),
		}
	}
	if opts.objectLockMode != "" {
		createBucketReq.ObjectLockEnabledForBucket = aws.Bool(true)
	}

	// Be clear about what we're doing in case the user needs to clean
	// this up later.
	t.Logf("creating S3 bucket %s in %s", bucketName, region)
	_, err := s3Client.CreateBucket(ctx, createBucketReq, s3WithRegion(region))
	if err != nil {
		t.Fatal("failed to create test S3 bucket:", err)
	}

	if opts.versioning {
		_, err := s3Client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
			Bucket: aws.String(bucketName),
			VersioningConfiguration: &s3types.VersioningConfiguration{
				Status: s3types.BucketVersioningStatusEnabled,
			},
		})
		if err != nil {
			t.Fatalf("failed enabling versioning: %s", err)
		}
	}

	if opts.objectLockMode != "" {
		_, err := s3Client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{
			Bucket: aws.String(bucketName),
			ObjectLockConfiguration: &s3types.ObjectLockConfiguration{
				ObjectLockEnabled: s3types.ObjectLockEnabledEnabled,
				Rule: &s3types.ObjectLockRule{
					DefaultRetention: &s3types.DefaultRetention{
						Days: aws.Int32(1),
						Mode: opts.objectLockMode,
					},
				},
			},
		})
		if err != nil {
			t.Fatalf("failed enabling object locking: %s", err)
		}
	}

	if opts.policy != "" {
		_, err := s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{
			Bucket: aws.String(bucketName),
			Policy: &opts.policy,
		})
		if err != nil {
			t.Fatalf("failed setting bucket policy: %s", err)
		}
	}
}

func s3BucketWithVersioning(opts *createS3BucketOptions) {
	opts.versioning = true
}

func s3BucketWithObjectLock(mode s3types.ObjectLockRetentionMode) createS3BucketOptionsFunc {
	return func(opts *createS3BucketOptions) {
		opts.objectLockMode = mode
	}
}

func s3BucketWithPolicy(policy string) createS3BucketOptionsFunc {
	return func(opts *createS3BucketOptions) {
		opts.policy = policy
	}
}

func deleteS3Bucket(ctx context.Context, t *testing.T, s3Client *s3.Client, bucketName, region string) {
	t.Helper()

	warning := "WARNING: Failed to delete the test S3 bucket. It may have been left in your AWS account and may incur storage charges. (error was %s)"

	// first we have to get rid of the env objects, or we can't delete the bucket
	resp, err := s3Client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{Bucket: &bucketName}, s3WithRegion(region))
	if err != nil {
		t.Logf(warning, err)
		return
	}

	for _, obj := range resp.Versions {
		input := &s3.DeleteObjectInput{
			Bucket:    &bucketName,
			Key:       obj.Key,
			VersionId: obj.VersionId,
		}
		if _, err := s3Client.DeleteObject(ctx, input, s3WithRegion(region)); err != nil {
			// this will need cleanup no matter what, so just warn and exit
			t.Logf(warning, err)
			return
		}
	}
	for _, obj := range resp.DeleteMarkers {
		input := &s3.DeleteObjectInput{
			Bucket:    &bucketName,
			Key:       obj.Key,
			VersionId: obj.VersionId,
		}
		if _, err := s3Client.DeleteObject(ctx, input, s3WithRegion(region)); err != nil {
			// this will need cleanup no matter what, so just warn and exit
			t.Logf(warning, err)
			return
		}
	}

	if _, err := s3Client.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucketName}, s3WithRegion(region)); err != nil {
		t.Logf(warning, err)
	}
}

func s3WithRegion(region string) func(o *s3.Options) {
	return func(o *s3.Options) {
		o.Region = region
	}
}

// create the dynamoDB table, and wait until we can query it.
func createDynamoDBTable(ctx context.Context, t *testing.T, dynClient *dynamodb.Client, tableName string) {
	createInput := &dynamodb.CreateTableInput{
		AttributeDefinitions: []dynamodbtypes.AttributeDefinition{
			{
				AttributeName: aws.String("LockID"),
				AttributeType: dynamodbtypes.ScalarAttributeTypeS,
			},
		},
		KeySchema: []dynamodbtypes.KeySchemaElement{
			{
				AttributeName: aws.String("LockID"),
				KeyType:       dynamodbtypes.KeyTypeHash,
			},
		},
		ProvisionedThroughput: &dynamodbtypes.ProvisionedThroughput{
			ReadCapacityUnits:  aws.Int64(5),
			WriteCapacityUnits: aws.Int64(5),
		},
		TableName: aws.String(tableName),
	}

	t.Logf("creating DynamoDB table %s", tableName)
	_, err := dynClient.CreateTable(ctx, createInput)
	if err != nil {
		t.Fatal(err)
	}

	// now wait until it's ACTIVE
	start := time.Now()
	time.Sleep(time.Second)

	describeInput := &dynamodb.DescribeTableInput{
		TableName: aws.String(tableName),
	}

	for {
		resp, err := dynClient.DescribeTable(ctx, describeInput)
		if err != nil {
			t.Fatal(err)
		}

		if resp.Table.TableStatus == dynamodbtypes.TableStatusActive {
			return
		}

		if time.Since(start) > time.Minute {
			t.Fatalf("timed out creating DynamoDB table %s", tableName)
		}

		time.Sleep(3 * time.Second)
	}

}

func deleteDynamoDBTable(ctx context.Context, t *testing.T, dynClient *dynamodb.Client, tableName string) {
	params := &dynamodb.DeleteTableInput{
		TableName: aws.String(tableName),
	}
	_, err := dynClient.DeleteTable(ctx, params)
	if err != nil {
		t.Logf("WARNING: Failed to delete the test DynamoDB table %q. It has been left in your AWS account and may incur charges. (error was %s)", tableName, err)
	}
}

func populateSchema(t *testing.T, schema *configschema.Block, value cty.Value) cty.Value {
	ty := schema.ImpliedType()
	var path cty.Path
	val, err := unmarshal(value, ty, path)
	if err != nil {
		t.Fatalf("populating schema: %s", err)
	}
	return val
}

func unmarshal(value cty.Value, ty cty.Type, path cty.Path) (cty.Value, error) {
	switch {
	case ty.IsPrimitiveType():
		return value, nil
	// case ty.IsListType():
	// 	return unmarshalList(value, ty.ElementType(), path)
	case ty.IsSetType():
		return unmarshalSet(value, ty.ElementType(), path)
	case ty.IsMapType():
		return unmarshalMap(value, ty.ElementType(), path)
	// case ty.IsTupleType():
	// 	return unmarshalTuple(value, ty.TupleElementTypes(), path)
	case ty.IsObjectType():
		return unmarshalObject(value, ty.AttributeTypes(), path)
	default:
		return cty.NilVal, path.NewErrorf("unsupported type %s", ty.FriendlyName())
	}
}

func unmarshalSet(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
	if dec.IsNull() {
		return dec, nil
	}

	length := dec.LengthInt()

	if length == 0 {
		return cty.SetValEmpty(ety), nil
	}

	vals := make([]cty.Value, 0, length)
	dec.ForEachElement(func(key, val cty.Value) (stop bool) {
		vals = append(vals, val)
		return
	})

	return cty.SetVal(vals), nil
}

// func unmarshalList(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
// 	if dec.IsNull() {
// 		return dec, nil
// 	}

// 	length := dec.LengthInt()

// 	if length == 0 {
// 		return cty.ListValEmpty(ety), nil
// 	}

// 	vals := make([]cty.Value, 0, length)
// 	dec.ForEachElement(func(key, val cty.Value) (stop bool) {
// 		vals = append(vals, must(unmarshal(val, ety, path.Index(key))))
// 		return
// 	})

// 	return cty.ListVal(vals), nil
// }

func unmarshalMap(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) {
	if dec.IsNull() {
		return dec, nil
	}

	length := dec.LengthInt()

	if length == 0 {
		return cty.MapValEmpty(ety), nil
	}

	vals := make(map[string]cty.Value, length)
	dec.ForEachElement(func(key, val cty.Value) (stop bool) {
		k := stringValue(key)
		vals[k] = val
		return
	})

	return cty.MapVal(vals), nil
}

func unmarshalObject(dec cty.Value, atys map[string]cty.Type, path cty.Path) (cty.Value, error) {
	if dec.IsNull() {
		return dec, nil
	}
	valueTy := dec.Type()

	vals := make(map[string]cty.Value, len(atys))
	path = append(path, nil)
	for key, aty := range atys {
		path[len(path)-1] = cty.IndexStep{
			Key: cty.StringVal(key),
		}

		if !valueTy.HasAttribute(key) {
			vals[key] = cty.NullVal(aty)
		} else {
			val, err := unmarshal(dec.GetAttr(key), aty, path)
			if err != nil {
				return cty.DynamicVal, err
			}
			vals[key] = val
		}
	}

	return cty.ObjectVal(vals), nil
}

func must[T any](v T, err error) T {
	if err != nil {
		panic(err)
	} else {
		return v
	}
}

// testBackendConfigDiags is an equivalent to `backend.TestBackendConfig` which returns the diags to the caller
// instead of failing the test
func testBackendConfigDiags(t *testing.T, b backend.Backend, c hcl.Body) (backend.Backend, tfdiags.Diagnostics) {
	t.Helper()

	t.Logf("TestBackendConfig on %T with %#v", b, c)

	var diags tfdiags.Diagnostics

	// To make things easier for test authors, we'll allow a nil body here
	// (even though that's not normally valid) and just treat it as an empty
	// body.
	if c == nil {
		c = hcl.EmptyBody()
	}

	schema := b.ConfigSchema()
	spec := schema.DecoderSpec()
	obj, decDiags := hcldec.Decode(c, spec, nil)
	diags = diags.Append(decDiags)

	newObj, valDiags := b.PrepareConfig(obj)
	diags = diags.Append(valDiags.InConfigBody(c, ""))

	// it's valid for a Backend to have warnings (e.g. a Deprecation) as such we should only raise on errors
	if diags.HasErrors() {
		return b, diags
	}

	obj = newObj

	confDiags := b.Configure(obj)

	return b, diags.Append(confDiags)
}

func addRetrieveEndpointURLMiddleware(t *testing.T, endpoint *string) func(*middleware.Stack) error {
	return func(stack *middleware.Stack) error {
		return stack.Finalize.Add(
			retrieveEndpointURLMiddleware(t, endpoint),
			middleware.After,
		)
	}
}

func retrieveEndpointURLMiddleware(t *testing.T, endpoint *string) middleware.FinalizeMiddleware {
	return middleware.FinalizeMiddlewareFunc(
		"Test: Retrieve Endpoint",
		func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
			t.Helper()

			request, ok := in.Request.(*smithyhttp.Request)
			if !ok {
				t.Fatalf("Expected *github.com/aws/smithy-go/transport/http.Request, got %s", fullTypeName(in.Request))
			}

			url := request.URL
			url.RawQuery = ""

			*endpoint = url.String()

			return next.HandleFinalize(ctx, in)
		})
}

var errCancelOperation = fmt.Errorf("Test: Cancelling request")

func addCancelRequestMiddleware() func(*middleware.Stack) error {
	return func(stack *middleware.Stack) error {
		return stack.Finalize.Add(
			cancelRequestMiddleware(),
			middleware.After,
		)
	}
}

// cancelRequestMiddleware creates a Smithy middleware that intercepts the request before sending and cancels it
func cancelRequestMiddleware() middleware.FinalizeMiddleware {
	return middleware.FinalizeMiddlewareFunc(
		"Test: Cancel Requests",
		func(_ context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) {
			return middleware.FinalizeOutput{}, middleware.Metadata{}, errCancelOperation
		})
}

func fullTypeName(i interface{}) string {
	return fullValueTypeName(reflect.ValueOf(i))
}

func fullValueTypeName(v reflect.Value) string {
	if v.Kind() == reflect.Ptr {
		return "*" + fullValueTypeName(reflect.Indirect(v))
	}

	requestType := v.Type()
	return fmt.Sprintf("%s.%s", requestType.PkgPath(), requestType.Name())
}

func defaultEndpointDynamo(region string) string {
	r := dynamodb.NewDefaultEndpointResolverV2()

	ep, err := r.ResolveEndpoint(context.TODO(), dynamodb.EndpointParameters{
		Region: aws.String(region),
	})
	if err != nil {
		return err.Error()
	}

	if ep.URI.Path == "" {
		ep.URI.Path = "/"
	}

	return ep.URI.String()
}

func defaultEndpointS3(region string) string {
	r := s3.NewDefaultEndpointResolverV2()

	ep, err := r.ResolveEndpoint(context.TODO(), s3.EndpointParameters{
		Region: aws.String(region),
	})
	if err != nil {
		return err.Error()
	}

	if ep.URI.Path == "" {
		ep.URI.Path = "/"
	}

	return ep.URI.String()
}

// objectLockPreCheck gates tests using object lock enabled buckets
// by checking for a configured environment variable.
//
// With object lock enabled, the statefile object written to the bucket
// cannot be deleted by the deleteS3Bucket test helper, resulting in an
// orphaned bucket after acceptance tests complete. Deletion of this
// leftover resource must be completed out of band by waiting until the
// default "Compliance" retention period of the objects has expired
// (1 day), emptying the bucket, and deleting it.
//
// Because clean up requires additional action outside the scope of the
// acceptance test, tests including this check are skipped by default.
func objectLockPreCheck(t *testing.T) {
	if os.Getenv("TF_S3_OBJECT_LOCK_TEST") == "" {
		t.Skip("s3 backend tests using object lock enabled buckets require setting TF_S3_OBJECT_LOCK_TEST")
	}
}

// TestBoolAttrDefaultEnvVarOk tests the boolAttrDefaultEnvVarOk helper function
// which reads boolean values from environment variables. This is a regression
// test for https://github.com/hashicorp/terraform/issues/37601 where setting
// AWS_USE_FIPS_ENDPOINT=false incorrectly enabled FIPS endpoints.
func TestBoolAttrDefaultEnvVarOk(t *testing.T) {
	testCases := map[string]struct {
		attrValue cty.Value
		envValue  string
		wantBool  bool
		wantOk    bool
	}{
		"attr set true": {
			attrValue: cty.BoolVal(true),
			envValue:  "",
			wantBool:  true,
			wantOk:    true,
		},
		"attr set false": {
			attrValue: cty.BoolVal(false),
			envValue:  "",
			wantBool:  false,
			wantOk:    true,
		},
		"attr null, env true": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "true",
			wantBool:  true,
			wantOk:    true,
		},
		"attr null, env TRUE": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "TRUE",
			wantBool:  true,
			wantOk:    true,
		},
		"attr null, env True": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "True",
			wantBool:  true,
			wantOk:    true,
		},
		"attr null, env false": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "false",
			wantBool:  false,
			wantOk:    true,
		},
		"attr null, env FALSE": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "FALSE",
			wantBool:  false,
			wantOk:    true,
		},
		"attr null, env False": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "False",
			wantBool:  false,
			wantOk:    true,
		},
		"attr null, env empty": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "",
			wantBool:  false,
			wantOk:    false,
		},
		"attr null, env invalid": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "invalid",
			wantBool:  false,
			wantOk:    false,
		},
		"attr null, env yes": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "yes",
			wantBool:  false,
			wantOk:    false,
		},
		"attr null, env 1": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "1",
			wantBool:  false,
			wantOk:    false,
		},
		"attr null, env 0": {
			attrValue: cty.NullVal(cty.Bool),
			envValue:  "0",
			wantBool:  false,
			wantOk:    false,
		},
		"attr takes precedence over env": {
			attrValue: cty.BoolVal(false),
			envValue:  "true",
			wantBool:  false,
			wantOk:    true,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			servicemocks.StashEnv(t)

			const testEnvVar = "TF_TEST_BOOL_ENV_VAR"
			if tc.envValue != "" {
				os.Setenv(testEnvVar, tc.envValue)
			}

			obj := cty.ObjectVal(map[string]cty.Value{
				"test_attr": tc.attrValue,
			})

			gotBool, gotOk := boolAttrDefaultEnvVarOk(obj, "test_attr", testEnvVar)

			if gotBool != tc.wantBool {
				t.Errorf("got bool %v, want %v", gotBool, tc.wantBool)
			}
			if gotOk != tc.wantOk {
				t.Errorf("got ok %v, want %v", gotOk, tc.wantOk)
			}
		})
	}
}
