/*
Copyright 2017 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 checkpoint

import (
	"fmt"
	"testing"

	"github.com/davecgh/go-spew/spew"

	apiv1 "k8s.io/api/core/v1"
	apiequality "k8s.io/apimachinery/pkg/api/equality"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig"
	kubeletscheme "k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig/scheme"
	kubeletconfigv1beta1 "k8s.io/kubernetes/pkg/kubelet/apis/kubeletconfig/v1beta1"
	utiltest "k8s.io/kubernetes/pkg/kubelet/kubeletconfig/util/test"
)

func TestNewConfigMapCheckpoint(t *testing.T) {
	cases := []struct {
		desc string
		cm   *apiv1.ConfigMap
		err  string
	}{
		{"nil v1/ConfigMap", nil, "must be non-nil"},
		{"empty v1/ConfigMap", &apiv1.ConfigMap{}, "must have a UID"},
		{"populated v1/ConfigMap",
			&apiv1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name: "name",
					UID:  types.UID("uid"),
				},
				Data: map[string]string{
					"key1": "value1",
					"key2": "value2",
				},
			}, ""},
	}

	for _, c := range cases {
		cpt, err := NewConfigMapCheckpoint(c.cm)
		if utiltest.SkipRest(t, c.desc, err, c.err) {
			continue
		}
		// underlying object should match the object passed in
		if !apiequality.Semantic.DeepEqual(cpt.object(), c.cm) {
			t.Errorf("case %q, expect Checkpoint %s but got %s", c.desc, spew.Sdump(c.cm), spew.Sdump(cpt))
		}
	}
}

func TestConfigMapCheckpointUID(t *testing.T) {
	_, kubeletCodecs, err := kubeletscheme.NewSchemeAndCodecs()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	cases := []string{"", "uid", "376dfb73-56db-11e7-a01e-42010a800002"}
	for _, uidIn := range cases {
		cpt := &configMapCheckpoint{
			kubeletCodecs,
			&apiv1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{UID: types.UID(uidIn)},
			},
		}
		// UID method should return the correct value of the UID
		uidOut := cpt.UID()
		if uidIn != uidOut {
			t.Errorf("expect UID() to return %q, but got %q", uidIn, uidOut)
		}
	}
}

func TestConfigMapCheckpointParse(t *testing.T) {
	kubeletScheme, kubeletCodecs, err := kubeletscheme.NewSchemeAndCodecs()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// get the built-in default configuration
	external := &kubeletconfigv1beta1.KubeletConfiguration{}
	kubeletScheme.Default(external)
	defaultConfig := &kubeletconfig.KubeletConfiguration{}
	err = kubeletScheme.Convert(external, defaultConfig, nil)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	cases := []struct {
		desc   string
		cm     *apiv1.ConfigMap
		expect *kubeletconfig.KubeletConfiguration
		err    string
	}{
		{"empty data", &apiv1.ConfigMap{}, nil, "config was empty"},
		// missing kubelet key
		{"missing kubelet key", &apiv1.ConfigMap{Data: map[string]string{
			"bogus": "stuff"}}, nil, fmt.Sprintf("key %q not found", configMapConfigKey)},
		// invalid format
		{"invalid yaml", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": "*"}}, nil, "failed to decode"},
		{"invalid json", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": "{*"}}, nil, "failed to decode"},
		// invalid object
		{"missing kind", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": `{"apiVersion":"kubelet.config.k8s.io/v1beta1"}`}}, nil, "failed to decode"},
		{"missing version", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": `{"kind":"KubeletConfiguration"}`}}, nil, "failed to decode"},
		{"unregistered kind", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": `{"kind":"BogusKind","apiVersion":"kubelet.config.k8s.io/v1beta1"}`}}, nil, "failed to decode"},
		{"unregistered version", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": `{"kind":"KubeletConfiguration","apiVersion":"bogusversion"}`}}, nil, "failed to decode"},
		// empty object with correct kind and version should result in the defaults for that kind and version
		{"default from yaml", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": `kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1`}}, defaultConfig, ""},
		{"default from json", &apiv1.ConfigMap{Data: map[string]string{
			"kubelet": `{"kind":"KubeletConfiguration","apiVersion":"kubelet.config.k8s.io/v1beta1"}`}}, defaultConfig, ""},
	}
	for _, c := range cases {
		cpt := &configMapCheckpoint{kubeletCodecs, c.cm}
		kc, err := cpt.Parse()
		if utiltest.SkipRest(t, c.desc, err, c.err) {
			continue
		}
		// we expect the parsed configuration to match what we described in the ConfigMap
		if !apiequality.Semantic.DeepEqual(c.expect, kc) {
			t.Errorf("case %q, expect config %s but got %s", c.desc, spew.Sdump(c.expect), spew.Sdump(kc))
		}
	}
}

func TestConfigMapCheckpointEncode(t *testing.T) {
	_, kubeletCodecs, err := kubeletscheme.NewSchemeAndCodecs()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// only one case, based on output from the existing encoder, and since
	// this is hard to test (key order isn't guaranteed), we should probably
	// just stick to this test case and mostly rely on the round-trip test.
	cases := []struct {
		desc   string
		cpt    *configMapCheckpoint
		expect string
	}{
		// we expect Checkpoints to be encoded as a json representation of the underlying API object
		{"one-key",
			&configMapCheckpoint{kubeletCodecs, &apiv1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{Name: "one-key"},
				Data:       map[string]string{"one": ""}}},
			`{"kind":"ConfigMap","apiVersion":"v1","metadata":{"name":"one-key","creationTimestamp":null},"data":{"one":""}}
`},
	}

	for _, c := range cases {
		data, err := c.cpt.Encode()
		// we don't expect any errors from encoding
		if utiltest.SkipRest(t, c.desc, err, "") {
			continue
		}
		if string(data) != c.expect {
			t.Errorf("case %q, expect encoding %q but got %q", c.desc, c.expect, string(data))
		}
	}
}

func TestConfigMapCheckpointRoundTrip(t *testing.T) {
	_, kubeletCodecs, err := kubeletscheme.NewSchemeAndCodecs()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	cases := []struct {
		desc      string
		cpt       *configMapCheckpoint
		decodeErr string
	}{
		// empty data
		{"empty data",
			&configMapCheckpoint{kubeletCodecs, &apiv1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name: "empty-data-sha256-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
					UID:  "uid",
				},
				Data: map[string]string{}}},
			""},
		// two keys
		{"two keys",
			&configMapCheckpoint{kubeletCodecs, &apiv1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name: "two-keys-sha256-2bff03d6249c8a9dc9a1436d087c124741361ccfac6615b81b67afcff5c42431",
					UID:  "uid",
				},
				Data: map[string]string{"one": "", "two": "2"}}},
			""},
		// missing uid
		{"missing uid",
			&configMapCheckpoint{kubeletCodecs, &apiv1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name: "two-keys-sha256-2bff03d6249c8a9dc9a1436d087c124741361ccfac6615b81b67afcff5c42431",
					UID:  "",
				},
				Data: map[string]string{"one": "", "two": "2"}}},
			"must have a UID"},
	}
	for _, c := range cases {
		// we don't expect any errors from encoding
		data, err := c.cpt.Encode()
		if utiltest.SkipRest(t, c.desc, err, "") {
			continue
		}
		after, err := DecodeCheckpoint(data)
		if utiltest.SkipRest(t, c.desc, err, c.decodeErr) {
			continue
		}
		if !apiequality.Semantic.DeepEqual(c.cpt.object(), after.object()) {
			t.Errorf("case %q, expect round-trip result %s but got %s", c.desc, spew.Sdump(c.cpt), spew.Sdump(after))
		}
	}
}
