// Copyright 2025 Microsoft Corporation
//
// 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 e2e

import (
	"context"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"

	hcpsdk20240610preview "github.com/Azure/ARO-HCP/test/sdk/resourcemanager/redhatopenshifthcp/armredhatopenshifthcp"
	"github.com/Azure/ARO-HCP/test/util/framework"
	"github.com/Azure/ARO-HCP/test/util/labels"
)

var _ = Describe("Customer", func() {
	BeforeEach(func() {
		// do nothing. per test initialization usually ages better than shared.
	})

	It("should be able to create a cluster with default autoscaling and a nodepool with autoscaling enabled up to replica limits",
		labels.RequireNothing,
		labels.Medium,
		labels.Positive,
		labels.AroRpApiCompatible,
		labels.MIContainers(1),
		func(ctx context.Context) {
			const (
				customerClusterName = "np-autoscale-cluster"

				azNodePoolName         = "autoscale-az"
				azAutoscalingMin int32 = 3
				azAutoscalingMax int32 = 500

				noAZNodePoolName         = "autoscale-noaz"
				noAZAutoscalingMin int32 = 3
				noAZAutoscalingMax int32 = 200
			)
			tc := framework.NewTestContext()

			if tc.UsePooledIdentities() {
				err := tc.AssignIdentityContainers(ctx, 1, framework.IdentityContainerAssignmentRetryInterval)
				Expect(err).NotTo(HaveOccurred(), "failed to assign pooled identity containers")
			}

			By("resolving the default worker VM size and its usable availability zones")
			workerVMSize, err := tc.SelectVMSize(ctx, framework.DefaultWorkerVMSizeSelector())
			Expect(err).NotTo(HaveOccurred(), "failed to resolve the default worker VM size")
			availableZones, err := tc.AvailableZones(ctx, workerVMSize)
			Expect(err).NotTo(HaveOccurred(), "failed to resolve usable availability zones for worker VM size %s", workerVMSize)
			hasAZ := len(availableZones) > 0
			var availabilityZone string
			if hasAZ {
				availabilityZone = availableZones[0]
			}

			By("creating a resource group")
			resourceGroup, err := tc.NewResourceGroup(ctx, "np-autoscaling", tc.Location())
			Expect(err).NotTo(HaveOccurred(), "failed to create resource group for nodepool autoscaling test")

			By("creating cluster parameters")
			clusterParams := framework.NewDefaultClusterParams20240610()
			clusterParams.ClusterName = customerClusterName
			managedResourceGroupName := framework.SuffixName(*resourceGroup.Name, "-managed", 64)
			clusterParams.ManagedResourceGroupName = managedResourceGroupName

			By("creating customer resources")
			clusterParams, err = tc.CreateClusterCustomerResources20240610(ctx,
				resourceGroup,
				clusterParams,
				map[string]any{},
				TestArtifactsFS,
				framework.RBACScopeResourceGroup,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create cluster customer resources")

			By("creating the HCP cluster")
			err = tc.CreateHCPClusterFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				clusterParams,
				framework.ClusterCreationTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create HCP cluster %s", customerClusterName)

			By("verifying the cluster has default autoscaling parameters")
			clusterResp, err := framework.GetHCPCluster20240610(ctx,
				tc.Get20240610ClientFactoryOrDie(ctx).NewHcpOpenShiftClustersClient(),
				*resourceGroup.Name,
				customerClusterName)
			Expect(err).NotTo(HaveOccurred(), "failed to get HCP cluster %s", customerClusterName)

			Expect(clusterResp.Properties).NotTo(BeNil(), "cluster response Properties was nil")
			Expect(clusterResp.Properties.Autoscaling).NotTo(BeNil(), "Expected cluster to have default autoscaling configuration")
			Expect(clusterResp.Properties.Autoscaling.MaxNodeProvisionTimeSeconds).To(Equal(to.Ptr(int32(900))), "Expected default MaxNodeProvisionTimeSeconds to be 900 seconds")
			Expect(clusterResp.Properties.Autoscaling.MaxPodGracePeriodSeconds).To(Equal(to.Ptr(int32(600))), "Expected default MaxPodGracePeriodSeconds to be 600 seconds")
			Expect(clusterResp.Properties.Autoscaling.PodPriorityThreshold).To(Equal(to.Ptr(int32(-10))), "Expected default PodPriorityThreshold to be -10")
			Expect(clusterResp.Properties.Autoscaling.MaxNodesTotal).To(BeNil(), "Expected MaxNodesTotal to be nil when not explicitly set")

			By("getting admin credentials for the cluster")
			adminRESTConfig, err := tc.GetAdminRESTConfigForHCPCluster20240610(
				ctx,
				tc.Get20240610ClientFactoryOrDie(ctx).NewHcpOpenShiftClustersClient(),
				*resourceGroup.Name,
				customerClusterName,
				framework.GetAdminRESTConfigTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to get admin REST config for cluster %s", customerClusterName)

			nodePoolsClient := tc.Get20240610ClientFactoryOrDie(ctx).NewNodePoolsClient()
			kubeClient, err := kubernetes.NewForConfig(adminRESTConfig)
			Expect(err).NotTo(HaveOccurred(), "failed to create Kubernetes client from admin REST config")

			if !hasAZ {
				By("skipping AZ nodepool creation: no usable availability zones for worker VM size " +
					workerVMSize + " in " + tc.Location())
			} else {
				By("creating the AZ nodepool with 500 max replicas in availability zone " + availabilityZone)
				azNodePoolParams := framework.NewDefaultNodePoolParams20240610()
				azNodePoolParams.ClusterName = customerClusterName
				azNodePoolParams.NodePoolName = azNodePoolName
				azNodePoolParams.AutoScaling = &framework.NodePoolAutoScalingParams{
					Min: azAutoscalingMin,
					Max: azAutoscalingMax,
				}
				azNodePoolParams.AvailabilityZone = availabilityZone

				err = tc.CreateNodePoolFromParam20240610(ctx,
					GinkgoLogr,
					*resourceGroup.Name,
					managedResourceGroupName,
					customerClusterName,
					azNodePoolParams,
					framework.NodePoolCreationTimeout,
				)
				Expect(err).NotTo(HaveOccurred(), "failed to create AZ nodepool %s with autoscaling", azNodePoolName)

				By("verifying the AZ nodepool has the correct autoscaling configuration")
				azNodePoolResp, err := framework.GetNodePool20240610(ctx,
					nodePoolsClient,
					*resourceGroup.Name,
					customerClusterName,
					azNodePoolName)
				Expect(err).NotTo(HaveOccurred(), "failed to get AZ nodepool %s", azNodePoolName)
				Expect(azNodePoolResp.Properties).NotTo(BeNil(), "nodepool response Properties was nil")
				Expect(azNodePoolResp.Properties.AutoScaling).NotTo(BeNil(), "Expected nodepool to have autoscaling configuration")
				Expect(azNodePoolResp.Properties.AutoScaling.Min).To(Equal(to.Ptr(azAutoscalingMin)), "expected AZ nodepool autoscaling min to equal %d", azAutoscalingMin)
				Expect(azNodePoolResp.Properties.AutoScaling.Max).To(Equal(to.Ptr(azAutoscalingMax)), "expected AZ nodepool autoscaling max to equal %d", azAutoscalingMax)

				By("verifying node count after AZ nodepool creation")
				nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
				Expect(err).NotTo(HaveOccurred(), "failed to list nodes after AZ nodepool creation")
				Expect(len(nodes.Items)).To(BeNumerically(">=", int(azAutoscalingMin)), "expected at least %d nodes after AZ nodepool creation", azAutoscalingMin)

				By("updating the AZ nodepool max replicas from 500 to 4 before creating the next nodepool")
				_, err = framework.UpdateNodePoolAndWait20240610(ctx,
					nodePoolsClient,
					*resourceGroup.Name,
					customerClusterName,
					azNodePoolName,
					hcpsdk20240610preview.NodePoolUpdate{
						Properties: &hcpsdk20240610preview.NodePoolPropertiesUpdate{
							AutoScaling: &hcpsdk20240610preview.NodePoolAutoScaling{
								Min: to.Ptr(azAutoscalingMin),
								Max: to.Ptr(int32(4)),
							},
						},
					},
					framework.NodePoolScalingTimeout,
				)
				Expect(err).NotTo(HaveOccurred(), "failed to update AZ nodepool %s max replicas to 4", azNodePoolName)

				By("verifying the AZ nodepool max replicas was updated to 4")
				azNodePoolUpdatedResp, err := framework.GetNodePool20240610(ctx,
					nodePoolsClient,
					*resourceGroup.Name,
					customerClusterName,
					azNodePoolName)
				Expect(err).NotTo(HaveOccurred(), "failed to get updated AZ nodepool %s", azNodePoolName)
				Expect(azNodePoolUpdatedResp.Properties).NotTo(BeNil(), "nodepool response Properties was nil")
				Expect(azNodePoolUpdatedResp.Properties.AutoScaling).NotTo(BeNil(), "Expected nodepool to have autoscaling configuration")
				Expect(azNodePoolUpdatedResp.Properties.AutoScaling.Max).To(Equal(to.Ptr(int32(4))), "Expected AZ nodepool max replicas to be updated to 4")
			}

			By("creating the no-AZ nodepool with 200 max replicas")
			noAZNodePoolParams := framework.NewDefaultNodePoolParams20240610()
			noAZNodePoolParams.ClusterName = customerClusterName
			noAZNodePoolParams.NodePoolName = noAZNodePoolName
			noAZNodePoolParams.AutoScaling = &framework.NodePoolAutoScalingParams{
				Min: noAZAutoscalingMin,
				Max: noAZAutoscalingMax,
			}

			err = tc.CreateNodePoolFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				managedResourceGroupName,
				customerClusterName,
				noAZNodePoolParams,
				framework.NodePoolCreationTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create no-AZ nodepool %s with autoscaling", noAZNodePoolName)

			By("verifying the no-AZ nodepool has the correct autoscaling configuration")
			noAZNodePoolResp, err := framework.GetNodePool20240610(ctx,
				nodePoolsClient,
				*resourceGroup.Name,
				customerClusterName,
				noAZNodePoolName)
			Expect(err).NotTo(HaveOccurred(), "failed to get no-AZ nodepool %s", noAZNodePoolName)
			Expect(noAZNodePoolResp.Properties).NotTo(BeNil(), "nodepool response Properties was nil")
			Expect(noAZNodePoolResp.Properties.AutoScaling).NotTo(BeNil(), "Expected nodepool to have autoscaling configuration")
			Expect(noAZNodePoolResp.Properties.AutoScaling.Min).To(Equal(to.Ptr(noAZAutoscalingMin)), "expected no-AZ nodepool autoscaling min to equal %d", noAZAutoscalingMin)
			Expect(noAZNodePoolResp.Properties.AutoScaling.Max).To(Equal(to.Ptr(noAZAutoscalingMax)), "expected no-AZ nodepool autoscaling max to equal %d", noAZAutoscalingMax)

			By("verifying node count after no-AZ nodepool creation")
			nodes, err := kubeClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
			Expect(err).NotTo(HaveOccurred(), "failed to list nodes after no-AZ nodepool creation")
			minExpected := int(noAZAutoscalingMin)
			if hasAZ {
				minExpected += int(azAutoscalingMin)
			}
			Expect(len(nodes.Items)).To(BeNumerically(">=", minExpected), "expected at least %d nodes after no-AZ nodepool creation", minExpected)

		})

	It("should respect cluster-wide node limits with nodepool autoscaling",
		labels.RequireNothing,
		labels.Medium,
		labels.Negative,
		labels.AroRpApiCompatible,
		labels.MIContainers(1),
		func(ctx context.Context) {
			const (
				customerClusterName  = "node-limit-cluster"
				customerNodePoolName = "exceeding-nodepool"
			)
			tc := framework.NewTestContext()

			if tc.UsePooledIdentities() {
				err := tc.AssignIdentityContainers(ctx, 1, framework.IdentityContainerAssignmentRetryInterval)
				Expect(err).NotTo(HaveOccurred(), "failed to assign pooled identity containers")
			}

			By("creating a resource group")
			resourceGroup, err := tc.NewResourceGroup(ctx, "node-limit-test", tc.Location())
			Expect(err).NotTo(HaveOccurred(), "failed to create resource group for node-limit test")

			By("creating cluster with MaxNodesTotal limit")
			clusterParams := framework.NewDefaultClusterParams20240610()
			clusterParams.ClusterName = customerClusterName
			managedResourceGroupName := framework.SuffixName(*resourceGroup.Name, "-managed", 64)
			clusterParams.ManagedResourceGroupName = managedResourceGroupName
			clusterParams.Autoscaling = &hcpsdk20240610preview.ClusterAutoscalingProfile{
				MaxNodesTotal: to.Ptr(int32(3)), // Set low limit
			}

			clusterParams, err = tc.CreateClusterCustomerResources20240610(ctx,
				resourceGroup,
				clusterParams,
				map[string]any{},
				TestArtifactsFS,
				framework.RBACScopeResourceGroup,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create cluster customer resources for node-limit test")

			err = tc.CreateHCPClusterFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				clusterParams,
				framework.ClusterCreationTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create HCP cluster %s with MaxNodesTotal limit", customerClusterName)

			By("attempting to create nodepool with Min > MaxNodesTotal")
			nodePoolParams := framework.NewDefaultNodePoolParams20240610()
			nodePoolParams.ClusterName = customerClusterName
			nodePoolParams.NodePoolName = customerNodePoolName
			nodePoolParams.AutoScaling = &framework.NodePoolAutoScalingParams{
				Min: 5, // Greater than cluster MaxNodesTotal (3)
				Max: 10,
			}

			// Should fail quickly during validation
			err = tc.CreateNodePoolFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				managedResourceGroupName,
				customerClusterName,
				nodePoolParams,
				5*time.Minute,
			)
			Expect(err).To(HaveOccurred(), "Expected nodepool creation to fail when Min exceeds cluster MaxNodesTotal")
		})
})
