package basic

import (
	"context"
	"fmt"
	"testing"

	v1 "k8s.io/api/authorization/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	fakedynamic "k8s.io/client-go/dynamic/fake"
	fakekube "k8s.io/client-go/kubernetes/fake"
	"k8s.io/client-go/rest"
	clienttesting "k8s.io/client-go/testing"

	workapiv1 "open-cluster-management.io/api/work/v1"

	testingcommon "open-cluster-management.io/ocm/pkg/common/testing"
)

func TestValidate(t *testing.T) {

	tests := map[string]struct {
		executor  *workapiv1.ManifestWorkExecutor
		namespace string
		name      string
		expect    error
	}{
		"executor nil": {
			executor: nil,
			expect:   nil,
		},
		"unsupported type": {
			executor: &workapiv1.ManifestWorkExecutor{
				Subject: workapiv1.ManifestWorkExecutorSubject{
					Type: "test",
				},
			},
			expect: fmt.Errorf("only support %s type for the executor", workapiv1.ExecutorSubjectTypeServiceAccount),
		},
		"sa nil": {
			executor: &workapiv1.ManifestWorkExecutor{
				Subject: workapiv1.ManifestWorkExecutorSubject{
					Type: workapiv1.ExecutorSubjectTypeServiceAccount,
				},
			},
			expect: fmt.Errorf("the executor service account is nil"),
		},
		"forbidden": {
			executor: &workapiv1.ManifestWorkExecutor{
				Subject: workapiv1.ManifestWorkExecutorSubject{
					Type: workapiv1.ExecutorSubjectTypeServiceAccount,
					ServiceAccount: &workapiv1.ManifestWorkSubjectServiceAccount{
						Namespace: "test-ns",
						Name:      "test-name",
					},
				},
			},
			namespace: "test-deny",
			name:      "test",
			expect:    fmt.Errorf("not allowed to apply the resource  secrets, test-deny test, will try again in 1m0s"),
		},
		"allow": {
			executor: &workapiv1.ManifestWorkExecutor{
				Subject: workapiv1.ManifestWorkExecutorSubject{
					Type: workapiv1.ExecutorSubjectTypeServiceAccount,
					ServiceAccount: &workapiv1.ManifestWorkSubjectServiceAccount{
						Namespace: "test-ns",
						Name:      "test-name",
					},
				},
			},
			namespace: "test-allow",
			name:      "test",
			expect:    nil,
		},
	}

	gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"}
	kubeClient := fakekube.NewSimpleClientset()
	kubeClient.PrependReactor("create", "subjectaccessreviews",
		func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
			obj := action.(clienttesting.CreateActionImpl).Object.(*v1.SubjectAccessReview)

			if obj.Spec.ResourceAttributes.Namespace == "test-allow" {
				return true, &v1.SubjectAccessReview{
					Status: v1.SubjectAccessReviewStatus{
						Allowed: true,
					},
				}, nil
			}

			if obj.Spec.ResourceAttributes.Namespace == "test-deny" {
				return true, &v1.SubjectAccessReview{
					Status: v1.SubjectAccessReviewStatus{
						Denied: true,
					},
				}, nil
			}
			return false, nil, nil
		},
	)
	validator := NewSARValidator(nil, kubeClient)
	for testName, test := range tests {
		t.Run(testName, func(t *testing.T) {
			err := validator.Validate(context.TODO(), test.executor, gvr, test.namespace, test.name, true, nil)
			if test.expect == nil {
				if err != nil {
					t.Errorf("expect nil but got %s", err)
				}
			} else if err == nil || err.Error() != test.expect.Error() {
				t.Errorf("expect %s but got %s", test.expect, err)
			}
		})
	}
}

func TestValidateEscalation(t *testing.T) {

	tests := map[string]struct {
		executor  *workapiv1.ManifestWorkExecutor
		namespace string
		name      string
		obj       *unstructured.Unstructured
		expect    error
	}{
		"forbidden": {
			executor: &workapiv1.ManifestWorkExecutor{
				Subject: workapiv1.ManifestWorkExecutorSubject{
					Type: workapiv1.ExecutorSubjectTypeServiceAccount,
					ServiceAccount: &workapiv1.ManifestWorkSubjectServiceAccount{
						Namespace: "test-ns",
						Name:      "test-name",
					},
				},
			},
			namespace: "test-deny",
			name:      "test",
			obj:       testingcommon.NewUnstructured("v1", "ClusterRole", "", "test"),
			expect: fmt.Errorf(
				"not allowed to apply the resource rbac.authorization.k8s.io roles, " +
					"test-deny test, error: permission escalation, will try again in 1m0s"),
		},
		"allow": {
			executor: &workapiv1.ManifestWorkExecutor{
				Subject: workapiv1.ManifestWorkExecutorSubject{
					Type: workapiv1.ExecutorSubjectTypeServiceAccount,
					ServiceAccount: &workapiv1.ManifestWorkSubjectServiceAccount{
						Namespace: "test-ns",
						Name:      "test-name",
					},
				},
			},
			namespace: "test-allow",
			name:      "test",
			obj:       testingcommon.NewUnstructured("v1", "Role", "ns1", "test"),
			expect:    nil,
		},
	}

	gvr := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "roles"}
	kubeClient := fakekube.NewSimpleClientset()
	kubeClient.PrependReactor("create", "subjectaccessreviews",
		func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
			return true, &v1.SubjectAccessReview{
				Status: v1.SubjectAccessReviewStatus{
					Allowed: true,
				},
			}, nil
		},
	)

	scheme := runtime.NewScheme()
	dynamicClient := fakedynamic.NewSimpleDynamicClient(scheme)
	dynamicClient.PrependReactor("create", "roles",
		func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
			obj := action.(clienttesting.CreateActionImpl).Object.(*unstructured.Unstructured)

			if obj.GetKind() == "Role" {
				return true, obj, nil
			}
			if obj.GetKind() == "ClusterRole" {
				return true, obj, apierrors.NewForbidden(
					schema.GroupResource{Group: "rbac.authorization.k8s.io", Resource: "clusterroles"},
					obj.GetName(),
					fmt.Errorf("escalation"))
			}
			return false, nil, nil
		})
	validator := &SarValidator{
		kubeClient: kubeClient,
		newImpersonateClientFunc: func(config *rest.Config, username string) (dynamic.Interface, error) {
			return dynamicClient, nil
		},
	}

	for testName, test := range tests {
		t.Run(testName, func(t *testing.T) {
			err := validator.Validate(context.TODO(), test.executor, gvr, test.namespace, test.name, true, test.obj)
			if test.expect == nil {
				if err != nil {
					t.Errorf("expect nil but got %s", err)
				}
			} else if err == nil || err.Error() != test.expect.Error() {
				t.Errorf("expect %s but got %s", test.expect, err)
			}
		})
	}
}
