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

package oci

import (
	"context"
	"fmt"
	"os"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/tfdiags"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/backend"
	"github.com/oracle/oci-go-sdk/v65/common"
	"github.com/oracle/oci-go-sdk/v65/objectstorage"
)

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

	ctx := context.Background()

	bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix())
	keyName := "testState.json"
	namespace := getEnvSettingWithBlankDefault(NamespaceAttrName)
	compartmentId := getEnvSettingWithBlankDefault("compartment_id")

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

	response := createOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, namespace, compartmentId)
	defer deleteOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, *response.ETag, namespace)

	backend.TestBackendStates(t, b)
}
func TestBackendLocked_ForceUnlock(t *testing.T) {
	testACC(t)
	ctx := context.Background()
	bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix())
	keyName := "testState.json"
	namespace := getEnvSettingWithBlankDefault(NamespaceAttrName)
	compartmentId := getEnvSettingWithBlankDefault("compartment_id")
	b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":    bucketName,
		"key":       keyName,
		"namespace": namespace,
	})).(*Backend)
	b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{
		"bucket":    bucketName,
		"key":       keyName,
		"namespace": namespace,
	})).(*Backend)
	response := createOCIBucket(ctx, t, b1.client.objectStorageClient, bucketName, namespace, compartmentId)
	defer deleteOCIBucket(ctx, t, b1.client.objectStorageClient, bucketName, *response.ETag, namespace)
	// Test state locking and force-unlock
	backend.TestBackendStateLocks(t, b1, b2)
	backend.TestBackendStateLocksInWS(t, b1, b2, "testenv")
	backend.TestBackendStateForceUnlock(t, b1, b2)
	backend.TestBackendStateForceUnlockInWS(t, b1, b2, "testenv")
}
func TestBackendBasic_multipart_Upload(t *testing.T) {
	testACC(t)

	ctx := context.Background()
	DefaultFilePartSize = 100 //	100 Bytes
	bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix())
	keyName := "testState.json"
	namespace := getEnvSettingWithBlankDefault(NamespaceAttrName)
	compartmentId := getEnvSettingWithBlankDefault("compartment_id")

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

	response := createOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, namespace, compartmentId)
	defer deleteOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, *response.ETag, namespace)

	backend.TestBackendStates(t, b)
}

// Helper functions to create and delete OCI bucket
func createOCIBucket(ctx context.Context, t *testing.T, client *objectstorage.ObjectStorageClient, bucketName, namespace, compartmentId string) objectstorage.CreateBucketResponse {
	req := objectstorage.CreateBucketRequest{
		NamespaceName: common.String(namespace),
		CreateBucketDetails: objectstorage.CreateBucketDetails{
			Name:          common.String(bucketName),
			CompartmentId: common.String(compartmentId),
			Versioning:    objectstorage.CreateBucketDetailsVersioningEnabled,
		},
	}

	response, err := client.CreateBucket(ctx, req)
	if err != nil {
		t.Fatalf("failed to create OCI bucket: %v", err)
	}
	return response
}

func deleteOCIBucket(ctx context.Context, t *testing.T, client *objectstorage.ObjectStorageClient, bucketName, etag, namespace string) {
	request := objectstorage.ListObjectVersionsRequest{
		BucketName:    common.String(bucketName),
		NamespaceName: common.String(namespace),
		Prefix:        common.String(""),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}

	response, err := client.ListObjectVersions(context.Background(), request)
	if err != nil {
		t.Fatalf("failed to list(First page) OCI bucket objects: %v", err)
	}

	request.Page = response.OpcNextPage

	for request.Page != nil {
		request.RequestMetadata.RetryPolicy = getDefaultRetryPolicy()

		listResponse, err := client.ListObjectVersions(context.Background(), request)
		if err != nil {
			t.Fatalf("failed to list OCI bucket objects: %v", err)
		}
		response.Items = append(response.Items, listResponse.Items...)
		request.Page = listResponse.OpcNextPage
	}

	var diagErr tfdiags.Diagnostics

	for _, objectVersion := range response.Items {

		deleteObjectVersionRequest := objectstorage.DeleteObjectRequest{
			BucketName:    common.String(bucketName),
			NamespaceName: common.String(namespace),
			ObjectName:    objectVersion.Name,
			VersionId:     objectVersion.VersionId,
			RequestMetadata: common.RequestMetadata{
				RetryPolicy: getDefaultRetryPolicy(),
			},
		}

		_, err := client.DeleteObject(context.Background(), deleteObjectVersionRequest)
		if err != nil {
			diagErr = diagErr.Append(err)
		}
	}
	if diagErr != nil {
		t.Fatalf("error while deleting object from bucket: %v", diagErr.Err())
	}

	req := objectstorage.DeleteBucketRequest{
		NamespaceName: common.String(namespace),
		BucketName:    common.String(bucketName),
		IfMatch:       common.String(etag),
	}

	_, err = client.DeleteBucket(ctx, req)
	if err != nil {
		t.Fatalf("failed to delete OCI bucket: %v", err)
	}
}

// verify that we are doing ACC tests or the oci backend tests specifically
func testACC(t *testing.T) {
	skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_OCI_BACKEND_TEST") == ""
	if skip {
		t.Log("oci backend tests require setting TF_ACC or TF_OCI_BACKEND_TEST")
		t.Skip()
	}
}

func TestOCIBackendConfig_PrepareConfigValidation(t *testing.T) {
	cases := map[string]struct {
		config        cty.Value
		expectedDiags tfdiags.Diagnostics
		mock          func()
	}{
		"null bucket": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.NullVal(cty.String),
				NamespaceAttrName: cty.StringVal("test-namespace"),
				KeyAttrName:       cty.StringVal("test-key"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName)),
			},
		},
		"empty bucket": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal(""),
				NamespaceAttrName: cty.StringVal("test-namespace"),
				KeyAttrName:       cty.StringVal("test-key"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName)),
			},
		},
		"null namespace": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal("test-bucket"),
				NamespaceAttrName: cty.NullVal(cty.String),
				KeyAttrName:       cty.StringVal("test-key"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(cty.GetAttrPath("namespace")),
			},
		},
		"empty namespace": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal("test-bucket"),
				NamespaceAttrName: cty.StringVal(""),
				KeyAttrName:       cty.StringVal("test-key"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				requiredAttributeErrDiag(cty.GetAttrPath("namespace")),
			},
		},
		"key with leading slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal("test-bucket"),
				NamespaceAttrName: cty.StringVal("test-namespace"),
				KeyAttrName:       cty.StringVal("/leading-slash"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start or end with "/" and also not contain consecutive "/"`,
					cty.GetAttrPath(KeyAttrName),
				),
			},
		},
		"key with trailing slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal("test-bucket"),
				NamespaceAttrName: cty.StringVal("test-namespace"),
				KeyAttrName:       cty.StringVal("trailing-slash/"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start or end with "/" and also not contain consecutive "/"`,
					cty.GetAttrPath(KeyAttrName),
				),
			},
		},
		"key with double slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal("test-bucket"),
				NamespaceAttrName: cty.StringVal("test-namespace"),
				KeyAttrName:       cty.StringVal("test/with/double//slash"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start or end with "/" and also not contain consecutive "/"`,
					cty.GetAttrPath(KeyAttrName),
				),
			},
		},
		"workspace_key_prefix with leading slash": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:             cty.StringVal("test-bucket"),
				NamespaceAttrName:          cty.StringVal("test-namespace"),
				KeyAttrName:                cty.StringVal("test-key"),
				WorkspaceKeyPrefixAttrName: cty.StringVal("/env"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Value",
					`The value must not start  with "/" and also not contain consecutive "/"`,
					cty.GetAttrPath(WorkspaceKeyPrefixAttrName),
				),
			},
		},
		"encryption key conflict": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:               cty.StringVal("test-bucket"),
				NamespaceAttrName:            cty.StringVal("test-namespace"),
				KeyAttrName:                  cty.StringVal("test-key"),
				KmsKeyIdAttrName:             cty.StringVal("ocid1.key.oc1..example"),
				SseCustomerKeyAttrName:       cty.StringVal("base64-encoded-key"),
				SseCustomerKeySHA256AttrName: cty.StringVal("base64-encoded-key md5"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Attribute Combination",
					`Only one of kms_key_id, sse_customer_key can be set.`,
					cty.GetAttrPath(KmsKeyIdAttrName),
				),
			},
		},
		"Invalid encryption key combination": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:         cty.StringVal("test-bucket"),
				NamespaceAttrName:      cty.StringVal("test-namespace"),
				KeyAttrName:            cty.StringVal("test-key"),
				SseCustomerKeyAttrName: cty.StringVal("base64-encoded-key"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Attribute Combination",
					`  sse_customer_key and its SHA both required.`,
					cty.GetAttrPath(SseCustomerKeySHA256AttrName)),
			},
		},
		"private_key and private_key_path conflict": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:         cty.StringVal("test-bucket"),
				NamespaceAttrName:      cty.StringVal("test-namespace"),
				KeyAttrName:            cty.StringVal("test-key"),
				PrivateKeyAttrName:     cty.StringVal("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"),
				PrivateKeyPathAttrName: cty.StringVal("/path/to/key.pem"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				attributeErrDiag(
					"Invalid Attribute Combination",
					`Only one of private_key, private_key_path can be set.`,
					cty.GetAttrPath(PrivateKeyPathAttrName),
				),
			},
		},
		"invalid auth method": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal("test-bucket"),
				NamespaceAttrName: cty.StringVal("test-namespace"),
				KeyAttrName:       cty.StringVal("test-key"),
				AuthAttrName:      cty.StringVal("invalid-auth"),
			}),
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.AttributeValue(tfdiags.Error,
					"Invalid authentication method",
					fmt.Sprintf("auth must be one of '%s' or '%s' or '%s' or '%s' or '%s' or '%s'", AuthAPIKeySetting, AuthInstancePrincipalSetting, AuthInstancePrincipalWithCertsSetting, AuthSecurityToken, ResourcePrincipal, AuthOKEWorkloadIdentity), cty.GetAttrPath(AuthAttrName),
				),
			},
		},
		"missing region for InstancePrinciple auth": {
			config: cty.ObjectVal(map[string]cty.Value{
				BucketAttrName:    cty.StringVal("test-bucket"),
				NamespaceAttrName: cty.StringVal("test-namespace"),
				KeyAttrName:       cty.StringVal("test-key"),
				AuthAttrName:      cty.StringVal(AuthInstancePrincipalSetting),
			}),
			expectedDiags: tfdiags.Diagnostics{
				tfdiags.AttributeValue(tfdiags.Error,
					"Missing region attribute required",
					fmt.Sprintf("The attribute %q is required by the backend for %s authentication.\n\n", RegionAttrName, AuthInstancePrincipalSetting), cty.GetAttrPath(RegionAttrName),
				),
			},
			mock: func() {
				os.Setenv("OCI_region", "")
				os.Setenv("region", "")
			},
		},
	}

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

			b := New()
			if tc.mock != nil {
				tc.mock()
			}
			_, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config))

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

}

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 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) {
		vals[key.AsString()] = 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
}
