/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package sgreconciler

import (
	"context"
	"errors"
	"fmt"
	"reflect"
	"sync"
	"testing"

	mainconstants "github.com/edgelesssys/constellation/v2/internal/constants"
	updatev1alpha1 "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api/v1alpha1"
	cspapi "github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/cloud/api"
	"github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/internal/constants"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestCreateScalingGroupIfNotExists(t *testing.T) {
	testCases := map[string]struct {
		createErr        error
		wantScalingGroup *updatev1alpha1.ScalingGroup
		wantErr          bool
	}{
		"create works": {
			wantScalingGroup: &updatev1alpha1.ScalingGroup{
				TypeMeta: metav1.TypeMeta{APIVersion: "update.edgeless.systems/v1alpha1", Kind: "ScalingGroup"},
				ObjectMeta: metav1.ObjectMeta{
					Name: "resource-name",
				},
				Spec: updatev1alpha1.ScalingGroupSpec{
					NodeVersion:         mainconstants.NodeVersionResourceName,
					GroupID:             "group-id",
					AutoscalerGroupName: "autoscaling-group-name",
					NodeGroupName:       "node-group-name",
					Min:                 1,
					Max:                 10,
					Role:                updatev1alpha1.WorkerRole,
				},
			},
		},
		"create fails": {
			createErr: errors.New("create failed"),
			wantErr:   true,
		},
		"scaling group exists": {
			createErr: k8sErrors.NewAlreadyExists(schema.GroupResource{}, constants.AutoscalingStrategyResourceName),
			wantScalingGroup: &updatev1alpha1.ScalingGroup{
				TypeMeta: metav1.TypeMeta{APIVersion: "update.edgeless.systems/v1alpha1", Kind: "ScalingGroup"},
				ObjectMeta: metav1.ObjectMeta{
					Name: "resource-name",
				},
				Spec: updatev1alpha1.ScalingGroupSpec{
					NodeVersion:         mainconstants.NodeVersionResourceName,
					GroupID:             "group-id",
					AutoscalerGroupName: "autoscaling-group-name",
					NodeGroupName:       "node-group-name",
					Min:                 1,
					Max:                 10,
					Role:                updatev1alpha1.WorkerRole,
				},
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			assert := assert.New(t)
			require := require.New(t)

			k8sClient := &fakeK8sClient{createErr: tc.createErr}
			newScalingGroupConfig := newScalingGroupConfig{
				k8sClient:            k8sClient,
				resourceName:         "resource-name",
				groupID:              "group-id",
				nodeGroupName:        "node-group-name",
				autoscalingGroupName: "autoscaling-group-name",
				role:                 updatev1alpha1.WorkerRole,
			}
			err := createScalingGroupIfNotExists(context.Background(), newScalingGroupConfig)
			if tc.wantErr {
				assert.Error(err)
				return
			}
			require.NoError(err)
			assert.Len(k8sClient.createdObjects, 1)
			assert.Equal(tc.wantScalingGroup, k8sClient.createdObjects[0])
		})
	}
}

func TestPatchNodeGroupName(t *testing.T) {
	testCases := map[string]struct {
		getRes     client.Object
		getErr     error
		updateErr  error
		wantExists bool
		wantErr    bool
	}{
		"patching works": {
			getRes: &updatev1alpha1.ScalingGroup{
				TypeMeta: metav1.TypeMeta{APIVersion: "update.edgeless.systems/v1alpha1", Kind: "ScalingGroup"},
				ObjectMeta: metav1.ObjectMeta{
					Name: "resource-name",
				},
				Spec: updatev1alpha1.ScalingGroupSpec{
					NodeVersion:         mainconstants.NodeVersionResourceName,
					GroupID:             "group-id",
					AutoscalerGroupName: "autoscaling-group-name",
					Autoscaling:         true,
					Min:                 1,
					Max:                 10,
					Role:                updatev1alpha1.ControlPlaneRole,
				},
			},
			wantExists: true,
		},
		"name already set": {
			getRes: &updatev1alpha1.ScalingGroup{
				TypeMeta: metav1.TypeMeta{APIVersion: "update.edgeless.systems/v1alpha1", Kind: "ScalingGroup"},
				ObjectMeta: metav1.ObjectMeta{
					Name: "resource-name",
				},
				Spec: updatev1alpha1.ScalingGroupSpec{
					NodeVersion:         mainconstants.NodeVersionResourceName,
					GroupID:             "group-id",
					NodeGroupName:       "node-group-name",
					AutoscalerGroupName: "autoscaling-group-name",
					Autoscaling:         true,
					Min:                 1,
					Max:                 10,
					Role:                updatev1alpha1.ControlPlaneRole,
				},
			},
			wantExists: true,
		},
		"does not exist": {
			getErr:     k8sErrors.NewNotFound(schema.GroupResource{}, "resource-name"),
			wantExists: false,
		},
		"getting fails": {
			getErr:  errors.New("get failed"),
			wantErr: true,
		},
		"patching fails": {
			getRes: &updatev1alpha1.ScalingGroup{
				TypeMeta: metav1.TypeMeta{APIVersion: "update.edgeless.systems/v1alpha1", Kind: "ScalingGroup"},
				ObjectMeta: metav1.ObjectMeta{
					Name: "resource-name",
				},
				Spec: updatev1alpha1.ScalingGroupSpec{
					NodeVersion:         mainconstants.NodeVersionResourceName,
					GroupID:             "group-id",
					AutoscalerGroupName: "autoscaling-group-name",
					Autoscaling:         true,
					Min:                 1,
					Max:                 10,
					Role:                updatev1alpha1.ControlPlaneRole,
				},
			},
			updateErr: errors.New("patch failed"),
			wantErr:   true,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			assert := assert.New(t)
			require := require.New(t)

			k8sClient := &fakeK8sClient{
				getRes:    tc.getRes,
				getErr:    tc.getErr,
				updateErr: tc.updateErr,
			}
			gotExists, gotErr := patchNodeGroupName(context.Background(), k8sClient, "resource-name", "node-group-name")
			if tc.wantErr {
				assert.Error(gotErr)
				return
			}
			require.NoError(gotErr)
			assert.Equal(tc.wantExists, gotExists)
		})
	}
}

type fakeK8sClient struct {
	getRes         client.Object
	createdObjects []client.Object
	createErr      error
	listErr        error
	getErr         error
	updateErr      error
	client.Client
}

func (s *fakeK8sClient) Create(_ context.Context, obj client.Object, _ ...client.CreateOption) error {
	for _, o := range s.createdObjects {
		if obj.GetName() == o.GetName() {
			return k8sErrors.NewAlreadyExists(schema.GroupResource{}, obj.GetName())
		}
	}

	s.createdObjects = append(s.createdObjects, obj.DeepCopyObject().(client.Object))
	return s.createErr
}

func (s *fakeK8sClient) Get(_ context.Context, _ types.NamespacedName, out client.Object, _ ...client.GetOption) error {
	if s.getErr != nil {
		return s.getErr
	}
	obj := s.getRes.DeepCopyObject()
	outVal := reflect.ValueOf(out)
	objVal := reflect.ValueOf(obj)
	if !objVal.Type().AssignableTo(outVal.Type()) {
		return fmt.Errorf("fake had type %s, but %s was asked for", objVal.Type(), outVal.Type())
	}
	reflect.Indirect(outVal).Set(reflect.Indirect(objVal))
	return nil
}

func (s *fakeK8sClient) Update(_ context.Context, _ client.Object, _ ...client.UpdateOption) error {
	return s.updateErr
}

func (s *fakeK8sClient) List(_ context.Context, _ client.ObjectList, _ ...client.ListOption) error {
	return s.listErr
}

type stubScalingGroupDiscoverer struct {
	sync.RWMutex
	groups []cspapi.ScalingGroup
}

func (d *stubScalingGroupDiscoverer) ListScalingGroups(_ context.Context, _ string,
) ([]cspapi.ScalingGroup, error) {
	d.RLock()
	defer d.RUnlock()
	ret := make([]cspapi.ScalingGroup, len(d.groups))
	copy(ret, d.groups)
	return ret, nil
}

func (d *stubScalingGroupDiscoverer) set(groups []cspapi.ScalingGroup) {
	d.Lock()
	defer d.Unlock()
	d.groups = groups
}

func (d *stubScalingGroupDiscoverer) reset() {
	d.Lock()
	defer d.Unlock()
	d.groups = nil
}