/*
Copyright 2017 The Kubernetes Authors All rights reserved.

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 kubernetes

import (
	"encoding/json"
	"fmt"
	"reflect"
	"strings"
	"testing"

	"github.com/compose-spec/compose-go/v2/types"

	"github.com/kubernetes/kompose/pkg/kobject"
	"github.com/kubernetes/kompose/pkg/loader/compose"
	"github.com/kubernetes/kompose/pkg/transformer"
	deployapi "github.com/openshift/api/apps/v1"
	"github.com/pkg/errors"
	appsv1 "k8s.io/api/apps/v1"
	api "k8s.io/api/core/v1"
	networkingv1 "k8s.io/api/networking/v1"
	networkingv1beta1 "k8s.io/api/networking/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

func newServiceConfig() kobject.ServiceConfig {
	return kobject.ServiceConfig{
		Name:            "app",
		ContainerName:   "name",
		Image:           "image",
		Environment:     []kobject.EnvVar{{Name: "env", Value: "value"}},
		Port:            []kobject.Ports{{HostPort: 123, ContainerPort: 456}, {HostPort: 123, ContainerPort: 456, Protocol: string(api.ProtocolUDP)}, {HostPort: 55564, ContainerPort: 55564}, {HostPort: 55563, ContainerPort: 55563}},
		Command:         []string{"cmd"},
		WorkingDir:      "dir",
		Args:            []string{"arg1", "arg2"},
		VolList:         []string{"/tmp/volume"},
		Network:         []string{"network1", "network2"}, // supported
		Labels:          nil,
		FsGroup:         1001,
		Annotations:     map[string]string{"abc": "def"},
		CPUQuota:        1, // not supported
		CapAdd:          []string{"cap_add"},
		CapDrop:         []string{"cap_drop"},
		Expose:          []string{"expose"}, // not supported
		Privileged:      true,
		Restart:         "always",
		ImagePullSecret: "regcred",
		Stdin:           true,
		Tty:             true,
		TmpFs:           []string{"/tmp"},
		Replicas:        2,
		Volumes:         []kobject.Volumes{{SvcName: "app", MountPath: "/tmp/volume", PVCName: "app-claim0"}},
		GroupAdd:        []int64{1003, 1005},
		Configs:         []types.ServiceConfigObjConfig{{Source: "config", Target: "/etc/world"}},
		ConfigsMetaData: types.Configs{"config": types.ConfigObjConfig{Name: "myconfig", File: "kubernetes_test.go"}},
	}
}

func newSimpleServiceConfig() kobject.ServiceConfig {
	return kobject.ServiceConfig{
		Name:          "app",
		ContainerName: "name",
		Image:         "image",
	}
}

func newKomposeObject() kobject.KomposeObject {
	return kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfig()},
	}
}

func newKomposeObjectHostPortProtocolConfig() kobject.ServiceConfig {
	return kobject.ServiceConfig{
		Name:                  "nginx",
		ContainerName:         "nginx",
		Image:                 "nginx",
		Port:                  []kobject.Ports{{HostPort: 80, Protocol: string(api.ProtocolTCP), ContainerPort: 80}},
		ExposeContainerToHost: true,
	}
}

func newServiceConfigWithExternalTrafficPolicy() kobject.ServiceConfig {
	loadBalancerServiceType := string(api.ServiceTypeLoadBalancer)
	return kobject.ServiceConfig{
		Name:                         "app",
		Port:                         []kobject.Ports{{HostPort: 123, ContainerPort: 456}},
		ServiceType:                  loadBalancerServiceType,
		ServiceExternalTrafficPolicy: "local",
	}
}

func newServiceConfigWithServiceVolumeMount(volumeMountSubPathValue string) kobject.ServiceConfig {
	return kobject.ServiceConfig{
		Name:               "app",
		VolumeMountSubPath: volumeMountSubPathValue,
	}
}

func equalStringSlice(s1, s2 []string) bool {
	if len(s1) != len(s2) {
		return false
	}
	for i := range s1 {
		if s1[i] != s2[i] {
			return false
		}
	}
	return true
}

func equalEnv(kobjectEnvs []kobject.EnvVar, k8sEnvs []api.EnvVar) bool {
	if len(kobjectEnvs) != len(k8sEnvs) {
		return false
	}
	for _, env := range kobjectEnvs {
		found := false
		for _, k8sEnv := range k8sEnvs {
			if env.Name == k8sEnv.Name && env.Value == k8sEnv.Value {
				found = true
			}
		}
		if !found {
			return false
		}
	}
	return true
}

func equalPorts(kobjectPorts []kobject.Ports, k8sPorts []api.ContainerPort) bool {
	if len(kobjectPorts) != len(k8sPorts) {
		return false
	}
	for _, port := range kobjectPorts {
		found := false
		for _, k8sPort := range k8sPorts {
			// FIXME: HostPort should be copied to container port
			//if port.HostPort == k8sPort.HostPort && port.Protocol == k8sPort.Protocol && port.ContainerPort == k8sPort.ContainerPort {
			if port.Protocol == string(k8sPort.Protocol) && port.ContainerPort == k8sPort.ContainerPort {
				found = true
			}
			// Name and HostIp shouldn't be set
			if len(k8sPort.Name) != 0 || len(k8sPort.HostIP) != 0 {
				return false
			}
		}
		if !found {
			return false
		}
	}
	return true
}

func equalStringMaps(map1 map[string]string, map2 map[string]string) bool {
	if len(map1) != len(map2) {
		return false
	}
	for k, v := range map1 {
		if map2[k] != v {
			return false
		}
	}
	return true
}

func checkPodTemplate(config kobject.ServiceConfig, template api.PodTemplateSpec, expectedLabels map[string]string) error {
	if len(template.Spec.Containers) == 0 {
		return fmt.Errorf("Failed to set container: %#v vs. %#v", config, template)
	}
	container := template.Spec.Containers[0]
	if config.ContainerName != container.Name {
		return fmt.Errorf("Found different container name: %v vs. %v", config.ContainerName, container.Name)
	}
	if config.Image != container.Image {
		return fmt.Errorf("Found different container image: %v vs. %v", config.Image, container.Image)
	}
	if !equalEnv(config.Environment, container.Env) {
		return fmt.Errorf("Found different container env: %#v vs. %#v", config.Environment, container.Env)
	}
	if !equalPorts(config.Port, container.Ports) {
		return fmt.Errorf("Found different container ports: %#v vs. %#v", config.Port, container.Ports)
	}
	if !equalStringSlice(config.Command, container.Command) {
		return fmt.Errorf("Found different container cmd: %#v vs. %#v", config.Command, container.Command)
	}
	if config.WorkingDir != container.WorkingDir {
		return fmt.Errorf("Found different container WorkingDir: %#v vs. %#v", config.WorkingDir, container.WorkingDir)
	}
	if !equalStringSlice(config.Args, container.Args) {
		return fmt.Errorf("Found different container args: %#v vs. %#v", config.Args, container.Args)
	}
	if len(template.Spec.Volumes) == 0 || len(template.Spec.Volumes[0].Name) == 0 || template.Spec.Volumes[0].VolumeSource.PersistentVolumeClaim == nil && template.Spec.Volumes[0].ConfigMap == nil {
		return fmt.Errorf("Found incorrect volumes: %v vs. %#v", config.Volumes, template.Spec.Volumes)
	}
	// We only set controller labels here and k8s server will take care of other defaults, such as selectors
	if !equalStringMaps(expectedLabels, template.Labels) {
		return fmt.Errorf("Found different template labels: %#v vs. %#v", expectedLabels, template.Labels)
	}
	restartPolicyMapping := map[string]api.RestartPolicy{"always": api.RestartPolicyAlways}
	if restartPolicyMapping[config.Restart] != template.Spec.RestartPolicy {
		return fmt.Errorf("Found incorrect restart policy: %v vs. %v", config.Restart, template.Spec.RestartPolicy)
	}
	if config.Privileged == privilegedNilOrFalse(template) {
		return fmt.Errorf("Found different template privileged: %#v vs. %#v", config.Privileged, template.Spec.Containers[0].SecurityContext)
	}
	if config.FsGroup != *template.Spec.SecurityContext.FSGroup {
		return fmt.Errorf("Found different pod security context fs group values: %#v vs. %#v", config.FsGroup, *template.Spec.SecurityContext.FSGroup)
	}
	if config.Stdin != template.Spec.Containers[0].Stdin {
		return fmt.Errorf("Found different values for stdin: %#v vs. %#v", config.Stdin, template.Spec.Containers[0].Stdin)
	}
	if config.Tty != template.Spec.Containers[0].TTY {
		return fmt.Errorf("Found different values for TTY: %#v vs. %#v", config.Tty, template.Spec.Containers[0].TTY)
	}
	if config.ImagePullSecret != template.Spec.ImagePullSecrets[0].Name {
		return fmt.Errorf("Found different values for ImagePullSecrets: %#v vs. %#v", config.ImagePullSecret, template.Spec.ImagePullSecrets[0].Name)
	}
	return nil
}

func privilegedNilOrFalse(template api.PodTemplateSpec) bool {
	return len(template.Spec.Containers) == 0 || template.Spec.Containers[0].SecurityContext == nil ||
		template.Spec.Containers[0].SecurityContext.Privileged == nil || !*template.Spec.Containers[0].SecurityContext.Privileged
}

func checkService(config kobject.ServiceConfig, svc *api.Service, expectedLabels map[string]string) error {
	if !equalStringMaps(expectedLabels, svc.Spec.Selector) {
		return fmt.Errorf("Found unexpected selector: %#v vs. %#v", expectedLabels, svc.Spec.Selector)
	}
	for _, port := range svc.Spec.Ports {
		name := port.Name
		expectedName := strings.ToLower(name)
		if expectedName != name {
			return fmt.Errorf("Found unexpected port name: %#v vs. %#v", expectedName, name)
		}
	}
	// TODO: finish this
	return nil
}

func checkMeta(config kobject.ServiceConfig, meta metav1.ObjectMeta, expectedName string, shouldSetLabels bool) error {
	if expectedName != meta.Name {
		return fmt.Errorf("Found unexpected name: %s vs. %s", expectedName, meta.Name)
	}

	if shouldSetLabels != (len(meta.Labels) > 0) {
		return fmt.Errorf("Unexpected labels: %#v", meta.Labels)
	}
	return nil
}

func TestKomposeConvertIngress(t *testing.T) {
	testCases := map[string]struct {
		komposeObject kobject.KomposeObject
		opt           kobject.ConvertOptions
		labelValue    string
	}{
		"Convert to Ingress: label set to true":        {newKomposeObject(), kobject.ConvertOptions{CreateD: true}, "true"},
		"Convert to Ingress: label set to example.com": {newKomposeObject(), kobject.ConvertOptions{CreateD: true}, "example.com"},
	}

	for name, test := range testCases {
		var expectedHost string

		t.Log("Test case:", name)
		k := Kubernetes{}

		appName := "app"

		// Setting value for ExposeService in ServiceConfig
		config := test.komposeObject.ServiceConfigs[appName]
		config.ExposeService = test.labelValue
		test.komposeObject.ServiceConfigs[appName] = config

		switch test.labelValue {
		case "true":
			expectedHost = ""
		default:
			expectedHost = test.labelValue
		}

		// Run Transform
		objs, err := k.Transform(test.komposeObject, test.opt)
		if err != nil {
			t.Error(errors.Wrap(err, "k.Transform failed"))
		}

		// Check results
		for _, obj := range objs {
			if ing, ok := obj.(*networkingv1beta1.Ingress); ok {
				if ing.ObjectMeta.Name != appName {
					t.Errorf("Expected ObjectMeta.Name to be %s, got %s instead", appName, ing.ObjectMeta.Name)
				}
				if ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServiceName != appName {
					t.Errorf("Expected Backend.ServiceName to be %s, got %s instead", appName, ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServiceName)
				}
				if ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServicePort.IntVal != config.Port[0].HostPort {
					t.Errorf("Expected Backend.ServicePort to be %d, got %v instead", config.Port[0].HostPort, ing.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.ServicePort.IntVal)
				}
				if ing.Spec.Rules[0].Host != expectedHost {
					t.Errorf("Expected Rules[0].Host to be %s, got %s instead", expectedHost, ing.Spec.Rules[0].Host)
				}
			}
		}
	}
}

func TestKomposeConvert(t *testing.T) {
	replicas := 3
	testCases := map[string]struct {
		komposeObject   kobject.KomposeObject
		opt             kobject.ConvertOptions
		expectedNumObjs int
	}{
		// objects generated are deployment, service network policies (2) and pvc
		"Convert to Deployments (D)":                  {newKomposeObject(), kobject.ConvertOptions{CreateD: true, Replicas: replicas, IsReplicaSetFlag: true}, 4},
		"Convert to Deployments (D) with v3 replicas": {newKomposeObject(), kobject.ConvertOptions{CreateD: true}, 4},
		"Convert to DaemonSets (DS)":                  {newKomposeObject(), kobject.ConvertOptions{CreateDS: true}, 4},
		// objects generated are deployment, daemonset, ReplicationController, service and pvc
		"Convert to D, DS, and RC":                  {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true, Replicas: replicas, IsReplicaSetFlag: true}, 5},
		"Convert to D, DS, and RC with v3 replicas": {newKomposeObject(), kobject.ConvertOptions{CreateD: true, CreateDS: true, CreateRC: true}, 5},
		// objects generated are statefulset
		"Convert to SS with replicas ":   {newKomposeObject(), kobject.ConvertOptions{Controller: StatefulStateController, Replicas: replicas, IsReplicaSetFlag: true}, 3},
		"Convert to SS without replicas": {newKomposeObject(), kobject.ConvertOptions{Controller: StatefulStateController}, 3},
	}

	for name, test := range testCases {
		t.Log("Test case:", name)
		k := Kubernetes{}
		// Run Transform
		objs, err := k.Transform(test.komposeObject, test.opt)
		if err != nil {
			t.Error(errors.Wrap(err, "k.Transform failed"))
		}
		if len(objs) != test.expectedNumObjs {
			t.Errorf("Expected %d objects returned, got %d", test.expectedNumObjs, len(objs))
		}

		var foundSVC, foundD, foundDS, foundDC, foundSS bool
		name := "app"
		labels := transformer.ConfigLabels(name)
		config := test.komposeObject.ServiceConfigs[name]
		labelsWithNetwork := transformer.ConfigLabelsWithNetwork(name, config.Network)
		// Check results
		for _, obj := range objs {
			if svc, ok := obj.(*api.Service); ok {
				if err := checkService(config, svc, labels); err != nil {
					t.Errorf("%v", err)
				}
				if err := checkMeta(config, svc.ObjectMeta, name, true); err != nil {
					t.Errorf("%v", err)
				}
				foundSVC = true
			}

			if test.opt.CreateD {
				if d, ok := obj.(*appsv1.Deployment); ok {
					if err := checkPodTemplate(config, d.Spec.Template, labelsWithNetwork); err != nil {
						t.Errorf("%v", err)
					}
					if err := checkMeta(config, d.ObjectMeta, name, true); err != nil {
						t.Errorf("%v", err)
					}
					if test.opt.IsReplicaSetFlag {
						if (int)(*d.Spec.Replicas) != replicas {
							t.Errorf("Expected %d replicas, got %d", replicas, d.Spec.Replicas)
						}
					} else {
						if (int)(*d.Spec.Replicas) != newServiceConfig().Replicas {
							t.Errorf("Expected %d replicas, got %d", newServiceConfig().Replicas, d.Spec.Replicas)
						}
					}
					foundD = true
				}

				if u, ok := obj.(*unstructured.Unstructured); ok {
					if u.GetKind() == "Deployment" {
						u.SetGroupVersionKind(schema.GroupVersionKind{
							Group:   "apps",
							Version: "v1",
							Kind:    "Deployment",
						})
						data, err := json.Marshal(u)
						if err != nil {
							t.Errorf("%v", err)
						}
						var d appsv1.Deployment
						if err := json.Unmarshal(data, &d); err == nil {
							if err := checkPodTemplate(config, d.Spec.Template, labelsWithNetwork); err != nil {
								t.Errorf("%v", err)
							}
							if err := checkMeta(config, d.ObjectMeta, name, true); err != nil {
								t.Errorf("%v", err)
							}
							if test.opt.IsReplicaSetFlag {
								if (int)(*d.Spec.Replicas) != replicas {
									t.Errorf("Expected %d replicas, got %d", replicas, d.Spec.Replicas)
								}
							} else {
								if (int)(*d.Spec.Replicas) != newServiceConfig().Replicas {
									t.Errorf("Expected %d replicas, got %d", newServiceConfig().Replicas, d.Spec.Replicas)
								}
							}
							foundD = true
						}
					}
				}
			}
			if test.opt.CreateDS {
				if ds, ok := obj.(*appsv1.DaemonSet); ok {
					if err := checkPodTemplate(config, ds.Spec.Template, labelsWithNetwork); err != nil {
						t.Errorf("%v", err)
					}
					if err := checkMeta(config, ds.ObjectMeta, name, true); err != nil {
						t.Errorf("%v", err)
					}
					if ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {
						t.Errorf("Expect selector be set, got: %#v", ds.Spec.Selector)
					}
					foundDS = true
				}

				if u, ok := obj.(*unstructured.Unstructured); ok {
					if u.GetKind() == "DaemonSet" {
						u.SetGroupVersionKind(schema.GroupVersionKind{
							Group:   "apps",
							Version: "v1",
							Kind:    "DaemonSet",
						})
						data, err := json.Marshal(u)
						if err != nil {
							t.Errorf("%v", err)
						}
						var ds appsv1.DaemonSet
						if err := json.Unmarshal(data, &ds); err == nil {
							if err := checkPodTemplate(config, ds.Spec.Template, labelsWithNetwork); err != nil {
								t.Errorf("%v", err)
							}
							if err := checkMeta(config, ds.ObjectMeta, name, true); err != nil {
								t.Errorf("%v", err)
							}
							foundDS = true
						}
					}
				}
			}

			if test.opt.Controller == StatefulStateController {
				if ss, ok := obj.(*appsv1.StatefulSet); ok {
					if err := checkPodTemplate(config, ss.Spec.Template, labelsWithNetwork); err != nil {
						t.Errorf("%v", err)
					}
					if err := checkMeta(config, ss.ObjectMeta, name, true); err != nil {
						t.Errorf("%v", err)
					}
					if test.opt.IsReplicaSetFlag {
						if (int)(*ss.Spec.Replicas) != replicas {
							t.Errorf("Expected %d replicas, got %d", replicas, ss.Spec.Replicas)
						}
					} else {
						if (int)(*ss.Spec.Replicas) != newServiceConfig().Replicas {
							t.Errorf("Expected %d replicas, got %d", newServiceConfig().Replicas, ss.Spec.Replicas)
						}
					}
					foundSS = true
				}

				if u, ok := obj.(*unstructured.Unstructured); ok {
					if u.GetKind() == "Statefulset" {
						u.SetGroupVersionKind(schema.GroupVersionKind{
							Group:   "apps",
							Version: "v1",
							Kind:    "Statefulset",
						})
						data, err := json.Marshal(u)
						if err != nil {
							t.Errorf("%v", err)
						}
						var d appsv1.Deployment
						if err := json.Unmarshal(data, &d); err == nil {
							if err := checkPodTemplate(config, d.Spec.Template, labelsWithNetwork); err != nil {
								t.Errorf("%v", err)
							}
							if err := checkMeta(config, d.ObjectMeta, name, true); err != nil {
								t.Errorf("%v", err)
							}
							if test.opt.IsReplicaSetFlag {
								if (int)(*d.Spec.Replicas) != replicas {
									t.Errorf("Expected %d replicas, got %d", replicas, d.Spec.Replicas)
								}
							} else {
								if (int)(*d.Spec.Replicas) != newServiceConfig().Replicas {
									t.Errorf("Expected %d replicas, got %d", newServiceConfig().Replicas, d.Spec.Replicas)
								}
							}
							foundSS = true
						}
					}
				}
			}

			// TODO: k8s & openshift transformer is now separated; either separate the test or combine the transformer
			if test.opt.CreateDeploymentConfig {
				if dc, ok := obj.(*deployapi.DeploymentConfig); ok {
					if err := checkPodTemplate(config, *dc.Spec.Template, labelsWithNetwork); err != nil {
						t.Errorf("%v", err)
					}
					if err := checkMeta(config, dc.ObjectMeta, name, true); err != nil {
						t.Errorf("%v", err)
					}
					if (int)(dc.Spec.Replicas) != replicas {
						t.Errorf("Expected %d replicas, got %d", replicas, dc.Spec.Replicas)
					}
					if len(dc.Spec.Selector) == 0 {
						t.Errorf("Expect selector be set, got: %#v", dc.Spec.Selector)
					}
					foundDC = true
				}
			}
		}
		if !foundSVC {
			t.Errorf("Unexpected Service not created")
		}
		if test.opt.CreateD != foundD {
			t.Errorf("Expected create Deployment: %v, found Deployment: %v", test.opt.CreateD, foundD)
		}
		if test.opt.CreateDS != foundDS {
			t.Errorf("Expected create Daemon Set: %v, found Daemon Set: %v", test.opt.CreateDS, foundDS)
		}

		if test.opt.Controller == StatefulStateController && !foundSS {
			t.Errorf("Expected create StatefulStateController")
		}
		if test.opt.CreateDeploymentConfig != foundDC {
			t.Errorf("Expected create Deployment Config: %v, found Deployment Config: %v", test.opt.CreateDeploymentConfig, foundDC)
		}
	}
}

func TestConvertRestartOptions(t *testing.T) {
	var opt kobject.ConvertOptions
	var k Kubernetes

	testCases := map[string]struct {
		svc           kobject.KomposeObject
		restartPolicy api.RestartPolicy
	}{
		"'restart' is set to 'no'":         {kobject.KomposeObject{ServiceConfigs: map[string]kobject.ServiceConfig{"app": {Image: "foobar", Restart: "no"}}}, api.RestartPolicyNever},
		"'restart' is set to 'on-failure'": {kobject.KomposeObject{ServiceConfigs: map[string]kobject.ServiceConfig{"app": {Image: "foobar", Restart: "on-failure"}}}, api.RestartPolicyOnFailure},
	}

	for name, test := range testCases {
		t.Log("Test Case:", name)

		objs, err := k.Transform(test.svc, opt)
		if err != nil {
			t.Error(errors.Wrap(err, "k.Transform failed"))
		}

		if len(objs) != 1 {
			t.Errorf("Expected only one pod, more elements generated.")
		}

		for _, obj := range objs {
			if pod, ok := obj.(*api.Pod); ok {
				if pod.Spec.RestartPolicy != test.restartPolicy {
					t.Errorf("Expected restartPolicy as %s, got %#v", test.restartPolicy, pod.Spec.RestartPolicy)
				}
			} else {
				t.Errorf("Expected 'pod' object not found one")
			}
		}
	}
}

func TestRestartOnFailure(t *testing.T) {
	kobjectWithRestartOnFailure := newKomposeObject()
	serviceConfig := kobjectWithRestartOnFailure.ServiceConfigs["app"]
	serviceConfig.Restart = "on-failure"
	kobjectWithRestartOnFailure.ServiceConfigs = map[string]kobject.ServiceConfig{"app": serviceConfig}

	// define all test cases for RestartOnFailure function
	replicas := 2
	testCase := map[string]struct {
		komposeObject kobject.KomposeObject
		opt           kobject.ConvertOptions
	}{
		// objects generated are deployment, service and replication controller
		"Do not Create Deployment (D) with restart:'on-failure'":             {kobjectWithRestartOnFailure, kobject.ConvertOptions{IsDeploymentFlag: true, Replicas: replicas}},
		"Do not Create DaemonSet (DS) with restart:'on-failure'":             {kobjectWithRestartOnFailure, kobject.ConvertOptions{IsDaemonSetFlag: true, Replicas: replicas}},
		"Do not Create ReplicationController (RC) with restart:'on-failure'": {kobjectWithRestartOnFailure, kobject.ConvertOptions{IsReplicationControllerFlag: true, Replicas: replicas}},
	}

	for name, test := range testCase {
		t.Log("Test case:", name)
		k := Kubernetes{}
		_, err := k.Transform(test.komposeObject, test.opt)
		if err != nil {
			t.Errorf("Expected nil error, got %v instead", err)
		}
	}
}

func TestInitPodSpec(t *testing.T) {
	name := "foo"
	k := Kubernetes{}
	result := k.InitPodSpec(name, newServiceConfig().Image, "")
	if result.Containers[0].Name != "foo" && result.Containers[0].Image != "image" {
		t.Fatalf("Pod object not found")
	}
}

func TestConfigTmpfs(t *testing.T) {
	name := "foo"
	k := Kubernetes{}
	resultVolumeMount, resultVolume := k.ConfigTmpfs(name, newServiceConfig())

	if resultVolumeMount[0].Name != "foo-tmpfs0" || resultVolume[0].EmptyDir.Medium != "Memory" {
		t.Fatalf("Tmpfs not found")
	}
}

func TestConfigCapabilities(t *testing.T) {
	testCases := map[string]struct {
		service kobject.ServiceConfig
		result  api.Capabilities
	}{
		"ConfigCapsWithAddDrop": {kobject.ServiceConfig{CapAdd: []string{"CHOWN", "SETUID"}, CapDrop: []string{"KILL"}}, api.Capabilities{Add: []api.Capability{api.Capability("CHOWN"), api.Capability("SETUID")}, Drop: []api.Capability{api.Capability("KILL")}}},
		"ConfigCapsNoAddDrop":   {kobject.ServiceConfig{CapAdd: nil, CapDrop: nil}, api.Capabilities{Add: []api.Capability{}, Drop: []api.Capability{}}},
	}

	for name, test := range testCases {
		t.Log("Test case:", name)
		result := ConfigCapabilities(test.service)
		if !reflect.DeepEqual(result.Add, test.result.Add) || !reflect.DeepEqual(result.Drop, test.result.Drop) {
			t.Errorf("Not expected result for ConfigCapabilities")
		}
	}
}

func TestConfigAffinity(t *testing.T) {
	testCases := map[string]struct {
		service kobject.ServiceConfig
		result  *api.Affinity
	}{
		"ConfigAffinity": {
			service: kobject.ServiceConfig{
				Placement: kobject.Placement{
					PositiveConstraints: map[string]string{
						"foo": "bar",
					},
					NegativeConstraints: map[string]string{
						"baz": "qux",
					},
				},
			},
			result: &api.Affinity{
				NodeAffinity: &api.NodeAffinity{
					RequiredDuringSchedulingIgnoredDuringExecution: &api.NodeSelector{
						NodeSelectorTerms: []api.NodeSelectorTerm{
							{
								MatchExpressions: []api.NodeSelectorRequirement{
									{Key: "foo", Operator: api.NodeSelectorOpIn, Values: []string{"bar"}},
									{Key: "baz", Operator: api.NodeSelectorOpNotIn, Values: []string{"qux"}},
								},
							},
						},
					},
				},
			},
		},
		"ConfigAffinity (nil)": {
			kobject.ServiceConfig{},
			nil,
		},
	}

	for name, test := range testCases {
		t.Log("Test case:", name)
		result := ConfigAffinity(test.service)
		if !reflect.DeepEqual(result, test.result) {
			t.Errorf("Not expected result for ConfigAffinity")
		}
	}
}

func TestConfigTopologySpreadConstraints(t *testing.T) {
	serviceName := "app"
	testCases := map[string]struct {
		service kobject.ServiceConfig
		result  []api.TopologySpreadConstraint
	}{
		"ConfigTopologySpreadConstraint": {
			service: kobject.ServiceConfig{
				Name: serviceName,
				Placement: kobject.Placement{
					Preferences: []string{
						"zone", "ssd",
					},
				},
			},
			result: []api.TopologySpreadConstraint{
				{
					MaxSkew:           2,
					TopologyKey:       "zone",
					WhenUnsatisfiable: api.ScheduleAnyway,
					LabelSelector: &metav1.LabelSelector{
						MatchLabels: transformer.ConfigLabels(serviceName),
					},
				},
				{
					MaxSkew:           1,
					TopologyKey:       "ssd",
					WhenUnsatisfiable: api.ScheduleAnyway,
					LabelSelector: &metav1.LabelSelector{
						MatchLabels: transformer.ConfigLabels(serviceName),
					},
				},
			},
		},
	}

	for name, test := range testCases {
		t.Log("Test case:", name)
		result := ConfigTopologySpreadConstraints(test.service)
		if !reflect.DeepEqual(result, test.result) {
			t.Errorf("Not expected result for ConfigTopologySpreadConstraints")
		}
	}
}

func TestMultipleContainersInPod(t *testing.T) {
	groupName := "pod_group"

	createConfig := func(name string, containerName string) kobject.ServiceConfig {
		config := newSimpleServiceConfig()
		config.Labels = map[string]string{compose.LabelServiceGroup: groupName}
		config.Name = name
		if containerName != "" {
			config.ContainerName = containerName
		}
		config.Volumes = []kobject.Volumes{
			{
				VolumeName: "mountVolume",
				MountPath:  "/data-dir",
			},
		}
		return config
	}

	testCases := map[string]struct {
		komposeObject   kobject.KomposeObject
		opt             kobject.ConvertOptions
		expectedNumObjs int
		expectedNames   []string
	}{
		"Converted multiple containers to Deployments (D)": {
			kobject.KomposeObject{
				ServiceConfigs: map[string]kobject.ServiceConfig{
					"app1": createConfig("app1", "app1"),
					"app2": createConfig("app2", "app2"),
				},
			}, kobject.ConvertOptions{ServiceGroupMode: "label", CreateD: true}, 2, []string{"app1", "app2"}},
	}

	for name, test := range testCases {
		t.Log("Test case:", name)
		k := Kubernetes{}
		// Run Transform
		objs, err := k.Transform(test.komposeObject, test.opt)
		if err != nil {
			t.Error(errors.Wrap(err, "k.Transform failed"))
		}
		if len(objs) != test.expectedNumObjs {
			t.Errorf("Expected %d objects returned, got %d", test.expectedNumObjs, len(objs))
		}

		// Check results
		for _, obj := range objs {
			if svc, ok := obj.(*api.Service); ok {
				if svc.Name != groupName {
					t.Errorf("Expected %v returned, got %v", groupName, svc.Name)
				}
			}
			if deployment, ok := obj.(*appsv1.Deployment); ok {
				if deployment.Name != groupName {
					t.Errorf("Expected %v returned, got %v", groupName, deployment.Name)
				}
				if len(deployment.Spec.Template.Spec.Containers) != 2 {
					t.Errorf("Expected %d returned, got %d", 2, len(deployment.Spec.Template.Spec.Containers))
				}
				nameSet := make(map[string]api.Container)
				for _, container := range deployment.Spec.Template.Spec.Containers {
					nameSet[container.Name] = container
				}
				if container, ok := nameSet[test.expectedNames[0]]; !ok {
					t.Errorf("Expected %v returned, got %v", test.expectedNames[0], container.Name)
				} else if len(container.VolumeMounts) != 1 {
					t.Errorf("Expected %v returned, got %v", 1, len(container.VolumeMounts))
				}
				if container, ok := nameSet[test.expectedNames[1]]; !ok {
					t.Errorf("Expected %v returned, got %v", test.expectedNames[1], container.Name)
				} else if len(container.VolumeMounts) != 1 {
					t.Errorf("Expected %v returned, got %v", 1, len(container.VolumeMounts))
				}
			}
		}
	}
}

func TestServiceAccountNameOnMultipleContainers(t *testing.T) {
	groupName := "pod_group"
	serviceAccountName := "my-service"

	createConfigs := func(labels map[string]string) map[string]kobject.ServiceConfig {
		createConfig := func(name string) kobject.ServiceConfig {
			config := newSimpleServiceConfig()
			config.Labels = map[string]string{compose.LabelServiceGroup: groupName}
			for k, v := range labels {
				config.Labels[k] = v
			}
			config.Name = name
			config.ContainerName = ""
			config.Volumes = []kobject.Volumes{
				{
					VolumeName: "mountVolume",
					MountPath:  "/data",
				},
			}
			return config
		}
		return map[string]kobject.ServiceConfig{"app1": createConfig("app1"), "app2": createConfig("app2")}
	}

	testCases := map[string]struct {
		komposeObject      kobject.KomposeObject
		expectedLabelNames []string
	}{
		"Converted multiple containers with ServiceAccountName": {
			kobject.KomposeObject{
				ServiceConfigs: createConfigs(map[string]string{compose.LabelServiceAccountName: serviceAccountName}),
			}, []string{serviceAccountName}},
	}

	for name, test := range testCases {
		t.Log("Test case:", name)
		k := Kubernetes{}
		// Run Transform
		objs, err := k.Transform(test.komposeObject, kobject.ConvertOptions{ServiceGroupMode: "label", CreateD: true})
		if err != nil {
			t.Error(errors.Wrap(err, "k.Transform failed"))
		}

		// Check results
		for _, obj := range objs {
			if deployment, ok := obj.(*appsv1.Deployment); ok {
				if deployment.Name != groupName {
					t.Errorf("Expected %v returned, got %v", groupName, deployment.Name)
				}
				if deployment.Spec.Template.Spec.ServiceAccountName != serviceAccountName {
					t.Errorf("Expected %v returned, got %v", serviceAccountName, deployment.Spec.Template.Spec.ServiceAccountName)
				}
			}
		}
	}
}

func TestHealthCheckOnMultipleContainers(t *testing.T) {
	groupName := "pod_group"

	createHealthCheck := func(TCPPort int32) kobject.HealthCheck {
		return kobject.HealthCheck{
			TCPPort: TCPPort,
		}
	}

	createConfig := func(name string, livenessTCPPort, readinessTCPPort int32) kobject.ServiceConfig {
		config := newSimpleServiceConfig()
		config.Labels = map[string]string{compose.LabelServiceGroup: groupName}
		config.Name = name
		config.ContainerName = name
		config.HealthChecks.Liveness = createHealthCheck(livenessTCPPort)
		config.HealthChecks.Readiness = createHealthCheck(readinessTCPPort)
		return config
	}

	testCases := map[string]struct {
		komposeObject      kobject.KomposeObject
		opt                kobject.ConvertOptions
		expectedContainers map[string]api.Container
	}{
		"Converted multiple containers to Deployments": {
			kobject.KomposeObject{
				ServiceConfigs: map[string]kobject.ServiceConfig{
					"app1": createConfig("app1", 8081, 9091),
					"app2": createConfig("app2", 8082, 9092),
				},
			},
			kobject.ConvertOptions{ServiceGroupMode: "label", CreateD: true},
			map[string]api.Container{
				"app1": {
					LivenessProbe:  configProbe(createHealthCheck(8081)),
					ReadinessProbe: configProbe(createHealthCheck(9091)),
				},
				"app2": {
					LivenessProbe:  configProbe(createHealthCheck(8082)),
					ReadinessProbe: configProbe(createHealthCheck(9092)),
				},
			},
		},
	}

	for name, test := range testCases {
		t.Log("Test case:", name)
		k := Kubernetes{}
		// Run Transform
		objs, err := k.Transform(test.komposeObject, test.opt)
		if err != nil {
			t.Error(errors.Wrap(err, "k.Transform failed"))
		}

		// Check results
		for _, obj := range objs {
			if deployment, ok := obj.(*appsv1.Deployment); ok {
				if len(deployment.Spec.Template.Spec.Containers) != len(test.expectedContainers) {
					t.Errorf("Containers len is not equal, expected %d, got %d",
						len(deployment.Spec.Template.Spec.Containers), len(test.expectedContainers))
				}
				for _, result := range deployment.Spec.Template.Spec.Containers {
					expected, ok := test.expectedContainers[result.Name]
					if !ok {
						t.Errorf("Container %s doesn't expected", result.Name)
					}
					if !reflect.DeepEqual(result.LivenessProbe, expected.LivenessProbe) {
						t.Errorf("Container %s: LivenessProbe expected %v returned, got %v", result.Name, expected.LivenessProbe, result.LivenessProbe)
					}
					if !reflect.DeepEqual(result.ReadinessProbe, expected.ReadinessProbe) {
						t.Errorf("Container %s: ReadinessProbe expected %v returned, got %v", result.Name, expected.ReadinessProbe, result.ReadinessProbe)
					}
				}
			}
		}
	}
}

func TestCreatePVC(t *testing.T) {
	storageClassName := "custom-storage-class-name"
	k := Kubernetes{}
	result, err := k.CreatePVC("", "", PVCRequestSize, "", storageClassName)
	if err != nil {
		t.Error(errors.Wrap(err, "k.CreatePVC failed"))
	}
	if *result.Spec.StorageClassName != storageClassName {
		t.Errorf("Expected %s returned, got %s", storageClassName, *result.Spec.StorageClassName)
	}
}

func TestCreateHostPortAndProtocol(t *testing.T) {
	groupName := "pod_group"
	komposeObject := kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": newKomposeObjectHostPortProtocolConfig()},
	}
	k := Kubernetes{}
	objs, err := k.Transform(komposeObject, kobject.ConvertOptions{ServiceGroupMode: groupName})
	if err != nil {
		t.Error(errors.Wrap(err, "k.Transform failed"))
	}
	for _, obj := range objs {
		if deployment, ok := obj.(*appsv1.Deployment); ok {
			container := deployment.Spec.Template.Spec.Containers[0]
			port := container.Ports[0]
			containerPort := port.ContainerPort
			hostPort := port.HostPort
			protocol := port.Protocol

			expectedPort := komposeObject.ServiceConfigs["app"].Port[0]
			expectedContainerPort := expectedPort.ContainerPort
			expectedHostPort := expectedPort.HostPort
			expectedProtocol := expectedPort.Protocol

			if containerPort != expectedContainerPort {
				t.Errorf("Expected container port %v, got %v", expectedContainerPort, containerPort)
			}

			if hostPort != expectedHostPort {
				t.Errorf("Expected host port %v, got %v", expectedHostPort, hostPort)
			}

			if protocol != api.Protocol(expectedProtocol) {
				t.Errorf("Expected protocol %v, got %v", expectedProtocol, protocol)
			}
		}
	}
}

func TestServiceExternalTrafficPolicy(t *testing.T) {
	groupName := "pod_group"
	komposeObject := kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfigWithExternalTrafficPolicy()},
	}
	k := Kubernetes{}
	objs, err := k.Transform(komposeObject, kobject.ConvertOptions{ServiceGroupMode: groupName})
	if err != nil {
		t.Error(errors.Wrap(err, "k.Transform failed"))
	}
	for _, obj := range objs {
		if service, ok := obj.(*api.Service); ok {
			serviceExternalTrafficPolicy := string(service.Spec.ExternalTrafficPolicy)
			if serviceExternalTrafficPolicy != strings.ToLower(string(api.ServiceExternalTrafficPolicyTypeLocal)) {
				t.Errorf("Expected Local as external lifecycle policy, got %v", serviceExternalTrafficPolicy)
			}
			serviceType := service.Spec.Type
			if serviceType != api.ServiceTypeLoadBalancer {
				t.Errorf("Expected LoadBalancer as service type, got %v", serviceType)
			}
		}
	}
}

func TestVolumeMountSubPath(t *testing.T) {
	groupName := "pod_group"
	expectedSubPathValue := "test-subpath"
	komposeObject := kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfigWithServiceVolumeMount(expectedSubPathValue)},
	}
	k := Kubernetes{}
	objs, err := k.Transform(komposeObject, kobject.ConvertOptions{ServiceGroupMode: groupName})
	if err != nil {
		t.Error(errors.Wrap(err, "k.Transform failed"))
	}
	for _, obj := range objs {
		if deployment, ok := obj.(*appsv1.Deployment); ok {
			volMountSubPath := deployment.Spec.Template.Spec.Containers[0].VolumeMounts[0].SubPath
			if volMountSubPath != expectedSubPathValue {
				t.Errorf("Expected VolumeMount Subpath %v, got %v", expectedSubPathValue, volMountSubPath)
			}
		}
	}
}

func TestNetworkPoliciesGeneration(t *testing.T) {
	groupName := "pod_group"
	komposeObject := kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfig()},
	}
	k := Kubernetes{}
	objs, err := k.Transform(komposeObject, kobject.ConvertOptions{ServiceGroupMode: groupName, GenerateNetworkPolicies: true})
	if err != nil {
		t.Error(errors.Wrap(err, "k.Transform failed"))
	}
	for _, obj := range objs {
		if np, ok := obj.(*networkingv1.NetworkPolicy); ok {
			matchLabelsLength := len(np.Spec.PodSelector.MatchLabels)
			if matchLabelsLength == 0 {
				t.Errorf("Expected length of Network Policy PodSelector to be greater than 0, got %v", matchLabelsLength)
			}
		}
	}
}

func TestServiceGroupModeImagePullSecrets(t *testing.T) {
	groupName := "pod_group"
	serviceConfig := newServiceConfig()
	komposeObject := kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": serviceConfig},
	}
	k := Kubernetes{}
	objs, err := k.Transform(komposeObject, kobject.ConvertOptions{ServiceGroupMode: groupName, GenerateNetworkPolicies: true})
	if err != nil {
		t.Error(errors.Wrap(err, "k.Transform failed"))
	}
	expectedSecretsLen := len(serviceConfig.ImagePullSecret)
	for _, obj := range objs {
		if deployment, ok := obj.(*appsv1.Deployment); ok {
			secretsLen := len(deployment.Spec.Template.Spec.ImagePullSecrets)
			if secretsLen != expectedSecretsLen {
				t.Errorf("Expected length of Deployment ImagePullSecrets to be equal to %v, got %v", expectedSecretsLen, secretsLen)
			}
		}
	}
}

func TestNamespaceGeneration(t *testing.T) {
	ns := "app"
	komposeObject := kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfig()},
		Namespace:      ns,
	}
	k := Kubernetes{}
	objs, err := k.Transform(komposeObject, kobject.ConvertOptions{})
	if err != nil {
		t.Error(errors.Wrap(err, "k.Transform failed"))
	}
	for _, obj := range objs {
		if namespace, ok := obj.(*api.Namespace); ok {
			if strings.ToLower(ns) != strings.ToLower(namespace.ObjectMeta.Name) {
				t.Errorf("Expected namespace name %v, got %v", ns, namespace.ObjectMeta.Name)
			}
		}
		if dep, ok := obj.(*appsv1.Deployment); ok {
			if dep.ObjectMeta.Namespace != ns {
				t.Errorf("Expected deployment namespace %v, got %v", ns, dep.ObjectMeta.Namespace)
			}
		}
	}
}

// Test namespace generation with namespace being blank / ""
func TestNamespaceGenerationBlank(t *testing.T) {
	ns := ""
	komposeObject := kobject.KomposeObject{
		ServiceConfigs: map[string]kobject.ServiceConfig{"app": newServiceConfig()},
		Namespace:      ns,
	}
	k := Kubernetes{}
	objs, err := k.Transform(komposeObject, kobject.ConvertOptions{})
	if err != nil {
		t.Error(errors.Wrap(err, "k.Transform failed"))
	}
	for _, obj := range objs {
		if namespace, ok := obj.(*api.Namespace); ok {
			if strings.ToLower(ns) != strings.ToLower(namespace.ObjectMeta.Name) {
				t.Errorf("Expected namespace name %v, got %v", ns, namespace.ObjectMeta.Name)
			}
		}
		if dep, ok := obj.(*appsv1.Deployment); ok {
			if dep.ObjectMeta.Namespace != ns {
				t.Errorf("Expected deployment namespace %v, got %v", ns, dep.ObjectMeta.Namespace)
			}
		}
	}
}

func TestKubernetes_CreateSecrets(t *testing.T) {
	var komposeDefaultObject []kobject.KomposeObject
	dataSecrets := []SecretsConfig{
		{
			nameSecretConfig: "config-ini",
			nameSecret:       "debug-config-ini",
			pathFile:         "../../../docs/CNAME",
		},
		{
			nameSecretConfig: "new-config-init",
			nameSecret:       "new-debug-config-ini",
			pathFile:         "../../../docs/CNAME",
		},
	}

	for i := 0; i < len(dataSecrets); i++ {
		komposeDefaultObject = append(komposeDefaultObject, newKomposeObject())
		komposeDefaultObject[i].Secrets = newSecrets(dataSecrets[i])
	}

	type fields struct {
		Opt kobject.ConvertOptions
	}
	type args struct {
		komposeObject kobject.KomposeObject
	}

	tests := []struct {
		name    string
		fields  fields
		args    args
		want    []*api.Secret
		wantErr bool
	}{
		{
			name: "CreateSecrets from default KomposeObject and secrets taken from CNAME file",
			args: args{
				komposeObject: komposeDefaultObject[0],
			},
			want: []*api.Secret{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Secret",
						APIVersion: "v1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:   FormatResourceName(dataSecrets[0].nameSecretConfig),
						Labels: transformer.ConfigLabels(dataSecrets[0].nameSecretConfig),
					},
					Type: api.SecretTypeOpaque,
					Data: map[string][]byte{dataSecrets[0].nameSecretConfig: []byte("kompose.io")},
				},
			},
		},
		{
			name: "CreateSecrets from default KomposeObject and secrets taken from CNAME file",
			args: args{
				komposeObject: komposeDefaultObject[1],
			},
			want: []*api.Secret{
				{
					TypeMeta: metav1.TypeMeta{
						Kind:       "Secret",
						APIVersion: "v1",
					},
					ObjectMeta: metav1.ObjectMeta{
						Name:   FormatResourceName(dataSecrets[1].nameSecretConfig),
						Labels: transformer.ConfigLabels(dataSecrets[1].nameSecretConfig),
					},
					Type: api.SecretTypeOpaque,
					Data: map[string][]byte{dataSecrets[1].nameSecretConfig: []byte("kompose.io")},
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			k := &Kubernetes{
				Opt: tt.fields.Opt,
			}
			got, err := k.CreateSecrets(tt.args.komposeObject)
			if (err != nil) != tt.wantErr {
				t.Errorf("Kubernetes.CreateSecrets() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("Kubernetes.CreateSecrets() = %v, want %v", got, tt.want)
			}
		})
	}
}

// struct defines the configuration parameters required for creating a secret
type SecretsConfig struct {
	nameSecretConfig string
	nameSecret       string
	pathFile         string
}

// creates a new instance of types.Secrets based on the provided SecretsConfig parameter
func newSecrets(stringsSecretConfig SecretsConfig) types.Secrets {
	return types.Secrets{
		stringsSecretConfig.nameSecretConfig: types.SecretConfig{
			Name: stringsSecretConfig.nameSecret,
			File: stringsSecretConfig.pathFile,
		},
	}
}
