// 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"
	"errors"
	"fmt"
	"regexp"
	"strings"
	"time"

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

	"github.com/blang/semver/v4"

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

	"github.com/Azure/ARO-HCP/internal/api/metadataapi"
	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"
	"github.com/Azure/ARO-HCP/test/util/verifiers"
)

var _ = Describe("Customer", func() {

	It("should not be able to perform various invalid operations on cluster resources",
		labels.RequireNothing,
		labels.Negative,
		labels.Medium,
		labels.AroRpApiCompatible,
		labels.MIContainers(1),
		func(ctx context.Context) {
			const (
				customerNetworkSecurityGroupName = "customer-nsg-name"
				customerVnetName                 = "customer-vnet-name"
				customerVnetSubnetName           = "customer-vnet-subnet"
				customerClusterName              = "negative-tests-cluster"
			)
			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, "rg-negative-tests", tc.Location())
			Expect(err).NotTo(HaveOccurred(), "failed to create resource group rg-negative-tests")

			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{
					"customerNsgName":        customerNetworkSecurityGroupName,
					"customerVnetName":       customerVnetName,
					"customerVnetSubnetName": customerVnetSubnetName,
				},
				TestArtifactsFS,
				framework.RBACScopeResource,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create cluster customer resources")

			nodePoolClient := tc.Get20240610ClientFactoryOrDie(ctx).NewNodePoolsClient()
			var errs []error

			// TEST CASE: ARO-22570
			By("attempting to list clusters in a non-existent resource group")
			clusterClient := tc.Get20240610ClientFactoryOrDie(ctx).NewHcpOpenShiftClustersClient()
			nonExistentRgName := "non-existent-rg"
			clusterPager := clusterClient.NewListByResourceGroupPager(nonExistentRgName, nil)
			_, err = clusterPager.NextPage(ctx)
			checkExpectedError(&errs, "cluster listing in non-existent resource group", err, "resource group not found")

			// TEST CASE: ARO-22571
			By("attempting to list node pools in a non-existent resource group")
			nodePoolPager := nodePoolClient.NewListByParentPager(nonExistentRgName, clusterParams.ClusterName, nil)
			_, err = nodePoolPager.NextPage(ctx)
			checkExpectedError(&errs, "node pool listing in non-existent resource group", err, "resource group not found")

			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("getting credentials")
			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)

			By("ensuring the cluster is viable")
			err = verifiers.VerifyHCPCluster(ctx, adminRESTConfig)
			Expect(err).NotTo(HaveOccurred(), "cluster %s is not viable", customerClusterName)

			baseNodePoolParams := framework.NewDefaultNodePoolParams20240610()
			baseNodePoolParams.ClusterName = clusterParams.ClusterName

			// TEST CASE: ARO-22576 test that verifies error is triggered when the configured nodepool replicas exceeds
			// the maximum number of node pool replicas when an availability zone is not specified when creating the node pool,
			// which means when the node pool is created as part of an availability set. There is a limit of virtual machines per availability set:
			// https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#azure-virtual-machines-limits---azure-resource-manager
			nodePoolParamsInvalidQuota := baseNodePoolParams
			nodePoolParamsInvalidQuota.Replicas = int32(201)
			nodePoolParamsInvalidQuota.NodePoolName = "np-inv-quota"

			By("attempting to create a node pool with invalid quota")
			err = tc.CreateNodePoolFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				managedResourceGroupName,
				clusterParams.ClusterName,
				nodePoolParamsInvalidQuota,
				5*time.Minute,
			)
			checkExpectedError(&errs, "node pool creation with invalid quota", err, "invalid value must be less than or equal to")

			nodePoolParams := baseNodePoolParams
			nodePoolParams.NodePoolName = "np-1"
			By("creating a nodepool")
			err = tc.CreateNodePoolFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				managedResourceGroupName,
				clusterParams.ClusterName,
				nodePoolParams,
				framework.NodePoolCreationTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create node pool %s", nodePoolParams.NodePoolName)

			// TEST CASE: ARO-23182
			By("attempting to update nodepool version to higher than cluster version")
			clusterVersion := metadataapi.Must(semver.ParseTolerant(clusterParams.OpenshiftVersionId))
			invalidNodePoolVersion := fmt.Sprintf("%d.%d.0", clusterVersion.Major, clusterVersion.Minor+1) // +1 y-stream, z set to 0

			npForVersionUpdate, err := nodePoolClient.Get(ctx, *resourceGroup.Name, clusterParams.ClusterName, nodePoolParams.NodePoolName, nil)
			Expect(err).ToNot(HaveOccurred(), "failed to GET node pool %s for version update test", nodePoolParams.NodePoolName)

			npForVersionUpdate.Properties.Version.ID = &invalidNodePoolVersion
			_, err = nodePoolClient.BeginCreateOrUpdate(ctx, *resourceGroup.Name, clusterParams.ClusterName, nodePoolParams.NodePoolName, npForVersionUpdate.NodePool, nil)
			checkExpectedError(&errs, "invalid node pool version update", err, "node pool version cannot exceed control plane")

			// TEST CASE: ARO-24877
			By("attempting to update immutable platform profile fields")
			nodePool, err := nodePoolClient.Get(ctx, *resourceGroup.Name, clusterParams.ClusterName, nodePoolParams.NodePoolName, nil)
			if err != nil {
				errs = append(errs, fmt.Errorf("failed to get nodepool: %w", err))
			} else if nodePool.Properties == nil {
				errs = append(errs, fmt.Errorf("nodepool properties are nil"))
			} else if nodePool.Properties.Platform == nil {
				errs = append(errs, fmt.Errorf("nodepool platform properties are nil"))
			} else {
				originalVMSize := ""
				if nodePool.Properties.Platform.VMSize != nil {
					originalVMSize = *nodePool.Properties.Platform.VMSize
				}
				nodePool.Properties.Platform.VMSize = to.Ptr("Standard_D16s_v3")
				nodePool.Properties.Platform.AvailabilityZone = to.Ptr("2")
				if nodePool.Properties.Platform.OSDisk != nil {
					nodePool.Properties.Platform.OSDisk.SizeGiB = to.Ptr[int32](256)
					nodePool.Properties.Platform.OSDisk.DiskStorageAccountType = to.Ptr(hcpsdk20240610preview.DiskStorageAccountTypePremiumLRS)
				}

				_, err = nodePoolClient.BeginCreateOrUpdate(ctx, *resourceGroup.Name, clusterParams.ClusterName, nodePoolParams.NodePoolName, nodePool.NodePool, nil)
				checkExpectedError(&errs, "updating immutable fields", err, "forbidden field immutable")
				if err != nil {
					updatedNodePool, getErr := nodePoolClient.Get(ctx, *resourceGroup.Name, clusterParams.ClusterName, nodePoolParams.NodePoolName, nil)
					if getErr != nil {
						errs = append(errs, fmt.Errorf("failed to verify nodepool after failed update: %w", getErr))
					} else if updatedNodePool.Properties == nil || updatedNodePool.Properties.Platform == nil {
						errs = append(errs, fmt.Errorf("updated nodepool properties or platform are nil"))
					} else {
						platform := updatedNodePool.Properties.Platform

						if platform.VMSize != nil && *platform.VMSize != originalVMSize {
							errs = append(errs, fmt.Errorf("vmSize was modified despite immutable error"))
						}

						if platform.AvailabilityZone != nil {
							errs = append(errs, fmt.Errorf("availabilityZone was modified despite immutable error"))
						}

						if platform.OSDisk != nil {
							if platform.OSDisk.SizeGiB != nil && *platform.OSDisk.SizeGiB != 64 {
								errs = append(errs, fmt.Errorf("osDisk.sizeGiB was modified despite immutable error"))
							}

							if platform.OSDisk.DiskStorageAccountType != nil && *platform.OSDisk.DiskStorageAccountType != hcpsdk20240610preview.DiskStorageAccountTypeStandardSSDLRS {
								errs = append(errs, fmt.Errorf("osDisk.diskStorageAccountType was modified despite immutable error"))
							}
						}
					}
				}
			}
			// TEST CASE: https://issues.redhat.com/browse/ARO-22240 to be implemented here

			if len(errs) > 0 {
				Expect(errors.Join(errs...)).NotTo(HaveOccurred(), "one or more negative test cases failed")
			}
		})

})

func checkExpectedError(errs *[]error, operation string, err error, expectedErrorKeywords string) {
	GinkgoLogr.Error(err, operation)

	if err == nil {
		*errs = append(*errs, fmt.Errorf("%s: expected error but none occurred", operation))
		return
	}

	pattern := ".*" + strings.ReplaceAll(strings.ToLower(expectedErrorKeywords), " ", ".*") + ".*"
	lowerError := strings.ToLower(err.Error())

	if matched, _ := regexp.MatchString(pattern, lowerError); !matched {
		*errs = append(*errs, fmt.Errorf("%s: expected error containing keywords '%s', got: %s", operation, expectedErrorKeywords, err.Error()))
	}
}
