/*
Copyright the Velero contributors.

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 cacert

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	corev1api "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"

	velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
	"github.com/vmware-tanzu/velero/pkg/builder"
	"github.com/vmware-tanzu/velero/pkg/util"
)

func TestGetCACertFromBackup(t *testing.T) {
	testCases := []struct {
		name           string
		backup         *velerov1api.Backup
		bsl            *velerov1api.BackupStorageLocation
		expectedCACert string
		expectedError  bool
	}{
		{
			name: "backup with BSL containing cacert",
			backup: builder.ForBackup("test-ns", "test-backup").
				StorageLocation("test-bsl").
				Result(),
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				CACert([]byte("test-cacert-content")).
				Result(),
			expectedCACert: "test-cacert-content",
			expectedError:  false,
		},
		{
			name: "backup with BSL without cacert",
			backup: builder.ForBackup("test-ns", "test-backup").
				StorageLocation("test-bsl").
				Result(),
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				Result(),
			expectedCACert: "",
			expectedError:  false,
		},
		{
			name: "backup without storage location",
			backup: builder.ForBackup("test-ns", "test-backup").
				Result(),
			bsl:            nil,
			expectedCACert: "",
			expectedError:  false,
		},
		{
			name: "BSL not found",
			backup: builder.ForBackup("test-ns", "test-backup").
				StorageLocation("missing-bsl").
				Result(),
			bsl:            nil,
			expectedCACert: "",
			expectedError:  false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var objs []runtime.Object
			objs = append(objs, tc.backup)
			if tc.bsl != nil {
				objs = append(objs, tc.bsl)
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(util.VeleroScheme).
				WithRuntimeObjects(objs...).
				Build()

			cacert, err := GetCACertFromBackup(t.Context(), fakeClient, "test-ns", tc.backup)

			if tc.expectedError {
				assert.Error(t, err)
			} else {
				require.NoError(t, err)
				assert.Equal(t, tc.expectedCACert, cacert)
			}
		})
	}
}

func TestGetCACertFromRestore(t *testing.T) {
	testCases := []struct {
		name           string
		restore        *velerov1api.Restore
		backup         *velerov1api.Backup
		bsl            *velerov1api.BackupStorageLocation
		expectedCACert string
		expectedError  bool
	}{
		{
			name: "restore with backup having BSL containing cacert",
			restore: builder.ForRestore("test-ns", "test-restore").
				Backup("test-backup").
				Result(),
			backup: builder.ForBackup("test-ns", "test-backup").
				StorageLocation("test-bsl").
				Result(),
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				CACert([]byte("test-cacert-content")).
				Result(),
			expectedCACert: "test-cacert-content",
			expectedError:  false,
		},
		{
			name: "restore with backup not found",
			restore: builder.ForRestore("test-ns", "test-restore").
				Backup("missing-backup").
				Result(),
			backup:         nil,
			bsl:            nil,
			expectedCACert: "",
			expectedError:  false,
		},
		{
			name: "restore with backup having BSL without cacert",
			restore: builder.ForRestore("test-ns", "test-restore").
				Backup("test-backup").
				Result(),
			backup: builder.ForBackup("test-ns", "test-backup").
				StorageLocation("test-bsl").
				Result(),
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				Result(),
			expectedCACert: "",
			expectedError:  false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var objs []runtime.Object
			objs = append(objs, tc.restore)
			if tc.backup != nil {
				objs = append(objs, tc.backup)
			}
			if tc.bsl != nil {
				objs = append(objs, tc.bsl)
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(util.VeleroScheme).
				WithRuntimeObjects(objs...).
				Build()

			cacert, err := GetCACertFromRestore(t.Context(), fakeClient, "test-ns", tc.restore)

			if tc.expectedError {
				assert.Error(t, err)
			} else {
				require.NoError(t, err)
				assert.Equal(t, tc.expectedCACert, cacert)
			}
		})
	}
}

func TestGetCACertFromBSL(t *testing.T) {
	testCases := []struct {
		name           string
		bslName        string
		bsl            *velerov1api.BackupStorageLocation
		expectedCACert string
		expectedError  bool
	}{
		{
			name:    "BSL with cacert",
			bslName: "test-bsl",
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				CACert([]byte("test-cacert-content")).
				Result(),
			expectedCACert: "test-cacert-content",
			expectedError:  false,
		},
		{
			name:    "BSL without cacert",
			bslName: "test-bsl",
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				Result(),
			expectedCACert: "",
			expectedError:  false,
		},
		{
			name:           "empty BSL name",
			bslName:        "",
			bsl:            nil,
			expectedCACert: "",
			expectedError:  false,
		},
		{
			name:           "BSL not found",
			bslName:        "missing-bsl",
			bsl:            nil,
			expectedCACert: "",
			expectedError:  false,
		},
		{
			name:    "BSL with invalid CA cert format",
			bslName: "test-bsl",
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				CACert([]byte("INVALID CERT DATA WITHOUT PEM HEADERS")).
				Result(),
			expectedCACert: "INVALID CERT DATA WITHOUT PEM HEADERS", // We still return it, validation happens during TLS handshake
			expectedError:  false,
		},
		{
			name:    "BSL with malformed PEM certificate",
			bslName: "test-bsl",
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				CACert([]byte("-----BEGIN CERTIFICATE-----\nINVALID BASE64 DATA!!!\n-----END CERTIFICATE-----\n")).
				Result(),
			expectedCACert: "-----BEGIN CERTIFICATE-----\nINVALID BASE64 DATA!!!\n-----END CERTIFICATE-----\n",
			expectedError:  false,
		},
		{
			name:    "BSL with nil config",
			bslName: "test-bsl",
			bsl: &velerov1api.BackupStorageLocation{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-bsl",
				},
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "aws",
					Config:   nil,
				},
			},
			expectedCACert: "",
			expectedError:  false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var objs []runtime.Object
			if tc.bsl != nil {
				objs = append(objs, tc.bsl)
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(util.VeleroScheme).
				WithRuntimeObjects(objs...).
				Build()

			cacert, err := GetCACertFromBSL(t.Context(), fakeClient, "test-ns", tc.bslName)

			if tc.expectedError {
				assert.Error(t, err)
			} else {
				require.NoError(t, err)
				assert.Equal(t, tc.expectedCACert, cacert)
			}
		})
	}
}

// TestGetCACertFromBSL_WithCACertRef tests the new caCertRef functionality
func TestGetCACertFromBSL_WithCACertRef(t *testing.T) {
	testCases := []struct {
		name           string
		bslName        string
		bsl            *velerov1api.BackupStorageLocation
		secret         *corev1api.Secret
		expectedCACert string
		expectedError  bool
		errorContains  string
	}{
		{
			name:    "BSL with caCertRef pointing to valid secret",
			bslName: "test-bsl",
			bsl: &velerov1api.BackupStorageLocation{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-bsl",
				},
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "aws",
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "test-bucket",
							CACertRef: &corev1api.SecretKeySelector{
								LocalObjectReference: corev1api.LocalObjectReference{
									Name: "test-secret",
								},
								Key: "ca-bundle.crt",
							},
						},
					},
				},
			},
			secret: &corev1api.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-secret",
				},
				Data: map[string][]byte{
					"ca-bundle.crt": []byte("test-cacert-from-secret"),
				},
			},
			expectedCACert: "test-cacert-from-secret",
			expectedError:  false,
		},
		{
			name:    "BSL with both caCertRef and caCert - caCertRef takes precedence",
			bslName: "test-bsl",
			bsl: &velerov1api.BackupStorageLocation{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-bsl",
				},
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "aws",
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "test-bucket",
							CACert: []byte("inline-cacert-deprecated"),
							CACertRef: &corev1api.SecretKeySelector{
								LocalObjectReference: corev1api.LocalObjectReference{
									Name: "test-secret",
								},
								Key: "ca-bundle.crt",
							},
						},
					},
				},
			},
			secret: &corev1api.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-secret",
				},
				Data: map[string][]byte{
					"ca-bundle.crt": []byte("cacert-from-secret-takes-precedence"),
				},
			},
			expectedCACert: "cacert-from-secret-takes-precedence",
			expectedError:  false,
		},
		{
			name:    "BSL with caCertRef but secret not found",
			bslName: "test-bsl",
			bsl: &velerov1api.BackupStorageLocation{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-bsl",
				},
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "aws",
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "test-bucket",
							CACertRef: &corev1api.SecretKeySelector{
								LocalObjectReference: corev1api.LocalObjectReference{
									Name: "missing-secret",
								},
								Key: "ca-bundle.crt",
							},
						},
					},
				},
			},
			secret:         nil,
			expectedCACert: "",
			expectedError:  true,
			errorContains:  "certificate secret missing-secret not found",
		},
		{
			name:    "BSL with caCertRef but key not found in secret",
			bslName: "test-bsl",
			bsl: &velerov1api.BackupStorageLocation{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-bsl",
				},
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "aws",
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "test-bucket",
							CACertRef: &corev1api.SecretKeySelector{
								LocalObjectReference: corev1api.LocalObjectReference{
									Name: "test-secret",
								},
								Key: "missing-key",
							},
						},
					},
				},
			},
			secret: &corev1api.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-secret",
				},
				Data: map[string][]byte{
					"ca-bundle.crt": []byte("test-cacert"),
				},
			},
			expectedCACert: "",
			expectedError:  true,
			errorContains:  "key missing-key not found in secret test-secret",
		},
		{
			name:    "BSL with caCertRef but empty key",
			bslName: "test-bsl",
			bsl: &velerov1api.BackupStorageLocation{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-bsl",
				},
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "aws",
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "test-bucket",
							CACertRef: &corev1api.SecretKeySelector{
								LocalObjectReference: corev1api.LocalObjectReference{
									Name: "test-secret",
								},
								Key: "",
							},
						},
					},
				},
			},
			secret: &corev1api.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-secret",
				},
				Data: map[string][]byte{
					"ca-bundle.crt": []byte("test-cacert"),
				},
			},
			expectedCACert: "",
			expectedError:  true,
			errorContains:  "caCertRef key is empty",
		},
		{
			name:    "BSL with caCertRef containing multi-line PEM certificate",
			bslName: "test-bsl",
			bsl: &velerov1api.BackupStorageLocation{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-bsl",
				},
				Spec: velerov1api.BackupStorageLocationSpec{
					Provider: "aws",
					StorageType: velerov1api.StorageType{
						ObjectStorage: &velerov1api.ObjectStorageLocation{
							Bucket: "test-bucket",
							CACertRef: &corev1api.SecretKeySelector{
								LocalObjectReference: corev1api.LocalObjectReference{
									Name: "test-secret",
								},
								Key: "ca.pem",
							},
						},
					},
				},
			},
			secret: &corev1api.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Namespace: "test-ns",
					Name:      "test-secret",
				},
				Data: map[string][]byte{
					"ca.pem": []byte("-----BEGIN CERTIFICATE-----\nMIIDETC...\n-----END CERTIFICATE-----\n"),
				},
			},
			expectedCACert: "-----BEGIN CERTIFICATE-----\nMIIDETC...\n-----END CERTIFICATE-----\n",
			expectedError:  false,
		},
		{
			name:    "BSL falls back to inline caCert when caCertRef is nil",
			bslName: "test-bsl",
			bsl: builder.ForBackupStorageLocation("test-ns", "test-bsl").
				Provider("aws").
				Bucket("test-bucket").
				CACert([]byte("fallback-inline-cacert")).
				Result(),
			secret:         nil,
			expectedCACert: "fallback-inline-cacert",
			expectedError:  false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var objs []runtime.Object
			if tc.bsl != nil {
				objs = append(objs, tc.bsl)
			}
			if tc.secret != nil {
				objs = append(objs, tc.secret)
			}

			scheme := runtime.NewScheme()
			_ = velerov1api.AddToScheme(scheme)
			_ = corev1api.AddToScheme(scheme)

			fakeClient := fake.NewClientBuilder().
				WithScheme(scheme).
				WithRuntimeObjects(objs...).
				Build()

			cacert, err := GetCACertFromBSL(t.Context(), fakeClient, "test-ns", tc.bslName)

			if tc.expectedError {
				require.Error(t, err)
				if tc.errorContains != "" {
					assert.Contains(t, err.Error(), tc.errorContains)
				}
			} else {
				require.NoError(t, err)
				assert.Equal(t, tc.expectedCACert, cacert)
			}
		})
	}
}

// TestGetCACertFromBackup_ClientError tests error scenarios where client.Get returns non-NotFound errors
func TestGetCACertFromBackup_ClientError(t *testing.T) {
	testCases := []struct {
		name          string
		backup        *velerov1api.Backup
		bsl           *velerov1api.BackupStorageLocation
		expectedError string
	}{
		{
			name: "client error getting BSL",
			backup: builder.ForBackup("test-ns", "test-backup").
				StorageLocation("test-bsl").
				Result(),
			bsl: builder.ForBackupStorageLocation("different-ns", "test-bsl"). // Different namespace to trigger error
												Provider("aws").
												Bucket("test-bucket").
												CACert([]byte("test-cacert-content")).
												Result(),
			expectedError: "not found",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var objs []runtime.Object
			objs = append(objs, tc.backup)
			if tc.bsl != nil {
				objs = append(objs, tc.bsl)
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(util.VeleroScheme).
				WithRuntimeObjects(objs...).
				Build()

			// Try to get BSL from wrong namespace to simulate error
			_, err := GetCACertFromBSL(t.Context(), fakeClient, "wrong-ns", tc.backup.Spec.StorageLocation)

			require.NoError(t, err) // Not found errors are handled gracefully
		})
	}
}

// TestGetCACertFromRestore_ClientError tests error scenarios for GetCACertFromRestore
func TestGetCACertFromRestore_ClientError(t *testing.T) {
	testCases := []struct {
		name          string
		restore       *velerov1api.Restore
		backup        *velerov1api.Backup
		expectedError string
	}{
		{
			name: "backup in different namespace",
			restore: builder.ForRestore("test-ns", "test-restore").
				Backup("test-backup").
				Result(),
			backup: builder.ForBackup("different-ns", "test-backup"). // Different namespace
											StorageLocation("test-bsl").
											Result(),
			expectedError: "not found",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			var objs []runtime.Object
			objs = append(objs, tc.restore)
			if tc.backup != nil {
				objs = append(objs, tc.backup)
			}

			fakeClient := fake.NewClientBuilder().
				WithScheme(util.VeleroScheme).
				WithRuntimeObjects(objs...).
				Build()

			// This should not find the backup in the wrong namespace
			cacert, err := GetCACertFromRestore(t.Context(), fakeClient, "test-ns", tc.restore)

			require.NoError(t, err) // Not found errors are handled gracefully, returning empty string
			assert.Empty(t, cacert)
		})
	}
}
