/*
Copyright 2021 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
    http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package azure

import (
	"context"
	"encoding/base64"
	"os"
	"testing"
	"time"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const (
	fakeTenantID = "14bec2db-7f9a-4f3d-97ca-2d384ac83389"
	fakeClientID = "04bec2db-7f9a-4f3d-97ca-3d384ac83389"

	// Base64 encoded test pfx cert - Expire date: 09/19/2119.
	testCert = "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"
)

func TestGetClientCert(t *testing.T) {
	settings, err := NewEnvironmentSettings(
		map[string]string{
			"azureCertificateFile":     "testfile",
			"azureCertificate":         "testcert",
			"azureCertificatePassword": "1234",
			"azureClientId":            fakeClientID,
			"azureTenantId":            fakeTenantID,
			"vaultName":                "vaultName",
		},
	)
	assert.NoError(t, err)

	testCertConfig, _ := settings.GetClientCert()

	assert.Equal(t, "testfile", testCertConfig.CertificatePath)
	assert.Equal(t, []byte("testcert"), testCertConfig.CertificateData)
	assert.Equal(t, "1234", testCertConfig.CertificatePassword)
	assert.Equal(t, fakeClientID, testCertConfig.ClientID)
	assert.Equal(t, fakeTenantID, testCertConfig.TenantID)
	require.NotNil(t, testCertConfig.AzureCloud)
	assert.Equal(t, "https://login.microsoftonline.com/", testCertConfig.AzureCloud.ActiveDirectoryAuthorityHost)
	assert.Equal(t, "core.windows.net", settings.EndpointSuffix(ServiceAzureStorage))
}

func TestAzureCloud(t *testing.T) {
	settings, err := NewEnvironmentSettings(
		map[string]string{
			"azureCertificateFile":     "testfile",
			"azureCertificate":         "testcert",
			"azureCertificatePassword": "1234",
			"azureClientId":            fakeClientID,
			"azureTenantId":            fakeTenantID,
			"vaultName":                "vaultName",
			"azureEnvironment":         "AzureChina",
		},
	)
	require.NoError(t, err)

	testCertConfig, _ := settings.GetClientCert()

	assert.Equal(t, "testfile", testCertConfig.CertificatePath)
	assert.Equal(t, []byte("testcert"), testCertConfig.CertificateData)
	assert.Equal(t, "1234", testCertConfig.CertificatePassword)
	assert.Equal(t, fakeClientID, testCertConfig.ClientID)
	assert.Equal(t, fakeTenantID, testCertConfig.TenantID)
	require.NotNil(t, testCertConfig.AzureCloud)
	assert.Equal(t, "https://login.chinacloudapi.cn/", testCertConfig.AzureCloud.ActiveDirectoryAuthorityHost)
	assert.Equal(t, "core.chinacloudapi.cn", settings.EndpointSuffix(ServiceAzureStorage))
}

func TestEndpointSuffix(t *testing.T) {
	es := EnvironmentSettings{}

	es.Cloud = nil
	assert.Equal(t, "vault.azure.net", es.EndpointSuffix(ServiceAzureKeyVault))

	es.Cloud = &cloud.AzurePublic
	assert.Equal(t, "vault.azure.net", es.EndpointSuffix(ServiceAzureKeyVault))

	es.Cloud = &cloud.AzureGovernment
	assert.Equal(t, "vault.usgovcloudapi.net", es.EndpointSuffix(ServiceAzureKeyVault))
}

//nolint:gosec
func TestAuthorizorWithCertFile(t *testing.T) {
	testCertFileName := "./.cert.pfx"
	certBytes := getTestCert()
	err := os.WriteFile(testCertFileName, certBytes, 0o644)
	require.NoError(t, err)

	settings, err := NewEnvironmentSettings(
		map[string]string{
			"azureCertificateFile":     testCertFileName,
			"azureCertificatePassword": "",
			"azureClientId":            fakeClientID,
			"azureTenantId":            fakeTenantID,
			"vaultName":                "vaultName",
		},
	)
	require.NoError(t, err)

	testCertConfig, _ := settings.GetClientCert()

	spt, err := testCertConfig.GetTokenCredential()
	assert.NoError(t, err)
	assert.NotNil(t, spt)

	err = os.Remove(testCertFileName)
	assert.NoError(t, err)
}

func TestAuthorizorWithCertBytes(t *testing.T) {
	t.Run("Certificate is valid", func(t *testing.T) {
		certBytes := getTestCert()

		settings, err := NewEnvironmentSettings(
			map[string]string{
				"azureCertificate":         string(certBytes),
				"azureCertificatePassword": "",
				"azureClientId":            fakeClientID,
				"azureTenantId":            fakeTenantID,
				"vaultName":                "vaultName",
			},
		)
		assert.NoError(t, err)

		testCertConfig, _ := settings.GetClientCert()
		assert.NotNil(t, testCertConfig)

		spt, err := testCertConfig.GetTokenCredential()
		assert.NoError(t, err)
		assert.NotNil(t, spt)
	})

	t.Run("Certificate is invalid", func(t *testing.T) {
		certBytes := getTestCert()

		settings, err := NewEnvironmentSettings(
			map[string]string{
				"azureCertificate":         string(certBytes[0:20]),
				"azureCertificatePassword": "",
				"azureClientId":            fakeClientID,
				"azureTenantId":            fakeTenantID,
				"vaultName":                "vaultName",
			},
		)
		assert.NoError(t, err)

		testCertConfig, _ := settings.GetClientCert()
		assert.NotNil(t, testCertConfig)

		_, err = testCertConfig.GetTokenCredential()
		assert.Error(t, err)
	})
}

func TestGetMSI(t *testing.T) {
	settings, err := NewEnvironmentSettings(
		map[string]string{
			"azureClientId": fakeClientID,
			"vaultName":     "vaultName",
		},
	)
	assert.NoError(t, err)

	testCertConfig := settings.GetMSI()

	assert.Equal(t, fakeClientID, testCertConfig.ClientID)
}

func TestFallbackToMSI(t *testing.T) {
	os.Setenv("MSI_ENDPOINT", "test")
	defer os.Unsetenv("MSI_ENDPOINT")
	settings, err := NewEnvironmentSettings(
		map[string]string{
			"azureClientId": fakeClientID,
			"vaultName":     "vaultName",
		},
	)
	assert.NoError(t, err)

	spt, err := settings.GetTokenCredential()
	assert.NoError(t, err)
	assert.NotNil(t, spt)
}

func TestAuthorizorWithMSI(t *testing.T) {
	os.Setenv("MSI_ENDPOINT", "test")
	defer os.Unsetenv("MSI_ENDPOINT")
	settings, err := NewEnvironmentSettings(
		map[string]string{
			"azureClientId": fakeClientID,
			"vaultName":     "vaultName",
		},
	)
	assert.NoError(t, err)

	testCertConfig := settings.GetMSI()
	assert.NotNil(t, testCertConfig)

	spt, err := settings.GetTokenCredential()
	assert.NoError(t, err)
	assert.NotNil(t, spt)
}

func TestAuthorizorWithMSIAndUserAssignedID(t *testing.T) {
	os.Setenv("MSI_ENDPOINT", "test")
	defer os.Unsetenv("MSI_ENDPOINT")
	settings, err := NewEnvironmentSettings(
		map[string]string{
			"azureClientId": fakeClientID,
			"vaultName":     "vaultName",
		},
	)
	assert.NoError(t, err)

	testCertConfig := settings.GetMSI()
	assert.NotNil(t, testCertConfig)

	spt, err := settings.GetTokenCredential()
	assert.NoError(t, err)
	assert.NotNil(t, spt)
}

func getTestCert() []byte {
	certBytes, _ := base64.StdEncoding.DecodeString(testCert)

	return certBytes
}

func TestFallbackToCLI(t *testing.T) {
	settings, err := NewEnvironmentSettings(
		map[string]string{
			"vaultName": "vaultName",
		},
	)
	assert.NoError(t, err)

	// check whether this test can be run (i.e. Azure CLI is installed and logged in)

	runTest := false
	cred, credErr := azidentity.NewAzureCLICredential(nil)
	if credErr == nil {
		ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancelFunc()
		token, err := cred.GetToken(ctx, policy.TokenRequestOptions{})
		if err == nil && token.Token != "" {
			runTest = true
		}
	}

	if runTest {
		spt, err := settings.GetTokenCredential()
		assert.Nil(t, err)

		token, _ := spt.GetToken(context.Background(), policy.TokenRequestOptions{})
		assert.NotNil(t, token)
		assert.NotEmpty(t, token.Token)
	} else {
		t.Skip("Skipping test as Azure CLI is not installed or logged in. This test would fall through to MSI which is not available in the test environment.")
	}
}
