package k8s

import (
	"errors"
	"fmt"
	"reflect"
	"sort"
	"testing"

	"k8s.io/apimachinery/pkg/labels"

	"github.com/linkerd/linkerd2/pkg/k8s"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
)

// newAPI constructs a mock controller/k8s.API object for testing
// retry: forces informer indexing, enabling informer lookups
// resourceConfigs: resources available via the API, and returned as runtime.Objects
// extraConfigs:    resources returned as runtime.Objects
func newAPI(retry bool, resourceConfigs []string, extraConfigs ...string) (*API, []runtime.Object, error) {
	k8sConfigs := []string{}
	k8sResults := []runtime.Object{}

	for _, config := range resourceConfigs {
		obj, err := k8s.ToRuntimeObject(config)
		if err != nil {
			return nil, nil, err
		}
		k8sConfigs = append(k8sConfigs, config)
		k8sResults = append(k8sResults, obj)
	}

	k8sConfigs = append(k8sConfigs, extraConfigs...)

	api, err := NewFakeAPI(k8sConfigs...)
	if err != nil {
		return nil, nil, fmt.Errorf("NewFakeAPI returned an error: %s", err)
	}

	if retry {
		api.Sync(nil)
	}

	return api, k8sResults, nil
}

func TestGetObjects(t *testing.T) {

	type getObjectsExpected struct {
		err error

		// input
		namespace string
		resType   string
		name      string

		// these are used to seed the k8s client
		k8sResResults []string // expected results from GetObjects
		k8sResMisc    []string // additional k8s objects for seeding the k8s client
	}

	t.Run("Returns expected objects based on input", func(t *testing.T) {
		expectations := []getObjectsExpected{
			{
				err:           status.Errorf(codes.Unimplemented, "unimplemented resource type: bar"),
				namespace:     "foo",
				resType:       "bar",
				name:          "baz",
				k8sResResults: []string{},
				k8sResMisc:    []string{},
			},
			{
				err:       nil,
				namespace: "my-ns",
				resType:   k8s.Pod,
				name:      "my-pod",
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-ns
spec:
  containers:
  - name: my-pod
status:
  phase: Running`,
				},
				k8sResMisc: []string{},
			},
			{
				err:           errors.New("pod \"my-pod\" not found"),
				namespace:     "not-my-ns",
				resType:       k8s.Pod,
				name:          "my-pod",
				k8sResResults: []string{},
				k8sResMisc: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "",
				resType:   k8s.ReplicationController,
				name:      "",
				k8sResResults: []string{`
apiVersion: v1
kind: ReplicationController
metadata:
  name: my-rc
  namespace: my-ns`,
				},
				k8sResMisc: []string{},
			},
			{
				err:       nil,
				namespace: "my-ns",
				resType:   k8s.Deployment,
				name:      "",
				k8sResResults: []string{`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deploy
  namespace: my-ns`,
				},
				k8sResMisc: []string{`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deploy
  namespace: not-my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "",
				resType:   k8s.DaemonSet,
				name:      "",
				k8sResResults: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: my-deploy
  namespace: my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "my-ns",
				resType:   k8s.DaemonSet,
				name:      "my-ds",
				k8sResResults: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: my-ds
  namespace: my-ns`,
				},
				k8sResMisc: []string{`
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: my-ds
  namespace: not-my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "my-ns",
				resType:   k8s.Job,
				name:      "my-job",
				k8sResResults: []string{`
apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
  namespace: my-ns`,
				},
				k8sResMisc: []string{`
apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
  namespace: not-my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "my-ns",
				resType:   k8s.CronJob,
				name:      "my-cronjob",
				k8sResResults: []string{`
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: my-cronjob
  namespace: my-ns`,
				},
				k8sResMisc: []string{`
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: my-cronjob
  namespace: not-my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "",
				resType:   k8s.StatefulSet,
				name:      "",
				k8sResResults: []string{`
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-deploy
  namespace: my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "my-ns",
				resType:   k8s.StatefulSet,
				name:      "my-ss",
				k8sResResults: []string{`
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-ss
  namespace: my-ns`,
				},
				k8sResMisc: []string{`
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: my-ss
  namespace: not-my-ns`,
				},
			},
			{
				err:       nil,
				namespace: "",
				resType:   k8s.Namespace,
				name:      "",
				k8sResResults: []string{`
apiVersion: v1
kind: Namespace
metadata:
  name: my-ns`,
				},
				k8sResMisc: []string{},
			},
		}

		for _, exp := range expectations {
			api, k8sResults, err := newAPI(true, exp.k8sResResults, exp.k8sResMisc...)
			if err != nil {
				t.Fatalf("newAPI error: %s", err)
			}

			pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
			if err != nil || exp.err != nil {
				if (err == nil && exp.err != nil) ||
					(err != nil && exp.err == nil) ||
					(err.Error() != exp.err.Error()) {
					t.Fatalf("api.GetObjects() unexpected error, expected [%s] got: [%s]", exp.err, err)
				}
			} else {
				if !reflect.DeepEqual(pods, k8sResults) {
					t.Fatalf("Expected: %+v, Got: %+v", k8sResults, pods)
				}
			}
		}
	})

	t.Run("If objects are pods", func(t *testing.T) {
		t.Run("Return running or pending pods", func(t *testing.T) {
			expectations := []getObjectsExpected{
				{
					err:       nil,
					namespace: "my-ns",
					resType:   k8s.Pod,
					name:      "my-pod",
					k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-ns
spec:
  containers:
  - name: my-pod
status:
  phase: Running`,
					},
				},
				{
					err:       nil,
					namespace: "my-ns",
					resType:   k8s.Pod,
					name:      "my-pod",
					k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-ns
spec:
  containers:
  - name: my-pod
status:
  phase: Pending`,
					},
				},
			}

			for _, exp := range expectations {
				api, k8sResults, err := newAPI(true, exp.k8sResResults)
				if err != nil {
					t.Fatalf("newAPI error: %s", err)
				}

				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
				if err != nil {
					t.Fatalf("api.GetObjects() unexpected error %s", err)
				}

				if !reflect.DeepEqual(pods, k8sResults) {
					t.Fatalf("Expected: %+v, Got: %+v", k8sResults, pods)
				}
			}
		})

		t.Run("Don't return failed or succeeded pods", func(t *testing.T) {
			expectations := []getObjectsExpected{
				{
					err:       nil,
					namespace: "my-ns",
					resType:   k8s.Pod,
					name:      "my-pod",
					k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-ns
spec:
  containers:
  - name: my-pod
status:
  phase: Succeeded`,
					},
				},
				{
					err:       nil,
					namespace: "my-ns",
					resType:   k8s.Pod,
					name:      "my-pod",
					k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-ns
spec:
  containers:
  - name: my-pod
status:
  phase: Failed`,
					},
				},
			}

			for _, exp := range expectations {
				api, _, err := newAPI(true, exp.k8sResResults)
				if err != nil {
					t.Fatalf("newAPI error: %s", err)
				}

				pods, err := api.GetObjects(exp.namespace, exp.resType, exp.name, labels.Everything())
				if err != nil {
					t.Fatalf("api.GetObjects() unexpected error %s", err)
				}

				if len(pods) != 0 {
					t.Errorf("Expected no terminating or failed pods to be returned but got %d pods", len(pods))
				}
			}

		})
	})
}

func TestGetPodsFor(t *testing.T) {

	type getPodsForExpected struct {
		err error

		// all 3 of these are used to seed the k8s client
		k8sResInput   string   // object used as input to GetPodFor()
		k8sResResults []string // expected results from GetPodsFor
		k8sResMisc    []string // additional k8s objects for seeding the k8s client
	}

	t.Run("Returns expected pods based on input", func(t *testing.T) {
		expectations := []getPodsForExpected{
			{
				err: nil,
				k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: emoji
  namespace: emojivoto
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				k8sResResults: []string{},
				k8sResMisc: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-finished
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: apps/v1
status:
  phase: Finished`,
				},
			},
			// Retrieve pods associated to a ClusterIP service
			{
				err: nil,
				k8sResInput: `
apiVersion: v1
kind: Service
metadata:
  name: emoji-svc
  namespace: emojivoto
  uid: serviceUIDdoesntMatter
spec:
  type: ClusterIP
  selector:
    app: emoji-svc`,
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-finished
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: apps/v1
status:
  phase: Running`,
				},
				k8sResMisc: []string{},
			},
			// ExternalName services shouldn't return any pods
			{
				err: nil,
				k8sResInput: `
apiVersion: v1
kind: Service
metadata:
  name: emoji-svc
  namespace: emojivoto
spec:
  type: ExternalName
  externalName: someapi.example.com`,
				k8sResResults: []string{},
				k8sResMisc: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-finished
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running`,
				},
			},
			// Cronjob
			{
				err: nil,
				k8sResInput: `
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: emoji
  namespace: emojivoto
  uid: cronjob`,
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: batch/v1
    uid: job
status:
  phase: Running`,
				},
				k8sResMisc: []string{`
apiVersion: batch/v1
kind: Job
metadata:
  name: emoji
  namespace: emojivoto
  uid: job
  ownerReferences:
  - apiVersion: batch/v1beta1
    uid: cronjob
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				},
			},
			// Daemonset
			{
				err: nil,
				k8sResInput: `
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: emoji
  namespace: emojivoto
  uid: daemonset
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: apps/v1
    uid: daemonset
status:
  phase: Running`,
				},
				k8sResMisc: []string{},
			},
			// replicaset
			{
				err: nil,
				k8sResInput: `
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: emoji
  namespace: emojivoto
  uid: replicaset
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: apps/v1
    uid: replicaset
status:
  phase: Running`,
				},
				k8sResMisc: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-finished
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: apps/v1
    uid: replicaset
status:
  phase: Finished`,
				},
			},
			// single pod
			{
				err: nil,
				k8sResInput: `
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: apps/v1
    uid: singlePod
status:
  phase: Running`,
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  labels:
    app: emoji-svc
  ownerReferences:
  - apiVersion: apps/v1
    uid: singlePod
status:
  phase: Running`,
				},
				k8sResMisc: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed_2
  namespace: emojivoto
  labels:
    app: emoji-svc
status:
  phase: Running`,
				},
			},
			// deployment
			{
				err: nil,
				k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed
  namespace: emojivoto
  uid: deployment
  labels:
    app: emoji-svc
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed
  namespace: emojivoto
  ownerReferences:
  - apiVersion: apps/v1
    uid: deploymentRS
  labels:
    app: emoji-svc
    pod-template-hash: deploymentPod
status:
  phase: Running`,
				},
				k8sResMisc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  uid: deploymentRS
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed_2
  namespace: emojivoto
  labels:
    app: emoji-svc
    pod-template-hash: deploymentPod
  ownerReferences:
  - apiVersion: apps/v1
    uid: deployment
spec:
  selector:
    matchLabels:
      app: emoji-svc
      pod-template-hash: deploymentPod`,
					`apiVersion: apps/v1
kind: ReplicaSet
metadata:
  uid: deploymentRSOld
  annotations:
    deployment.kubernetes.io/revision: "1"
  name: emojivoto-meshed_1
  namespace: emojivoto
  labels:
    app: emoji-svc
    pod-template-hash: deploymentPodOld
  ownerReferences:
  - apiVersion: apps/v1
    uid: deployment
spec:
  selector:
    matchLabels:
      app: emoji-svc
      pod-template-hash: deploymentPodOld`,
				},
			},
			// deployment without RS
			{
				err: nil,
				k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed
  namespace: emojivoto
  uid: deploymentWithoutRS
  labels:
    app: emoji-svc
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				k8sResResults: []string{},
				k8sResMisc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  uid: AnotherRS
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed_2
  namespace: emojivoto
  labels:
    app: emoji-svc
    pod-template-hash: doesntMatter
  ownerReferences:
  - apiVersion: apps/v1
    uid: doesntMatch
spec:
  selector:
    matchLabels:
      app: emoji-svc
      pod-template-hash: doesntMatter`,
				},
			},
			// Deployment with 2 replicasets
			{
				err: nil,
				k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed
  namespace: emojivoto
  uid: deployment2RS
  labels:
    app: emoji-svc
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				k8sResResults: []string{`
apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-pod1
  namespace: emojivoto
  ownerReferences:
  - apiVersion: apps/v1
    uid: RS1
  labels:
    app: emoji-svc
    pod-template-hash: pod1
status:
  phase: Running`,
					`apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-pod2
  namespace: emojivoto
  ownerReferences:
  - apiVersion: apps/v1
    uid: RS2
  labels:
    app: emoji-svc
    pod-template-hash: pod2
status:
  phase: Running`,
				},
				k8sResMisc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  uid: RS1
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed_2
  namespace: emojivoto
  labels:
    app: emoji-svc
    pod-template-hash: pod1
  ownerReferences:
  - apiVersion: apps/v1
    uid: deployment2RS
spec:
  selector:
    matchLabels:
      app: emoji-svc
      pod-template-hash: pod1`,
					`apiVersion: apps/v1
kind: ReplicaSet
metadata:
  uid: RS2
  annotations:
    deployment.kubernetes.io/revision: "1"
  name: emojivoto-meshed_1
  namespace: emojivoto
  labels:
    app: emoji-svc
    pod-template-hash: pod2
  ownerReferences:
  - apiVersion: apps/v1
    uid: deployment2RS
spec:
  selector:
    matchLabels:
      app: emoji-svc
      pod-template-hash: pod2`,
				},
			},
			// Deployment 2 Pods just one valid
			{
				err: nil,
				k8sResInput: `
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed
  namespace: emojivoto
  uid: deployment2Pods
  labels:
    app: emoji-svc
spec:
  selector:
    matchLabels:
      app: emoji-svc`,
				k8sResResults: []string{`apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-with-RS
  namespace: emojivoto
  ownerReferences:
  - apiVersion: apps/v1
    uid: validRS
  labels:
    app: emoji-svc
    pod-template-hash: podWithRS
status:
  phase: Running`,
				},
				k8sResMisc: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  uid: validRS
  annotations:
    deployment.kubernetes.io/revision: "2"
  name: emojivoto-meshed_2
  namespace: emojivoto
  labels:
    app: emoji-svc
    pod-template-hash: podWithRS
  ownerReferences:
  - apiVersion: apps/v1
    uid: deployment2Pods
spec:
  selector:
    matchLabels:
      app: emoji-svc
      pod-template-hash: podWithRS`,
					`apiVersion: v1
kind: Pod
metadata:
  name: emojivoto-meshed-without-RS
  namespace: emojivoto
  ownerReferences:
  - apiVersion: apps/v1
    uid: notHere
  labels:
    app: emoji-svc
    pod-template-hash: invalidPod
status:
  phase: Running`,
				},
			},
		}

		for _, exp := range expectations {
			k8sInputObj, err := k8s.ToRuntimeObject(exp.k8sResInput)
			if err != nil {
				t.Fatalf("could not decode yml: %s", err)
			}

			api, k8sResults, err := newAPI(true, exp.k8sResResults, exp.k8sResMisc...)
			if err != nil {
				t.Fatalf("newAPI error: %s", err)
			}

			k8sResultPods := []*corev1.Pod{}
			for _, obj := range k8sResults {
				k8sResultPods = append(k8sResultPods, obj.(*corev1.Pod))
			}

			pods, err := api.GetPodsFor(k8sInputObj, false)
			if err != exp.err {
				t.Fatalf("api.GetPodsFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
			}

			sort.Sort(byPod(pods))
			sort.Sort(byPod(k8sResultPods))
			if !reflect.DeepEqual(pods, k8sResultPods) {
				t.Fatalf("Expected: %+v, Got: %+v", k8sResultPods, pods)
			}
		}
	})
}

func TestGetOwnerKindAndName(t *testing.T) {
	for i, tt := range []struct {
		expectedOwnerKind string
		expectedOwnerName string
		podConfig         string
		extraConfigs      []string
	}{
		{
			expectedOwnerKind: "deployment",
			expectedOwnerName: "t2",
			podConfig: `
apiVersion: v1
kind: Pod
metadata:
  name: t2-5f79f964bc-d5jvf
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    kind: ReplicaSet
    name: t2-5f79f964bc`,
			extraConfigs: []string{`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: t2-5f79f964bc
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    kind: Deployment
    name: t2`,
			},
		},
		{
			expectedOwnerKind: "replicaset",
			expectedOwnerName: "t1-b4f55d87f",
			podConfig: `
apiVersion: v1
kind: Pod
metadata:
  name: t1-b4f55d87f-98dbz
  namespace: default
  ownerReferences:
  - apiVersion: apps/v1
    kind: ReplicaSet
    name: t1-b4f55d87f`,
		},
		{
			expectedOwnerKind: "job",
			expectedOwnerName: "slow-cooker",
			podConfig: `
apiVersion: v1
kind: Pod
metadata:
  name: slow-cooker-bxtnq
  namespace: default
  ownerReferences:
  - apiVersion: batch/v1
    kind: Job
    name: slow-cooker`,
		},
		{
			expectedOwnerKind: "replicationcontroller",
			expectedOwnerName: "web",
			podConfig: `
apiVersion: v1
kind: Pod
metadata:
  name: web-dcfq4
  namespace: default
  ownerReferences:
  - apiVersion: v1
    kind: ReplicationController
    name: web`,
		},
		{
			expectedOwnerKind: "pod",
			expectedOwnerName: "vote-bot",
			podConfig: `
apiVersion: v1
kind: Pod
metadata:
  name: vote-bot
  namespace: default`,
		},
		{
			expectedOwnerKind: "cronjob",
			expectedOwnerName: "my-cronjob",
			podConfig: `
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: my-ns
  ownerReferences:
  - apiVersion: batch/v1
    kind: Job
    name: my-job`,
			extraConfigs: []string{`
apiVersion: batch/v1
kind: Job
metadata:
  name: my-job
  namespace: my-ns
  ownerReferences:
  - apiVersion: batch/v1beta1
    kind: CronJob
    name: my-cronjob`,
			},
		},
	} {
		tt := tt // pin
		for _, retry := range []bool{
			false,
			true,
		} {
			retry := retry // pin
			t.Run(fmt.Sprintf("%d/retry:%t", i, retry), func(t *testing.T) {
				api, objs, err := newAPI(retry, []string{tt.podConfig}, tt.extraConfigs...)
				if err != nil {
					t.Fatalf("newAPI error: %s", err)
				}

				pod := objs[0].(*corev1.Pod)
				ownerKind, ownerName := api.GetOwnerKindAndName(pod, !retry)

				if ownerKind != tt.expectedOwnerKind {
					t.Fatalf("Expected kind to be [%s], got [%s]", tt.expectedOwnerKind, ownerKind)
				}

				if ownerName != tt.expectedOwnerName {
					t.Fatalf("Expected name to be [%s], got [%s]", tt.expectedOwnerName, ownerName)
				}
			})
		}
	}
}

func TestGetServiceProfileFor(t *testing.T) {
	for _, tt := range []struct {
		expectedRouteNames []string
		profileConfigs     []string
	}{
		// No service profiles -> default service profile
		{
			expectedRouteNames: []string{},
			profileConfigs:     []string{},
		},
		// Service profile in unrelated namespace -> default service profile
		{
			expectedRouteNames: []string{},
			profileConfigs: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
  name: books.server.svc.cluster.local
  namespace: linkerd
spec:
  routes:
  - condition:
      pathRegex: /server
    name: server`,
			},
		},
		// Uses service profile in server namespace
		{
			expectedRouteNames: []string{"server"},
			profileConfigs: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
  name: books.server.svc.cluster.local
  namespace: server
spec:
  routes:
  - condition:
      pathRegex: /server
    name: server`,
			},
		},
		// Uses service profile in client namespace
		{
			expectedRouteNames: []string{"client"},
			profileConfigs: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
  name: books.server.svc.cluster.local
  namespace: client
spec:
  routes:
  - condition:
      pathRegex: /client
    name: client`,
			},
		},
		// Service porfile in client namespace takes priority
		{
			expectedRouteNames: []string{"client"},
			profileConfigs: []string{`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
  name: books.server.svc.cluster.local
  namespace: server
spec:
  routes:
  - condition:
      pathRegex: /server
    name: server`,
				`
apiVersion: linkerd.io/v1alpha2
kind: ServiceProfile
metadata:
  name: books.server.svc.cluster.local
  namespace: client
spec:
  routes:
  - condition:
      pathRegex: /client
    name: client`,
			},
		},
	} {
		api, _, err := newAPI(true, tt.profileConfigs)
		if err != nil {
			t.Fatalf("newAPI error: %s", err)
		}

		svc := corev1.Service{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "books",
				Namespace: "server",
			},
		}

		sp := api.GetServiceProfileFor(&svc, "client", "cluster.local")

		if len(sp.Spec.Routes) != len(tt.expectedRouteNames) {
			t.Fatalf("Expected %d routes, got %d", len(tt.expectedRouteNames), len(sp.Spec.Routes))
		}

		for i, route := range sp.Spec.Routes {
			if tt.expectedRouteNames[i] != route.Name {
				t.Fatalf("Expected route [%s], got [%s]", tt.expectedRouteNames[i], route.Name)
			}
		}
	}
}

func TestGetServicesFor(t *testing.T) {

	type getServicesForExpected struct {
		err error

		// all 3 of these are used to seed the k8s client
		k8sResInput   string   // object used as input to GetServicesFor()
		k8sResResults []string // expected results from GetServicesFor
		k8sResMisc    []string // additional k8s objects for seeding the k8s client
	}

	t.Run("GetServicesFor", func(t *testing.T) {
		expectations := []getServicesForExpected{
			// If a service contains a pod, GetPodsFor should return the service.
			{
				err: nil,
				k8sResInput: `
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
  namespace: emojivoto
  labels:
    app: my-pod
status:
  phase: Running`,
				k8sResResults: []string{`
apiVersion: v1
kind: Service
metadata:
  name: my-svc
  namespace: emojivoto
spec:
  type: ClusterIP
  selector:
    app: my-pod`,
				},
				k8sResMisc: []string{},
			},
			// If a service is the apex of a traffic split, GetPodsFor should
			// return the pods of the backends.
			{
				err: nil,
				k8sResInput: `
apiVersion: v1
kind: Pod
metadata:
  name: leaf-1-pod
  namespace: emojivoto
  labels:
    app: leaf-1
status:
  phase: Running`,
				k8sResResults: []string{`
apiVersion: v1
kind: Service
metadata:
  name: apex
  namespace: emojivoto
spec:
  type: ClusterIP
  selector:
    app: apex`,
					`
apiVersion: v1
kind: Service
metadata:
  name: leaf-1
  namespace: emojivoto
spec:
  type: ClusterIP
  selector:
    app: leaf-1`,
				},
				k8sResMisc: []string{`

apiVersion: v1
kind: Service
metadata:
  name: leaf-2
  namespace: emojivoto
spec:
  type: ClusterIP
  selector:
    app: leaf-2`,
					`
apiVersion: split.smi-spec.io/v1alpha1
kind: TrafficSplit
metadata:
  name: banana-split
  namespace: emojivoto
spec:
  service: apex
  backends:
  - service: leaf-1
    weight: 500m
  - service: leaf-2
    weight: 500m`,
				},
			},
		}

		for _, exp := range expectations {
			k8sInputObj, err := k8s.ToRuntimeObject(exp.k8sResInput)
			if err != nil {
				t.Fatalf("could not decode yml: %s", err)
			}

			api, k8sResults, err := newAPI(true, exp.k8sResResults, append(exp.k8sResMisc, exp.k8sResInput)...)
			if err != nil {
				t.Fatalf("newAPI error: %s", err)
			}

			k8sResultServices := []*corev1.Service{}
			for _, obj := range k8sResults {
				k8sResultServices = append(k8sResultServices, obj.(*corev1.Service))
			}

			services, err := api.GetServicesFor(k8sInputObj, false)
			if err != exp.err {
				t.Fatalf("api.GetServicesFor() unexpected error, expected [%s] got: [%s]", exp.err, err)
			}

			sort.Sort(byService(k8sResultServices))
			sort.Sort(byService(services))
			if !reflect.DeepEqual(services, k8sResultServices) {
				t.Fatalf("Expected: %+v, Got: %+v", k8sResultServices, services)
			}
		}

	})
}
