/*
Copyright 2020 The Kubernetes 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

    http://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 certificates

import (
	"bytes"
	"io/ioutil"
	"net/http"
	"reflect"
	"strings"
	"testing"

	"github.com/spf13/cobra"

	certificatesv1 "k8s.io/api/certificates/v1"
	certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/cli-runtime/pkg/resource"
	"k8s.io/client-go/rest/fake"
	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
	cmdutil "k8s.io/kubectl/pkg/cmd/util"
	"k8s.io/kubectl/pkg/scheme"
)

func TestCertificates(t *testing.T) {
	testcases := []struct {
		name            string
		nov1            bool
		nov1beta1       bool
		command         string
		force           bool
		args            []string
		expectFailure   bool
		expectActions   []string
		expectOutput    string
		expectErrOutput string
	}{
		{
			name:    "approve existing",
			command: "approve",
			args:    []string{"existing"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get
				`PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval`,
			},
			expectOutput: `approved`,
		},
		{
			name:    "approve existing, no v1",
			nov1:    true,
			command: "approve",
			args:    []string{"certificatesigningrequests.v1beta1.certificates.k8s.io/existing"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`,      // typed get, 404
				`GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // typed get fallback
				`PUT /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing/approval`,
			},
			expectOutput: `approved`,
		},
		{
			name:      "approve existing, no v1 or v1beta1",
			nov1:      true,
			nov1beta1: true,
			command:   "approve",
			args:      []string{"existing"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get, 404
			},
			expectFailure:   true,
			expectErrOutput: `could not find the requested resource`,
		},
		{
			name:    "approve already approved",
			command: "approve",
			args:    []string{"approved"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get
			},
			expectOutput: `approved`,
		},
		{
			name:    "approve already approved, force",
			command: "approve",
			args:    []string{"approved"},
			force:   true,
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get
				`PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/approved/approval`,
			},
			expectOutput: `approved`,
		},
		{
			name:    "approve already denied",
			command: "approve",
			args:    []string{"denied"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get
			},
			expectFailure:   true,
			expectErrOutput: `is already Denied`,
		},

		{
			name:    "deny existing",
			command: "deny",
			args:    []string{"existing"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get
				`PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval`,
			},
			expectOutput: `denied`,
		},
		{
			name:    "deny existing, no v1",
			nov1:    true,
			command: "deny",
			args:    []string{"certificatesigningrequests.v1beta1.certificates.k8s.io/existing"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`,      // typed get, 404
				`GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // typed get fallback
				`PUT /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing/approval`,
			},
			expectOutput: `denied`,
		},
		{
			name:      "deny existing, no v1 or v1beta1",
			nov1:      true,
			nov1beta1: true,
			command:   "deny",
			args:      []string{"existing"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get, 404
			},
			expectFailure:   true,
			expectErrOutput: `could not find the requested resource`,
		},
		{
			name:    "deny already denied",
			command: "deny",
			args:    []string{"denied"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get
			},
			expectOutput: `denied`,
		},
		{
			name:    "deny already denied, force",
			command: "deny",
			args:    []string{"denied"},
			force:   true,
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get
				`PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/denied/approval`,
			},
			expectOutput: `denied`,
		},
		{
			name:    "deny already approved",
			command: "deny",
			args:    []string{"approved"},
			expectActions: []string{
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get
				`GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get
			},
			expectFailure:   true,
			expectErrOutput: `is already Approved`,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			tf := cmdtesting.NewTestFactory().WithNamespace("test")
			defer tf.Cleanup()
			codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)

			existingV1 := &certificatesv1.CertificateSigningRequest{
				TypeMeta:   metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"},
				ObjectMeta: metav1.ObjectMeta{Name: "existing"},
			}
			existingV1beta1 := &certificatesv1beta1.CertificateSigningRequest{
				TypeMeta:   metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"},
				ObjectMeta: metav1.ObjectMeta{Name: "existing"},
			}

			approvedV1 := &certificatesv1.CertificateSigningRequest{
				TypeMeta:   metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"},
				ObjectMeta: metav1.ObjectMeta{Name: "approved"},
				Status:     certificatesv1.CertificateSigningRequestStatus{Conditions: []certificatesv1.CertificateSigningRequestCondition{{Type: certificatesv1.CertificateApproved}}},
			}
			approvedV1beta1 := &certificatesv1beta1.CertificateSigningRequest{
				TypeMeta:   metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"},
				ObjectMeta: metav1.ObjectMeta{Name: "existing"},
				Status:     certificatesv1beta1.CertificateSigningRequestStatus{Conditions: []certificatesv1beta1.CertificateSigningRequestCondition{{Type: certificatesv1beta1.CertificateApproved}}},
			}

			deniedV1 := &certificatesv1.CertificateSigningRequest{
				TypeMeta:   metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"},
				ObjectMeta: metav1.ObjectMeta{Name: "denied"},
				Status:     certificatesv1.CertificateSigningRequestStatus{Conditions: []certificatesv1.CertificateSigningRequestCondition{{Type: certificatesv1.CertificateDenied}}},
			}
			deniedV1beta1 := &certificatesv1beta1.CertificateSigningRequest{
				TypeMeta:   metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"},
				ObjectMeta: metav1.ObjectMeta{Name: "denied"},
				Status:     certificatesv1beta1.CertificateSigningRequestStatus{Conditions: []certificatesv1beta1.CertificateSigningRequestCondition{{Type: certificatesv1beta1.CertificateDenied}}},
			}

			actions := []string{}
			fakeClient := fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
				actions = append(actions, req.Method+" "+req.URL.Path)
				switch p, m := req.URL.Path, req.Method; {
				case tc.nov1 && strings.HasPrefix(p, "/apis/certificates.k8s.io/v1/"):
					return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(bytes.NewBuffer([]byte{}))}, nil
				case tc.nov1beta1 && strings.HasPrefix(p, "/apis/certificates.k8s.io/v1beta1/"):
					return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(bytes.NewBuffer([]byte{}))}, nil

				case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/missing" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusNotFound}, nil
				case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/missing" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusNotFound}, nil

				case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/existing" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1)}, nil
				case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval" && m == http.MethodPut:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1)}, nil
				case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1beta1)}, nil
				case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing/approval" && m == http.MethodPut:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1beta1)}, nil

				case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/approved" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1)}, nil
				case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/approved/approval" && m == http.MethodPut:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1)}, nil
				case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/approved" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1beta1)}, nil
				case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/approved/approval" && m == http.MethodPut:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1beta1)}, nil

				case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/denied" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1)}, nil
				case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/denied/approval" && m == http.MethodPut:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1)}, nil
				case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/denied" && m == http.MethodGet:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1beta1)}, nil
				case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/denied/approval" && m == http.MethodPut:
					return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1beta1)}, nil

				default:
					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
					return nil, nil
				}
			})
			tf.UnstructuredClientForMappingFunc = func(gv schema.GroupVersion) (resource.RESTClient, error) {
				versionedAPIPath := ""
				if gv.Group == "" {
					versionedAPIPath = "/api/" + gv.Version
				} else {
					versionedAPIPath = "/apis/" + gv.Group + "/" + gv.Version
				}
				return &fake.RESTClient{
					VersionedAPIPath:     versionedAPIPath,
					NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
					Client:               fakeClient,
				}, nil
			}
			tf.Client = &fake.RESTClient{
				NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer,
				Client:               fakeClient,
			}
			streams, _, buf, errbuf := genericclioptions.NewTestIOStreams()

			defer func() {
				// Restore cmdutil behavior.
				cmdutil.DefaultBehaviorOnFatal()
			}()
			// Check exit code.
			cmdutil.BehaviorOnFatal(func(e string, code int) {
				if !tc.expectFailure {
					t.Log(e)
					t.Errorf("unexpected failure exit code %d", code)
				}
				errbuf.Write([]byte(e))
			})

			var cmd *cobra.Command
			switch tc.command {
			case "approve":
				cmd = NewCmdCertificateApprove(tf, streams)
			case "deny":
				cmd = NewCmdCertificateDeny(tf, streams)
			default:
				t.Errorf("unknown command: %s", tc.command)
			}

			if tc.force {
				cmd.Flags().Set("force", "true")
			}
			cmd.Run(cmd, tc.args)

			if !strings.Contains(buf.String(), tc.expectOutput) {
				t.Errorf("expected output to contain %q:\n%s", tc.expectOutput, buf.String())
			}
			if !strings.Contains(errbuf.String(), tc.expectErrOutput) {
				t.Errorf("expected error output to contain %q:\n%s", tc.expectErrOutput, errbuf.String())
			}
			if !reflect.DeepEqual(tc.expectActions, actions) {
				t.Logf("stdout:\n%s", buf.String())
				t.Logf("stderr:\n%s", errbuf.String())
				t.Errorf("expected\n%s\ngot\n%s", strings.Join(tc.expectActions, "\n"), strings.Join(actions, "\n"))
			}
		})
	}
}
