/*
Copyright 2020 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 fieldmanager

import (
	"fmt"
	"reflect"
	"testing"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
	"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
	"sigs.k8s.io/structured-merge-diff/v4/merge"
	"sigs.k8s.io/yaml"
)

// TestApplyUsingLastAppliedAnnotation tests that applying to an object
// created with the client-side apply last-applied annotation
// will not give conflicts
func TestApplyUsingLastAppliedAnnotation(t *testing.T) {
	f := NewDefaultTestFieldManager(schema.FromAPIVersionAndKind("apps/v1", "Deployment"))

	tests := []struct {
		lastApplied       []byte
		original          []byte
		applied           []byte
		fieldManager      string
		expectConflictSet *fieldpath.Set
	}{
		{
			fieldManager: "kubectl",
			lastApplied: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image-v1
      - name: my-c2
        image: my-image2
`),
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app # missing from last-applied
spec:
  replicas: 100 # does not match last-applied
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image-v2 # does no match last-applied
      # note that second container in last-applied is missing
`),
			applied: []byte(`
# test conflicts due to fields not allowed by last-applied

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-new-label # NOT allowed: update label
spec:
  replicas: 333 # NOT allowed: update replicas
  selector:
    matchLabels:
      app: my-new-label # allowed: update label
  template:
    metadata:
      labels:
        app: my-new-label # allowed: update-label
    spec:
      containers:
      - name: my-c
        image: my-image-new # NOT allowed: update image
`),
			expectConflictSet: fieldpath.NewSet(
				fieldpath.MakePathOrDie("metadata", "labels", "app"),
				fieldpath.MakePathOrDie("spec", "replicas"),
				fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
			),
		},
		{
			fieldManager: "kubectl",
			lastApplied: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 100 # does not match last applied
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
			applied: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-new-label
spec:
  replicas: 3 # expect conflict
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
			expectConflictSet: fieldpath.NewSet(
				fieldpath.MakePathOrDie("spec", "replicas"),
			),
		},
		{
			fieldManager: "kubectl",
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 100
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
			applied: []byte(`
# applied object matches original

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 100
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
		},
		{
			fieldManager: "kubectl",
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
			applied: []byte(`
# test allowed update with no conflicts

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-new-label # update label
spec:
  replicas: 333 # update replicas
  selector:
    matchLabels:
      app: my-new-label # update label
  template:
    metadata:
      labels:
        app: my-new-label # update-label
    spec:
      containers:
      - name: my-c
        image: my-image
`),
		},
		{
			fieldManager: "not_kubectl",
			lastApplied: []byte(`
# expect conflicts because field manager is NOT kubectl

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image-v1
`),
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 100 # does not match last-applied
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image-v2 # does no match last-applied
`),
			applied: []byte(`
# test conflicts due to fields not allowed by last-applied

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-new-label # update label
spec:
  replicas: 333 # update replicas
  selector:
    matchLabels:
      app: my-new-label # update label
  template:
    metadata:
      labels:
        app: my-new-label # update-label
    spec:
      containers:
      - name: my-c
        image: my-image-new # update image
`),
			expectConflictSet: fieldpath.NewSet(
				fieldpath.MakePathOrDie("metadata", "labels", "app"),
				fieldpath.MakePathOrDie("spec", "replicas"),
				fieldpath.MakePathOrDie("spec", "selector"), // selector is atomic
				fieldpath.MakePathOrDie("spec", "template", "metadata", "labels", "app"),
				fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
			),
		},
		{
			fieldManager: "kubectl",
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
			applied: []byte(`
# test allowed update with no conflicts

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-new-label
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-new-image # update image
`),
		},
		{
			fieldManager: "not_kubectl",
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-app
spec:
  replicas: 100
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-image
`),
			applied: []byte(`

# expect changes to fail because field manager is not kubectl

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
  labels:
    app: my-new-label # update label
spec:
  replicas: 3 # update replicas
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-c
        image: my-new-image # update image
`),
			expectConflictSet: fieldpath.NewSet(
				fieldpath.MakePathOrDie("metadata", "labels", "app"),
				fieldpath.MakePathOrDie("spec", "replicas"),
				fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
			),
		},
		{
			fieldManager: "kubectl",
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
`),
			applied: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 100 # update replicas
`),
		},
		{
			fieldManager: "kubectl",
			lastApplied: []byte(`
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
`),
			original: []byte(`
apiVersion: apps/v1 # expect conflict due to apiVersion mismatch with last-applied
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
`),
			applied: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 100 # update replicas
`),
			expectConflictSet: fieldpath.NewSet(
				fieldpath.MakePathOrDie("spec", "replicas"),
			),
		},
		{
			fieldManager: "kubectl",
			lastApplied: []byte(`
apiVerison: foo
kind: bar
spec: expect conflict due to invalid object
`),
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
`),
			applied: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 100 # update replicas
`),
			expectConflictSet: fieldpath.NewSet(
				fieldpath.MakePathOrDie("spec", "replicas"),
			),
		},
		{
			fieldManager: "kubectl",
			// last-applied is empty
			lastApplied: []byte{},
			original: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 3
`),
			applied: []byte(`
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-deployment
spec:
  replicas: 100 # update replicas
`),
			expectConflictSet: fieldpath.NewSet(
				fieldpath.MakePathOrDie("spec", "replicas"),
			),
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
			f.Reset()

			originalObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
			if err := yaml.Unmarshal(test.original, &originalObj.Object); err != nil {
				t.Errorf("error decoding YAML: %v", err)
			}

			if test.lastApplied == nil {
				test.lastApplied = test.original
			}
			if err := setLastAppliedFromEncoded(originalObj, test.lastApplied); err != nil {
				t.Errorf("failed to set last applied: %v", err)
			}

			if err := f.Update(originalObj, "test_client_side_apply"); err != nil {
				t.Errorf("failed to apply object: %v", err)
			}

			appliedObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
			if err := yaml.Unmarshal(test.applied, &appliedObj.Object); err != nil {
				t.Errorf("error decoding YAML: %v", err)
			}

			err := f.Apply(appliedObj, test.fieldManager, false)

			if test.expectConflictSet == nil {
				if err != nil {
					t.Errorf("expected no error but got %v", err)
				}
				return
			}

			if err == nil || !apierrors.IsConflict(err) {
				t.Errorf("expected to get conflicts but got %v", err)
			}

			expectedConflicts := merge.Conflicts{}
			test.expectConflictSet.Iterate(func(p fieldpath.Path) {
				expectedConflicts = append(expectedConflicts, merge.Conflict{
					Manager: `{"manager":"test_client_side_apply","operation":"Update","apiVersion":"apps/v1"}`,
					Path:    p,
				})
			})
			expectedConflictErr := internal.NewConflictError(expectedConflicts)
			if !reflect.DeepEqual(expectedConflictErr, err) {
				t.Errorf("expected to get\n%+v\nbut got\n%+v", expectedConflictErr, err)
			}
		})
	}
}
