// 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"
	"fmt"
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/api/operation"
	"k8s.io/apimachinery/pkg/util/validation/field"

	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/utils"
)

func TestValidateExternalAuth(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		name         string
		newObj       *coreapi.HCPOpenShiftClusterExternalAuth
		oldObj       *coreapi.HCPOpenShiftClusterExternalAuth
		op           operation.Operation
		expectErrors []utils.ExpectedError
	}{
		{
			name:         "valid external auth create",
			newObj:       createValidExternalAuth(),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name: "valid external auth with console confidential client",
			newObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"console-client",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
			),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name: "valid external auth with multiple unique clients",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"client1", "client2", "client3"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component1",
							AuthClientNamespace: "namespace1",
						},
						ClientID: "client1",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component2",
							AuthClientNamespace: "namespace2",
						},
						ClientID: "client2",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component1", // Same name but different namespace is OK
							AuthClientNamespace: "namespace3",
						},
						ClientID: "client3",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name: "valid external auth without CA certificate",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.CA = "" // CA is optional
				return obj
			}(),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name:   "missing required issuer URL",
			newObj: createMinimalExternalAuth(),
			op:     operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.url", Message: "Required value"},
				{FieldPath: "properties.issuer.audiences", Message: "Required value"},
			},
		},
		{
			name: "invalid issuer URL - not HTTPS",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "http://insecure.example.com"
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.url", Message: "must be https URL"},
				{FieldPath: "properties.issuer.audiences", Message: "Required value"},
			},
		},
		{
			name: "missing issuer audiences",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = []string{}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.audiences", Message: "Required value"},
				{FieldPath: "properties.issuer.audiences", Message: "must have at least 1 items"},
			},
		},
		{
			name: "too many issuer audiences",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = make([]string, 11)
				for i := range obj.Properties.Issuer.Audiences {
					obj.Properties.Issuer.Audiences[i] = "audience" + string(rune('0'+i))
				}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.audiences", Message: "must have at most 10 items"},
			},
		},
		{
			name: "empty issuer audience",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = []string{""}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.audiences[0]", Message: "Required value"},
			},
		},
		{
			name: "empty issuer audience among valid audiences",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = []string{"audience1", ""}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.audiences[1]", Message: "Required value"},
			},
		},
		{
			name: "multiple empty issuer audiences",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = []string{"", ""}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.audiences[0]", Message: "Required value"},
				{FieldPath: "properties.issuer.audiences[1]", Message: "Required value"},
			},
		},
		{
			name: "invalid CA certificate",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = []string{"audience1"}
				obj.Properties.Issuer.CA = "invalid-pem"
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.issuer.ca", Message: "not a valid PEM"},
			},
		},
		{
			name: "too many clients",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = []string{"audience1"}
				obj.Properties.Clients = make([]coreapi.ExternalAuthClientProfile, 21)
				for i := range obj.Properties.Clients {
					obj.Properties.Clients[i] = coreapi.ExternalAuthClientProfile{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component" + string(rune('0'+i)),
							AuthClientNamespace: "namespace" + string(rune('0'+i)),
						},
						ClientID: "audience1",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					}
				}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients", Message: "must have at most 20 items"},
			},
		},
		{
			name: "missing client component name",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createMinimalExternalAuth()
				obj.Properties.Issuer.URL = "https://valid.example.com"
				obj.Properties.Issuer.Audiences = []string{"audience1"}
				obj.Properties.Issuer.CA = validCertPEM()
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							AuthClientNamespace: "test-namespace",
						},
						ClientID: "audience1",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].component.name", Message: "Required value"},
			},
		},
		{
			name: "client component name too long",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				longName := make([]byte, 257)
				for i := range longName {
					longName[i] = 'a'
				}
				obj.Properties.Clients[0].Component.Name = string(longName)
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].component.name", Message: "may not be more than 256 bytes"},
			},
		},
		{
			name: "username claim too long",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				longClaim := make([]byte, 257)
				for i := range longClaim {
					longClaim[i] = 'a'
				}
				obj.Properties.Claim.Mappings.Username.Claim = string(longClaim)
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.mappings.username.claim", Message: "may not be more than 256 bytes"},
			},
		},
		{
			name: "group claim too long",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				longClaim := make([]byte, 257)
				for i := range longClaim {
					longClaim[i] = 'a'
				}
				obj.Properties.Claim.Mappings.Groups.Claim = string(longClaim)
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.mappings.groups.claim", Message: "may not be more than 256 bytes"},
			},
		},
		{
			name: "missing username claim",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username.Claim = ""
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.mappings.username.claim", Message: "Required value"},
			},
		},
		{
			name: "duplicate client components (unique validation)",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"client1", "client2"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "same-component",
							AuthClientNamespace: "same-namespace",
						},
						ClientID: "client1",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "same-component", // Same component name and namespace
							AuthClientNamespace: "same-namespace",
						},
						ClientID: "client2",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[1]", Message: "Duplicate value"},
			},
		},
		{
			name: "client ID not matching any issuer audience",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"audience1", "audience2"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "test-component",
							AuthClientNamespace: "test-namespace",
						},
						ClientID: "nonexistent-client", // This doesn't match any audience
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].clientId", Message: "must match an audience in issuer audiences"},
			},
		},
		{
			name: "multiple clients with mismatched audiences",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"audience1"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component1",
							AuthClientNamespace: "namespace1",
						},
						ClientID: "audience1", // This matches
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component2",
							AuthClientNamespace: "namespace2",
						},
						ClientID: "bad-audience", // This doesn't match
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[1].clientId", Message: "must match an audience in issuer audiences"},
			},
		},
		{
			name: "invalid client type",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Clients[0].Type = "InvalidType"
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].type", Message: "supported values"},
			},
		},
		{
			name: "valid client extraScopes",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Clients[0].ExtraScopes = []string{"email", "profile"}
				return obj
			}(),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name: "empty client extraScope",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Clients[0].ExtraScopes = []string{""}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].extraScopes[0]", Message: "Required value"},
			},
		},
		{
			name: "empty client extraScope among valid extraScopes",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Clients[0].ExtraScopes = []string{"email", ""}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].extraScopes[1]", Message: "Required value"},
			},
		},
		{
			name: "multiple empty client extraScopes",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Clients[0].ExtraScopes = []string{"", ""}
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].extraScopes[0]", Message: "Required value"},
				{FieldPath: "properties.clients[0].extraScopes[1]", Message: "Required value"},
			},
		},
		{
			name: "invalid external auth resource name - empty",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = ""
				obj.Name = ""
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "trackedResource.resource.id", Message: "resource name is required"},
				{FieldPath: "id", Message: "resource name is required"},
			},
		},
		{
			name: "invalid external auth resource name - special character",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = "$"
				obj.Name = "$"
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "id", Message: "must be a valid DNS RFC 1035 label"},
			},
		},
		{
			name: "invalid external auth resource name - starts with hyphen",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = "-abcde"
				obj.Name = "-abcde"
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "id", Message: "must be a valid DNS RFC 1035 label"},
			},
		},
		{
			name: "invalid external auth resource name - starts with number",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = "1externalauth"
				obj.Name = "1externalauth"
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "id", Message: "must be a valid DNS RFC 1035 label"},
			},
		},
		{
			name: "invalid external auth resource name - ends with hyphen",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = "my-auth-"
				obj.Name = "my-auth-"
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "id", Message: "must be a valid DNS RFC 1035 label"},
			},
		},
		{
			name: "invalid external auth resource name - too long",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				long := "07B4gc00vjA2C8KL3Ns4No9fi"
				obj.ID.Name = long
				obj.Name = long
				return obj
			}(),
			op: operation.Operation{Type: operation.Create},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "id", Message: "may not be more than 15 bytes"},
				{FieldPath: "id", Message: "must be a valid DNS RFC 1035 label"},
			},
		},
		{
			name: "valid external auth resource name - minimum length",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = "a"
				obj.Name = "a"
				return obj
			}(),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name: "valid external auth resource name - with hyphens",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = "my-auth-1"
				obj.Name = "my-auth-1"
				return obj
			}(),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name: "valid external auth resource name - maximum length",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.ID.Name = "myExternalAuth1" // 15 chars — max for this pattern
				obj.Name = "myExternalAuth1"
				return obj
			}(),
			op:           operation.Operation{Type: operation.Create},
			expectErrors: nil,
		},
		{
			name: "immutable provisioning state on update",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.ProvisioningState = coreapi.ProvisioningStateSucceeded
				// Set ValidationRules to empty to avoid nil pointer in discriminated union validation
				obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{}
				return obj
			}(),
			oldObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.ProvisioningState = coreapi.ProvisioningStateProvisioning
				// Set ValidationRules to empty to avoid nil pointer in discriminated union validation
				obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{}
				return obj
			}(),
			op: operation.Operation{Type: operation.Update},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.provisioningState", Message: "field is immutable"},
			},
		},
		{
			name: "update rejects changing console client type to Public",
			newObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"console-client",
					metadataapi.ExternalAuthClientTypePublic,
				),
			),
			oldObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"console-client",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
			),
			op: operation.Operation{Type: operation.Update},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].type", Message: fmt.Sprintf("must be %s when component name is %s and component namespace is %s",
					metadataapi.ExternalAuthClientTypeConfidential,
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
				)},
			},
		},
		{
			name: "update accepts keeping console client Confidential",
			newObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"console-client",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
			),
			oldObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"console-client",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
			),
			op:           operation.Operation{Type: operation.Update},
			expectErrors: nil,
		},
		{
			name: "update rejects confidential on unsupported component",
			newObj: testExternalAuthWithClients(
				[]string{"other-client"},
				testExternalAuthClientProfile(
					"component1",
					"namespace1",
					"other-client",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
			),
			oldObj: testExternalAuthWithClients(
				[]string{"other-client"},
				testExternalAuthClientProfile(
					"component1",
					"namespace1",
					"other-client",
					metadataapi.ExternalAuthClientTypePublic,
				),
			),
			op: operation.Operation{Type: operation.Update},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].type", Message: "confidential client type is not allowed for this component"},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var errs field.ErrorList
			if tt.op.Type == operation.Create {
				errs = ValidateExternalAuthCreate(ctx, tt.newObj)
			} else {
				errs = ValidateExternalAuthUpdate(ctx, tt.newObj, tt.oldObj)
			}

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

func TestValidateExternalAuthDiscriminatedUnions(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		name         string
		setupObject  func() *coreapi.HCPOpenShiftClusterExternalAuth
		expectErrors []utils.ExpectedError
	}{
		{
			name: "username prefix policy - valid None with no prefix",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username = coreapi.UsernameClaimProfile{
					Claim:        "sub",
					PrefixPolicy: metadataapi.UsernameClaimPrefixPolicyNone,
				}
				return obj
			},
			expectErrors: nil,
		},
		{
			name: "username prefix policy - valid NoPrefix with no prefix",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username = coreapi.UsernameClaimProfile{
					Claim:        "sub",
					PrefixPolicy: metadataapi.UsernameClaimPrefixPolicyNoPrefix,
				}
				return obj
			},
			expectErrors: nil,
		},
		{
			name: "username prefix policy - valid Prefix with prefix",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username = coreapi.UsernameClaimProfile{
					Claim:        "sub",
					Prefix:       "custom:",
					PrefixPolicy: metadataapi.UsernameClaimPrefixPolicyPrefix,
				}
				return obj
			},
			expectErrors: nil,
		},
		{
			name: "username prefix policy - invalid None with prefix (discriminated union violation)",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username = coreapi.UsernameClaimProfile{
					Claim:        "sub",
					Prefix:       "custom:",
					PrefixPolicy: metadataapi.UsernameClaimPrefixPolicyNone,
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.mappings.username.prefix", Message: "may only be specified when `prefixPolicy` is \"Prefix\""},
			},
		},
		{
			name: "username prefix policy - invalid Prefix without prefix (discriminated union violation)",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username = coreapi.UsernameClaimProfile{
					Claim:        "sub",
					PrefixPolicy: metadataapi.UsernameClaimPrefixPolicyPrefix,
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.mappings.username.prefix", Message: "must be specified when `prefixPolicy` is \"Prefix\""},
			},
		},
		{
			name: "token validation rule - valid RequiredClaim with claim",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{
					{
						Type: metadataapi.TokenValidationRuleTypeRequiredClaim,
						RequiredClaim: coreapi.TokenRequiredClaim{
							Claim:         "iss",
							RequiredValue: "https://valid.example.com",
						},
					},
				}
				return obj
			},
			expectErrors: nil,
		},
		{
			name: "token validation rule - invalid RequiredClaim without claim (discriminated union violation)",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{
					{
						Type: metadataapi.TokenValidationRuleTypeRequiredClaim,
					},
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.validationRules[0].requiredClaim", Message: "must be specified when `type` is \"RequiredClaim\""},
				{FieldPath: "properties.claim.validationRules[0].requiredClaim.claim", Message: "Required value"},
				{FieldPath: "properties.claim.validationRules[0].requiredClaim.requiredValue", Message: "Required value"},
			},
		},
		{
			name: "token validation rule - invalid RequiredClaim with empty claim field",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{
					{
						Type: metadataapi.TokenValidationRuleTypeRequiredClaim,
						RequiredClaim: coreapi.TokenRequiredClaim{
							Claim:         "", // Empty claim should be rejected
							RequiredValue: "https://valid.example.com",
						},
					},
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.validationRules[0].requiredClaim.claim", Message: "Required value"},
			},
		},
		{
			name: "token validation rule - invalid RequiredClaim with empty required value",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{
					{
						Type: metadataapi.TokenValidationRuleTypeRequiredClaim,
						RequiredClaim: coreapi.TokenRequiredClaim{
							Claim:         "iss",
							RequiredValue: "", // Empty required value should be rejected
						},
					},
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.validationRules[0].requiredClaim.requiredValue", Message: "Required value"},
			},
		},
		{
			name: "username prefix policy - invalid NoPrefix with non-empty prefix (discriminated union violation)",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username = coreapi.UsernameClaimProfile{
					Claim:        "sub",
					Prefix:       "should-not-be-set:", // Prefix should not be set when PrefixPolicy is NoPrefix
					PrefixPolicy: metadataapi.UsernameClaimPrefixPolicyNoPrefix,
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.mappings.username.prefix", Message: "may only be specified when `prefixPolicy` is \"Prefix\""},
			},
		},
		{
			name: "username prefix policy - invalid empty prefixPolicy",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.Mappings.Username = coreapi.UsernameClaimProfile{
					Claim:        "sub",
					PrefixPolicy: "", // Empty prefixPolicy should be rejected
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.mappings.username.prefixPolicy", Message: "supported values"},
			},
		},
		{
			name: "token validation rule - invalid empty type",
			setupObject: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{
					{
						Type: "", // Empty type should be rejected
						RequiredClaim: coreapi.TokenRequiredClaim{
							Claim:         "iss",
							RequiredValue: "https://valid.example.com",
						},
					},
				}
				return obj
			},
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.claim.validationRules[0].type", Message: "supported values"},
				{FieldPath: "properties.claim.validationRules[0].requiredClaim", Message: "may only be specified when `type` is \"RequiredClaim\""},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			obj := tt.setupObject()
			errs := ValidateExternalAuthCreate(ctx, obj)
			utils.VerifyErrorsMatch(t, tt.expectErrors, errs)
		})
	}
}

func TestValidateExternalAuthCustomValidation(t *testing.T) {
	ctx := context.Background()

	tests := []struct {
		name         string
		newObj       *coreapi.HCPOpenShiftClusterExternalAuth
		expectErrors []utils.ExpectedError
	}{
		{
			name: "client ID matches audience",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"client1", "client2"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component1",
							AuthClientNamespace: "namespace1",
						},
						ClientID: "client1",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			expectErrors: nil,
		},
		{
			name: "client ID does not match any audience",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"audience1", "audience2"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component1",
							AuthClientNamespace: "namespace1",
						},
						ClientID: "nonexistent-client",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0]", Message: "must match an audience in issuer audiences"},
			},
		},
		{
			name: "unique client identifiers",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"client1", "client2"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component1",
							AuthClientNamespace: "namespace1",
						},
						ClientID: "client1",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "component2",
							AuthClientNamespace: "namespace2",
						},
						ClientID: "client2",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			expectErrors: nil,
		},
		{
			name: "console client must be Confidential",
			newObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"console-client",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
			),
			expectErrors: nil,
		},
		{
			name: "console client rejects Public type",
			newObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"console-client",
					metadataapi.ExternalAuthClientTypePublic,
				),
			),
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].type", Message: fmt.Sprintf("must be %s when component name is %s and component namespace is %s",
					metadataapi.ExternalAuthClientTypeConfidential,
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
				)},
			},
		},
		{
			name: "confidential client rejects unsupported component",
			newObj: testExternalAuthWithClients(
				[]string{"other-client"},
				testExternalAuthClientProfile(
					"component1",
					"namespace1",
					"other-client",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
			),
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[0].type", Message: "confidential client type is not allowed for this component"},
			},
		},
		{
			name: "cli client in openshift-console namespace may be Public",
			newObj: testExternalAuthWithClients(
				[]string{"cli-client"},
				testExternalAuthClientProfile(
					"cli",
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"cli-client",
					metadataapi.ExternalAuthClientTypePublic,
				),
			),
			expectErrors: nil,
		},
		{
			name: "console name in different namespace may be Public",
			newObj: testExternalAuthWithClients(
				[]string{"console-client"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					"other-namespace",
					"console-client",
					metadataapi.ExternalAuthClientTypePublic,
				),
			),
			expectErrors: nil,
		},
		{
			name: "openshift-console namespace with different name may be Public",
			newObj: testExternalAuthWithClients(
				[]string{"oauth-client"},
				testExternalAuthClientProfile(
					"oauth",
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"oauth-client",
					metadataapi.ExternalAuthClientTypePublic,
				),
			),
			expectErrors: nil,
		},
		{
			name: "default console and cli client pair",
			newObj: testExternalAuthWithClients(
				[]string{"shared-client-id"},
				testExternalAuthClientProfile(
					coreapi.ExternalAuthConsoleClientComponentName,
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"shared-client-id",
					metadataapi.ExternalAuthClientTypeConfidential,
				),
				testExternalAuthClientProfile(
					"cli",
					coreapi.ExternalAuthConsoleClientComponentNamespace,
					"shared-client-id",
					metadataapi.ExternalAuthClientTypePublic,
				),
			),
			expectErrors: nil,
		},
		{
			name: "duplicate client identifiers",
			newObj: func() *coreapi.HCPOpenShiftClusterExternalAuth {
				obj := createValidExternalAuth()
				obj.Properties.Issuer.Audiences = []string{"client1"}
				obj.Properties.Clients = []coreapi.ExternalAuthClientProfile{
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "same-component",
							AuthClientNamespace: "same-namespace",
						},
						ClientID: "client1-a",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
					{
						Component: coreapi.ExternalAuthClientComponentProfile{
							Name:                "same-component",
							AuthClientNamespace: "same-namespace",
						},
						ClientID: "client1-b",
						Type:     metadataapi.ExternalAuthClientTypePublic,
					},
				}
				return obj
			}(),
			expectErrors: []utils.ExpectedError{
				{FieldPath: "properties.clients[1]", Message: "Duplicate value"},
				{FieldPath: "properties.clients[0].clientId", Message: "must match an audience in issuer audiences"},
				{FieldPath: "properties.clients[1].clientId", Message: `Invalid value: "client1-b": must match an audience in issuer audiences`},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			errs := ValidateExternalAuthCreate(ctx, tt.newObj)
			utils.VerifyErrorsMatch(t, tt.expectErrors, errs)
		})
	}
}

func testExternalAuthClientProfile(name, namespace, clientID string, clientType metadataapi.ExternalAuthClientType) coreapi.ExternalAuthClientProfile {
	return coreapi.ExternalAuthClientProfile{
		Component: coreapi.ExternalAuthClientComponentProfile{
			Name:                name,
			AuthClientNamespace: namespace,
		},
		ClientID: clientID,
		Type:     clientType,
	}
}

func testExternalAuthWithClients(audiences []string, clients ...coreapi.ExternalAuthClientProfile) *coreapi.HCPOpenShiftClusterExternalAuth {
	obj := createValidExternalAuth()
	obj.Properties.Issuer.Audiences = audiences
	obj.Properties.Clients = clients
	// Avoid nil pointer issues in discriminated union validation during update tests.
	obj.Properties.Claim.ValidationRules = []coreapi.TokenClaimValidationRule{}
	return obj
}

func createMinimalExternalAuth() *coreapi.HCPOpenShiftClusterExternalAuth {
	resourceID, _ := azcorearm.ParseResourceID("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/test-cluster/externalAuths/test-auth")
	obj := coreapi.NewDefaultHCPOpenShiftClusterExternalAuth(resourceID)
	obj.Properties.Claim.Mappings.Username.Claim = "sub"
	// Add required systemData fields
	createdAt := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	obj.SystemData = &coreapi.SystemData{
		CreatedBy:     "test-user",
		CreatedByType: coreapi.CreatedByTypeUser,
		CreatedAt:     &createdAt,
	}
	return obj
}

func createValidExternalAuth() *coreapi.HCPOpenShiftClusterExternalAuth {
	createdAt := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	return &coreapi.HCPOpenShiftClusterExternalAuth{
		CosmosMetadata: coreapi.CosmosMetadata{ResourceID: metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/test-cluster/externalAuths/test-auth"))},
		ProxyResource: coreapi.ProxyResource{
			Resource: coreapi.Resource{
				ID:   metadataapi.Must(azcorearm.ParseResourceID("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.RedHatOpenShift/hcpOpenShiftClusters/test-cluster/externalAuths/test-auth")),
				Name: "test-auth",
				Type: "Microsoft.RedHatOpenShift/hcpOpenShiftClusters/externalAuths",
				SystemData: &coreapi.SystemData{
					CreatedBy:     "test-user",
					CreatedByType: coreapi.CreatedByTypeUser,
					CreatedAt:     &createdAt,
				},
			},
		},
		Properties: coreapi.HCPOpenShiftClusterExternalAuthProperties{
			Issuer: coreapi.TokenIssuerProfile{
				URL:       "https://issuer.example.com",
				Audiences: []string{"audience1", "audience2"},
				CA:        validCertPEM(),
			},
			Clients: []coreapi.ExternalAuthClientProfile{
				{
					Component: coreapi.ExternalAuthClientComponentProfile{
						Name:                "test-component",
						AuthClientNamespace: "test-namespace",
					},
					ClientID: "audience1",
					Type:     metadataapi.ExternalAuthClientTypePublic,
				},
			},
			Claim: coreapi.ExternalAuthClaimProfile{
				Mappings: coreapi.TokenClaimMappingsProfile{
					Username: coreapi.UsernameClaimProfile{
						Claim:        "sub",
						PrefixPolicy: metadataapi.UsernameClaimPrefixPolicyNone,
					},
					Groups: &coreapi.GroupClaimProfile{
						Claim: "groups",
					},
				},
				ValidationRules: []coreapi.TokenClaimValidationRule{
					{
						Type: metadataapi.TokenValidationRuleTypeRequiredClaim,
						RequiredClaim: coreapi.TokenRequiredClaim{
							Claim:         "iss",
							RequiredValue: "https://issuer.example.com",
						},
					},
				},
			},
		},
	}
}

func validCertPEM() string {
	return `-----BEGIN CERTIFICATE-----
MIICMzCCAZygAwIBAgIJALiPnVsvq8dsMA0GCSqGSIb3DQEBBQUAMFMxCzAJBgNV
BAYTAlVTMQwwCgYDVQQIEwNmb28xDDAKBgNVBAcTA2ZvbzEMMAoGA1UEChMDZm9v
MQwwCgYDVQQLEwNmb28xDDAKBgNVBAMTA2ZvbzAeFw0xMzAzMTkxNTQwMTlaFw0x
ODAzMTgxNTQwMTlaMFMxCzAJBgNVBAYTAlVTMQwwCgYDVQQIEwNmb28xDDAKBgNV
BAcTA2ZvbzEMMAoGA1UEChMDZm9vMQwwCgYDVQQLEwNmb28xDDAKBgNVBAMTA2Zv
bzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzdGfxi9CNbMf1UUcvDQh7MYB
OveIHyc0E0KIbhjK5FkCBU4CiZrbfHagaW7ZEcN0tt3EvpbOMxxc/ZQU2WN/s/wP
xph0pSfsfFsTKM4RhTWD2v4fgk+xZiKd1p0+L4hTtpwnEw0uXRVd0ki6muwV5y/P
+5FHUeldq+pgTcgzuK8CAwEAAaMPMA0wCwYDVR0PBAQDAgLkMA0GCSqGSIb3DQEB
BQUAA4GBAJiDAAtY0mQQeuxWdzLRzXmjvdSuL9GoyT3BF/jSnpxz5/58dba8pWen
v3pj4P3w5DoOso0rzkZy2jEsEitlVM2mLSbQpMM+MUVQCQoiG6W9xuCFuxSrwPIS
pAqEAuV4DNoxQKKWmhVv+J0ptMWD25Pnpxeq5sXzghfJnslJlQND
-----END CERTIFICATE-----`
}
