// 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 validation

import (
	"context"
	"strings"
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/api/operation"
	"k8s.io/utils/ptr"

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

	"github.com/Azure/ARO-HCP/internal/api/coreapi"
	"github.com/Azure/ARO-HCP/internal/api/metadataapi"
	"github.com/Azure/ARO-HCP/internal/apitesting/coreapitesting"
	"github.com/Azure/ARO-HCP/internal/utils"
)

// Comprehensive tests for ValidateNodePoolCreate
func TestValidateNodePoolCreate(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		name         string
		nodePool     *coreapi.HCPOpenShiftClusterNodePool
		expectErrors []utils.ExpectedError
	}{
		{
			name:         "valid nodepool - create",
			nodePool:     createValidNodePool(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with autoscaling - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 1,
					Max: 5,
				}
				np.Properties.Replicas = 0 // Should be 0 when autoscaling is enabled
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with autoscaling min=0 - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 0,
					Max: 5,
				}
				np.Properties.Replicas = 0
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with labels - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"environment":           "test",
					"app.kubernetes.io/env": "production",
					"team":                  "platform",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with taints - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "dedicated",
						Value:  "gpu",
						Effect: metadataapi.EffectNoSchedule,
					},
					{
						Key:    "environment",
						Value:  "test",
						Effect: metadataapi.EffectPreferNoSchedule,
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with encryption set ID - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.EncryptionSetID = metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-des"))
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with custom OS disk size - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](64)
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with different storage account type - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskStorageAccountType = metadataapi.DiskStorageAccountTypeStandardSSD_LRS
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with availability zone - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "1"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with node drain timeout - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](60)
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with node drain timeout zero - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](0)
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool with node drain timeout at maximum - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](10080)
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "negative node drain timeout - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](-1)
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 0", FieldPath: "properties.nodeDrainTimeoutMinutes"},
			},
		},
		{
			name: "node drain timeout too large - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](10081)
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 10080", FieldPath: "properties.nodeDrainTimeoutMinutes"},
			},
		},
		{
			name: "valid nodepool with version ID - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.8"
				np.Properties.Version.ChannelGroup = "fast"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "invalid version ID - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "invalid-version"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "No Major.Minor.Patch elements found", FieldPath: "properties.version.id"},
				{Message: "Short version cannot contain PreRelease/Build meta data", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "version ID lower than minimum version without experimental flag - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.0"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be at least 4.20.8", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "missing channel group - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ChannelGroup = ""
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "properties.version.channelGroup"},
				{Message: "Unsupported value", FieldPath: "properties.version.channelGroup"},
			},
		},
		{
			name: "invalid channel group - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ChannelGroup = "invalid-cg"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.version.channelGroup"},
			},
		},
		{
			name: "candidate channel group rejected without feature flag - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.21.0-rc.1"
				np.Properties.Version.ChannelGroup = "candidate"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.version.channelGroup"},
			},
		},
		{
			name: "nightly channel group rejected without feature flag - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.21.0-0.nightly-2024-01-15-123456"
				np.Properties.Version.ChannelGroup = "nightly"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.version.channelGroup"},
			},
		},
		{
			name: "missing version ID when channel group is not stable - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = ""
				np.Properties.Version.ChannelGroup = "fast"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "missing version ID when channel group is stable - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = ""
				np.Properties.Version.ChannelGroup = "stable"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "nil subnet ID - valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				// SubnetID is optional for nodepools
				np.Properties.Platform.SubnetID = nil
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "wrong subnet resource type - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.SubnetID = metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet"))
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "resource ID must reference an instance of type", FieldPath: "properties.platform.subnetId"},
			},
		},
		{
			name: "missing VM size - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.VMSize = ""
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "properties.platform.vmSize"},
				{Message: "Unsupported value", FieldPath: "properties.platform.vmSize"},
			},
		},
		{
			name: "invalid VM size - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.VMSize = "invalid_vm_size"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.platform.vmSize"},
			},
		},
		{
			name: "OS disk size too small - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](63)
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 64", FieldPath: "properties.platform.osDisk.sizeGiB"},
			},
		},
		{
			name: "managed OS disk size at maximum - valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskType = metadataapi.OsDiskTypeManaged
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](MaxManagedOSDiskSizeGiB)
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "managed OS disk size over maximum - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskType = metadataapi.OsDiskTypeManaged
				np.Properties.Platform.OSDisk.DiskStorageAccountType = metadataapi.DiskStorageAccountTypeStandardSSD_LRS
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](MaxManagedOSDiskSizeGiB + 1)
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 4095", FieldPath: "properties.platform.osDisk.sizeGiB"},
			},
		},
		{
			name: "ephemeral OS disk size at maximum - valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskType = metadataapi.OsDiskTypeEphemeral
				np.Properties.AutoRepair = true
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](MaxEphemeralOSDiskSizeGiB)
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "ephemeral OS disk size over maximum - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskType = metadataapi.OsDiskTypeEphemeral
				np.Properties.AutoRepair = true
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](MaxEphemeralOSDiskSizeGiB + 1)
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 2040", FieldPath: "properties.platform.osDisk.sizeGiB"},
			},
		},
		{
			name: "invalid disk storage account type - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskStorageAccountType = "InvalidType"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.platform.osDisk.diskStorageAccountType"},
			},
		},
		{
			name: "invalid disk type - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskType = "InvalidType"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.platform.osDisk.diskType"},
			},
		},
		{
			name: "ephemeral disk requires autoRepair true - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskType = metadataapi.OsDiskTypeEphemeral
				np.Properties.AutoRepair = false
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be true when platform.osDisk.diskType is Ephemeral", FieldPath: "properties.autoRepair"},
			},
		},
		{
			name: "ephemeral disk with autoRepair true - valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.DiskType = metadataapi.OsDiskTypeEphemeral
				np.Properties.AutoRepair = true
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "wrong encryption set resource type - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.EncryptionSetID = metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet"))
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "resource ID must reference an instance of type", FieldPath: "properties.platform.osDisk.encryptionSetId"},
			},
		},
		{
			name: "encryption set name at maximum length - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.EncryptionSetID = metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/" + strings.Repeat("a", MaxDiskEncryptionSetNameLen)))
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "encryption set name too long - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.EncryptionSetID = metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/" + strings.Repeat("a", MaxDiskEncryptionSetNameLen+1)))
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Too long", FieldPath: "properties.platform.osDisk.encryptionSetId"},
			},
		},
		{
			name: "encryption set name with invalid characters - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.EncryptionSetID = metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-des"))
				np.Properties.Platform.OSDisk.EncryptionSetID.Name = "test.des"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must contain only alphanumeric characters, underscores, and hyphens", FieldPath: "properties.platform.osDisk.encryptionSetId"},
			},
		},
		{
			name: "replicas at maximum limit (200) - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = MaxNodePoolNodes
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "negative replicas - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = -1
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 0", FieldPath: "properties.replicas"},
			},
		},
		{
			name: "replicas exceeds maximum limit (201) - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = MaxNodePoolNodes + 1
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 200", FieldPath: "properties.replicas"},
			},
		},
		{
			name: "non-zero replicas with autoscaling - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 3
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 1,
					Max: 5,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "cannot specify replicas when autoScaling is enabled", FieldPath: "properties.replicas"},
			},
		},
		{
			name: "autoscaling max at maximum limit (200) - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 1,
					Max: MaxNodePoolNodes,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "autoscaling min too small - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: -1,
					Max: 5,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 0", FieldPath: "properties.autoScaling.min"},
			},
		},
		{
			// When Min is invalid (too large), Max is valid, we should only get Min error (not Max >= Min error).
			name: "autoscaling min exceeds limit but max is valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: MaxNodePoolNodes + 1,
					Max: 100,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 200", FieldPath: "properties.autoScaling.min"},
			},
		},
		{
			// When Min is invalid (too small), Max is valid, we should only get Min error (not Max >= Min error).
			name: "autoscaling min negative but max is valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: -1,
					Max: 100,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 0", FieldPath: "properties.autoScaling.min"},
			},
		},
		{
			name: "autoscaling max less than min - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 5,
					Max: 3,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 5", FieldPath: "properties.autoScaling.max"},
			},
		},
		{
			// Note: Both Min and Max validate max=200 (though logically redundant) for explicit error messages on both fields.
			// When Min is invalid, we skip the Min<=Max check to avoid misleading "Max must be >= invalid_min" errors.
			name: "autoscaling min and max both exceed limit with min > max - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: MaxNodePoolNodes + 2,
					Max: MaxNodePoolNodes + 1,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 200", FieldPath: "properties.autoScaling.min"},
				{Message: "must be less than or equal to 200", FieldPath: "properties.autoScaling.max"},
			},
		},
		{
			name: "invalid label key - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"invalid key with spaces": "value",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.labels"},
			},
		},
		{
			name: "invalid label value - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"valid-key": "invalid value with spaces and special chars!@#",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "a valid label must be an empty string or consist of alphanumeric characters", FieldPath: "properties.labels[valid-key]"},
			},
		},
		{
			name: "restricted node role master label key - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyNodeRoleMaster: "true",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label key is not allowed on node pools", FieldPath: "properties.labels[node-role.kubernetes.io/master]"},
			},
		},
		{
			name: "restricted node role worker label key - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyNodeRoleWorker: "true",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label key is not allowed on node pools", FieldPath: "properties.labels[node-role.kubernetes.io/worker]"},
			},
		},
		{
			name: "restricted machine role label value master - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineRole: "master",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label value is not allowed for this label key", FieldPath: "properties.labels[machine.openshift.io/cluster-api-machine-role]"},
			},
		},
		{
			name: "restricted machine role label value infra - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineRole: "infra",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label value is not allowed for this label key", FieldPath: "properties.labels[machine.openshift.io/cluster-api-machine-role]"},
			},
		},
		{
			name: "restricted machine type label value master - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineType: "master",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label value is not allowed for this label key", FieldPath: "properties.labels[machine.openshift.io/cluster-api-machine-type]"},
			},
		},
		{
			name: "restricted machine type label value infra - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineType: "infra",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label value is not allowed for this label key", FieldPath: "properties.labels[machine.openshift.io/cluster-api-machine-type]"},
			},
		},
		{
			name: "valid machine role label value worker - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineRole: "worker",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "empty label key - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"": "value",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "name part must be non-empty", FieldPath: "properties.labels"},
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.labels"},
			},
		},
		{
			name: "taint missing key - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Value:  "test",
						Effect: metadataapi.EffectNoSchedule,
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "properties.taints[0].key"},
				{Message: "name part must be non-empty", FieldPath: "properties.taints[0].key"},
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.taints[0].key"},
			},
		},
		{
			name: "taint invalid key - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "invalid key with spaces",
						Value:  "test",
						Effect: metadataapi.EffectNoSchedule,
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.taints[0].key"},
			},
		},
		{
			name: "taint invalid value - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "dedicated",
						Value:  "invalid value with spaces and special chars!@#",
						Effect: metadataapi.EffectNoSchedule,
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "a valid label must be an empty string or consist of alphanumeric characters", FieldPath: "properties.taints[0].value"},
			},
		},
		{
			name: "taint invalid effect - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "dedicated",
						Value:  "gpu",
						Effect: "InvalidEffect",
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.taints[0].effect"},
			},
		},
		{
			name: "multiple validation errors - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "invalid-version"
				np.Properties.Platform.VMSize = ""
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](0)
				np.Properties.Replicas = -1
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "No Major.Minor.Patch elements found", FieldPath: "properties.version.id"},
				{Message: "Short version cannot contain PreRelease/Build meta data", FieldPath: "properties.version.id"},
				{Message: "Required value", FieldPath: "properties.platform.vmSize"},
				{Message: "Unsupported value", FieldPath: "properties.platform.vmSize"},
				{Message: "must be greater than or equal to 64", FieldPath: "properties.platform.osDisk.sizeGiB"},
				{Message: "must be greater than or equal to 0", FieldPath: "properties.replicas"},
			},
		},
		{
			name: "multiple taint errors - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Value:  "test",
						Effect: metadataapi.EffectNoSchedule,
					},
					{
						Key:    "invalid key with spaces",
						Value:  "invalid value with spaces",
						Effect: "InvalidEffect",
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "properties.taints[0].key"},
				{Message: "name part must be non-empty", FieldPath: "properties.taints[0].key"},
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.taints[0].key"},
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.taints[1].key"},
				{Message: "a valid label must be an empty string or consist of alphanumeric characters", FieldPath: "properties.taints[1].value"},
				{Message: "Unsupported value", FieldPath: "properties.taints[1].effect"},
			},
		},
		{
			name: "multiple label errors - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"":                        "value1",
					"invalid key with spaces": "value2",
					"valid-key":               "invalid value with spaces!@#",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "name part must be non-empty", FieldPath: "properties.labels"},
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.labels"},
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.labels"},
				{Message: "a valid label must be an empty string or consist of alphanumeric characters", FieldPath: "properties.labels[valid-key]"},
			},
		},
		{
			name: "valid empty optional fields - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.SubnetID = nil
				np.Properties.Platform.AvailabilityZone = ""
				np.Properties.Platform.OSDisk.EncryptionSetID = nil
				np.Properties.Labels = nil
				np.Properties.Taints = nil
				np.Properties.NodeDrainTimeoutMinutes = nil
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "missing location - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Location = ""
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "trackedResource.location"},
			},
		},
		{
			name: "replicas exceeds 200 with availability zone set - valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "1"
				np.Properties.Replicas = 250
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "autoscaling both min and max exceed 200 with availability zone set - valid - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "1"
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 300,
					Max: 1000,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		// Node pool resource naming validation tests (covering middleware_validatestatic_test.go patterns)
		{
			name: "invalid nodepool resource name - special character",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "$"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be equal to", FieldPath: "trackedResource.resource.name"},
				{Message: "must be a valid DNS RFC 1035 label", FieldPath: "id"},
			},
		},
		{
			name: "invalid nodepool resource name - starts with hyphen",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "-abcde"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be equal to", FieldPath: "trackedResource.resource.name"},
				{Message: "must be a valid DNS RFC 1035 label", FieldPath: "id"},
			},
		},
		{
			name: "invalid nodepool resource name - starts with number",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "1nodepool"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be equal to", FieldPath: "trackedResource.resource.name"},
				{Message: "must be a valid DNS RFC 1035 label", FieldPath: "id"},
			},
		},
		{
			name: "invalid nodepool resource name - ends with hyphen",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "my-pool-"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be equal to", FieldPath: "trackedResource.resource.name"},
				{Message: "must be a valid DNS RFC 1035 label", FieldPath: "id"},
			},
		},
		{
			name: "invalid nodepool resource name - too long",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "07B4gc00vjA2C8KL3Ns4No9fi" // Too long for node pool name
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be equal to", FieldPath: "trackedResource.resource.name"},
				{Message: "Too long", FieldPath: "id"},
				{Message: "must be a valid DNS RFC 1035 label", FieldPath: "id"},
			},
		},
		{
			name: "invalid nodepool resource name - empty",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = ""
				np.Name = ""
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "resource name is required", FieldPath: "trackedResource.resource.id"},
				{Message: "resource name is required", FieldPath: "id"},
			},
		},
		{
			name: "valid nodepool resource name - minimum length",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "a"
				np.Name = "a"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool resource name - with hyphens",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "my-pool-1"
				np.Name = "my-pool-1"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool resource name - maximum length",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.ID.Name = "myNodePool12345" // 15 chars total - at max length
				np.Name = "myNodePool12345"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			op := operation.Operation{Type: operation.Create}
			errs := ValidateNodePool(ctx, op, tt.nodePool, nil)

			utils.VerifyErrorsMatch(t, tt.expectErrors, errs)
		})
	}
}

// Comprehensive tests for ValidateNodePool update
func TestValidateNodePoolUpdate(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		name         string
		newNodePool  *coreapi.HCPOpenShiftClusterNodePool
		oldNodePool  *coreapi.HCPOpenShiftClusterNodePool
		expectErrors []utils.ExpectedError
	}{
		{
			name:         "valid nodepool update - no changes",
			newNodePool:  createValidNodePool(),
			oldNodePool:  createValidNodePool(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool update - replicas change",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 5
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 3
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool update - autoscaling change",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 2,
					Max: 10,
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 1,
					Max: 5,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool update - labels change",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"environment": "production",
					"team":        "platform",
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"environment": "test",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool update - taints change",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "dedicated",
						Value:  "gpu",
						Effect: metadataapi.EffectNoSchedule,
					},
					{
						Key:    "environment",
						Value:  "test",
						Effect: metadataapi.EffectPreferNoSchedule,
					},
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "dedicated",
						Value:  "cpu",
						Effect: metadataapi.EffectNoSchedule,
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "valid nodepool update - node drain timeout change",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](120)
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](60)
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "negative node drain timeout - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](-1)
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 0", FieldPath: "properties.nodeDrainTimeoutMinutes"},
			},
		},
		{
			name: "node drain timeout too large - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.NodeDrainTimeoutMinutes = ptr.To[int32](10081)
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 10080", FieldPath: "properties.nodeDrainTimeoutMinutes"},
			},
		},
		{
			name: "valid nodepool update - version change",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.9"
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.8"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "immutable provisioning state - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.ProvisioningState = coreapi.ProvisioningStateProvisioning
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.ProvisioningState = coreapi.ProvisioningStateSucceeded
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "field is immutable", FieldPath: "properties.provisioningState"},
			},
		},
		{
			name: "immutable platform profile - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.VMSize = "Standard_D4s_v3"
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.VMSize = "Standard_D8s_v3"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "field is immutable", FieldPath: "properties.platform"},
				{Message: "field is immutable", FieldPath: "properties.platform.vmSize"},
			},
		},
		{
			name: "immutable OS disk size - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](64)
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.OSDisk.SizeGiB = ptr.To[int32](128)
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "field is immutable", FieldPath: "properties.platform"},
				{Message: "field is immutable", FieldPath: "properties.platform.osDisk"},
			},
		},
		{
			name: "immutable auto repair - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.AutoRepair = false
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.AutoRepair = true
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "field is immutable", FieldPath: "properties.autoRepair"},
			},
		},
		{
			name: "immutable location - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Location = "westus2"
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Location = "eastus"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "field is immutable", FieldPath: "trackedResource.location"},
			},
		},
		{
			name: "invalid new field value on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = -1
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 0", FieldPath: "properties.replicas"},
			},
		},
		{
			name: "scale up to maximum limit - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = MaxNodePoolNodes
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "replicas exceeds maximum limit - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = MaxNodePoolNodes + 1
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 200", FieldPath: "properties.replicas"},
			},
		},
		{
			name: "autoscaling min and max to maximum limit - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: MaxNodePoolNodes,
					Max: MaxNodePoolNodes,
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "non-zero replicas with autoscaling - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 3
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 1,
					Max: 5,
				}
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "cannot specify replicas when autoScaling is enabled", FieldPath: "properties.replicas"},
			},
		},
		{
			name: "invalid autoscaling on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 5,
					Max: 3,
				}
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be greater than or equal to 5", FieldPath: "properties.autoScaling.max"},
			},
		},
		{
			name: "autoscaling min and max exceeds maximum limit with min > max - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: MaxNodePoolNodes + 2,
					Max: MaxNodePoolNodes + 1,
				}
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be less than or equal to 200", FieldPath: "properties.autoScaling.min"},
				{Message: "must be less than or equal to 200", FieldPath: "properties.autoScaling.max"},
			},
		},
		{
			name: "invalid labels on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"invalid key with spaces": "value",
				}
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.labels"},
			},
		},
		{
			name: "restricted node role master label key on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyNodeRoleMaster: "true",
				}
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "label key is not allowed on node pools", FieldPath: "properties.labels[node-role.kubernetes.io/master]"},
			},
		},
		{
			name: "restricted machine role label value on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineRole: "master",
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineRole: "worker",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label value is not allowed for this label key", FieldPath: "properties.labels[machine.openshift.io/cluster-api-machine-role]"},
			},
		},
		{
			name: "restricted label key unchanged on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyNodeRoleMaster: "true",
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyNodeRoleMaster: "true",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label key is not allowed on node pools", FieldPath: "properties.labels[node-role.kubernetes.io/master]"},
			},
		},
		{
			name: "restricted machine role label value unchanged on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineRole: "master",
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					nodePoolK8sLabelKeyMachineRole: "master",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "label value is not allowed for this label key", FieldPath: "properties.labels[machine.openshift.io/cluster-api-machine-role]"},
			},
		},
		{
			name: "invalid taints on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "invalid key with spaces",
						Value:  "test",
						Effect: metadataapi.EffectNoSchedule,
					},
				}
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "name part must consist of alphanumeric characters", FieldPath: "properties.taints[0].key"},
			},
		},
		{
			name: "invalid version on update - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "invalid-version"
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "No Major.Minor.Patch elements found", FieldPath: "properties.version.id"},
				{Message: "Short version cannot contain PreRelease/Build meta data", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "version ID lower than minimum version - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.0"
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "must be at least 4.20.8", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "update with same version skips validation - allows unrelated changes",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.8" // Old X.Y format
				np.Properties.Replicas = 5          // Unrelated change
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.8" // Same version
				np.Properties.Replicas = 3
				return np
			}(),
			expectErrors: []utils.ExpectedError{}, // No error because version validation is skipped
		},
		// Version.id is required since 20251223preview, but legacy nodepools may not have it.
		// On update with oldObj.ID empty: version.id is NOT required (legacy migration support).
		// On update with oldObj.ID set: version.id is required (cannot be cleared).
		{
			name: "update: version.id can be empty when old nodepool had no version.id (legacy migration)",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = ""
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = ""
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "update: version.id cannot be cleared when old nodepool had version.id",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = ""
				return np
			}(),
			oldNodePool: createValidNodePool(),
			expectErrors: []utils.ExpectedError{
				{Message: "Required value", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "multiple immutable field changes - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.ProvisioningState = coreapi.ProvisioningStateProvisioning
				np.Properties.Platform.VMSize = "Standard_D4s_v3"
				np.Properties.AutoRepair = false
				np.Location = "westus2"
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.ProvisioningState = coreapi.ProvisioningStateSucceeded
				np.Properties.Platform.VMSize = "Standard_D8s_v3"
				np.Properties.AutoRepair = true
				np.Location = "eastus"
				return np
			}(),
			expectErrors: []utils.ExpectedError{
				{Message: "field is immutable", FieldPath: "trackedResource.location"},
				{Message: "field is immutable", FieldPath: "properties.provisioningState"},
				{Message: "field is immutable", FieldPath: "properties.platform"},
				{Message: "field is immutable", FieldPath: "properties.platform.vmSize"},
				{Message: "field is immutable", FieldPath: "properties.autoRepair"},
			},
		},
		{
			name: "enable autoscaling - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 1,
					Max: 5,
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 3
				np.Properties.AutoScaling = nil
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "disable autoscaling - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 3
				np.Properties.AutoScaling = nil
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 1,
					Max: 5,
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "clear labels - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = nil
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Labels = map[string]string{
					"environment": "test",
					"team":        "platform",
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "clear taints - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = nil
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Taints = []coreapi.Taint{
					{
						Key:    "dedicated",
						Value:  "gpu",
						Effect: metadataapi.EffectNoSchedule,
					},
				}
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "replicas exceeds 200 with availability zone set - valid - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "1"
				np.Properties.Replicas = 250
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "1"
				np.Properties.Replicas = 3
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "autoscaling min exceeds 200 with availability zone set - valid - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "2"
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 250,
					Max: 300,
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "2"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "autoscaling both min and max exceed 200 with availability zone set - valid - update",
			newNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "1"
				np.Properties.Replicas = 0
				np.Properties.AutoScaling = &coreapi.NodePoolAutoScaling{
					Min: 300,
					Max: 1000,
				}
				return np
			}(),
			oldNodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Platform.AvailabilityZone = "1"
				return np
			}(),
			expectErrors: []utils.ExpectedError{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			op := operation.Operation{Type: operation.Update}
			errs := ValidateNodePool(ctx, op, tt.newNodePool, tt.oldNodePool)

			utils.VerifyErrorsMatch(t, tt.expectErrors, errs)
		})
	}
}

// testNodePoolFeatureOptions creates validation options from feature names,
// matching production behavior by normalizing to lowercase.
func testNodePoolFeatureOptions(names ...string) []string {
	features := make([]coreapi.Feature, len(names))
	for i, name := range names {
		features[i] = coreapi.Feature{
			Name:  ptr.To(name),
			State: ptr.To("Registered"),
		}
	}
	return AFECsToValidationOptions(features)
}

// Helper function to create a valid nodepool for testing
func createValidNodePool() *coreapi.HCPOpenShiftClusterNodePool {
	nodePool := coreapi.NewDefaultHCPOpenShiftClusterNodePool(
		metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/test-cluster/nodePools/test-nodepool")),
		coreapitesting.TestLocation,
	)

	// Set required fields that are not in the default
	nodePool.Location = "eastus" // Required for TrackedResource validation
	nodePool.Properties.Version.ID = "4.20.8"
	nodePool.Properties.Version.ChannelGroup = "stable"
	nodePool.Properties.Platform.SubnetID = metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"))
	nodePool.Properties.Platform.VMSize = "Standard_D8s_v3"
	nodePool.Properties.Replicas = 3

	// Add required systemData fields
	createdAt := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	nodePool.SystemData = &coreapi.SystemData{
		CreatedBy:     "test-user",
		CreatedByType: coreapi.CreatedByTypeUser,
		CreatedAt:     &createdAt,
	}

	return nodePool
}

// TestValidateNodePoolVersionWithFeatureFlags tests version validation with feature flags
func TestValidateNodePoolVersionWithFeatureFlags(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		name         string
		nodePool     *coreapi.HCPOpenShiftClusterNodePool
		opOptions    []string
		expectErrors []utils.ExpectedError
	}{
		{
			name:         "valid node pool with X.Y.Z version - no feature flag",
			nodePool:     createValidNodePool(),
			opOptions:    nil,
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "X.Y format rejected for stable channel ",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20"
				np.Properties.Version.ChannelGroup = "stable"
				return np
			}(),
			opOptions: testNodePoolFeatureOptions(metadataapi.FeatureExperimentalReleaseFeatures),
			expectErrors: []utils.ExpectedError{
				{Message: "No Major.Minor.Patch elements found", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "X.Y.Z.A format rejected for stable channel ",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.14.12"
				np.Properties.Version.ChannelGroup = "stable"
				return np
			}(),
			opOptions: testNodePoolFeatureOptions(metadataapi.FeatureExperimentalReleaseFeatures),
			expectErrors: []utils.ExpectedError{
				{Message: "Invalid character(s) found in patch number", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "prerelease version allowed for non-stable channel with experimental flag",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.21.0-rc.1"
				np.Properties.Version.ChannelGroup = "candidate"
				return np
			}(),
			opOptions:    testNodePoolFeatureOptions(metadataapi.FeatureExperimentalReleaseFeatures),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "nightly version allowed with experimental flag",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.21.0-0.nightly-2024-01-15-123456"
				np.Properties.Version.ChannelGroup = "nightly"
				return np
			}(),
			opOptions:    testNodePoolFeatureOptions(metadataapi.FeatureExperimentalReleaseFeatures),
			expectErrors: []utils.ExpectedError{},
		},
		{
			name: "invalid channel group rejected even with experimental flag",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ChannelGroup = "invalid-cg"
				return np
			}(),
			opOptions: testNodePoolFeatureOptions(metadataapi.FeatureExperimentalReleaseFeatures),
			expectErrors: []utils.ExpectedError{
				{Message: "Unsupported value", FieldPath: "properties.version.channelGroup"},
			},
		},
		{
			name: "malformed version rejected",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "invalid-version"
				return np
			}(),
			opOptions: nil,
			expectErrors: []utils.ExpectedError{
				{Message: "No Major.Minor.Patch elements found", FieldPath: "properties.version.id"},
				{Message: "Short version cannot contain PreRelease/Build meta data", FieldPath: "properties.version.id"},
			},
		},
		{
			name: "version ID lower than minimum version with experimental flag - create",
			nodePool: func() *coreapi.HCPOpenShiftClusterNodePool {
				np := createValidNodePool()
				np.Properties.Version.ID = "4.20.0"
				return np
			}(),
			opOptions:    testNodePoolFeatureOptions(metadataapi.FeatureExperimentalReleaseFeatures),
			expectErrors: []utils.ExpectedError{},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			op := operation.Operation{Type: operation.Create, Options: tt.opOptions}
			errs := ValidateNodePool(ctx, op, tt.nodePool, nil)

			utils.VerifyErrorsMatch(t, tt.expectErrors, errs)
		})
	}
}
