// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package supervisorconfig

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"sort"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	coretesting "k8s.io/client-go/testing"
	clocktesting "k8s.io/utils/clock/testing"
	"k8s.io/utils/ptr"

	configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
	idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
	pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
	pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
	"go.pinniped.dev/internal/celtransformer"
	"go.pinniped.dev/internal/controllerlib"
	"go.pinniped.dev/internal/federationdomain/federationdomainproviders"
	"go.pinniped.dev/internal/here"
	"go.pinniped.dev/internal/idtransform"
	"go.pinniped.dev/internal/testutil"
)

func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) {
	t.Parallel()

	federationDomainInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).Config().V1alpha1().FederationDomains()
	oidcIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().OIDCIdentityProviders()
	ldapIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().LDAPIdentityProviders()
	adIdentityProviderInformer := pinnipedinformers.NewSharedInformerFactoryWithOptions(nil, 0).IDP().V1alpha1().ActiveDirectoryIdentityProviders()

	tests := []struct {
		name       string
		obj        metav1.Object
		informer   controllerlib.InformerGetter
		wantAdd    bool
		wantUpdate bool
		wantDelete bool
	}{
		{
			name:       "any FederationDomain changes",
			obj:        &configv1alpha1.FederationDomain{},
			informer:   federationDomainInformer,
			wantAdd:    true,
			wantUpdate: true,
			wantDelete: true,
		},
		{
			name:       "any OIDCIdentityProvider adds or deletes, but updates are ignored",
			obj:        &idpv1alpha1.OIDCIdentityProvider{},
			informer:   oidcIdentityProviderInformer,
			wantAdd:    true,
			wantUpdate: false,
			wantDelete: true,
		},
		{
			name:       "any LDAPIdentityProvider adds or deletes, but updates are ignored",
			obj:        &idpv1alpha1.LDAPIdentityProvider{},
			informer:   ldapIdentityProviderInformer,
			wantAdd:    true,
			wantUpdate: false,
			wantDelete: true,
		},
		{
			name:       "any ActiveDirectoryIdentityProvider adds or deletes, but updates are ignored",
			obj:        &idpv1alpha1.ActiveDirectoryIdentityProvider{},
			informer:   adIdentityProviderInformer,
			wantAdd:    true,
			wantUpdate: false,
			wantDelete: true,
		},
	}
	for _, test := range tests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()

			withInformer := testutil.NewObservableWithInformerOption()

			NewFederationDomainWatcherController(
				nil,
				"",
				nil,
				nil,
				federationDomainInformer,
				oidcIdentityProviderInformer,
				ldapIdentityProviderInformer,
				adIdentityProviderInformer,
				withInformer.WithInformer, // make it possible to observe the behavior of the Filters
			)

			unrelatedObj := corev1.Secret{}
			filter := withInformer.GetFilterForInformer(test.informer)
			require.Equal(t, test.wantAdd, filter.Add(test.obj))
			require.Equal(t, test.wantUpdate, filter.Update(&unrelatedObj, test.obj))
			require.Equal(t, test.wantUpdate, filter.Update(test.obj, &unrelatedObj))
			require.Equal(t, test.wantDelete, filter.Delete(test.obj))
		})
	}
}

type fakeFederationDomainsSetter struct {
	SetFederationDomainsWasCalled bool
	FederationDomainsReceived     []*federationdomainproviders.FederationDomainIssuer
}

func (f *fakeFederationDomainsSetter) SetFederationDomains(federationDomains ...*federationdomainproviders.FederationDomainIssuer) {
	f.SetFederationDomainsWasCalled = true
	f.FederationDomainsReceived = federationDomains
}

var federationDomainGVR = schema.GroupVersionResource{
	Group:    configv1alpha1.SchemeGroupVersion.Group,
	Version:  configv1alpha1.SchemeGroupVersion.Version,
	Resource: "federationdomains",
}

func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
	t.Parallel()

	const namespace = "some-namespace"
	const apiGroupSuffix = "custom.suffix.pinniped.dev"
	const apiGroupSupervisor = "idp.supervisor." + apiGroupSuffix

	frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
	frozenMetav1Now := metav1.NewTime(frozenNow)

	oidcIdentityProvider := &idpv1alpha1.OIDCIdentityProvider{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "some-oidc-idp",
			Namespace: namespace,
			UID:       "some-oidc-uid",
		},
	}

	ldapIdentityProvider := &idpv1alpha1.LDAPIdentityProvider{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "some-ldap-idp",
			Namespace: namespace,
			UID:       "some-ldap-uid",
		},
	}

	adIdentityProvider := &idpv1alpha1.ActiveDirectoryIdentityProvider{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "some-ad-idp",
			Namespace: namespace,
			UID:       "some-ad-uid",
		},
	}

	federationDomain1 := &configv1alpha1.FederationDomain{
		ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
		Spec:       configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"},
	}

	federationDomain2 := &configv1alpha1.FederationDomain{
		ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123},
		Spec:       configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"},
	}

	invalidIssuerURLFederationDomain := &configv1alpha1.FederationDomain{
		ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123},
		Spec:       configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"},
	}

	federationDomainIssuerWithIDPs := func(t *testing.T, fedDomainIssuer string, fdIDPs []*federationdomainproviders.FederationDomainIdentityProvider) *federationdomainproviders.FederationDomainIssuer {
		fdIssuer, err := federationdomainproviders.NewFederationDomainIssuer(fedDomainIssuer, fdIDPs)
		require.NoError(t, err)
		return fdIssuer
	}

	federationDomainIssuerWithDefaultIDP := func(t *testing.T, fedDomainIssuer string, idpObjectMeta metav1.ObjectMeta) *federationdomainproviders.FederationDomainIssuer {
		fdIDP := &federationdomainproviders.FederationDomainIdentityProvider{
			DisplayName: idpObjectMeta.Name,
			UID:         idpObjectMeta.UID,
			Transforms:  idtransform.NewTransformationPipeline(),
		}
		fdIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(fedDomainIssuer, fdIDP)
		require.NoError(t, err)
		return fdIssuer
	}

	happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "Ready",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message: fmt.Sprintf("the FederationDomain is ready and its endpoints are available: "+
				"the discovery endpoint is %s/.well-known/openid-configuration", issuer),
		}
	}

	sadReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "Ready",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "NotReady",
			Message:            "the FederationDomain is not ready: see other conditions for details",
		}
	}

	happyIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IssuerIsUnique",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "spec.issuer is unique among all FederationDomains",
		}
	}

	unknownIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IssuerIsUnique",
			Status:             "Unknown",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "UnableToValidate",
			Message:            "unable to check if spec.issuer is unique among all FederationDomains because URL cannot be parsed",
		}
	}

	sadIssuerIsUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IssuerIsUnique",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "DuplicateIssuer",
			Message:            "multiple FederationDomains have the same spec.issuer URL: these URLs must be unique (can use different hosts or paths)",
		}
	}

	happyOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "OneTLSSecretPerIssuerHostname",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL",
		}
	}

	unknownOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "OneTLSSecretPerIssuerHostname",
			Status:             "Unknown",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "UnableToValidate",
			Message:            "unable to check if all FederationDomains are using the same TLS secret when using the same hostname in the spec.issuer URL because URL cannot be parsed",
		}
	}

	sadOneTLSSecretPerIssuerHostnameCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "OneTLSSecretPerIssuerHostname",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "DifferentSecretRefsFound",
			Message:            "when different FederationDomains are using the same hostname in the spec.issuer URL then they must also use the same TLS secretRef: different secretRefs found",
		}
	}

	happyIssuerURLValidCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IssuerURLValid",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "spec.issuer is a valid URL",
		}
	}

	sadIssuerURLValidConditionCannotHaveQuery := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IssuerURLValid",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "InvalidIssuerURL",
			Message:            "issuer must not have query",
		}
	}

	sadIssuerURLValidConditionCannotParse := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IssuerURLValid",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "InvalidIssuerURL",
			Message:            `could not parse issuer as URL: parse ":/host//path": missing protocol scheme`,
		}
	}

	happyIdentityProvidersFoundConditionLegacyConfigurationSuccess := func(idpName string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersFound",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "LegacyConfigurationSuccess",
			Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+
				"identity provider resource has been found: using %q as "+
				"identity provider: please explicitly list identity providers in .spec.identityProviders "+
				"(this legacy configuration mode may be removed in a future version of Pinniped)", idpName),
		}
	}

	happyIdentityProvidersFoundConditionSuccess := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersFound",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "the resources specified by .spec.identityProviders[].objectRef were found",
		}
	}

	sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersFound",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "LegacyConfigurationIdentityProviderNotFound",
			Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider " +
				"resources have been found: please create an identity provider resource",
		}
	}

	sadIdentityProvidersFoundConditionIdentityProviderNotSpecified := func(idpCRsCount int, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersFound",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "IdentityProviderNotSpecified",
			Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef "+
				"and %d identity provider resources have been found: "+
				"please update .spec.identityProviders to specify which identity providers "+
				"this federation domain should use", idpCRsCount),
		}
	}

	sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound := func(errorMessages string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersFound",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "IdentityProvidersObjectRefsNotFound",
			Message:            errorMessages,
		}
	}

	happyDisplayNamesUniqueCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersDisplayNamesUnique",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "the names specified by .spec.identityProviders[].displayName are unique",
		}
	}

	sadDisplayNamesUniqueCondition := func(duplicateNames string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersDisplayNamesUnique",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "DuplicateDisplayNames",
			Message:            fmt.Sprintf("the names specified by .spec.identityProviders[].displayName contain duplicates: %s", duplicateNames),
		}
	}

	happyTransformationExpressionsCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "TransformsExpressionsValid",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "the expressions specified by .spec.identityProviders[].transforms.expressions[] are valid",
		}
	}

	sadTransformationExpressionsCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "TransformsExpressionsValid",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "InvalidTransformsExpressions",
			Message:            errorMessages,
		}
	}

	happyTransformationExamplesCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "TransformsExamplesPassed",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "the examples specified by .spec.identityProviders[].transforms.examples[] had no errors",
		}
	}

	sadTransformationExamplesCondition := func(errorMessages string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "TransformsExamplesPassed",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "TransformsExamplesFailed",
			Message:            errorMessages,
		}
	}

	happyAPIGroupSuffixCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersObjectRefAPIGroupSuffixValid",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "the API groups specified by .spec.identityProviders[].objectRef.apiGroup are recognized",
		}
	}

	sadAPIGroupSuffixCondition := func(badApiGroups string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersObjectRefAPIGroupSuffixValid",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "APIGroupUnrecognized",
			Message: fmt.Sprintf("some API groups specified by .spec.identityProviders[].objectRef.apiGroup "+
				"are not recognized (should be \"idp.supervisor.%s\"): %s", apiGroupSuffix, badApiGroups),
		}
	}

	happyKindCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersObjectRefKindValid",
			Status:             "True",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "Success",
			Message:            "the kinds specified by .spec.identityProviders[].objectRef.kind are recognized",
		}
	}

	sadKindCondition := func(badKinds string, time metav1.Time, observedGeneration int64) metav1.Condition {
		return metav1.Condition{
			Type:               "IdentityProvidersObjectRefKindValid",
			Status:             "False",
			ObservedGeneration: observedGeneration,
			LastTransitionTime: time,
			Reason:             "KindUnrecognized",
			Message: fmt.Sprintf(`some kinds specified by .spec.identityProviders[].objectRef.kind are `+
				`not recognized (should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): %s`, badKinds),
		}
	}

	sortConditionsByType := func(c []metav1.Condition) []metav1.Condition {
		cp := make([]metav1.Condition, len(c))
		copy(cp, c)
		sort.SliceStable(cp, func(i, j int) bool {
			return cp[i].Type < cp[j].Type
		})
		return cp
	}

	replaceConditions := func(conditions []metav1.Condition, sadConditions []metav1.Condition) []metav1.Condition {
		for _, sadReplaceCondition := range sadConditions {
			for origIndex, origCondition := range conditions {
				if origCondition.Type == sadReplaceCondition.Type {
					conditions[origIndex] = sadReplaceCondition
					break
				}
			}
		}
		return conditions
	}

	allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []metav1.Condition {
		return sortConditionsByType([]metav1.Condition{
			happyTransformationExamplesCondition(frozenMetav1Now, 123),
			happyTransformationExpressionsCondition(frozenMetav1Now, 123),
			happyKindCondition(frozenMetav1Now, 123),
			happyAPIGroupSuffixCondition(frozenMetav1Now, 123),
			happyDisplayNamesUniqueCondition(frozenMetav1Now, 123),
			happyIdentityProvidersFoundConditionSuccess(frozenMetav1Now, 123),
			happyIssuerIsUniqueCondition(frozenMetav1Now, 123),
			happyIssuerURLValidCondition(frozenMetav1Now, 123),
			happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
			happyReadyCondition(issuer, frozenMetav1Now, 123),
		})
	}

	allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []metav1.Condition {
		return replaceConditions(
			allHappyConditionsSuccess(issuer, time, observedGeneration),
			[]metav1.Condition{
				happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration),
			},
		)
	}

	invalidIssuerURL := ":/host//path"
	_, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid.
	require.Error(t, err)

	tests := []struct {
		name              string
		inputObjects      []runtime.Object
		configClient      func(*pinnipedfake.Clientset)
		wantErr           string
		wantStatusUpdates []*configv1alpha1.FederationDomain
		wantFDIssuers     []*federationdomainproviders.FederationDomainIssuer
	}{
		{
			name:          "when there are no FederationDomains, no update actions happen and the list of FederationDomainIssuers is set to the empty list",
			inputObjects:  []runtime.Object{},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
		},
		{
			name: "legacy config: when no identity provider is specified on federation domains, but exactly one OIDC identity " +
				"provider resource exists on cluster, the controller will set a default IDP on each federation domain " +
				"matching the only identity provider found",
			inputObjects: []runtime.Object{
				federationDomain1,
				federationDomain2,
				oidcIdentityProvider,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
				federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(federationDomain1,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "legacy config: when no identity provider is specified on federation domains, but exactly one LDAP identity " +
				"provider resource exists on cluster, the controller will set a default IDP on each federation domain " +
				"matching the only identity provider found",
			inputObjects: []runtime.Object{
				federationDomain1,
				federationDomain2,
				ldapIdentityProvider,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, ldapIdentityProvider.ObjectMeta),
				federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, ldapIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(federationDomain1,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, ldapIdentityProvider.Name, frozenMetav1Now, 123),
				),
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, ldapIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "legacy config: when no identity provider is specified on federation domains, but exactly one AD identity " +
				"provider resource exists on cluster, the controller will set a default IDP on each federation domain " +
				"matching the only identity provider found",
			inputObjects: []runtime.Object{
				federationDomain1,
				federationDomain2,
				adIdentityProvider,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, adIdentityProvider.ObjectMeta),
				federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, adIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(federationDomain1,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, adIdentityProvider.Name, frozenMetav1Now, 123),
				),
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, adIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " +
				"the out-of-date FederationDomain",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: federationDomain1.Name, Namespace: federationDomain1.Namespace, Generation: 123},
					Spec:       configv1alpha1.FederationDomainSpec{Issuer: federationDomain1.Spec.Issuer},
					Status: configv1alpha1.FederationDomainStatus{
						Phase:      configv1alpha1.FederationDomainPhaseReady,
						Conditions: allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
					},
				},
				federationDomain2,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
				federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				// only one update, because the other FederationDomain already had the right status
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "when the status of the FederationDomains is based on an old generation, it is updated",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: federationDomain1.Name, Namespace: federationDomain1.Namespace, Generation: 123},
					Spec:       configv1alpha1.FederationDomainSpec{Issuer: federationDomain1.Spec.Issuer},
					Status: configv1alpha1.FederationDomainStatus{
						Phase: configv1alpha1.FederationDomainPhaseReady,
						Conditions: allHappyConditionsLegacyConfigurationSuccess(
							federationDomain1.Spec.Issuer,
							oidcIdentityProvider.Name,
							frozenMetav1Now,
							2, // this is an older generation
						),
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				// only one update, because the other FederationDomain already had the right status
				expectedFederationDomainStatusUpdate(federationDomain1,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(
						federationDomain1.Spec.Issuer,
						oidcIdentityProvider.Name,
						frozenMetav1Now,
						123, // all conditions are updated to the new observed generation
					),
				),
			},
		},
		{
			name: "when there are two valid FederationDomains, but updating one fails, the status on the FederationDomain will not change",
			inputObjects: []runtime.Object{
				federationDomain1,
				federationDomain2,
				oidcIdentityProvider,
			},
			configClient: func(client *pinnipedfake.Clientset) {
				client.PrependReactor(
					"update",
					"federationdomains",
					func(action coretesting.Action) (bool, runtime.Object, error) {
						fd := action.(coretesting.UpdateAction).GetObject().(*configv1alpha1.FederationDomain)
						if fd.Name == federationDomain1.Name {
							return true, nil, errors.New("some update error")
						}
						return false, nil, nil
					},
				)
			},
			wantErr: "could not update status: some update error",
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
				federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(federationDomain1,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "when there are both valid and invalid FederationDomains, the status will be correctly set on each " +
				"FederationDomain individually",
			inputObjects: []runtime.Object{
				invalidIssuerURLFederationDomain,
				federationDomain2,
				oidcIdentityProvider,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				// only the valid FederationDomain
				federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain,
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "when there are both valid and invalid FederationDomains, but updating the invalid one fails, the " +
				"existing status will be unchanged",
			inputObjects: []runtime.Object{
				invalidIssuerURLFederationDomain,
				federationDomain2,
				oidcIdentityProvider,
			},
			configClient: func(client *pinnipedfake.Clientset) {
				client.PrependReactor(
					"update",
					"federationdomains",
					func(action coretesting.Action) (bool, runtime.Object, error) {
						fd := action.(coretesting.UpdateAction).GetObject().(*configv1alpha1.FederationDomain)
						if fd.Name == invalidIssuerURLFederationDomain.Name {
							return true, nil, errors.New("some update error")
						}
						return false, nil, nil
					},
				)
			},
			wantErr: "could not update status: some update error",
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				// only the valid FederationDomain
				federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain,
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "when there are FederationDomains with duplicate issuer strings these particular FederationDomains " +
				"will report error on IssuerUnique conditions",
			inputObjects: []runtime.Object{
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123},
					Spec:       configv1alpha1.FederationDomainSpec{Issuer: "https://iSSueR-duPlicAte.cOm/a"},
				},
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123},
					Spec:       configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/a"},
				},
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123},
					Spec:       configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive)
				},
				oidcIdentityProvider,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte.cOm/a", oidcIdentityProvider.Name, frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/a", oidcIdentityProvider.Name, frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "when there are FederationDomains with the same issuer DNS hostname using different secretNames these " +
				"particular FederationDomains will report errors on OneTLSSecretPerIssuerHostname conditions",
			inputObjects: []runtime.Object{
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://iSSueR-duPlicAte-adDress.cOm/path1",
						TLS:    &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"},
					},
				},
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						// Validation treats these as the same DNS hostname even though they have different port numbers,
						// because SNI information on the incoming requests is not going to include port numbers.
						Issuer: "https://issuer-duplicate-address.com:1234/path2",
						TLS:    &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret2"},
					},
				},
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer-not-duplicate.com",
						TLS:    &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"},
					},
				},
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: invalidIssuerURL,
						TLS:    &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"},
					},
				},
				oidcIdentityProvider,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", oidcIdentityProvider.ObjectMeta),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte-adDress.cOm/path1", oidcIdentityProvider.Name, frozenMetav1Now, 123),
						[]metav1.Condition{
							sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate-address.com:1234/path2", oidcIdentityProvider.Name, frozenMetav1Now, 123),
						[]metav1.Condition{
							sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess(invalidIssuerURL, oidcIdentityProvider.Name, frozenMetav1Now, 123),
						[]metav1.Condition{
							unknownIssuerIsUniqueCondition(frozenMetav1Now, 123),
							sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123),
							unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "differentIssuerAddressFederationDomain", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", oidcIdentityProvider.Name, frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "legacy config: no identity provider specified in federation domain and no identity providers found results in not found status",
			inputObjects: []runtime.Object{
				federationDomain1,
				federationDomain2,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(federationDomain1,
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(federationDomain2,
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, "", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "legacy config: no identity provider specified in federation domain and multiple identity providers found results in not specified status",
			inputObjects: []runtime.Object{
				federationDomain1,
				oidcIdentityProvider,
				ldapIdentityProvider,
				adIdentityProvider,
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(federationDomain1,
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain specifies identity providers that cannot be found",
			inputObjects: []runtime.Object{
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "cant-find-me",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     "cant-find-me-name",
								},
							},
							{
								DisplayName: "cant-find-me-either",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     "cant-find-me-either-name",
								},
							},
							{
								DisplayName: "cant-find-me-still",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "ActiveDirectoryIdentityProvider",
									Name:     "cant-find-me-still-name",
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
								`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "cant-find-me-name")

								 cannot find resource specified by .spec.identityProviders[1].objectRef (with name "cant-find-me-either-name")

								 cannot find resource specified by .spec.identityProviders[2].objectRef (with name "cant-find-me-still-name")`,
							), frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain specifies identity providers that all exist",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				ldapIdentityProvider,
				adIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "can-find-me",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
							},
							{
								DisplayName: "can-find-me-too",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "LDAPIdentityProvider",
									Name:     ldapIdentityProvider.Name,
								},
							},
							{
								DisplayName: "can-find-me-three",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "ActiveDirectoryIdentityProvider",
									Name:     adIdentityProvider.Name,
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithIDPs(t, "https://issuer1.com",
					[]*federationdomainproviders.FederationDomainIdentityProvider{
						{
							DisplayName: "can-find-me",
							UID:         oidcIdentityProvider.UID,
							Transforms:  idtransform.NewTransformationPipeline(),
						},
						{
							DisplayName: "can-find-me-too",
							UID:         ldapIdentityProvider.UID,
							Transforms:  idtransform.NewTransformationPipeline(),
						},
						{
							DisplayName: "can-find-me-three",
							UID:         adIdentityProvider.UID,
							Transforms:  idtransform.NewTransformationPipeline(),
						},
					}),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "the federation domain has duplicate display names for IDPs",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				ldapIdentityProvider,
				adIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "duplicate1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
							},
							{
								DisplayName: "duplicate1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "LDAPIdentityProvider",
									Name:     ldapIdentityProvider.Name,
								},
							},
							{
								DisplayName: "duplicate1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "LDAPIdentityProvider",
									Name:     ldapIdentityProvider.Name,
								},
							},
							{
								DisplayName: "unique",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "ActiveDirectoryIdentityProvider",
									Name:     adIdentityProvider.Name,
								},
							},
							{
								DisplayName: "duplicate2",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "LDAPIdentityProvider",
									Name:     ldapIdentityProvider.Name,
								},
							},
							{
								DisplayName: "duplicate2",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "ActiveDirectoryIdentityProvider",
									Name:     adIdentityProvider.Name,
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain has unrecognized api group names in objectRefs",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				ldapIdentityProvider,
				adIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To("wrong.example.com"),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
							},
							{
								DisplayName: "name2",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(""), // empty string is wrong
									Kind:     "LDAPIdentityProvider",
									Name:     ldapIdentityProvider.Name,
								},
							},
							{
								DisplayName: "name3",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: nil, // nil is wrong, and gets treated like an empty string in the error condition
									Kind:     "LDAPIdentityProvider",
									Name:     ldapIdentityProvider.Name,
								},
							},
							{
								DisplayName: "name4",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor), // correct
									Kind:     "ActiveDirectoryIdentityProvider",
									Name:     adIdentityProvider.Name,
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123),
							sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
								`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "some-oidc-idp")

								 cannot find resource specified by .spec.identityProviders[1].objectRef (with name "some-ldap-idp")

								 cannot find resource specified by .spec.identityProviders[2].objectRef (with name "some-ldap-idp")`,
							), frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain has unrecognized kind names in objectRefs",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				ldapIdentityProvider,
				adIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider", // correct
									Name:     oidcIdentityProvider.Name,
								},
							},
							{
								DisplayName: "name2",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "wrong",
									Name:     ldapIdentityProvider.Name,
								},
							},
							{
								DisplayName: "name3",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "", // empty is also wrong
									Name:     ldapIdentityProvider.Name,
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123),
							sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
								`cannot find resource specified by .spec.identityProviders[1].objectRef (with name "some-ldap-idp")

								 cannot find resource specified by .spec.identityProviders[2].objectRef (with name "some-ldap-idp")`,
							), frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain has transformation expressions which don't compile",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "username/v1", Expression: "this is not a valid cel expression"},
										{Type: "groups/v1", Expression: "this is also not a valid cel expression"},
										{Type: "username/v1", Expression: "username"}, // valid
										{Type: "policy/v1", Expression: "still not a valid cel expression"},
									},
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadTransformationExpressionsCondition(here.Doc(
								`spec.identityProvider[0].transforms.expressions[0].expression was invalid:
								 CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>
								  | this is not a valid cel expression
								  | .....^

								 spec.identityProvider[0].transforms.expressions[1].expression was invalid:
								 CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>
								  | this is also not a valid cel expression
								  | .....^

								 spec.identityProvider[0].transforms.expressions[3].expression was invalid:
								 CEL expression compile error: ERROR: <input>:1:7: Syntax error: mismatched input 'not' expecting <EOF>
								  | still not a valid cel expression
								  | ......^`,
							), frozenMetav1Now, 123),
							sadTransformationExamplesCondition(
								"unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid",
								frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain has transformation examples which don't pass",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "policy/v1", Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`, Message: "only ryan allowed"},
										{Type: "policy/v1", Expression: `username != "rejectMeWithDefaultMessage"`}, // no message specified
										{Type: "username/v1", Expression: `"pre:" + username`},
										{Type: "groups/v1", Expression: `groups.map(g, "pre:" + g)`},
									},
									Examples: []configv1alpha1.FederationDomainTransformsExample{
										{ // this example should pass
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "pre:ryan",
												Groups:   []string{"pre:b", "pre:a", "pre:b", "pre:a"}, // order and repeats don't matter, treated like a set
												Rejected: false,
											},
										},
										{ // this example should pass
											Username: "other",
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Rejected: true,
												Message:  "only ryan allowed",
											},
										},
										{ // this example should fail because it expects the user to be rejected but the user was actually not rejected
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Rejected: true,
												Message:  "this input is ignored in this case",
											},
										},
										{ // this example should fail because it expects the user not to be rejected but they were actually rejected
											Username: "other",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "pre:other",
												Groups:   []string{"pre:a", "pre:b"},
												Rejected: false,
											},
										},
										{ // this example should fail because it expects the wrong rejection message
											Username: "other",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Rejected: true,
												Message:  "wrong message",
											},
										},
										{ // this example should pass even though it does not make any assertion about the rejection message
											// because the message assertions defaults to asserting the default rejection message
											Username: "rejectMeWithDefaultMessage",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Rejected: true,
											},
										},
										{ // this example should fail because it expects both the wrong username and groups
											Username: "ryan",
											Groups:   []string{"b", "a"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "wrong",
												Groups:   []string{},
												Rejected: false,
											},
										},
										{ // this example should fail because it expects the wrong username only
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "wrong",
												Groups:   []string{"pre:b", "pre:a"},
												Rejected: false,
											},
										},
										{ // this example should fail because it expects the wrong groups only
											Username: "ryan",
											Groups:   []string{"b", "a"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "pre:ryan",
												Groups:   []string{"wrong2", "wrong1"},
												Rejected: false,
											},
										},
										{ // this example should fail because it does not expect anything but the auth actually was successful
											Username: "ryan",
											Groups:   []string{"b", "a"},
											Expects:  configv1alpha1.FederationDomainTransformsExampleExpects{},
										},
									},
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadTransformationExamplesCondition(here.Doc(
								`.spec.identityProviders[0].transforms.examples[2] example failed:
								 expected: authentication to be rejected
								 actual:   authentication was not rejected

								 .spec.identityProviders[0].transforms.examples[3] example failed:
								 expected: authentication not to be rejected
								 actual:   authentication was rejected with message "only ryan allowed"

								 .spec.identityProviders[0].transforms.examples[4] example failed:
								 expected: authentication rejection message "wrong message"
								 actual:   authentication rejection message "only ryan allowed"

								 .spec.identityProviders[0].transforms.examples[6] example failed:
								 expected: username "wrong"
								 actual:   username "pre:ryan"

								 .spec.identityProviders[0].transforms.examples[6] example failed:
								 expected: groups []
								 actual:   groups ["pre:a", "pre:b"]

								 .spec.identityProviders[0].transforms.examples[7] example failed:
								 expected: username "wrong"
								 actual:   username "pre:ryan"

								 .spec.identityProviders[0].transforms.examples[8] example failed:
								 expected: groups ["wrong1", "wrong2"]
								 actual:   groups ["pre:a", "pre:b"]

								 .spec.identityProviders[0].transforms.examples[9] example failed:
								 expected: username ""
								 actual:   username "pre:ryan"

								 .spec.identityProviders[0].transforms.examples[9] example failed:
								 expected: groups []
								 actual:   groups ["pre:a", "pre:b"]`,
							), frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain has transformation expressions that return illegal values with examples which exercise them",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "username/v1", Expression: `username == "ryan" ? "" : username`}, // not allowed to return an empty string as the transformed username
									},
									Examples: []configv1alpha1.FederationDomainTransformsExample{
										{ // every example which encounters an unexpected error should fail because the transformation pipeline returned an error
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects:  configv1alpha1.FederationDomainTransformsExampleExpects{},
										},
										{ // every example which encounters an unexpected error should fail because the transformation pipeline returned an error
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects:  configv1alpha1.FederationDomainTransformsExampleExpects{},
										},
										{ // this should pass
											Username: "other",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "other",
												Groups:   []string{"a", "b"},
												Rejected: false,
											},
										},
									},
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadTransformationExamplesCondition(here.Doc(
								`.spec.identityProviders[0].transforms.examples[0] example failed:
								 expected: no transformation errors
								 actual:   transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"

								 .spec.identityProviders[0].transforms.examples[1] example failed:
								 expected: no transformation errors
								 actual:   transformations resulted in an unexpected error "identity transformation returned an empty username, which is not allowed"`,
							), frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain has lots of errors including errors from multiple IDPs, which are all shown in the status conditions using IDP indices in the messages",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://not-unique.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "not unique",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     "this will not be found",
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Constants: []configv1alpha1.FederationDomainTransformsConstant{
										{Name: "foo", Type: "string", StringValue: "bar"},
										{Name: "bar", Type: "string", StringValue: "baz"},
									},
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "username/v1", Expression: `username + ":suffix"`},
									},
									Examples: []configv1alpha1.FederationDomainTransformsExample{
										{ // this should fail
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "this is wrong string",
												Groups:   []string{"this is wrong string list"},
											},
										},
										{ // this should fail
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "this is also wrong string",
												Groups:   []string{"this is also wrong string list"},
											},
										},
									},
								},
							},
							{
								DisplayName: "not unique",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "this is wrong",
									Name:     "foo",
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Constants: []configv1alpha1.FederationDomainTransformsConstant{
										{Name: "foo", Type: "string", StringValue: "bar"},
										{Name: "bar", Type: "string", StringValue: "baz"},
									},
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "username/v1", Expression: `username + ":suffix"`},
									},
									Examples: []configv1alpha1.FederationDomainTransformsExample{
										{ // this should pass
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "ryan:suffix",
												Groups:   []string{"a", "b"},
											},
										},
										{ // this should fail
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "this is still wrong string",
												Groups:   []string{"this is still wrong string list"},
											},
										},
									},
								},
							},
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To("this is wrong"),
									Kind:     "OIDCIdentityProvider",
									Name:     "foo",
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "username/v1", Expression: `username`},
										{Type: "username/v1", Expression: `this does not compile`},
										{Type: "username/v1", Expression: `username`},
										{Type: "username/v1", Expression: `this also does not compile`},
									},
								},
							},
						},
					},
				},
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://not-unique.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "username/v1", Expression: `username`},
										{Type: "username/v1", Expression: `this still does not compile`},
										{Type: "username/v1", Expression: `username`},
										{Type: "username/v1", Expression: `this really does not compile`},
									},
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123),
							sadDisplayNamesUniqueCondition(`"not unique"`, frozenMetav1Now, 123),
							sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
								`cannot find resource specified by .spec.identityProviders[0].objectRef (with name "this will not be found")

								 cannot find resource specified by .spec.identityProviders[1].objectRef (with name "foo")

								 cannot find resource specified by .spec.identityProviders[2].objectRef (with name "foo")`,
							), frozenMetav1Now, 123),
							sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
							sadKindCondition(`"this is wrong"`, frozenMetav1Now, 123),
							sadTransformationExpressionsCondition(here.Doc(
								`spec.identityProvider[2].transforms.expressions[1].expression was invalid:
								 CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'does' expecting <EOF>
								  | this does not compile
								  | .....^

								 spec.identityProvider[2].transforms.expressions[3].expression was invalid:
								 CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'also' expecting <EOF>
								  | this also does not compile
								  | .....^`,
							), frozenMetav1Now, 123),
							sadTransformationExamplesCondition(here.Doc(
								`.spec.identityProviders[0].transforms.examples[0] example failed:
								 expected: username "this is wrong string"
								 actual:   username "ryan:suffix"

								 .spec.identityProviders[0].transforms.examples[0] example failed:
								 expected: groups ["this is wrong string list"]
								 actual:   groups ["a", "b"]

								 .spec.identityProviders[0].transforms.examples[1] example failed:
								 expected: username "this is also wrong string"
								 actual:   username "ryan:suffix"

								 .spec.identityProviders[0].transforms.examples[1] example failed:
								 expected: groups ["this is also wrong string list"]
								 actual:   groups ["a", "b"]

								 .spec.identityProviders[1].transforms.examples[1] example failed:
								 expected: username "this is still wrong string"
								 actual:   username "ryan:suffix"

								 .spec.identityProviders[1].transforms.examples[1] example failed:
								 expected: groups ["this is still wrong string list"]
								 actual:   groups ["a", "b"]

								 unable to check if the examples specified by .spec.identityProviders[2].transforms.examples[] had errors because an expression was invalid`,
							), frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseError,
					replaceConditions(
						allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123),
						[]metav1.Condition{
							sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
							sadTransformationExpressionsCondition(here.Doc(
								`spec.identityProvider[0].transforms.expressions[1].expression was invalid:
								 CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'still' expecting <EOF>
								  | this still does not compile
								  | .....^

								 spec.identityProvider[0].transforms.expressions[3].expression was invalid:
								 CEL expression compile error: ERROR: <input>:1:6: Syntax error: mismatched input 'really' expecting <EOF>
								  | this really does not compile
								  | .....^`,
							), frozenMetav1Now, 123),
							sadTransformationExamplesCondition(
								"unable to check if the examples specified by .spec.identityProviders[0].transforms.examples[] had errors because an expression was invalid",
								frozenMetav1Now, 123),
							sadReadyCondition(frozenMetav1Now, 123),
						}),
				),
			},
		},
		{
			name: "the federation domain has valid IDPs and transformations and examples",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				ldapIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "name1",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "policy/v1", Expression: `username == "ryan" || username == "rejectMeWithDefaultMessage"`, Message: "only ryan allowed"},
										{Type: "policy/v1", Expression: `username != "rejectMeWithDefaultMessage"`}, // no message specified
										{Type: "username/v1", Expression: `"pre:" + username`},
										{Type: "groups/v1", Expression: `groups.map(g, "pre:" + g)`},
									},
									Constants: []configv1alpha1.FederationDomainTransformsConstant{
										{Name: "str", Type: "string", StringValue: "abc"},
										{Name: "strL", Type: "stringList", StringListValue: []string{"def"}},
									},
									Examples: []configv1alpha1.FederationDomainTransformsExample{
										{
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "pre:ryan",
												Groups:   []string{"pre:b", "pre:a"},
												Rejected: false,
											},
										},
										{
											Username: "other",
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Rejected: true,
												Message:  "only ryan allowed",
											},
										},
										{
											Username: "rejectMeWithDefaultMessage",
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Rejected: true,
												// Not specifying message is the same as expecting the default message.
											},
										},
										{
											Username: "rejectMeWithDefaultMessage",
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Rejected: true,
												Message:  "authentication was rejected by a configured policy", // this is the default message
											},
										},
									},
								},
							},
							{
								DisplayName: "name2",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "LDAPIdentityProvider",
									Name:     ldapIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{Type: "username/v1", Expression: `"pre:" + username`},
									},
									Examples: []configv1alpha1.FederationDomainTransformsExample{
										{
											Username: "ryan",
											Groups:   []string{"a", "b"},
											Expects: configv1alpha1.FederationDomainTransformsExampleExpects{
												Username: "pre:ryan",
												Groups:   []string{"b", "a"},
												Rejected: false,
											},
										},
									},
								},
							},
						},
					},
				},
			},
			wantFDIssuers: []*federationdomainproviders.FederationDomainIssuer{
				federationDomainIssuerWithIDPs(t, "https://issuer1.com", []*federationdomainproviders.FederationDomainIdentityProvider{
					{
						DisplayName: "name1",
						UID:         oidcIdentityProvider.UID,
						Transforms: newTransformationPipeline(t, &celtransformer.TransformationConstants{
							StringConstants:     map[string]string{"str": "abc"},
							StringListConstants: map[string][]string{"strL": {"def"}},
						},
							&celtransformer.AllowAuthenticationPolicy{
								Expression:                    `username == "ryan" || username == "rejectMeWithDefaultMessage"`,
								RejectedAuthenticationMessage: "only ryan allowed",
							},
							&celtransformer.AllowAuthenticationPolicy{Expression: `username != "rejectMeWithDefaultMessage"`},
							&celtransformer.UsernameTransformation{Expression: `"pre:" + username`},
							&celtransformer.GroupsTransformation{Expression: `groups.map(g, "pre:" + g)`},
						),
					},
					{
						DisplayName: "name2",
						UID:         ldapIdentityProvider.UID,
						Transforms: newTransformationPipeline(t, &celtransformer.TransformationConstants{},
							&celtransformer.UsernameTransformation{Expression: `"pre:" + username`},
						),
					},
				}),
			},
			wantStatusUpdates: []*configv1alpha1.FederationDomain{
				expectedFederationDomainStatusUpdate(
					&configv1alpha1.FederationDomain{
						ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					},
					configv1alpha1.FederationDomainPhaseReady,
					allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
				),
			},
		},
		{
			name: "the federation domain specifies illegal const type, which shouldn't really happen since the CRD validates it",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "can-find-me",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Constants: []configv1alpha1.FederationDomainTransformsConstant{
										{
											Type: "this is illegal",
										},
									},
								},
							},
						},
					},
				},
			},
			wantErr: `one of spec.identityProvider[].transforms.constants[].type is invalid: "this is illegal"`,
		},
		{
			name: "the federation domain specifies illegal expression type, which shouldn't really happen since the CRD validates it",
			inputObjects: []runtime.Object{
				oidcIdentityProvider,
				&configv1alpha1.FederationDomain{
					ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
					Spec: configv1alpha1.FederationDomainSpec{
						Issuer: "https://issuer1.com",
						IdentityProviders: []configv1alpha1.FederationDomainIdentityProvider{
							{
								DisplayName: "can-find-me",
								ObjectRef: corev1.TypedLocalObjectReference{
									APIGroup: ptr.To(apiGroupSupervisor),
									Kind:     "OIDCIdentityProvider",
									Name:     oidcIdentityProvider.Name,
								},
								Transforms: configv1alpha1.FederationDomainTransforms{
									Expressions: []configv1alpha1.FederationDomainTransformsExpression{
										{
											Type: "this is illegal",
										},
									},
								},
							},
						},
					},
				},
			},
			wantErr: `one of spec.identityProvider[].transforms.expressions[].type is invalid: "this is illegal"`,
		},
	}

	for _, tt := range tests {
		tt := tt
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			federationDomainsSetter := &fakeFederationDomainsSetter{}
			pinnipedAPIClient := pinnipedfake.NewSimpleClientset()
			pinnipedInformerClient := pinnipedfake.NewSimpleClientset()
			for _, o := range tt.inputObjects {
				require.NoError(t, pinnipedAPIClient.Tracker().Add(o))
				require.NoError(t, pinnipedInformerClient.Tracker().Add(o))
			}
			if tt.configClient != nil {
				tt.configClient(pinnipedAPIClient)
			}
			pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0)

			controller := NewFederationDomainWatcherController(
				federationDomainsSetter,
				apiGroupSuffix,
				clocktesting.NewFakeClock(frozenNow),
				pinnipedAPIClient,
				pinnipedInformers.Config().V1alpha1().FederationDomains(),
				pinnipedInformers.IDP().V1alpha1().OIDCIdentityProviders(),
				pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(),
				pinnipedInformers.IDP().V1alpha1().ActiveDirectoryIdentityProviders(),
				controllerlib.WithInformer,
			)

			ctx, cancel := context.WithCancel(context.Background())
			defer cancel()

			pinnipedInformers.Start(ctx.Done())
			controllerlib.TestRunSynchronously(t, controller)

			syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{Namespace: namespace, Name: "config-name"}}

			if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
				require.EqualError(t, err, tt.wantErr)
			} else {
				require.NoError(t, err)
			}

			if tt.wantFDIssuers != nil {
				require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled)
				// This is ugly, but we cannot test equality on compiled identity transformations because cel.Program
				// cannot be compared for equality. This converts them to a type which can be tested for equality,
				// which should be good enough for the purposes of this test.
				require.ElementsMatch(t,
					convertToComparableType(tt.wantFDIssuers),
					convertToComparableType(federationDomainsSetter.FederationDomainsReceived))
			} else {
				require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled)
			}

			if tt.wantStatusUpdates != nil {
				// This controller should only perform updates to FederationDomain statuses.
				// In this controller we don't actually care about the order of the actions, since the FederationDomains
				// statuses can be updated in any order.  Therefore, we are sorting here so we can use require.Equal
				// to make the test output easier to read. Unfortunately the timezone nested in the condition can still
				// make the test failure diffs ugly sometimes, but we do want to assert about timestamps so there's not
				// much we can do about those.
				actualFederationDomainUpdates := getFederationDomainStatusUpdates(t, pinnipedAPIClient.Actions())
				sortFederationDomainsByName(actualFederationDomainUpdates)
				sortFederationDomainsByName(tt.wantStatusUpdates)
				// Use require.Equal instead of require.ElementsMatch because require.Equal prints a nice diff.
				require.Equal(t, tt.wantStatusUpdates, actualFederationDomainUpdates)
			} else {
				require.Empty(t, pinnipedAPIClient.Actions())
			}
		})
	}
}

type comparableFederationDomainIssuer struct {
	issuer                  string
	identityProviders       []*comparableFederationDomainIdentityProvider
	defaultIdentityProvider *comparableFederationDomainIdentityProvider
}

type comparableFederationDomainIdentityProvider struct {
	DisplayName      string
	UID              types.UID
	TransformsSource []interface{}
}

func makeFederationDomainIdentityProviderComparable(fdi *federationdomainproviders.FederationDomainIdentityProvider) *comparableFederationDomainIdentityProvider {
	if fdi == nil {
		return nil
	}
	return &comparableFederationDomainIdentityProvider{
		DisplayName:      fdi.DisplayName,
		UID:              fdi.UID,
		TransformsSource: fdi.Transforms.Source(),
	}
}

func convertToComparableType(fdis []*federationdomainproviders.FederationDomainIssuer) []*comparableFederationDomainIssuer {
	result := []*comparableFederationDomainIssuer{}
	for _, fdi := range fdis {
		comparableFDIs := make([]*comparableFederationDomainIdentityProvider, len(fdi.IdentityProviders()))
		for _, idp := range fdi.IdentityProviders() {
			comparableFDIs = append(comparableFDIs, makeFederationDomainIdentityProviderComparable(idp))
		}
		converted := &comparableFederationDomainIssuer{
			issuer:                  fdi.Issuer(),
			identityProviders:       comparableFDIs,
			defaultIdentityProvider: makeFederationDomainIdentityProviderComparable(fdi.DefaultIdentityProvider()),
		}
		result = append(result, converted)
	}
	return result
}

func expectedFederationDomainStatusUpdate(
	fd *configv1alpha1.FederationDomain,
	phase configv1alpha1.FederationDomainPhase,
	conditions []metav1.Condition,
) *configv1alpha1.FederationDomain {
	fdCopy := fd.DeepCopy()

	// We don't care about the spec of a FederationDomain in an update status action,
	// so clear it out to make it easier to write expected values.
	fdCopy.Spec = configv1alpha1.FederationDomainSpec{}

	fdCopy.Status.Phase = phase
	fdCopy.Status.Conditions = conditions

	return fdCopy
}

func getFederationDomainStatusUpdates(t *testing.T, actions []coretesting.Action) []*configv1alpha1.FederationDomain {
	federationDomains := []*configv1alpha1.FederationDomain{}

	for _, action := range actions {
		updateAction, ok := action.(coretesting.UpdateAction)
		require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction: %#v", action)
		require.Equal(t, federationDomainGVR, updateAction.GetResource(), "an update action should have updated a FederationDomain but updated something else")
		require.Equal(t, "status", updateAction.GetSubresource(), "an update action should have updated the status subresource but updated something else")

		fd, ok := updateAction.GetObject().(*configv1alpha1.FederationDomain)
		require.True(t, ok, "failed to cast an action's object as a FederationDomain: %#v", updateAction.GetObject())
		require.Equal(t, fd.Namespace, updateAction.GetNamespace(), "an update action might have been called on the wrong namespace for a FederationDomain")

		// We don't care about the spec of a FederationDomain in an update status action,
		// so clear it out to make it easier to write expected values.
		copyOfFD := fd.DeepCopy()
		copyOfFD.Spec = configv1alpha1.FederationDomainSpec{}

		federationDomains = append(federationDomains, copyOfFD)
	}

	return federationDomains
}

func sortFederationDomainsByName(federationDomains []*configv1alpha1.FederationDomain) {
	sort.SliceStable(federationDomains, func(a, b int) bool {
		return federationDomains[a].GetName() < federationDomains[b].GetName()
	})
}

func newTransformationPipeline(
	t *testing.T,
	consts *celtransformer.TransformationConstants,
	transformations ...celtransformer.CELTransformation,
) *idtransform.TransformationPipeline {
	pipeline := idtransform.NewTransformationPipeline()

	compiler, err := celtransformer.NewCELTransformer(celTransformerMaxExpressionRuntime)
	require.NoError(t, err)

	if consts.StringConstants == nil {
		consts.StringConstants = map[string]string{}
	}
	if consts.StringListConstants == nil {
		consts.StringListConstants = map[string][]string{}
	}

	for _, transform := range transformations {
		compiledTransform, err := compiler.CompileTransformation(transform, consts)
		require.NoError(t, err)
		pipeline.AppendTransformation(compiledTransform)
	}

	return pipeline
}

func TestTransformationPipelinesCanBeTestedForEqualityUsingSourceToMakeTestingEasier(t *testing.T) {
	compiler, err := celtransformer.NewCELTransformer(5 * time.Second)
	require.NoError(t, err)

	transforms := []celtransformer.CELTransformation{
		&celtransformer.AllowAuthenticationPolicy{
			Expression:                    `username == "ryan" || username == "rejectMeWithDefaultMessage"`,
			RejectedAuthenticationMessage: "only ryan allowed",
		},
		&celtransformer.UsernameTransformation{Expression: `"pre:" + username`},
		&celtransformer.GroupsTransformation{Expression: `groups.map(g, "pre:" + g)`},
	}

	differentTransforms := []celtransformer.CELTransformation{
		&celtransformer.AllowAuthenticationPolicy{
			Expression:                    `username == "ryan" || username == "different"`,
			RejectedAuthenticationMessage: "different",
		},
		&celtransformer.UsernameTransformation{Expression: `"different:" + username`},
		&celtransformer.GroupsTransformation{Expression: `groups.map(g, "different:" + g)`},
	}

	consts := &celtransformer.TransformationConstants{
		StringConstants: map[string]string{
			"foo": "bar",
			"baz": "bat",
		},
		StringListConstants: map[string][]string{
			"foo": {"a", "b"},
			"bar": {"c", "d"},
		},
	}

	differentConsts := &celtransformer.TransformationConstants{
		StringConstants: map[string]string{
			"foo": "barDifferent",
			"baz": "bat",
		},
		StringListConstants: map[string][]string{
			"foo": {"aDifferent", "b"},
			"bar": {"c", "d"},
		},
	}

	pipeline := idtransform.NewTransformationPipeline()
	equalPipeline := idtransform.NewTransformationPipeline()
	differentPipeline1 := idtransform.NewTransformationPipeline()
	differentPipeline2 := idtransform.NewTransformationPipeline()
	expectedSourceList := []interface{}{}

	for i, transform := range transforms {
		// Compile and append to a pipeline.
		compiledTransform1, err := compiler.CompileTransformation(transform, consts)
		require.NoError(t, err)
		pipeline.AppendTransformation(compiledTransform1)

		// Recompile the same thing and append it to another pipeline.
		// This pipeline should end up being equal to the first one.
		compiledTransform2, err := compiler.CompileTransformation(transform, consts)
		require.NoError(t, err)
		equalPipeline.AppendTransformation(compiledTransform2)

		// Build up a test expectation value.
		expectedSourceList = append(expectedSourceList, &celtransformer.CELTransformationSource{Expr: transform, Consts: consts})

		// Compile a different expression using the same constants and append it to a different pipeline.
		// This should not be equal to the other pipelines.
		compiledDifferentExpressionSameConsts, err := compiler.CompileTransformation(differentTransforms[i], consts)
		require.NoError(t, err)
		differentPipeline1.AppendTransformation(compiledDifferentExpressionSameConsts)

		// Compile the same expression using the different constants and append it to a different pipeline.
		// This should not be equal to the other pipelines.
		compiledSameExpressionDifferentConsts, err := compiler.CompileTransformation(transform, differentConsts)
		require.NoError(t, err)
		differentPipeline2.AppendTransformation(compiledSameExpressionDifferentConsts)
	}

	require.Equal(t, expectedSourceList, pipeline.Source())
	require.Equal(t, expectedSourceList, equalPipeline.Source())

	// The source of compiled pipelines can be compared to each other in this way for testing purposes.
	require.Equal(t, pipeline.Source(), equalPipeline.Source())
	require.NotEqual(t, pipeline.Source(), differentPipeline1.Source())
	require.NotEqual(t, pipeline.Source(), differentPipeline2.Source())
}