package v1

import (
	"context"
	"testing"

	storagev1 "k8s.io/api/storage/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/json"

	. "github.com/onsi/gomega"
	"github.com/samber/lo"
)

func TestAWSStorageClassMapper(t *testing.T) {
	cases := map[string]struct {
		kmsARN      string
		inputName   string
		expectedErr bool
		mappedName  string
	}{
		"valid ARN": {
			kmsARN:     "arn:aws:kms:us-west-2:123456789012:key/keyName",
			inputName:  "gp2",
			mappedName: "gp2-123456789012-key-keyName",
		},
		"valid ARN with multiple slashes": {
			kmsARN:     "arn:aws:kms:us-west-2:123456789012:key/path/to/keyName",
			inputName:  "gp3",
			mappedName: "gp3-123456789012-key-path-to-keyName",
		},
		"invalid ARN": {
			kmsARN:      "invalid:arn",
			inputName:   "io1",
			expectedErr: true,
		},
		"empty ARN": {
			kmsARN:      "",
			inputName:   "io1",
			expectedErr: true,
		},
	}

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

			mapper, err := NewAWSStorageClassMapper(tc.kmsARN)
			if tc.expectedErr {
				g.Expect(err).NotTo(BeNil())
				return
			}

			g.Expect(err).NotTo(HaveOccurred())
			mappedName := mapper.Name(tc.inputName)
			g.Expect(mappedName).To(Equal(tc.mappedName))
			sc := &storagev1.StorageClass{
				ObjectMeta: metav1.ObjectMeta{
					Name: tc.inputName,
				},
			}
			cloned := sc.DeepCopy()
			// Test StorageClass method
			convertedSC, ok := mapper.StorageClass(context.Background(), &Cluster{}, cloned)
			g.Expect(ok).To(BeTrue())
			g.Expect(convertedSC).NotTo(BeNil())
			g.Expect(convertedSC.Name).To(Equal(mappedName))
			g.Expect(convertedSC.Parameters).To(Equal(map[string]string{
				"kmsKeyId": tc.kmsARN,
			}))
			// Ensure the original storage class is not modified
			g.Expect(sc).To(Equal(cloned))

			// Test Name method matches Map
			g.Expect(mapper.Name(tc.inputName)).To(Equal(mappedName))
		})
	}
}

func TestAzureStorageClassMapper(t *testing.T) {
	cases := map[string]struct {
		spec         *AzureCMEKSpec
		cluster      *Cluster
		inputName    string
		expectedName string
		expectedSC   *storagev1.StorageClass
		expectedSCOk bool
		expectErr    bool
	}{
		"valid Azure KeyURI and ready cluster": {
			spec: &AzureCMEKSpec{
				KeyVaultName:              "myvault",
				KeyName:                   "mykey",
				KeyVersion:                "123456",
				ApplicationID:             "app-id",
				ManagedIdentityResourceID: "mi-id",
			},
			cluster: &Cluster{
				Status: ClusterStatus{
					Azure: &AzureClusterStatus{
						DiskEncryptionSetResourceID: "/subscriptions/xxx/resourceGroups/yyy/providers/Microsoft.Compute/diskEncryptionSets/zzz",
					},
				},
			},
			inputName:    "azure-sc",
			expectedName: "azure-sc-cluster1",
			expectedSC: &storagev1.StorageClass{
				ObjectMeta: metav1.ObjectMeta{
					Name: "azure-sc-cluster1",
					Annotations: map[string]string{
						AnnoAzureKeyvaultKeyURI: "https://myvault.vault.azure.net/keys/mykey/123456",
					},
					Labels: map[string]string{
						ClusterTagKey: "cluster1",
						LabelUsedFor:  ClusterFeatureUseCMEK,
					},
				},
				Parameters: map[string]string{
					AzureStorageClassParameterDiskEncryptionSetID: "/subscriptions/xxx/resourceGroups/yyy/providers/Microsoft.Compute/diskEncryptionSets/zzz",
				},
			},
			expectedSCOk: true,
			expectErr:    false,
		},
		"Azure CMEK spec is empty": {
			spec:      nil,
			cluster:   &Cluster{},
			inputName: "azure-sc",
			expectErr: true,
		},
		"invalid KeyURI (missing key version)": {
			spec: &AzureCMEKSpec{
				KeyVaultName:              "myvault",
				KeyName:                   "mykey",
				KeyVersion:                "",
				ApplicationID:             "app-id",
				ManagedIdentityResourceID: "mi-id",
			},
			cluster:   &Cluster{},
			inputName: "azure-sc",
			expectErr: true,
		},
		"invalid KeyURI (missing keyvault name)": {
			spec: &AzureCMEKSpec{
				KeyVaultName:              "",
				KeyName:                   "mykey",
				KeyVersion:                "123456",
				ApplicationID:             "app-id",
				ManagedIdentityResourceID: "mi-id",
			},
			cluster:   &Cluster{},
			inputName: "azure-sc",
			expectErr: true,
		},
		"invalid KeyURI (missing key name)": {
			spec: &AzureCMEKSpec{
				KeyVaultName:              "myvault",
				KeyName:                   "",
				KeyVersion:                "",
				ApplicationID:             "app-id",
				ManagedIdentityResourceID: "mi-id",
			},
			cluster:   &Cluster{},
			inputName: "azure-sc",
			expectErr: true,
		},
		"cluster Azure status not ready": {
			spec: &AzureCMEKSpec{
				KeyVaultName:              "myvault",
				KeyName:                   "mykey",
				KeyVersion:                "123456",
				ApplicationID:             "app-id",
				ManagedIdentityResourceID: "mi-id",
			},
			cluster:      &Cluster{Status: ClusterStatus{Azure: nil}},
			inputName:    "azure-sc",
			expectedName: "azure-sc-cluster1",
			expectedSCOk: false,
			expectErr:    false,
		},
		"cluster Azure DiskEncryptionSetResourceID empty": {
			spec: &AzureCMEKSpec{
				KeyVaultName:              "myvault",
				KeyName:                   "mykey",
				KeyVersion:                "123456",
				ApplicationID:             "app-id",
				ManagedIdentityResourceID: "mi-id",
			},
			cluster: &Cluster{
				Status: ClusterStatus{
					Azure: &AzureClusterStatus{
						DiskEncryptionSetResourceID: "",
					},
				},
			},
			inputName:    "azure-sc",
			expectedName: "azure-sc-cluster1",
			expectedSCOk: false,
			expectErr:    false,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			// init cluster CR
			tc.cluster.Name = "cluster1"
			aksSpec, _ := AKSClusterSpecForCluster(tc.cluster)
			aksSpec.AzureCMEK = tc.spec
			SetAKSClusterSpec(tc.cluster, aksSpec)

			mapper, err := NewAzureStorageClassMapper(tc.cluster)
			if tc.expectErr {
				g.Expect(err).NotTo(BeNil())
				return
			}
			g.Expect(err).To(BeNil())

			// Test Name method
			nameResult := mapper.Name(tc.inputName)
			g.Expect(nameResult).To(Equal(tc.expectedName))

			// Test StorageClass method
			sc := &storagev1.StorageClass{
				ObjectMeta: metav1.ObjectMeta{
					Name: tc.inputName,
				},
			}
			convertedSC, ok := mapper.StorageClass(context.Background(), tc.cluster, sc)
			g.Expect(ok).To(Equal(tc.expectedSCOk))
			if tc.expectedSCOk {
				g.Expect(convertedSC).NotTo(BeNil())
				g.Expect(convertedSC).To(Equal(tc.expectedSC))
			}
		})
	}
}

func TestNopMapper(t *testing.T) {
	g := NewGomegaWithT(t)
	mapper := NewNopMapper()

	cases := map[string]struct {
		inputName string
	}{
		"basic storage class": {
			inputName: "standard",
		},
		"empty name": {
			inputName: "",
		},
		"complex name": {
			inputName: "custom-storage-class-123",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			// Test Map method
			mappedName := mapper.Map(tc.inputName)
			g.Expect(mappedName).To(Equal(tc.inputName))

			// Test Name method
			g.Expect(mapper.Name(tc.inputName)).To(Equal(tc.inputName))

			// Test StorageClass method
			sc := &storagev1.StorageClass{
				ObjectMeta: metav1.ObjectMeta{
					Name: tc.inputName,
				},
			}
			convertedSC, ok := mapper.StorageClass(context.Background(), &Cluster{}, sc)
			g.Expect(ok).To(BeFalse())
			g.Expect(convertedSC).To(BeNil())
		})
	}
}

func TestSetEKSCMEKAnnotations(t *testing.T) {
	cases := map[string]struct {
		annotations map[string]string
		kmsARN      string
		expected    map[string]string
	}{
		"aws provider with kms key": {
			annotations: map[string]string{},
			kmsARN:      "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012",
			expected: map[string]string{
				AWSARNAnnotationKey: "arn:aws:kms:us-west-2:123456789012:key/12345678-1234-1234-1234-123456789012",
				CMEKProvider:        ProviderTypeEKS,
			},
		},
	}
	for _, tc := range cases {
		g := NewGomegaWithT(t)
		obj := &metav1.ObjectMeta{
			Annotations: tc.annotations,
		}
		SetEKSCMEKAnnotations(obj, tc.kmsARN)
		g.Expect(obj.Annotations).To(Equal(tc.expected))
	}
}

func TestRetrieveMapper(t *testing.T) {
	cases := map[string]struct {
		cluster             *Cluster
		name                string
		mappedName          string
		expectErrorContains string
	}{
		"empty cluster(cmek is not enabled)": {
			cluster:    &Cluster{},
			name:       "sc",
			mappedName: "sc",
		},
		"cluster with EKS CMEK": {
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnoKeyFeatureNamePrefix + ClusterFeatureUseCMEK: "true",
						AWSARNAnnotationKey: "arn:aws:kms:us-west-2:123456789012:key/keyName",
					},
					Labels: map[string]string{
						ProviderTypeLabel: ProviderTypeEKS,
					},
				},
			},
			name:       "sc",
			mappedName: "sc-123456789012-key-keyName",
		},
		"cluster with AKS CMEK": {
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnoKeyFeatureNamePrefix + ClusterFeatureUseCMEK: "true",
					},
					Labels: map[string]string{
						ProviderTypeLabel: ProviderTypeAKS,
					},
					Name: "cluster1",
				},
				Spec: ClusterSpec{
					ForProvider: &runtime.RawExtension{
						Raw: lo.Must(json.Marshal(AKSClusterSpec{
							AzureCMEK: &AzureCMEKSpec{
								KeyVaultName:              "myvault",
								KeyName:                   "mykey",
								KeyVersion:                "123456",
								ApplicationID:             "app-id",
								ManagedIdentityResourceID: "mi-id",
							},
						})),
					},
				},
				Status: ClusterStatus{
					Azure: &AzureClusterStatus{
						DiskEncryptionSetResourceID: "/subscriptions/xxx/resourceGroups/yyy/providers/Microsoft.Compute/diskEncryptionSets/zzz",
					},
				},
			},
			name:       "sc",
			mappedName: "sc-cluster1",
		},
		"GKE is not supported": {
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnoKeyFeatureNamePrefix + ClusterFeatureUseCMEK: "true",
					},
					Labels: map[string]string{
						ProviderTypeLabel: ProviderTypeGKE,
					},
					Name: "cluster1",
				},
			},
			expectErrorContains: "GKE does not support CMEKStorageClassMapper yet",
		},
		"the CMEKProvider will be ignore": {
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnoKeyFeatureNamePrefix + ClusterFeatureUseCMEK: "true",
						CMEKProvider:        "unknown",
						AWSARNAnnotationKey: "arn:aws:kms:us-west-2:123456789012:key/keyName",
					},
					Labels: map[string]string{
						ProviderTypeLabel: ProviderTypeEKS,
					},
				},
			},
			name:       "sc",
			mappedName: "sc-123456789012-key-keyName",
		},
		"cluster without AWSARNAnnotationKey": {
			cluster: &Cluster{
				ObjectMeta: metav1.ObjectMeta{
					Annotations: map[string]string{
						AnnoKeyFeatureNamePrefix + ClusterFeatureUseCMEK: "true",
					},
					Labels: map[string]string{
						ProviderTypeLabel: ProviderTypeEKS,
					},
				},
			},
			name:                "sc",
			expectErrorContains: "is required",
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			mapper, err := RetrieveMapper(tc.cluster)
			if tc.expectErrorContains != "" {
				g.Expect(err).NotTo(BeNil())
				g.Expect(err.Error()).To(ContainSubstring(tc.expectErrorContains))
				return
			}
			g.Expect(err).To(BeNil())

			mappedName := mapper.Name(tc.name)
			g.Expect(mappedName).To(Equal(tc.mappedName))
		})
	}
}

func TestParseAzureKeyvaultKeyURI(t *testing.T) {
	cases := map[string]struct {
		keyURI          string
		expectedVault   string
		expectedKeyName string
		expectedVersion string
		expectErr       bool
	}{
		"valid key URI with version": {
			keyURI:          "https://myvault.vault.azure.net/keys/mykey/123456",
			expectedVault:   "myvault",
			expectedKeyName: "mykey",
			expectedVersion: "123456",
			expectErr:       false,
		},
		"valid key URI without version": {
			keyURI:          "https://myvault.vault.azure.net/keys/mykey",
			expectedVault:   "myvault",
			expectedKeyName: "mykey",
			expectedVersion: "",
			expectErr:       false,
		},
		"valid key URI with empty version": {
			keyURI:          "https://testkv.vault.azure.net/keys/testkey/",
			expectedVault:   "testkv",
			expectedKeyName: "testkey",
			expectedVersion: "",
			expectErr:       false,
		},
		"valid key URI with special characters": {
			keyURI:          "https://my-vault-123.vault.azure.net/keys/my-key_123/abc-def",
			expectedVault:   "my-vault-123",
			expectedKeyName: "my-key_123",
			expectedVersion: "abc-def",
			expectErr:       false,
		},
		"invalid key URI (bad URL)": {
			keyURI:    "not a url",
			expectErr: true,
		},
		"invalid key URI (wrong domain)": {
			keyURI:    "https://myvault.notazure.net/keys/mykey/123456",
			expectErr: true,
		},
		"invalid key URI (missing key name)": {
			keyURI:    "https://myvault.vault.azure.net/keys/",
			expectErr: true,
		},
		"invalid key URI (too many segments)": {
			keyURI:    "https://myvault.vault.azure.net/keys/mykey/123456/extra",
			expectErr: true,
		},
		"invalid key URI (empty key name)": {
			keyURI:    "https://myvault.vault.azure.net/keys//123456",
			expectErr: true,
		},
		"invalid key URI (with query parameters)": {
			keyURI:    "https://myvault.vault.azure.net/keys/mykey/123456?query=param",
			expectErr: true,
		},
	}

	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			g := NewGomegaWithT(t)
			vault, keyName, version, err := ParseAzureKeyvaultKeyURI(tc.keyURI)
			if tc.expectErr {
				g.Expect(err).NotTo(BeNil())
				return
			}
			g.Expect(err).To(BeNil())
			g.Expect(vault).To(Equal(tc.expectedVault))
			g.Expect(keyName).To(Equal(tc.expectedKeyName))
			g.Expect(version).To(Equal(tc.expectedVersion))
		})
	}
}
