package kube

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	extv1beta1 "k8s.io/api/extensions/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	fakedisco "k8s.io/client-go/discovery/fake"
	"k8s.io/client-go/rest"
	testcore "k8s.io/client-go/testing"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
	"sigs.k8s.io/yaml"
)

const depWithLabel = `
apiVersion: extensions/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    foo: bar
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx:1.7.9
        name: nginx
        ports:
        - containerPort: 80
`

var standardVerbs = metav1.Verbs{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}

func TestUnsetLabels(t *testing.T) {
	for _, yamlStr := range [][]byte{[]byte(depWithLabel)} {
		var obj unstructured.Unstructured
		err := yaml.Unmarshal(yamlStr, &obj)
		require.NoError(t, err)

		UnsetLabel(&obj, "foo")

		manifestBytes, err := json.MarshalIndent(obj.Object, "", "  ")
		require.NoError(t, err)

		var dep extv1beta1.Deployment
		err = json.Unmarshal(manifestBytes, &dep)
		require.NoError(t, err)
		assert.Empty(t, dep.Labels)
	}
}

func TestCleanKubectlOutput(t *testing.T) {
	{
		s := `error: error validating "STDIN": error validating data: ValidationError(Deployment.spec): missing required field "selector" in io.k8s.api.apps.v1beta2.DeploymentSpec; if you choose to ignore these errors, turn validation off with --validate=false`
		assert.Equal(t, `error validating data: ValidationError(Deployment.spec): missing required field "selector" in io.k8s.api.apps.v1beta2.DeploymentSpec`, cleanKubectlOutput(s))
	}
	{
		s := `error when applying patch:
{"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test-immutable-change\"},\"name\":\"my-service\",\"namespace\":\"argocd-e2e--test-immutable-change-ysfud\"},\"spec\":{\"clusterIP\":\"10.96.0.44\",\"ports\":[{\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376}],\"selector\":{\"app\":\"MyApp\"}}}\n"}},"spec":{"clusterIP":"10.96.0.44"}}
to:
Resource: "/v1, Resource=services", GroupVersionKind: "/v1, Kind=Service"
Name: "my-service", Namespace: "argocd-e2e--test-immutable-change-ysfud"
Object: &{map["apiVersion":"v1" "kind":"Service" "metadata":map["annotations":map["kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test-immutable-change\"},\"name\":\"my-service\",\"namespace\":\"argocd-e2e--test-immutable-change-ysfud\"},\"spec\":{\"clusterIP\":\"10.96.0.43\",\"ports\":[{\"port\":80,\"protocol\":\"TCP\",\"targetPort\":9376}],\"selector\":{\"app\":\"MyApp\"}}}\n"] "creationTimestamp":"2019-12-11T15:29:56Z" "labels":map["app.kubernetes.io/instance":"test-immutable-change"] "name":"my-service" "namespace":"argocd-e2e--test-immutable-change-ysfud" "resourceVersion":"157426" "selfLink":"/api/v1/namespaces/argocd-e2e--test-immutable-change-ysfud/services/my-service" "uid":"339cf96f-47eb-4759-ac95-30a169dce004"] "spec":map["clusterIP":"10.96.0.43" "ports":[map["port":'P' "protocol":"TCP" "targetPort":'\u24a0']] "selector":map["app":"MyApp"] "sessionAffinity":"None" "type":"ClusterIP"] "status":map["loadBalancer":map[]]]}
for: "/var/folders/_m/991sn1ds7g39lnbhp6wvqp9d_j5476/T/224503547": Service "my-service" is invalid: spec.clusterIP: Invalid value: "10.96.0.44": field is immutable`
		assert.Equal(t, `Service "my-service" is invalid: spec.clusterIP: Invalid value: "10.96.0.44": field is immutable`, cleanKubectlOutput(s))
	}
	{
		s := `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/745145319": "" is invalid: patch: Invalid value: "map[data:map[email:aaaaa password:<nil> username:<nil>] metadata:map[annotations:map[kubectl.kubernetes.io/last-applied-configuration:{\"apiVersion\":\"v1\",\"data\":{\"email\":\"aaaaa\"},\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test\"},\"name\":\"my-secret\",\"namespace\":\"default\"},\"stringData\":{\"id\":1,\"password\":0,\"username\":\"username\"},\"type\":\"Opaque\"}\n]] stringData:map[id:1 password:0 username:username]]": error decoding from json: illegal base64 data at input byte 4`
		assert.Equal(t, `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/745145319": "" is invalid: patch: Invalid value: "": error decoding from json: illegal base64 data at input byte 4`, cleanKubectlOutput(s))
	}
	{
		s := `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/2250018703": "" is invalid: patch: Invalid value: "map[data:<nil> metadata:map[annotations:map[kubectl.kubernetes.io/last-applied-configuration:{\"apiVersion\":\"v1\",\"kind\":\"Secret\",\"metadata\":{\"annotations\":{},\"labels\":{\"app.kubernetes.io/instance\":\"test\"},\"name\":\"my-secret\",\"namespace\":\"default\"},\"stringData\":{\"id\":1,\"password\":0,\"username\":\"username\"},\"type\":\"Opaque\"}\n]]]": cannot convert int64 to string`
		assert.Equal(t, `error when patching "/var/folders/mj/c96jcs7j2cq7xcjfhqjq3m2w0000gn/T/2250018703": "" is invalid: patch: Invalid value: "": cannot convert int64 to string`, cleanKubectlOutput(s))
	}
	{
		s := `Secret in version "v1" cannot be handled as a Secret: json: cannot unmarshal bool into Go struct field Secret.data of type []uint8`
		assert.Equal(t, `Secret in version "v1" cannot be handled as a Secret: json: cannot unmarshal bool into Go struct field Secret.data of type []uint8`, cleanKubectlOutput(s))
	}
}

func TestInClusterKubeConfig(t *testing.T) {
	restConfig := &rest.Config{}
	kubeConfig := NewKubeConfig(restConfig, "")
	assert.NotEmpty(t, kubeConfig.AuthInfos[kubeConfig.CurrentContext].TokenFile)

	restConfig = &rest.Config{
		Password: "foo",
	}
	kubeConfig = NewKubeConfig(restConfig, "")
	assert.Empty(t, kubeConfig.AuthInfos[kubeConfig.CurrentContext].TokenFile)

	restConfig = &rest.Config{
		ExecProvider: &clientcmdapi.ExecConfig{
			APIVersion: "client.authentication.k8s.io/v1alpha1",
			Command:    "aws",
		},
	}
	kubeConfig = NewKubeConfig(restConfig, "")
	assert.Empty(t, kubeConfig.AuthInfos[kubeConfig.CurrentContext].TokenFile)
}

func TestNewKubeConfig_TLSServerName(t *testing.T) {
	const (
		host          = "something.test"
		tlsServerName = "something.else.test"
	)

	restConfig := &rest.Config{
		Host: host,
	}
	kubeConfig := NewKubeConfig(restConfig, "")
	assert.Empty(t, kubeConfig.Clusters[host].TLSServerName)

	restConfig = &rest.Config{
		Host: host,
		TLSClientConfig: rest.TLSClientConfig{
			ServerName: tlsServerName,
		},
	}
	kubeConfig = NewKubeConfig(restConfig, "")
	assert.Equal(t, tlsServerName, kubeConfig.Clusters[host].TLSServerName)
}

func TestGetDeploymentReplicas(t *testing.T) {
	manifest := []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80	
`)
	deployment := unstructured.Unstructured{}
	err := yaml.Unmarshal(manifest, &deployment)
	require.NoError(t, err)
	assert.Equal(t, int64(2), *GetDeploymentReplicas(&deployment))
}

func TestGetNilDeploymentReplicas(t *testing.T) {
	manifest := []byte(`
apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - image: nginx:1.7.9
    name: nginx
    resources:
      requests:
        cpu: 0.2
`)
	noDeployment := unstructured.Unstructured{}
	err := yaml.Unmarshal(manifest, &noDeployment)
	require.NoError(t, err)
	assert.Nil(t, GetDeploymentReplicas(&noDeployment))
}

func TestGetResourceImages(t *testing.T) {
	testCases := []struct {
		manifest    []byte
		expected    []string
		description string
	}{
		{
			manifest: []byte(`
apiVersion: extensions/v1beta2
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    foo: bar
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
          - containerPort: 80
      - name: agent
        image: agent:1.0.0`),
			expected:    []string{"nginx:1.7.9", "agent:1.0.0"},
			description: "deployment with two containers",
		},
		{
			manifest: []byte(`
apiVersion: v1
kind: Pod
metadata:
  name: example-pod
  labels:
    app: my-app
spec:
  containers:
  - name: nginx-container
    image: nginx:1.21
    ports:
    - containerPort: 80
  - name: sidecar-container
    image: busybox:1.35
    command: ["sh", "-c", "echo Hello from the sidecar; sleep 3600"]
`),
			expected:    []string{"nginx:1.21", "busybox:1.35"},
			description: "pod with containers",
		},
		{
			manifest: []byte(`
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "* * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox:1.28
`),
			expected:    []string{"busybox:1.28"},
			description: "cronjob with containers",
		},
		{
			manifest: []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
  name: example-config
  namespace: default
  labels:
    app: my-app
data:
  app.properties: |
    key1=value1
    key2=value2
    key3=value3
  log.level: debug
`),
			expected:    nil,
			description: "configmap without containers",
		},
		{
			manifest: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-no-containers
  labels:
    foo: bar
spec:
  replicas: 1
  selector:
    matchLabels:
      app: agent
  template:
    metadata:
      labels:
        app: agent
    spec:
      volumes:
      - name: config-volume
        configMap:
          name: config
`),
			expected:    nil,
			description: "deployment without containers",
		},
		{
			manifest: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-without-image
spec:
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: text-service
          command: ["echo", "hello"]
`),
			expected:    nil,
			description: "deployment with container without image",
		},
		{
			manifest: []byte(`
apiVersion: v1
kind: Pod
metadata:
  name: example-pod
  labels:
    app: my-app
spec:
  containers:
  - name: no-image-container
    command: ["echo", "hello"]
`),
			expected:    nil,
			description: "pod with container without image",
		},
	}

	for _, tc := range testCases {
		t.Run(tc.description, func(t *testing.T) {
			resource := unstructured.Unstructured{}
			err := yaml.Unmarshal(tc.manifest, &resource)
			require.NoError(t, err)
			images := GetResourceImages(&resource)
			require.Equal(t, tc.expected, images)
		})
	}
}

func TestSplitYAML_SingleObject(t *testing.T) {
	objs, err := SplitYAML([]byte(depWithLabel))
	require.NoError(t, err)
	assert.Len(t, objs, 1)
}

func TestSplitYAML_MultipleObjects(t *testing.T) {
	objs, err := SplitYAML([]byte(depWithLabel + "\n---\n" + depWithLabel))
	require.NoError(t, err)
	assert.Len(t, objs, 2)
}

func TestSplitYAML_TrailingNewLines(t *testing.T) {
	objs, err := SplitYAML([]byte("\n\n\n---" + depWithLabel))
	require.NoError(t, err)
	assert.Len(t, objs, 1)
}

func TestServerResourceGroupForGroupVersionKind(t *testing.T) {
	fakeDisco := &fakedisco.FakeDiscovery{Fake: &testcore.Fake{}}
	fakeDisco.Resources = append(make([]*metav1.APIResourceList, 0),
		&metav1.APIResourceList{
			GroupVersion: "test.argoproj.io/v1alpha1",
			APIResources: []metav1.APIResource{
				{Kind: "TestAllVerbs", Group: "test.argoproj.io", Version: "v1alpha1", Namespaced: true, Verbs: standardVerbs},
				{Kind: "TestSomeVerbs", Group: "test.argoproj.io", Version: "v1alpha1", Namespaced: true, Verbs: []string{"get", "list"}},
			},
		})

	t.Run("Successfully resolve for all verbs", func(t *testing.T) {
		for _, v := range standardVerbs {
			_, err := ServerResourceForGroupVersionKind(fakeDisco, schema.FromAPIVersionAndKind("test.argoproj.io/v1alpha1", "TestAllVerbs"), v)
			assert.NoError(t, err, "Could not resolve verb %s", v)
		}
	})
	t.Run("Successfully resolve for some verbs", func(t *testing.T) {
		for _, v := range []string{"get", "list"} {
			_, err := ServerResourceForGroupVersionKind(fakeDisco, schema.FromAPIVersionAndKind("test.argoproj.io/v1alpha1", "TestSomeVerbs"), v)
			assert.NoError(t, err, "Could not resolve verb %s", v)
		}
	})
	t.Run("Verb not supported", func(t *testing.T) {
		for _, v := range []string{"patch"} {
			_, err := ServerResourceForGroupVersionKind(fakeDisco, schema.FromAPIVersionAndKind("test.argoproj.io/v1alpha1", "TestSomeVerbs"), v)
			assert.Equal(t, err, apierrors.NewMethodNotSupported(schema.GroupResource{Group: "test.argoproj.io", Resource: "TestSomeVerbs"}, v))
		}
	})
	t.Run("Resource not found", func(t *testing.T) {
		for _, v := range standardVerbs {
			_, err := ServerResourceForGroupVersionKind(fakeDisco, schema.FromAPIVersionAndKind("test.argoproj.io/v1alpha1", "TestNonExisting"), v)
			assert.Equal(t, err, apierrors.NewNotFound(schema.GroupResource{Group: "test.argoproj.io", Resource: "TestNonExisting"}, ""))
		}
	})
}
