/*
Copyright © The ESO 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

    https://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 barbican

import (
	"context"
	"testing"

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

	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)

const (
	testAuthURL    = "https://keystone.example.com/v3"
	testTenantName = "test-tenant"
	testDomainName = "default"
	testRegion     = "RegionOne"
	testUsername   = "test-user"
	testPassword   = "test-password"
	testSecretName = "barbican-creds"
	testNamespace  = "default"
)

type validateStoreTestCase struct {
	name        string
	store       esv1.GenericStore
	expectError bool
	errorMsg    string
}

func TestProviderCapabilities(t *testing.T) {
	provider := &Provider{}
	capabilities := provider.Capabilities()

	assert.Equal(t, esv1.SecretStoreReadOnly, capabilities)
}

func TestValidateStore(t *testing.T) {
	provider := &Provider{}

	testCases := []validateStoreTestCase{
		{
			name:        "nil store should return error",
			store:       nil,
			expectError: true,
			errorMsg:    "store is nil",
		},
		{
			name:        "valid store should pass validation",
			store:       makeValidSecretStore(),
			expectError: false,
		},
		{
			name:        "username as value should pass validation",
			store:       makeSecretStoreWithValueUsername(),
			expectError: false,
		},
		{
			name:        "nil barbican provider should return error",
			store:       makeSecretStoreWithNilBarbican(),
			expectError: true,
			errorMsg:    "provider barbican is nil",
		},
		{
			name:        "missing authURL should return error",
			store:       makeSecretStoreWithMissingAuthURL(),
			expectError: true,
			errorMsg:    "authURL is required",
		},
		{
			name:        "username without value or secretRef should return error",
			store:       makeSecretStoreWithEmptyUsername(),
			expectError: true,
			errorMsg:    "auth.username",
		},
		{
			name:        "missing password secretRef should return error",
			store:       makeSecretStoreWithNoPasswordRef(),
			expectError: true,
			errorMsg:    "auth.password",
		},
		{
			name:        "cluster store without secretRef namespace should return error",
			store:       makeClusterSecretStoreNoNamespace(),
			expectError: true,
			errorMsg:    "namespace",
		},
		{
			name:        "cluster store with secretRef namespace should pass",
			store:       makeClusterSecretStoreWithNamespace(),
			expectError: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			warnings, err := provider.ValidateStore(tc.store)

			if tc.expectError {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tc.errorMsg)
				assert.Nil(t, warnings)
			} else {
				assert.NoError(t, err)
				assert.Nil(t, warnings)
			}
		})
	}
}

func TestGetProvider(t *testing.T) {
	testCases := []struct {
		name        string
		store       esv1.GenericStore
		expectError bool
		errorMsg    string
	}{
		{
			name:        "valid store with barbican provider",
			store:       makeValidSecretStore(),
			expectError: false,
		},
		{
			name:        "nil provider should return error",
			store:       makeSecretStoreWithNilProvider(),
			expectError: true,
			errorMsg:    "provider barbican is nil",
		},
		{
			name:        "nil barbican provider should return error",
			store:       makeSecretStoreWithNilBarbican(),
			expectError: true,
			errorMsg:    "provider barbican is nil",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			provider, err := getProvider(tc.store)

			if tc.expectError {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tc.errorMsg)
				assert.Nil(t, provider)
			} else {
				assert.NoError(t, err)
				assert.NotNil(t, provider)
				assert.Equal(t, testAuthURL, provider.AuthURL)
				assert.Equal(t, testTenantName, provider.TenantName)
				assert.Equal(t, testDomainName, provider.DomainName)
				assert.Equal(t, testRegion, provider.Region)
			}
		})
	}
}

func TestNewClient(t *testing.T) {
	testCases := []struct {
		name        string
		store       esv1.GenericStore
		kube        *clientfake.ClientBuilder
		expectError bool
		errorMsg    string
	}{
		{
			name:        "missing authURL should return error",
			store:       makeSecretStoreWithMissingAuthURL(),
			kube:        clientfake.NewClientBuilder().WithObjects(makeValidSecret()),
			expectError: true,
			errorMsg:    "missing required field",
		},
		{
			name:        "username as value should pass",
			store:       makeSecretStoreWithValueUsername(),
			kube:        clientfake.NewClientBuilder().WithObjects(makeValidSecretWithNoUsername()),
			expectError: false,
		},
		{
			name:        "username as value and secret should pass",
			store:       makeSecretStoreWithValueUsername(),
			kube:        clientfake.NewClientBuilder().WithObjects(makeValidSecret()),
			expectError: false,
		},
		{
			name:        "missing username secret should return error",
			store:       makeValidSecretStore(),
			kube:        clientfake.NewClientBuilder(),
			expectError: true,
			errorMsg:    "missing required field",
		},
		{
			name:        "missing password in secret should return error",
			store:       makeValidSecretStore(),
			kube:        clientfake.NewClientBuilder().WithObjects(makeSecretWithMissingPassword()),
			expectError: true,
			errorMsg:    "missing required field",
		},
		{
			name:        "nil barbican provider should return error",
			store:       makeSecretStoreWithNilBarbican(),
			kube:        clientfake.NewClientBuilder().WithObjects(makeValidSecret()),
			expectError: true,
			errorMsg:    "provider barbican is nil",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			provider := &Provider{}
			fakeClient := tc.kube.Build()

			// Note: This test will fail when trying to actually connect to OpenStack
			// In a real test environment, we would need to mock the OpenStack client
			_, err := provider.NewClient(context.Background(), tc.store, fakeClient, testNamespace)

			if tc.expectError {
				assert.Error(t, err)
				assert.Contains(t, err.Error(), tc.errorMsg)
			} else {
				// This would only pass with proper OpenStack mocking
				assert.Error(t, err) // We expect an error due to missing OpenStack mock
			}
		})
	}
}

// Helper functions to create test fixtures

func makeValidSecretStore() *esv1.SecretStore {
	return &esv1.SecretStore{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-store",
			Namespace: testNamespace,
		},
		Spec: esv1.SecretStoreSpec{
			Provider: &esv1.SecretStoreProvider{
				Barbican: &esv1.BarbicanProvider{
					AuthURL:    testAuthURL,
					TenantName: testTenantName,
					DomainName: testDomainName,
					Region:     testRegion,
					Auth: esv1.BarbicanAuth{
						Username: esv1.BarbicanProviderUsernameRef{
							SecretRef: &esmeta.SecretKeySelector{
								Name: testSecretName,
								Key:  "username",
							},
						},
						Password: esv1.BarbicanProviderPasswordRef{
							SecretRef: &esmeta.SecretKeySelector{
								Name: testSecretName,
								Key:  "password",
							},
						},
					},
				},
			},
		},
	}
}

func makeSecretStoreWithValueUsername() *esv1.SecretStore {
	store := makeValidSecretStore()
	store.Spec.Provider.Barbican.Auth.Username = esv1.BarbicanProviderUsernameRef{
		Value: testUsername,
	}
	return store
}

func makeSecretStoreWithNilProvider() *esv1.SecretStore {
	store := makeValidSecretStore()
	store.Spec.Provider = nil
	return store
}

func makeSecretStoreWithNilBarbican() *esv1.SecretStore {
	store := makeValidSecretStore()
	store.Spec.Provider.Barbican = nil
	return store
}

func makeSecretStoreWithMissingAuthURL() *esv1.SecretStore {
	store := makeValidSecretStore()
	store.Spec.Provider.Barbican.AuthURL = ""
	return store
}

func makeSecretStoreWithEmptyUsername() *esv1.SecretStore {
	store := makeValidSecretStore()
	store.Spec.Provider.Barbican.Auth.Username = esv1.BarbicanProviderUsernameRef{}
	return store
}

func makeSecretStoreWithNoPasswordRef() *esv1.SecretStore {
	store := makeValidSecretStore()
	store.Spec.Provider.Barbican.Auth.Password = esv1.BarbicanProviderPasswordRef{}
	return store
}

func makeClusterSecretStoreNoNamespace() *esv1.ClusterSecretStore {
	return &esv1.ClusterSecretStore{
		TypeMeta:   metav1.TypeMeta{Kind: esv1.ClusterSecretStoreKind},
		ObjectMeta: metav1.ObjectMeta{Name: "test-cluster-store"},
		Spec:       makeValidSecretStore().Spec,
	}
}

func makeClusterSecretStoreWithNamespace() *esv1.ClusterSecretStore {
	store := makeClusterSecretStoreNoNamespace()
	ns := testNamespace
	store.Spec.Provider.Barbican.Auth.Username.SecretRef.Namespace = &ns
	store.Spec.Provider.Barbican.Auth.Password.SecretRef.Namespace = &ns
	return store
}

func makeValidSecret() *corev1.Secret {
	return &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testSecretName,
			Namespace: testNamespace,
		},
		Data: map[string][]byte{
			"username": []byte(testUsername),
			"password": []byte(testPassword),
		},
	}
}

func makeValidSecretWithNoUsername() *corev1.Secret {
	return &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testSecretName,
			Namespace: testNamespace,
		},
		Data: map[string][]byte{
			"password": []byte(testPassword),
		},
	}
}

func makeSecretWithMissingPassword() *corev1.Secret {
	return &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      testSecretName,
			Namespace: testNamespace,
		},
		Data: map[string][]byte{
			"username": []byte(testUsername),
			// missing password key
		},
	}
}
