// Copyright 2022-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package oidcclientsecretstorage

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	kubefake "k8s.io/client-go/kubernetes/fake"
	coretesting "k8s.io/client-go/testing"

	"go.pinniped.dev/internal/testutil"
)

func TestGet(t *testing.T) {
	tests := []struct {
		name       string
		uid        types.UID
		secret     *corev1.Secret
		wantRV     string
		wantHashes []string
		wantErr    string
	}{
		{
			name: "happy path",
			uid:  types.UID("some-example-uid1"),
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:            "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
					Namespace:       "some-namespace",
					UID:             "",
					ResourceVersion: "123",
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
				Data: map[string][]byte{
					"pinniped-storage-data": []byte(`{
						"hashes": ["foo", "bar"],
						"version": "1"
					}`),
					"pinniped-storage-version": []byte("1"),
				},
			},
			wantRV:     "123",
			wantHashes: []string{"foo", "bar"},
		},
		{
			name: "incorrect storage version: Data.pinniped-storage-version does not match crud storage version value",
			uid:  types.UID("some-example-uid1"),
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:            "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
					Namespace:       "some-namespace",
					UID:             "",
					ResourceVersion: "123",
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
				Data: map[string][]byte{
					"pinniped-storage-data": []byte(`{
						"hashes": ["orcs", "goblins"],
						"version": "1"
					}`),
					"pinniped-storage-version": []byte("9999999999"),
				},
			},
			wantRV:     "",
			wantHashes: []string(nil),
			wantErr:    "failed to get client secret for uid some-example-uid1: error during get for signature c29tZS1leGFtcGxlLXVpZDE: secret storage data has incorrect version",
		},
		{
			name: "incorrect storage version: Data.pinniped-storage-data.version does not match package const oidcClientSecretStorageVersion",
			uid:  types.UID("some-example-uid1"),
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:            "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
					Namespace:       "some-namespace",
					UID:             "",
					ResourceVersion: "123",
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
				Data: map[string][]byte{
					"pinniped-storage-data": []byte(`{
						"hashes": ["orcs", "goblins"],
						"version": "9999999999"
					}`),
					"pinniped-storage-version": []byte("1"),
				},
			},
			wantRV:     "",
			wantHashes: []string(nil),
			wantErr:    "OIDC client secret storage data has wrong version: OIDC client secret storage has version 9999999999 instead of 1",
		}, {
			name:       "not found",
			uid:        types.UID("some-example-uid1"),
			secret:     nil,
			wantRV:     "",
			wantHashes: nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			kubeClient := kubefake.NewClientset()
			if tt.secret != nil {
				require.NoError(t, kubeClient.Tracker().Add(tt.secret))
			}
			subject := New(kubeClient.CoreV1().Secrets("some-namespace"))
			rv, secretHashes, err := subject.Get(context.Background(), tt.uid)
			if tt.wantErr == "" {
				require.NoError(t, err)
			} else {
				require.EqualError(t, err, tt.wantErr)
			}
			require.Equal(t, tt.wantRV, rv)
			require.Equal(t, tt.wantHashes, secretHashes)
		})
	}
}

func TestSet(t *testing.T) {
	secretsGVR := schema.GroupVersionResource{
		Group:    "",
		Version:  "v1",
		Resource: "secrets",
	}
	namespace := "some-namespace"
	tests := []struct {
		name           string
		rv             string
		oidcClientName string
		oidcClientUID  types.UID
		hashes         []string
		seedSecret     *corev1.Secret
		addReactors    func(*kubefake.Clientset)
		wantErr        string
		wantActions    []coretesting.Action
	}{
		{
			name:           "happy path: new secret",
			oidcClientName: "some-client",
			oidcClientUID:  types.UID("some-example-uid1"),
			hashes:         []string{"foo"},
			seedSecret:     nil,
			wantActions: []coretesting.Action{
				coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name: "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
						Labels: map[string]string{
							"storage.pinniped.dev/type": "oidc-client-secret",
						},
						OwnerReferences: []metav1.OwnerReference{{
							APIVersion: "config.supervisor.pinniped.dev/v1alpha1",
							Kind:       "OIDCClient",
							Name:       "some-client",
							UID:        "some-example-uid1",
						}},
					},
					Type: "storage.pinniped.dev/oidc-client-secret",
					Data: map[string][]byte{
						"pinniped-storage-data":    []byte(`{"hashes":["foo"],"version":"1"}`),
						"pinniped-storage-version": []byte("1"),
					},
				}),
			},
		},
		{
			name:           "happy path: update existing secret and maintains existing owner reference",
			rv:             "9999",
			oidcClientName: "some-client",
			oidcClientUID:  types.UID("some-example-uid1"),
			hashes:         []string{"foo", "bar"},
			seedSecret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
					Namespace: namespace,
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
					ResourceVersion: "9999",
					OwnerReferences: []metav1.OwnerReference{{
						APIVersion: "config.supervisor.pinniped.dev/v1alpha1",
						Kind:       "OIDCClient",
						Name:       "some-client",
						UID:        "some-example-uid1",
					}},
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
				Data: map[string][]byte{
					"pinniped-storage-data":    []byte(`{"hashes":["foo"],"version":"1"}`),
					"pinniped-storage-version": []byte("1"),
				},
			},
			wantActions: []coretesting.Action{
				coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq"),
				coretesting.NewUpdateAction(secretsGVR, namespace, &corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name: "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
						Labels: map[string]string{
							"storage.pinniped.dev/type": "oidc-client-secret",
						},
						ResourceVersion: "9999",
						OwnerReferences: []metav1.OwnerReference{{
							APIVersion: "config.supervisor.pinniped.dev/v1alpha1",
							Kind:       "OIDCClient",
							Name:       "some-client",
							UID:        "some-example-uid1",
						}},
					},
					Type: "storage.pinniped.dev/oidc-client-secret",
					Data: map[string][]byte{
						"pinniped-storage-data":    []byte(`{"hashes":["foo","bar"],"version":"1"}`),
						"pinniped-storage-version": []byte("1"),
					},
				}),
			},
		},
		{
			name:           "failed to create client secret",
			oidcClientName: "some-client",
			oidcClientUID:  types.UID("some-example-uid1"),
			hashes:         []string{"foo", "bar"},
			addReactors: func(clientSet *kubefake.Clientset) {
				clientSet.PrependReactor("create", "secrets", func(action coretesting.Action) (bool, runtime.Object, error) {
					return true, nil, errors.New("some create error")
				})
			},
			wantActions: []coretesting.Action{
				coretesting.NewCreateAction(secretsGVR, namespace, &corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name: "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
						Labels: map[string]string{
							"storage.pinniped.dev/type": "oidc-client-secret",
						},
						OwnerReferences: []metav1.OwnerReference{{
							APIVersion: "config.supervisor.pinniped.dev/v1alpha1",
							Kind:       "OIDCClient",
							Name:       "some-client",
							UID:        "some-example-uid1",
						}},
					},
					Type: "storage.pinniped.dev/oidc-client-secret",
					Data: map[string][]byte{
						"pinniped-storage-data":    []byte(`{"hashes":["foo","bar"],"version":"1"}`),
						"pinniped-storage-version": []byte("1"),
					},
				}),
			},
			wantErr: "failed to create client secret for uid some-example-uid1: failed to create oidc-client-secret for signature c29tZS1leGFtcGxlLXVpZDE: some create error",
		},
		{
			name:           "failed to update",
			rv:             "23",
			oidcClientName: "some-client",
			oidcClientUID:  types.UID("some-example-uid1"),
			hashes:         []string{"foo", "bar"},
			seedSecret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
					Namespace: namespace,
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
					ResourceVersion: "23",
					OwnerReferences: []metav1.OwnerReference{{
						APIVersion: "config.supervisor.pinniped.dev/v1alpha1",
						Kind:       "OIDCClient",
						Name:       "some-client",
						UID:        "some-example-uid1",
					}},
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
				Data: map[string][]byte{
					"pinniped-storage-data":    []byte(`{"hashes":["foo"],"version":"1"}`),
					"pinniped-storage-version": []byte("1"),
				},
			},
			addReactors: func(clientSet *kubefake.Clientset) {
				clientSet.PrependReactor("update", "secrets", func(action coretesting.Action) (bool, runtime.Object, error) {
					return true, nil, errors.New("some update error maybe a conflict or something else")
				})
			},
			wantActions: []coretesting.Action{
				coretesting.NewGetAction(secretsGVR, namespace, "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq"),
				coretesting.NewUpdateAction(secretsGVR, namespace, &corev1.Secret{
					ObjectMeta: metav1.ObjectMeta{
						Name: "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
						Labels: map[string]string{
							"storage.pinniped.dev/type": "oidc-client-secret",
						},
						ResourceVersion: "23",
						OwnerReferences: []metav1.OwnerReference{{
							APIVersion: "config.supervisor.pinniped.dev/v1alpha1",
							Kind:       "OIDCClient",
							Name:       "some-client",
							UID:        "some-example-uid1",
						}},
					},
					Type: "storage.pinniped.dev/oidc-client-secret",
					Data: map[string][]byte{
						"pinniped-storage-data":    []byte(`{"hashes":["foo","bar"],"version":"1"}`),
						"pinniped-storage-version": []byte("1"),
					},
				}),
			},
			wantErr: "failed to update client secret for uid some-example-uid1: failed to update oidc-client-secret for signature c29tZS1leGFtcGxlLXVpZDE at resource version 23: some update error maybe a conflict or something else",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			kubeClient := kubefake.NewClientset()
			if tt.seedSecret != nil {
				require.NoError(t, kubeClient.Tracker().Add(tt.seedSecret))
			}
			if tt.addReactors != nil {
				tt.addReactors(kubeClient)
			}
			subject := New(kubeClient.CoreV1().Secrets("some-namespace"))
			err := subject.Set(
				context.Background(),
				tt.rv,
				tt.oidcClientName,
				tt.oidcClientUID,
				tt.hashes,
			)

			if tt.wantErr == "" {
				require.NoError(t, err)
			} else {
				require.EqualError(t, err, tt.wantErr)
			}

			require.Equal(t, tt.wantActions, kubeClient.Actions())
		})
	}
}

func TestGetStorageSecret(t *testing.T) {
	tests := []struct {
		name       string
		uid        types.UID
		secret     *corev1.Secret
		wantSecret *corev1.Secret
		wantErr    string
	}{
		{
			name: "happy path",
			uid:  types.UID("some-example-uid1"),
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
					Namespace: "some-namespace",
				},
				Data: map[string][]byte{
					"foo": []byte("bar"),
				},
			},
			wantSecret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
					Namespace: "some-namespace",
				},
				Data: map[string][]byte{
					"foo": []byte("bar"),
				},
			},
			wantErr: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			kubeClient := kubefake.NewClientset()
			require.NoError(t, kubeClient.Tracker().Add(tt.secret))
			subject := New(kubeClient.CoreV1().Secrets("some-namespace"))

			secret, err := subject.GetStorageSecret(context.Background(), tt.uid)
			require.Equal(t, tt.wantSecret, secret)

			if tt.wantErr == "" {
				require.NoError(t, err)
			} else {
				require.EqualError(t, err, tt.wantErr)
			}
		})
	}
}

func TestGetName(t *testing.T) {
	// Note that GetName() should not depend on the constructor params, to make it easier to use in various contexts.
	subject := New(nil)

	require.Equal(t,
		"pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq",
		subject.GetName("some-example-uid1"))

	require.Equal(t,
		"pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqza",
		subject.GetName("some-example-uid2"))
}

func TestReadFromSecret(t *testing.T) {
	tests := []struct {
		name       string
		secret     *corev1.Secret
		wantHashes []string
		wantErr    string
	}{
		{
			name: "happy path",
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:            "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4",
					ResourceVersion: "",
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
				},
				Data: map[string][]byte{
					"pinniped-storage-data":    []byte(`{"hashes":["first-hash","second-hash"],"version":"1"}`),
					"pinniped-storage-version": []byte("1"),
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
			},
			wantHashes: []string{"first-hash", "second-hash"},
		},
		{
			name: "wrong secret type",
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:            "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4",
					ResourceVersion: "",
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
				},
				Data: map[string][]byte{
					"pinniped-storage-data":    []byte(`{"hashes":["first-hash","second-hash"],"version":"1"}`),
					"pinniped-storage-version": []byte("1"),
				},
				Type: "storage.pinniped.dev/not-oidc-client-secret",
			},
			wantErr: "secret storage data has incorrect type: storage.pinniped.dev/not-oidc-client-secret must equal storage.pinniped.dev/oidc-client-secret",
		},
		{
			name: "wrong stored StoredClientSecret version",
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:            "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4",
					ResourceVersion: "",
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
				},
				Data: map[string][]byte{
					"pinniped-storage-data":    []byte(`{"hashes":["first-hash","second-hash"],"version":"wrong-version-here"}`),
					"pinniped-storage-version": []byte("1"),
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
			},
			wantErr: "OIDC client secret storage data has wrong version: OIDC client secret storage has version wrong-version-here instead of 1",
		},
		{
			name: "wrong storage version",
			secret: &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:            "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4",
					ResourceVersion: "",
					Labels: map[string]string{
						"storage.pinniped.dev/type": "oidc-client-secret",
					},
				},
				Data: map[string][]byte{
					"pinniped-storage-data":    []byte(`{"hashes":["first-hash","second-hash"],"version":"1"}`),
					"pinniped-storage-version": []byte("wrong-version-here"),
				},
				Type: "storage.pinniped.dev/oidc-client-secret",
			},
			wantErr: "secret storage data has incorrect version",
		},
		{
			name: "OIDCClientSecretStorageSecretForUID() test helper generates readable format, to ensure that test helpers are kept up to date",
			secret: testutil.OIDCClientSecretStorageSecretForUID(t,
				"some-namespace", "some-uid", []string{"first-hash", "second-hash"},
			),
			wantHashes: []string{"first-hash", "second-hash"},
		},
		{
			name: "OIDCClientSecretStorageSecretWithoutName() test helper generates readable format, to ensure that test helpers are kept up to date",
			secret: testutil.OIDCClientSecretStorageSecretWithoutName(t,
				"some-namespace", []string{"first-hash", "second-hash"},
			),
			wantHashes: []string{"first-hash", "second-hash"},
		},
		{
			name:    "OIDCClientSecretStorageSecretForUIDWithWrongVersion() test helper generates readable format, to ensure that test helpers are kept up to date",
			secret:  testutil.OIDCClientSecretStorageSecretForUIDWithWrongVersion(t, "some-namespace", "some-uid"),
			wantErr: "OIDC client secret storage data has wrong version: OIDC client secret storage has version wrong-version instead of 1",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			hashes, err := ReadFromSecret(tt.secret)
			if tt.wantErr == "" {
				require.NoError(t, err)
				require.Equal(t, tt.wantHashes, hashes)
			} else {
				require.EqualError(t, err, tt.wantErr)
				require.Nil(t, hashes)
			}
		})
	}
}
