// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package task

import (
	"testing"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/cli-utils/pkg/apply/cache"
	"sigs.k8s.io/cli-utils/pkg/apply/event"
	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
	"sigs.k8s.io/cli-utils/pkg/inventory"
	"sigs.k8s.io/cli-utils/pkg/object"
	"sigs.k8s.io/cli-utils/pkg/testutil"
)

var objInvalid = &unstructured.Unstructured{
	Object: map[string]interface{}{
		"apiVersion": "apps/v1",
		"kind":       "Deployment",
	},
}

func TestInvSetTask(t *testing.T) {
	id1 := object.UnstructuredToObjMetadata(obj1)
	id2 := object.UnstructuredToObjMetadata(obj2)
	id3 := object.UnstructuredToObjMetadata(obj3)
	idInvalid := object.UnstructuredToObjMetadata(objInvalid)

	tests := map[string]struct {
		prevInventory  object.ObjMetadataSet
		appliedObjs    object.ObjMetadataSet
		failedApplies  object.ObjMetadataSet
		failedDeletes  object.ObjMetadataSet
		skippedApplies object.ObjMetadataSet
		skippedDeletes object.ObjMetadataSet
		abandonedObjs  object.ObjMetadataSet
		invalidObjs    object.ObjMetadataSet
		expectedObjs   object.ObjMetadataSet
	}{
		"no apply objs, no prune failures; no inventory": {
			expectedObjs: object.ObjMetadataSet{},
		},
		"one apply objs, no prune failures; one inventory": {
			appliedObjs:  object.ObjMetadataSet{id1},
			expectedObjs: object.ObjMetadataSet{id1},
		},
		"no apply objs, one prune failure, in prev inventory; one inventory": {
			prevInventory: object.ObjMetadataSet{id1},
			failedDeletes: object.ObjMetadataSet{id1},
			expectedObjs:  object.ObjMetadataSet{id1},
		},
		"no apply objs, one prune failure, not in prev inventory; no inventory": {
			// aritifical use case: prunes come from the inventory
			failedDeletes: object.ObjMetadataSet{id1},
			expectedObjs:  object.ObjMetadataSet{},
		},
		"one apply objs, one prune failures; one inventory": {
			// aritifical use case: applies and prunes are mutually exclusive.
			// Delete failure overwrites apply success in object status.
			appliedObjs:   object.ObjMetadataSet{id3},
			failedDeletes: object.ObjMetadataSet{id3},
			expectedObjs:  object.ObjMetadataSet{},
		},
		"two apply objs, two prune failures; three inventory": {
			// aritifical use case: applies and prunes are mutually exclusive
			prevInventory: object.ObjMetadataSet{id2, id3},
			appliedObjs:   object.ObjMetadataSet{id1, id2},
			failedDeletes: object.ObjMetadataSet{id2, id3},
			expectedObjs:  object.ObjMetadataSet{id1, id2, id3},
		},
		"no apply objs, no apply failures, no prune failures; no inventory": {
			failedApplies: object.ObjMetadataSet{id3},
			expectedObjs:  object.ObjMetadataSet{},
		},
		"one apply failure not in prev inventory; no inventory": {
			failedApplies: object.ObjMetadataSet{id3},
			expectedObjs:  object.ObjMetadataSet{},
		},
		"one apply obj, one apply failure not in prev inventory; one inventory": {
			appliedObjs:   object.ObjMetadataSet{id2},
			failedApplies: object.ObjMetadataSet{id3},
			expectedObjs:  object.ObjMetadataSet{id2},
		},
		"one apply obj, one apply failure in prev inventory; one inventory": {
			appliedObjs:   object.ObjMetadataSet{id2},
			failedApplies: object.ObjMetadataSet{id3},
			prevInventory: object.ObjMetadataSet{id3},
			expectedObjs:  object.ObjMetadataSet{id2, id3},
		},
		"one apply obj, two apply failures with one in prev inventory; two inventory": {
			appliedObjs:   object.ObjMetadataSet{id2},
			failedApplies: object.ObjMetadataSet{id1, id3},
			prevInventory: object.ObjMetadataSet{id3},
			expectedObjs:  object.ObjMetadataSet{id2, id3},
		},
		"three apply failures with two in prev inventory; two inventory": {
			failedApplies: object.ObjMetadataSet{id1, id2, id3},
			prevInventory: object.ObjMetadataSet{id2, id3},
			expectedObjs:  object.ObjMetadataSet{id2, id3},
		},
		"three apply failures with three in prev inventory; three inventory": {
			failedApplies: object.ObjMetadataSet{id1, id2, id3},
			prevInventory: object.ObjMetadataSet{id2, id3, id1},
			expectedObjs:  object.ObjMetadataSet{id2, id1, id3},
		},
		"one skipped apply from prev inventory; one inventory": {
			prevInventory:  object.ObjMetadataSet{id1},
			skippedApplies: object.ObjMetadataSet{id1},
			expectedObjs:   object.ObjMetadataSet{id1},
		},
		"one skipped apply, no prev inventory; no inventory": {
			skippedApplies: object.ObjMetadataSet{id1},
			expectedObjs:   object.ObjMetadataSet{},
		},
		"one apply obj, one skipped apply, two prev inventory; two inventory": {
			prevInventory:  object.ObjMetadataSet{id1, id2},
			appliedObjs:    object.ObjMetadataSet{id2},
			skippedApplies: object.ObjMetadataSet{id1},
			expectedObjs:   object.ObjMetadataSet{id1, id2},
		},
		"one skipped delete from prev inventory; one inventory": {
			prevInventory:  object.ObjMetadataSet{id1},
			skippedDeletes: object.ObjMetadataSet{id1},
			expectedObjs:   object.ObjMetadataSet{id1},
		},
		"one apply obj, one skipped delete, two prev inventory; two inventory": {
			prevInventory:  object.ObjMetadataSet{id1, id2},
			appliedObjs:    object.ObjMetadataSet{id2},
			skippedDeletes: object.ObjMetadataSet{id1},
			expectedObjs:   object.ObjMetadataSet{id1, id2},
		},
		"two apply obj, one abandoned, three in prev inventory; two inventory": {
			prevInventory: object.ObjMetadataSet{id1, id2, id3},
			appliedObjs:   object.ObjMetadataSet{id1, id2},
			abandonedObjs: object.ObjMetadataSet{id3},
			expectedObjs:  object.ObjMetadataSet{id1, id2},
		},
		"two abandoned, two in prev inventory; no inventory": {
			prevInventory: object.ObjMetadataSet{id2, id3},
			abandonedObjs: object.ObjMetadataSet{id2, id3},
			expectedObjs:  object.ObjMetadataSet{},
		},
		"same obj skipped delete and abandoned, one in prev inventory; no inventory": {
			prevInventory:  object.ObjMetadataSet{id3},
			skippedDeletes: object.ObjMetadataSet{id3},
			abandonedObjs:  object.ObjMetadataSet{id3},
			expectedObjs:   object.ObjMetadataSet{},
		},
		"preserve invalid objects in the inventory": {
			prevInventory: object.ObjMetadataSet{id3, idInvalid},
			appliedObjs:   object.ObjMetadataSet{id3},
			invalidObjs:   object.ObjMetadataSet{idInvalid},
			expectedObjs:  object.ObjMetadataSet{id3, idInvalid},
		},
		"ignore invalid objects not in the inventory": {
			prevInventory: object.ObjMetadataSet{id3},
			appliedObjs:   object.ObjMetadataSet{id3},
			invalidObjs:   object.ObjMetadataSet{idInvalid},
			expectedObjs:  object.ObjMetadataSet{id3},
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			client := inventory.NewFakeClient(object.ObjMetadataSet{})
			eventChannel := make(chan event.Event)
			resourceCache := cache.NewResourceCacheMap()
			context := taskrunner.NewTaskContext(eventChannel, resourceCache)

			task := InvSetTask{
				TaskName:      taskName,
				InvClient:     client,
				InvInfo:       nil,
				PrevInventory: tc.prevInventory,
			}
			im := context.InventoryManager()
			for _, applyObj := range tc.appliedObjs {
				im.AddSuccessfulApply(applyObj, "unusued-uid", int64(0))
			}
			for _, applyFailure := range tc.failedApplies {
				im.AddFailedApply(applyFailure)
			}
			for _, pruneObj := range tc.failedDeletes {
				im.AddFailedDelete(pruneObj)
			}
			for _, skippedApply := range tc.skippedApplies {
				im.AddSkippedApply(skippedApply)
			}
			for _, skippedDelete := range tc.skippedDeletes {
				im.AddSkippedDelete(skippedDelete)
			}
			for _, abandonedObj := range tc.abandonedObjs {
				context.AddAbandonedObject(abandonedObj)
			}
			for _, invalidObj := range tc.invalidObjs {
				context.AddInvalidObject(invalidObj)
			}
			if taskName != task.Name() {
				t.Errorf("expected task name (%s), got (%s)", taskName, task.Name())
			}
			task.Start(context)
			result := <-context.TaskChannel()
			if result.Err != nil {
				t.Errorf("unexpected error running InvAddTask: %s", result.Err)
			}
			actual, _ := client.GetClusterObjs(nil)
			testutil.AssertEqual(t, tc.expectedObjs, actual,
				"Actual cluster objects (%d) do not match expected cluster objects (%d)",
				len(actual), len(tc.expectedObjs))
		})
	}
}