/*
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 beyondtrustworkloadcredentialsdynamic

import (
	"context"
	"errors"
	"testing"

	"github.com/google/go-cmp/cmp"
	corev1 "k8s.io/api/core/v1"
	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	kclient "sigs.k8s.io/controller-runtime/pkg/client"
	clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"

	"github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/fake"
	btwcutil "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/util"
)

type args struct {
	jsonSpec     *apiextensions.JSON
	kube         kclient.Client
	btsClientFn  func(server, token string) (btwcutil.Client, error)
	generateMock func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error)
}

type want struct {
	val        map[string][]byte
	partialVal map[string][]byte
	err        error
}

type testCase struct {
	reason string
	args   args
	want   want
}

func TestBeyondtrustWorkloadCredentialsDynamicSecretGenerator(t *testing.T) {
	namespace := "test-namespace"

	cases := map[string]testCase{
		"NilSpec": {
			reason: "Raise an error with empty spec.",
			args: args{
				jsonSpec: nil,
			},
			want: want{
				err: errors.New("no config spec provided"),
			},
		},
		"InvalidSpec": {
			reason: "Raise an error with invalid spec.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(``),
				},
			},
			want: want{
				err: errors.New("no beyondtrustworkloadcredentials provider config in spec"),
			},
		},
		"MissingFolderPath": {
			reason: "Raise error if path is not provided.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(specMissingFolderPath),
				},
				kube: clientfake.NewClientBuilder().Build(),
			},
			want: want{
				err: errors.New("path is required in spec"),
			},
		},
		"EmptySecretName": {
			reason: "Raise error if path ends with slash resulting in empty secret name.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(specEmptySecretName),
				},
				kube: clientfake.NewClientBuilder().Build(),
			},
			want: want{
				err: errors.New("invalid path: missing secret name in \"test/folder/\""),
			},
		},
		"MissingAuth": {
			reason: "Raise error if auth is not provided.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(specMissingAuth),
				},
				kube: clientfake.NewClientBuilder().Build(),
			},
			want: want{
				err: errors.New(
					"failed to create BeyondtrustWorkloadCredentials client: failed to load credentials: missing or invalid BeyondtrustWorkloadCredentials API Token in BeyondtrustWorkloadCredentials SecretStore",
				),
			},
		},
		"SecretNotFound": {
			reason: "Raise error if secret containing api token does not exist.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(specSecretNotFound),
				},
				kube: clientfake.NewClientBuilder().Build(),
			},
			want: want{
				err: errors.New(
					"failed to create BeyondtrustWorkloadCredentials client: failed to load credentials: " +
						"cannot get Kubernetes secret \"nonexistent-secret\" from namespace \"test-namespace\": " +
						"secrets \"nonexistent-secret\" not found",
				),
			},
		},
		"SuccessfulGeneration": {
			reason: "Successfully generate dynamic secret.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(validDynamicSecretSpec),
				},
				kube: clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "beyondtrustworkloadcredentials-token",
						Namespace: namespace,
					},
					Data: map[string][]byte{
						"token": []byte("test-token"),
					},
				}).Build(),
				btsClientFn: func(server, token string) (btwcutil.Client, error) {
					client := &fake.BeyondtrustWorkloadCredentialsClient{}
					client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)
					return client, nil
				},
				generateMock: func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error) {
					return &btwcutil.GeneratedSecret{
						AccessKeyID:     "AKIAIOSFODNN7EXAMPLE",
						SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
						SessionToken:    "FwoGZXIvYXdzEBYaD...",
						Expiration:      "2025-12-08T12:00:00Z",
						LeaseID:         "aws/creds/example/abc123",
					}, nil
				},
			},
			want: want{
				val: map[string][]byte{
					"accessKeyId":     []byte("AKIAIOSFODNN7EXAMPLE"),
					"secretAccessKey": []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
					"sessionToken":    []byte("FwoGZXIvYXdzEBYaD..."),
					"expiration":      []byte("2025-12-08T12:00:00Z"),
					"leaseId":         []byte("aws/creds/example/abc123"),
				},
			},
		},
		"SuccessfulGenerationAtRootFolder": {
			reason: "Successfully generate dynamic secret at root folder path.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(validDynamicSecretSpecNoFolder),
				},
				kube: clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "beyondtrustworkloadcredentials-token",
						Namespace: namespace,
					},
					Data: map[string][]byte{
						"token": []byte("test-token"),
					},
				}).Build(),
				btsClientFn: func(server, token string) (btwcutil.Client, error) {
					client := &fake.BeyondtrustWorkloadCredentialsClient{}
					client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)
					return client, nil
				},
				generateMock: func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error) {
					if folderPath != nil {
						return nil, errors.New("expected nil folder path")
					}
					// For generic secrets, we'll just return empty fields
					return &btwcutil.GeneratedSecret{
						AccessKeyID:     "admin",
						SecretAccessKey: "secret123",
					}, nil
				},
			},
			want: want{
				partialVal: map[string][]byte{
					"accessKeyId":     []byte("admin"),
					"secretAccessKey": []byte("secret123"),
				},
			},
		},
		"GenerationError": {
			reason: "Raise error when generation fails.",
			args: args{
				jsonSpec: &apiextensions.JSON{
					Raw: []byte(validDynamicSecretSpecWithFolder),
				},
				kube: clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "beyondtrustworkloadcredentials-token",
						Namespace: namespace,
					},
					Data: map[string][]byte{
						"token": []byte("test-token"),
					},
				}).Build(),
				btsClientFn: func(server, token string) (btwcutil.Client, error) {
					client := &fake.BeyondtrustWorkloadCredentialsClient{}
					client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)
					return client, nil
				},
				generateMock: func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error) {
					return nil, errors.New("API error: insufficient permissions")
				},
			},
			want: want{
				err: errors.New("unable to generate dynamic secret: API error: insufficient permissions"),
			},
		},
	}
	for name, tc := range cases {
		t.Run(name, func(t *testing.T) {
			// Create client factory function with mock setup
			newClientFn := func(server, token string) (btwcutil.Client, error) {
				client := &fake.BeyondtrustWorkloadCredentialsClient{}
				client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)

				// Set up the generate mock if provided
				if tc.args.generateMock != nil {
					client.WithGenerateDynamicSecret(tc.args.generateMock)
				}

				return client, nil
			}

			if tc.args.btsClientFn != nil {
				// If a custom client function is provided, wrap it to add the generate mock
				customFn := tc.args.btsClientFn
				newClientFn = func(server, token string) (btwcutil.Client, error) {
					client, err := customFn(server, token)
					if err != nil {
						return nil, err
					}

					// If it's a fake client, set up the generate mock
					if fakeClient, ok := client.(*fake.BeyondtrustWorkloadCredentialsClient); ok && tc.args.generateMock != nil {
						fakeClient.WithGenerateDynamicSecret(tc.args.generateMock)
					}

					return client, nil
				}
			}

			// Create generator with injected client factory
			gen := &Generator{
				NewBeyondtrustWorkloadCredentialsClient: newClientFn,
			}

			// Call gen.Generate() for all test cases
			val, _, err := gen.Generate(context.Background(), tc.args.jsonSpec, tc.args.kube, namespace)

			// Assertions
			if tc.want.err != nil {
				if err != nil {
					if diff := cmp.Diff(tc.want.err.Error(), err.Error()); diff != "" {
						t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): -want error, +got error:\n%s", tc.reason, diff)
					}
				} else {
					t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): -want error, +got val:\n%v", tc.reason, val)
				}
			} else if tc.want.partialVal != nil {
				// Success case: expect no error
				if err != nil {
					t.Fatalf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): unexpected error: expected nil, got %v", tc.reason, err)
				}
				for k, v := range tc.want.partialVal {
					if diff := cmp.Diff(v, val[k]); diff != "" {
						t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...) -> %s: -want partial, +got partial:\n%s", tc.reason, k, diff)
					}
				}
			} else {
				// Success case: expect no error
				if err != nil {
					t.Fatalf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): unexpected error: expected nil, got %v", tc.reason, err)
				}
				if diff := cmp.Diff(tc.want.val, val); diff != "" {
					t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): -want val, +got val:\n%s", tc.reason, diff)
				}
			}
		})
	}
}

func TestPathParsing(t *testing.T) {
	tests := []struct {
		name           string
		input          string
		wantFolder     *string
		wantSecretName string
	}{
		{
			name:           "Path with folder",
			input:          "test/subfolder/secret-name",
			wantFolder:     new("test/subfolder"),
			wantSecretName: "secret-name",
		},
		{
			name:           "Path without folder",
			input:          "secret-name",
			wantFolder:     nil,
			wantSecretName: "secret-name",
		},
		{
			name:           "Path with single folder",
			input:          "folder/secret",
			wantFolder:     new("folder"),
			wantSecretName: "secret",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotFolder, gotSecretName := parsePath(tt.input)

			if tt.wantFolder == nil && gotFolder != nil {
				t.Errorf("parsePath() gotFolder = %v, want nil", *gotFolder)
			} else if tt.wantFolder != nil && gotFolder == nil {
				t.Errorf("parsePath() gotFolder = nil, want %v", *tt.wantFolder)
			} else if tt.wantFolder != nil && gotFolder != nil && *gotFolder != *tt.wantFolder {
				t.Errorf("parsePath() gotFolder = %v, want %v", *gotFolder, *tt.wantFolder)
			}

			if gotSecretName != tt.wantSecretName {
				t.Errorf("parsePath() gotSecretName = %v, want %v", gotSecretName, tt.wantSecretName)
			}
		})
	}
}

func TestConvertToByteMap(t *testing.T) {
	tests := []struct {
		name  string
		input *btwcutil.GeneratedSecret
		want  map[string][]byte
	}{
		{
			name: "Valid string values",
			input: &btwcutil.GeneratedSecret{
				AccessKeyID:     "AKIAIOSFODNN7EXAMPLE",
				SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
				SessionToken:    "FwoGZXIvYXdzEBYaD...",
				LeaseID:         "aws/creds/example/abc123",
				Expiration:      "2025-12-08T12:00:00Z",
			},
			want: map[string][]byte{
				"accessKeyId":     []byte("AKIAIOSFODNN7EXAMPLE"),
				"secretAccessKey": []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
				"sessionToken":    []byte("FwoGZXIvYXdzEBYaD..."),
				"leaseId":         []byte("aws/creds/example/abc123"),
				"expiration":      []byte("2025-12-08T12:00:00Z"),
			},
		},
		{
			name: "Empty session token",
			input: &btwcutil.GeneratedSecret{
				AccessKeyID:     "AKIAIOSFODNN7EXAMPLE",
				SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
				LeaseID:         "aws/creds/example/abc123",
				Expiration:      "2025-12-08T12:00:00Z",
			},
			want: map[string][]byte{
				"accessKeyId":     []byte("AKIAIOSFODNN7EXAMPLE"),
				"secretAccessKey": []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
				"leaseId":         []byte("aws/creds/example/abc123"),
				"expiration":      []byte("2025-12-08T12:00:00Z"),
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := convertToByteMap(tt.input)
			if diff := cmp.Diff(tt.want, got); diff != "" {
				t.Errorf("convertToByteMap() mismatch (-want +got):\n%s", diff)
			}
		})
	}
}
