/*
Copyright 2017 The Kubernetes Authors.
Copyright 2020 Authors of Arktos - file modified.

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 scale

import (
	"encoding/json"
	"path"
	"strings"
	"testing"

	"github.com/coreos/pkg/capnslog"
	_ "go.etcd.io/etcd/etcdserver/api/v3rpc" // Force package logger init.

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/kubernetes"
	apitesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
	"k8s.io/kubernetes/test/integration/framework"
)

type subresourceTest struct {
	resource schema.GroupVersionResource
	kind     schema.GroupVersionKind
}

func makeGVR(group, version, resource string) schema.GroupVersionResource {
	return schema.GroupVersionResource{Group: group, Version: version, Resource: resource}
}
func makeGVK(group, version, kind string) schema.GroupVersionKind {
	return schema.GroupVersionKind{Group: group, Version: version, Kind: kind}
}

func TestMain(m *testing.M) {
	framework.EtcdMain(m.Run)
}

func TestScaleSubresources(t *testing.T) {
	clientSet, tearDown := setupWithOptions(t, nil, []string{
		"--runtime-config",
		"api/all=true",
	})
	defer tearDown()

	resourceLists, err := clientSet.Discovery().ServerResources()
	if err != nil {
		t.Fatal(err)
	}

	expectedScaleSubresources := map[schema.GroupVersionResource]schema.GroupVersionKind{
		makeGVR("", "v1", "replicationcontrollers/scale"): makeGVK("autoscaling", "v1", "Scale"),

		// TODO(liggitt): remove these once apps/v1beta1, apps/v1beta2, and extensions/v1beta1 can no longer be served
		makeGVR("extensions", "v1beta1", "deployments/scale"):            makeGVK("extensions", "v1beta1", "Scale"),
		makeGVR("extensions", "v1beta1", "replicationcontrollers/scale"): makeGVK("extensions", "v1beta1", "Scale"),
		makeGVR("extensions", "v1beta1", "replicasets/scale"):            makeGVK("extensions", "v1beta1", "Scale"),

		makeGVR("apps", "v1beta1", "deployments/scale"):  makeGVK("apps", "v1beta1", "Scale"),
		makeGVR("apps", "v1beta1", "statefulsets/scale"): makeGVK("apps", "v1beta1", "Scale"),

		makeGVR("apps", "v1beta2", "deployments/scale"):  makeGVK("apps", "v1beta2", "Scale"),
		makeGVR("apps", "v1beta2", "replicasets/scale"):  makeGVK("apps", "v1beta2", "Scale"),
		makeGVR("apps", "v1beta2", "statefulsets/scale"): makeGVK("apps", "v1beta2", "Scale"),

		makeGVR("apps", "v1", "deployments/scale"):  makeGVK("autoscaling", "v1", "Scale"),
		makeGVR("apps", "v1", "replicasets/scale"):  makeGVK("autoscaling", "v1", "Scale"),
		makeGVR("apps", "v1", "statefulsets/scale"): makeGVK("autoscaling", "v1", "Scale"),
	}

	autoscalingGVK := schema.GroupVersionKind{Group: "autoscaling", Version: "v1", Kind: "Scale"}

	discoveredScaleSubresources := map[schema.GroupVersionResource]schema.GroupVersionKind{}
	for _, resourceList := range resourceLists {
		containingGV, err := schema.ParseGroupVersion(resourceList.GroupVersion)
		if err != nil {
			t.Fatalf("error getting group version for %#v: %v", resourceList, err)
		}

		for _, resource := range resourceList.APIResources {
			if !strings.HasSuffix(resource.Name, "/scale") {
				continue
			}

			gvr := containingGV.WithResource(resource.Name)
			if _, exists := discoveredScaleSubresources[gvr]; exists {
				t.Errorf("scale subresource %#v listed multiple times in discovery", gvr)
				continue
			}

			gvk := containingGV.WithKind(resource.Kind)
			if resource.Group != "" {
				gvk.Group = resource.Group
			}
			if resource.Version != "" {
				gvk.Version = resource.Version
			}
			discoveredScaleSubresources[gvr] = gvk
		}
	}

	// Ensure nothing is missing
	for gvr, gvk := range expectedScaleSubresources {
		if _, ok := discoveredScaleSubresources[gvr]; !ok {
			t.Errorf("expected scale subresource %#v of kind %#v was missing from discovery", gvr, gvk)
		}
	}

	// Ensure discovery lists expected types
	for gvr, gvk := range discoveredScaleSubresources {
		if expectedGVK, expected := expectedScaleSubresources[gvr]; !expected {
			if gvk == autoscalingGVK {
				t.Errorf("unexpected scale subresource %#v of kind %#v. new scale subresource should be added to expectedScaleSubresources", gvr, gvk)
			} else {
				t.Errorf("unexpected scale subresource %#v of kind %#v. new scale resources are expected to use Scale from the autoscaling/v1 API group", gvr, gvk)
			}
			continue
		} else if expectedGVK != gvk {
			t.Errorf("scale subresource %#v should be of kind %#v, but %#v was listed in discovery", gvr, expectedGVK, gvk)
			continue
		}
	}

	// Create objects required to exercise scale subresources
	if _, err := clientSet.CoreV1().ReplicationControllers("default").Create(rcStub); err != nil {
		t.Fatal(err)
	}
	if _, err := clientSet.AppsV1().ReplicaSets("default").Create(rsStub); err != nil {
		t.Fatal(err)
	}
	if _, err := clientSet.AppsV1().Deployments("default").Create(deploymentStub); err != nil {
		t.Fatal(err)
	}
	if _, err := clientSet.AppsV1().StatefulSets("default").Create(ssStub); err != nil {
		t.Fatal(err)
	}

	// Ensure scale subresources return and accept expected kinds
	for gvr, gvk := range discoveredScaleSubresources {
		prefix := "/apis"
		if gvr.Group == corev1.GroupName {
			prefix = "/api"
		}

		resourceParts := strings.SplitN(gvr.Resource, "/", 2)

		urlPath := path.Join(prefix, gvr.Group, gvr.Version, "namespaces", "default", resourceParts[0], "test", resourceParts[1])
		obj := &unstructured.Unstructured{}

		getData, err := clientSet.CoreV1().RESTClient().Get().AbsPath(urlPath).DoRaw()
		if err != nil {
			t.Errorf("error fetching %s: %v", urlPath, err)
			continue
		}
		if err := json.Unmarshal(getData, obj); err != nil {
			t.Errorf("error decoding %s: %v", urlPath, err)
			t.Log(string(getData))
			continue
		}

		if obj.GetObjectKind().GroupVersionKind() != gvk {
			t.Errorf("expected %#v, got %#v from %s", gvk, obj.GetObjectKind().GroupVersionKind(), urlPath)
			t.Log(string(getData))
			continue
		}

		updateData, err := clientSet.CoreV1().RESTClient().Put().AbsPath(urlPath).Body(getData).DoRaw()
		if err != nil {
			t.Errorf("error putting to %s: %v", urlPath, err)
			t.Log(string(getData))
			t.Log(string(updateData))
			continue
		}
	}
}

var (
	replicas = int32(1)

	podStub = corev1.PodTemplateSpec{
		ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
		Spec:       corev1.PodSpec{Containers: []corev1.Container{{Name: "test", Image: "busybox"}}},
	}

	rcStub = &corev1.ReplicationController{
		ObjectMeta: metav1.ObjectMeta{Name: "test"},
		Spec:       corev1.ReplicationControllerSpec{Selector: podStub.Labels, Replicas: &replicas, Template: &podStub},
	}

	rsStub = &appsv1.ReplicaSet{
		ObjectMeta: metav1.ObjectMeta{Name: "test"},
		Spec:       appsv1.ReplicaSetSpec{Selector: &metav1.LabelSelector{MatchLabels: podStub.Labels}, Replicas: &replicas, Template: podStub},
	}

	deploymentStub = &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{Name: "test"},
		Spec:       appsv1.DeploymentSpec{Selector: &metav1.LabelSelector{MatchLabels: podStub.Labels}, Replicas: &replicas, Template: podStub},
	}

	ssStub = &appsv1.StatefulSet{
		ObjectMeta: metav1.ObjectMeta{Name: "test"},
		Spec:       appsv1.StatefulSetSpec{Selector: &metav1.LabelSelector{MatchLabels: podStub.Labels}, Replicas: &replicas, Template: podStub},
	}
)

func setup(t *testing.T) (client kubernetes.Interface, tearDown func()) {
	return setupWithOptions(t, nil, nil)
}

func setupWithOptions(t *testing.T, instanceOptions *apitesting.TestServerInstanceOptions, flags []string) (client kubernetes.Interface, tearDown func()) {
	result := apitesting.StartTestServerOrDie(t, instanceOptions, flags, framework.SharedEtcd())

	// TODO: Disable logging here until we resolve teardown issues which result in
	// massive log spam. Another path forward would be to refactor
	// StartTestServerOrDie to work with the etcd instance already started by the
	// integration test scripts.
	// See https://github.com/kubernetes/kubernetes/issues/49489.
	repo, err := capnslog.GetRepoLogger("go.etcd.io/etcd")
	if err != nil {
		t.Fatalf("couldn't configure logging: %v", err)
	}
	repo.SetLogLevel(map[string]capnslog.LogLevel{
		"etcdserver/api/v3rpc": capnslog.CRITICAL,
	})

	for _, clientConfig := range result.ClientConfig.GetAllConfigs() {
		clientConfig.AcceptContentTypes = ""
		clientConfig.ContentType = ""
		clientConfig.NegotiatedSerializer = nil
	}
	clientSet, err := kubernetes.NewForConfig(result.ClientConfig)
	if err != nil {
		t.Fatalf("error creating clientset: %v", err)
	}

	return clientSet, result.TearDownFn
}
