// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package azure

import (
	"errors"
	"os"
	"testing"
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
	"github.com/opentofu/opentofu/internal/backend"
	"github.com/opentofu/opentofu/internal/backend/remote-state/azure/auth"
	"github.com/opentofu/opentofu/internal/encryption"
	"github.com/opentofu/opentofu/internal/legacy/helper/acctest"
	"github.com/opentofu/opentofu/internal/states/remote"
)

func TestRemoteClient_impl(t *testing.T) {
	var _ remote.Client = new(RemoteClient)
	var _ remote.ClientLocker = new(RemoteClient)
}

func TestPutMaintainsMetadata(t *testing.T) {
	testAccAzureBackend(t)
	rs := acctest.RandString(4)
	res := testResourceNames(rs, "testState")

	authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}
	authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}

	resourceGroupClient, containerClient, err := createTestResources(t, &res, authCred)

	t.Cleanup(func() {
		destroyTestResources(t, resourceGroupClient, res)
	})
	if err != nil {
		t.Fatal(err)
	}

	headerName := "acceptancetest"
	expectedValue := "f3b56bad-33ad-4b93-a600-7a66e9cbd1eb"

	blobClient := containerClient.NewBlockBlobClient(res.storageKeyName)

	// PUT
	_, err = blobClient.UploadBuffer(t.Context(), []byte{}, nil)
	if err != nil {
		t.Fatalf("Error Creating Block Blob: %+v", err)
	}

	remoteClient := RemoteClient{
		blobClient: blobClient,
		timeout:    time.Duration(180) * time.Second,
	}

	// GET PROPERTIES
	blobReference, err := remoteClient.getBlobProperties(t.Context())
	if err != nil {
		t.Fatalf("Error loading Metadata: %+v", err)
	}
	// CHANGE + SET METADATA
	// Metadata should be empty; this is a new blob.
	blobReference.Metadata = make(map[string]*string)
	blobReference.Metadata[headerName] = &expectedValue
	_, err = blobClient.SetMetadata(t.Context(), blobReference.Metadata, nil)
	if err != nil {
		t.Fatalf("Error setting Metadata: %+v", err)
	}

	// UPDATE WITH PUT
	bytes := []byte(acctest.RandString(20))
	err = remoteClient.Put(t.Context(), bytes)
	if err != nil {
		t.Fatalf("Error putting data: %+v", err)
	}
	// CHECK METADATA AGAIN, SEE THAT IT IS NOT SQUOOSHED
	blobReference, err = remoteClient.getBlobProperties(t.Context())
	if err != nil {
		t.Fatalf("Error loading Metadata: %+v", err)
	}

	if metaval, ok := blobReference.Metadata[headerName]; !ok || *metaval != expectedValue {
		t.Fatalf("%q was not set to %q in the Metadata: %+v", headerName, expectedValue, blobReference.Metadata)
	}
}

func TestAccRemoteClientAccessKeyBasic(t *testing.T) {
	testAccAzureBackend(t)
	rs := acctest.RandString(4)
	res := testResourceNames(rs, "testState")

	authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}
	authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}

	resourceGroupClient, _, err := createTestResources(t, &res, authCred)

	t.Cleanup(func() {
		destroyTestResources(t, resourceGroupClient, res)
	})
	if err != nil {
		t.Fatal(err)
	}

	b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": res.storageAccountName,
		"container_name":       res.storageContainerName,
		"key":                  res.storageKeyName,
		"access_key":           res.storageAccountAccessKey,
		"use_cli":              false,
	})).(*Backend)

	s1, err := b1.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestClient(t, s1.(*remote.State).Client)

	b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": res.storageAccountName,
		"container_name":       res.storageContainerName,
		"key":                  res.storageKeyName,
		"access_key":           res.storageAccountAccessKey,
		"use_cli":              false,
	})).(*Backend)

	s2, err := b2.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
}

func TestAccRemoteClientSASToken(t *testing.T) {
	testAccAzureBackend(t)
	rs := acctest.RandString(4)
	res := testResourceNames(rs, "testState")

	authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}
	authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}

	resourceGroupClient, _, err := createTestResources(t, &res, authCred)

	t.Cleanup(func() {
		destroyTestResources(t, resourceGroupClient, res)
	})
	if err != nil {
		t.Fatal(err)
	}

	keycred, err := azblob.NewSharedKeyCredential(res.storageAccountName, res.storageAccountAccessKey)
	if err != nil {
		t.Fatal(err)
	}

	sasToken, err := getSASToken(keycred)
	if err != nil {
		t.Fatal(err)
	}

	b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": res.storageAccountName,
		"container_name":       res.storageContainerName,
		"key":                  res.storageKeyName,
		"sas_token":            sasToken,
		"use_cli":              false,
	})).(*Backend)

	s1, err := b1.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestClient(t, s1.(*remote.State).Client)

	b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": res.storageAccountName,
		"container_name":       res.storageContainerName,
		"key":                  res.storageKeyName,
		"sas_token":            sasToken,
		"use_cli":              false,
	})).(*Backend)

	s2, err := b2.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
}

func TestAccRemoteClientServicePrincipalClientSecret(t *testing.T) {
	testAccAzureBackend(t)
	rs := acctest.RandString(4)
	res := testResourceNames(rs, "testState")

	client_id := os.Getenv("TF_AZURE_TEST_CLIENT_ID")
	client_secret := os.Getenv("TF_AZURE_TEST_CLIENT_SECRET")
	if client_id == "" || client_secret == "" {
		t.Skip(`
A client ID or client secret was not provided.
Please set TF_AZURE_TEST_CLIENT_ID and TF_AZURE_TEST_CLIENT_SECRET, either manually or using the terraform plan in the meta-test folder.`)
	}
	if res.tenantID == "" {
		t.Fatal(errors.New("A tenant ID must be provided through ARM_TENANT_ID in order to run this test."))
	}

	authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}
	authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}

	resourceGroupClient, _, err := createTestResources(t, &res, authCred)

	t.Cleanup(func() {
		destroyTestResources(t, resourceGroupClient, res)
	})
	if err != nil {
		t.Fatal(err)
	}

	b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": res.storageAccountName,
		"container_name":       res.storageContainerName,
		"key":                  res.storageKeyName,
		"resource_group_name":  res.resourceGroup,
		"client_id":            client_id,
		"client_secret":        client_secret,
		"use_cli":              false,
	})).(*Backend)

	s1, err := b1.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestClient(t, s1.(*remote.State).Client)

	b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": res.storageAccountName,
		"container_name":       res.storageContainerName,
		"key":                  res.storageKeyName,
		"access_key":           res.storageAccountAccessKey,
		"resource_group_name":  res.resourceGroup,
		"client_id":            client_id,
		"client_secret":        client_secret,
		"use_cli":              false,
	})).(*Backend)

	s2, err := b2.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
}

func TestAccRemoteClientCPK(t *testing.T) {
	testAccAzureBackend(t)
	rs := acctest.RandString(4)
	res := testResourceNames(rs, "testState")
	cpkKey := testCPKKey(t)

	authMethod, err := auth.GetAuthMethod(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}
	authCred, err := authMethod.Construct(t.Context(), testAuthConfig())
	if err != nil {
		t.Fatal(err)
	}

	resourceGroupClient, _, err := createTestResources(t, &res, authCred)

	t.Cleanup(func() {
		destroyTestResources(t, resourceGroupClient, res)
	})
	if err != nil {
		t.Fatal(err)
	}

	b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name":  res.storageAccountName,
		"container_name":        res.storageContainerName,
		"key":                   res.storageKeyName,
		"resource_group_name":   res.resourceGroup,
		"customer_provided_key": cpkKey,
	})).(*Backend)

	s1, err := b1.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestClient(t, s1.(*remote.State).Client)

	b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name":  res.storageAccountName,
		"container_name":        res.storageContainerName,
		"key":                   res.storageKeyName,
		"resource_group_name":   res.resourceGroup,
		"customer_provided_key": cpkKey,
	})).(*Backend)

	s2, err := b2.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)

}

// Note: This test does NOT create it's own infrastructure. Please refer to the meta-test folder for resource creation.
func TestAccRemoteClientCMK(t *testing.T) {
	testAccAzureBackend(t)

	storageAccountName := os.Getenv("TF_AZURE_TEST_STORAGE_ACCOUNT_NAME")
	resourceGroupName := os.Getenv("TF_AZURE_TEST_RESOURCE_GROUP_NAME")
	containerName := os.Getenv("TF_AZURE_TEST_CONTAINER_NAME")
	encryptionScope := os.Getenv("TF_AZURE_TEST_ENCRYPTION_SCOPE")

	if storageAccountName == "" || resourceGroupName == "" || containerName == "" || encryptionScope == "" {
		t.Skip(`
CMK testing requires pre-provisioned infrastructure.
Please set the following environment variables using the meta-test/cmk folder:
  TF_AZURE_TEST_STORAGE_ACCOUNT_NAME
  TF_AZURE_TEST_RESOURCE_GROUP_NAME
  TF_AZURE_TEST_CONTAINER_NAME
  TF_AZURE_TEST_ENCRYPTION_SCOPE`)
	}

	b1 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": storageAccountName,
		"container_name":       containerName,
		"key":                  "testState",
		"resource_group_name":  resourceGroupName,
		"encryption_scope":     encryptionScope,
	})).(*Backend)

	s1, err := b1.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestClient(t, s1.(*remote.State).Client)

	b2 := backend.TestBackendConfig(t, New(encryption.StateEncryptionDisabled()), backend.TestWrapConfig(map[string]any{
		"storage_account_name": storageAccountName,
		"container_name":       containerName,
		"key":                  "testState",
		"resource_group_name":  resourceGroupName,
		"encryption_scope":     encryptionScope,
	})).(*Backend)

	s2, err := b2.StateMgr(t.Context(), backend.DefaultStateName)
	if err != nil {
		t.Fatal(err)
	}

	remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client)
}
