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

import (
	"strings"
	"testing"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

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

// storeOfKind returns a minimal GenericStore whose GetKind() reports the
// requested kind. TypeMeta is set as well so the selector validators, which
// read GetObjectKind().GroupVersionKind().Kind, agree with GetKind().
func storeOfKind(kind string) esv1.GenericStore {
	if kind == esv1.ClusterSecretStoreKind {
		return &esv1.ClusterSecretStore{TypeMeta: metav1.TypeMeta{Kind: kind}}
	}
	return &esv1.SecretStore{TypeMeta: metav1.TypeMeta{Kind: kind}}
}

func TestIsReferentKubernetesAuth(t *testing.T) {
	tests := []struct {
		name string
		auth *esv1.KubernetesAuth
		want bool
	}{
		{name: "nil auth", auth: nil, want: false},
		{name: "empty auth", auth: &esv1.KubernetesAuth{}, want: false},
		{
			name: "cert clientCert without namespace is referent",
			auth: &esv1.KubernetesAuth{Cert: &esv1.CertAuth{
				ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt"},
				ClientKey:  esmeta.SecretKeySelector{Name: "c", Key: "tls.key", Namespace: new("ns")},
			}},
			want: true,
		},
		{
			name: "cert clientKey without namespace is referent",
			auth: &esv1.KubernetesAuth{Cert: &esv1.CertAuth{
				ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt", Namespace: new("ns")},
				ClientKey:  esmeta.SecretKeySelector{Name: "c", Key: "tls.key"},
			}},
			want: true,
		},
		{
			name: "cert with both namespaces set is not referent",
			auth: &esv1.KubernetesAuth{Cert: &esv1.CertAuth{
				ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt", Namespace: new("ns")},
				ClientKey:  esmeta.SecretKeySelector{Name: "c", Key: "tls.key", Namespace: new("ns")},
			}},
			want: false,
		},
		{
			name: "serviceAccount without namespace is referent",
			auth: &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa"}},
			want: true,
		},
		{
			name: "serviceAccount with namespace is not referent",
			auth: &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa", Namespace: new("ns")}},
			want: false,
		},
		{
			name: "token without namespace is referent",
			auth: &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token"}}},
			want: true,
		},
		{
			name: "token with namespace is not referent",
			auth: &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token", Namespace: new("ns")}}},
			want: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := IsReferentKubernetesAuth(tt.auth); got != tt.want {
				t.Errorf("IsReferentKubernetesAuth() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestValidateKubernetesConnection(t *testing.T) {
	tests := []struct {
		name        string
		kind        string
		server      esv1.KubernetesServer
		auth        *esv1.KubernetesAuth
		authRef     *esmeta.SecretKeySelector
		wantErr     bool
		errContains string
		wantWarning bool
	}{
		{
			name:        "no CA and no authRef warns",
			kind:        esv1.SecretStoreKind,
			wantWarning: true,
		},
		{
			name:    "authRef suppresses the no-CA warning",
			kind:    esv1.SecretStoreKind,
			authRef: &esmeta.SecretKeySelector{Name: "kubeconfig", Key: "config"},
		},
		{
			name:   "CABundle suppresses the no-CA warning",
			kind:   esv1.SecretStoreKind,
			server: esv1.KubernetesServer{CABundle: []byte("ca")},
		},
		{
			name:   "SecretStore with CAProvider (no namespace) is valid",
			kind:   esv1.SecretStoreKind,
			server: esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca"}},
		},
		{
			name:        "ClusterSecretStore CAProvider requires namespace",
			kind:        esv1.ClusterSecretStoreKind,
			server:      esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca"}},
			wantErr:     true,
			errContains: "CAProvider.namespace must not be empty",
		},
		{
			name:        "SecretStore rejects CAProvider namespace",
			kind:        esv1.SecretStoreKind,
			server:      esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca", Namespace: new("ns")}},
			wantErr:     true,
			errContains: "CAProvider.namespace must be empty",
		},
		{
			name:   "ClusterSecretStore CAProvider with namespace is valid",
			kind:   esv1.ClusterSecretStoreKind,
			server: esv1.KubernetesServer{CAProvider: &esv1.CAProvider{Name: "ca", Namespace: new("ns")}},
		},
		{
			name:        "cert missing clientCert name",
			kind:        esv1.SecretStoreKind,
			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:        &esv1.KubernetesAuth{Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Key: "tls.crt"}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}}},
			wantErr:     true,
			errContains: "ClientCert.Name cannot be empty",
		},
		{
			name:        "cert missing clientCert key",
			kind:        esv1.SecretStoreKind,
			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:        &esv1.KubernetesAuth{Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Name: "c"}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}}},
			wantErr:     true,
			errContains: "ClientCert.Key cannot be empty",
		},
		{
			name:   "cert valid",
			kind:   esv1.SecretStoreKind,
			server: esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:   &esv1.KubernetesAuth{Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt"}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}}},
		},
		{
			name:        "token missing name",
			kind:        esv1.SecretStoreKind,
			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:        &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Key: "token"}}},
			wantErr:     true,
			errContains: "BearerToken.Name cannot be empty",
		},
		{
			name:        "token missing key",
			kind:        esv1.SecretStoreKind,
			server:      esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:        &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t"}}},
			wantErr:     true,
			errContains: "BearerToken.Key cannot be empty",
		},
		{
			name:   "token valid",
			kind:   esv1.SecretStoreKind,
			server: esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:   &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token"}}},
		},
		{
			name:   "referent serviceAccount on ClusterSecretStore is allowed",
			kind:   esv1.ClusterSecretStoreKind,
			server: esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:   &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa"}},
		},
		{
			// ClientCert has a cross-namespace selector on a SecretStore, which
			// the delegated ValidateSecretSelector rejects. Exercises the cert
			// selector error-propagation path.
			name:   "cert selector namespace mismatch is rejected",
			kind:   esv1.SecretStoreKind,
			server: esv1.KubernetesServer{CABundle: []byte("ca")},
			auth: &esv1.KubernetesAuth{
				Cert: &esv1.CertAuth{ClientCert: esmeta.SecretKeySelector{Name: "c", Key: "tls.crt", Namespace: new("other")}, ClientKey: esmeta.SecretKeySelector{Name: "c", Key: "tls.key"}},
			},
			wantErr: true,
		},
		{
			name:    "token selector namespace mismatch is rejected",
			kind:    esv1.SecretStoreKind,
			server:  esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:    &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token", Namespace: new("other")}}},
			wantErr: true,
		},
		{
			name:    "serviceAccount selector namespace mismatch is rejected",
			kind:    esv1.SecretStoreKind,
			server:  esv1.KubernetesServer{CABundle: []byte("ca")},
			auth:    &esv1.KubernetesAuth{ServiceAccount: &esmeta.ServiceAccountSelector{Name: "sa", Namespace: new("other")}},
			wantErr: true,
		},
		{
			name:        "valid config without CA warns only",
			kind:        esv1.SecretStoreKind,
			auth:        &esv1.KubernetesAuth{Token: &esv1.TokenAuth{BearerToken: esmeta.SecretKeySelector{Name: "t", Key: "token"}}},
			wantWarning: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			store := storeOfKind(tt.kind)
			warnings, err := ValidateKubernetesConnection(store, tt.server, tt.auth, tt.authRef)
			if (err != nil) != tt.wantErr {
				t.Fatalf("ValidateKubernetesConnection() error = %v, wantErr %v", err, tt.wantErr)
			}
			if tt.errContains != "" && (err == nil || !strings.Contains(err.Error(), tt.errContains)) {
				t.Errorf("ValidateKubernetesConnection() error = %v, want contains %q", err, tt.errContains)
			}
			if tt.wantWarning {
				if len(warnings) != 1 || warnings[0] != WarnNoCAConfigured {
					t.Errorf("ValidateKubernetesConnection() warnings = %v, want [%q]", warnings, WarnNoCAConfigured)
				}
			} else if len(warnings) > 0 {
				t.Errorf("ValidateKubernetesConnection() unexpected warnings: %v", warnings)
			}
		})
	}
}
