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

package integration

import (
	"context"
	"encoding/base64"
	"fmt"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"

	idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
	"go.pinniped.dev/internal/here"
	"go.pinniped.dev/internal/testutil"

	"go.pinniped.dev/test/testlib"
)

const generateGitHubNamePrefix = "integration-test-github-idp-"

func TestGitHubIDPStaticValidationOnCreate_Parallel(t *testing.T) {
	adminClient := testlib.NewKubernetesClientset(t)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	t.Cleanup(cancel)

	namespaceClient := adminClient.CoreV1().Namespaces()
	skipCELTests := !testutil.KubeServerMinorVersionAtLeastInclusive(t, adminClient.Discovery(), 26)

	ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
		},
	}, metav1.CreateOptions{})
	require.NoError(t, err)

	t.Cleanup(func() {
		require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{}))
	})

	gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name)

	tests := []struct {
		name              string
		usesCELValidation bool
		inputSpec         idpv1alpha1.GitHubIdentityProviderSpec
		wantSpec          idpv1alpha1.GitHubIdentityProviderSpec
		wantErr           string
	}{
		{
			name: "all fields set",
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				GitHubAPI: idpv1alpha1.GitHubAPIConfig{
					Host: ptr.To("some-host.example.com"),
					TLS: &idpv1alpha1.TLSSpec{
						CertificateAuthorityData: func() string {
							return base64.StdEncoding.EncodeToString([]byte("-----BEGIN CERTIFICATE-----\ndata goes here"))
						}(),
					},
				},
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Allowed: []string{
							"org1",
							"that-other-org",
						},
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
					},
				},
				Claims: idpv1alpha1.GitHubClaims{
					Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
					Groups:   ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "any-name-goes-here",
				},
			},
			wantSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				GitHubAPI: idpv1alpha1.GitHubAPIConfig{
					Host: ptr.To("some-host.example.com"),
					TLS: &idpv1alpha1.TLSSpec{
						CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCmRhdGEgZ29lcyBoZXJl",
					},
				},
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Allowed: []string{
							"org1",
							"that-other-org",
						},
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
					},
				},
				Claims: idpv1alpha1.GitHubClaims{
					Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
					Groups:   ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "any-name-goes-here",
				},
			},
		},
		{
			name: "minimum fields set - inherit defaults",
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
					},
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "name-of-a-secret",
				},
			},
			wantSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				GitHubAPI: idpv1alpha1.GitHubAPIConfig{
					Host: ptr.To("github.com"),
				},
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
					},
				},
				Claims: idpv1alpha1.GitHubClaims{
					Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
					Groups:   ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "name-of-a-secret",
				},
			},
		},
		{
			name: fmt.Sprintf(
				"cannot set AllowedOrganizationsPolicy=%s and set AllowedOrganizations",
				string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)),
			usesCELValidation: true,
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Allowed: []string{
							"some-org",
						},
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
					},
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "name-of-a-secret",
				},
			},
			wantErr: "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",
		},
		{
			name:              fmt.Sprintf("spec.allowAuthentication.organizations.policy must be '%s' when spec.allowAuthentication.organizations.allowed is empty (nil)", string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)),
			usesCELValidation: true,
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
					},
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "name-of-a-secret",
				},
			},
			wantErr: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",
		},
		{
			name:              fmt.Sprintf("spec.allowAuthentication.organizations.policy must be '%s' when spec.allowAuthentication.organizations.allowed is empty", string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)),
			usesCELValidation: true,
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Allowed: []string{},
						Policy:  ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
					},
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "name-of-a-secret",
				},
			},
			wantErr: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",
		},
		{
			name:      "spec.client.secretName in body should be at least 1 chars long",
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{},
			wantErr:   "spec.client.secretName in body should be at least 1 chars long",
		},
		{
			name: "spec.githubAPI.host in body should be at least 1 chars long",
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				GitHubAPI: idpv1alpha1.GitHubAPIConfig{
					Host: ptr.To(""),
				},
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
					},
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "name-of-a-secret",
				},
			},
			wantErr: "spec.githubAPI.host in body should be at least 1 chars long",
		},
		{
			name: "duplicates not permitted in spec.allowAuthentication.organizations.allowed",
			inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
				AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
					Organizations: idpv1alpha1.GitHubOrganizationsSpec{
						Allowed: []string{
							"org1",
							"org1",
						},
						Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
					},
				},
				Client: idpv1alpha1.GitHubClientSpec{
					SecretName: "name-of-a-secret",
				},
			},
			wantErr: `spec.allowAuthentication.organizations.allowed[1]: Duplicate value: "org1"`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			if tt.usesCELValidation && skipCELTests {
				t.Skip("CEL is not available for current K8s version")
			}

			input := &idpv1alpha1.GitHubIdentityProvider{
				ObjectMeta: metav1.ObjectMeta{
					GenerateName: generateGitHubNamePrefix,
				},
				Spec: tt.inputSpec,
			}

			outputGitHubIDP, err := gitHubIDPClient.Create(ctx, input, metav1.CreateOptions{})
			if tt.wantErr == "" {
				require.NoError(t, err)
				require.Equal(t, tt.wantSpec, outputGitHubIDP.Spec)
			} else {
				require.ErrorContains(t, err, tt.wantErr)
			}
		})
	}
}

func TestGitHubIDPSetsDefaultsWithKubectl_Parallel(t *testing.T) {
	env := testlib.IntegrationEnv(t)

	adminClient := testlib.NewKubernetesClientset(t)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	t.Cleanup(cancel)

	namespaceClient := adminClient.CoreV1().Namespaces()

	ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
		},
	}, metav1.CreateOptions{})
	require.NoError(t, err)

	t.Cleanup(func() {
		require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{}))
	})
	t.Logf("Created namespace %s", ns.Name)

	idpName := generateGitHubNamePrefix + testlib.RandHex(t, 16)

	githubIDPYaml := []byte(here.Doc(fmt.Sprintf(`
	---
	apiVersion: idp.supervisor.%s/v1alpha1
	kind: GitHubIdentityProvider
	metadata:
	  name: %s
	  namespace: %s
	spec:
	  allowAuthentication:
	    organizations:
	      policy: AllGitHubUsers
	  client:
	    secretName: any-secret-name`, env.APIGroupSuffix, idpName, ns.Name)))

	githubIDPYamlFilepath := filepath.Join(t.TempDir(), "github-idp.yaml")

	require.NoError(t, os.WriteFile(githubIDPYamlFilepath, githubIDPYaml, 0600))

	stdOut, stdErr := runTestKubectlCommand(t, "create", "-f", githubIDPYamlFilepath)

	require.Equal(t, fmt.Sprintf("githubidentityprovider.idp.supervisor.%s/%s created\n", env.APIGroupSuffix, idpName), stdOut)
	require.Empty(t, stdErr)

	gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name)

	idp, err := gitHubIDPClient.Get(ctx, idpName, metav1.GetOptions{})
	require.NoError(t, err)
	require.Equal(t, idpv1alpha1.GitHubIdentityProviderSpec{
		GitHubAPI: idpv1alpha1.GitHubAPIConfig{
			Host: ptr.To("github.com"),
		},
		Claims: idpv1alpha1.GitHubClaims{
			Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
			Groups:   ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
		},
		AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
			Organizations: idpv1alpha1.GitHubOrganizationsSpec{
				Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
			},
		},
		Client: idpv1alpha1.GitHubClientSpec{
			SecretName: "any-secret-name",
		},
	}, idp.Spec)
}

func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) {
	// These operations must be performed in the Supervisor's namespace so that the controller can find GitHubIdentityProvider
	supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	t.Cleanup(cancel)

	kubernetesClient := testlib.NewKubernetesClientset(t)
	secretsClient := kubernetesClient.CoreV1().Secrets(supervisorNamespace)
	gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(supervisorNamespace)

	happySecretName := generateGitHubNamePrefix + testlib.RandHex(t, 16)
	invalidSecretName := generateGitHubNamePrefix + testlib.RandHex(t, 16)

	tests := []struct {
		name           string
		secrets        []*corev1.Secret // Secrets will be created first, and the first secret found will be listed as the configured GitHub Client secret
		idps           []*idpv1alpha1.GitHubIdentityProvider
		wantPhase      idpv1alpha1.GitHubIdentityProviderPhase
		wantConditions []*metav1.Condition
	}{
		{
			name: "Happy Path",
			secrets: []*corev1.Secret{
				{
					ObjectMeta: metav1.ObjectMeta{
						Name: happySecretName,
					},
					Type: "secrets.pinniped.dev/github-client",
					Data: map[string][]byte{
						"clientID":     []byte("foo"),
						"clientSecret": []byte("bar"),
					},
				},
			},
			idps: []*idpv1alpha1.GitHubIdentityProvider{
				{
					Spec: idpv1alpha1.GitHubIdentityProviderSpec{
						GitHubAPI: idpv1alpha1.GitHubAPIConfig{
							Host: ptr.To("github.com"),
						},
						AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
							Organizations: idpv1alpha1.GitHubOrganizationsSpec{
								Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
							},
						},
					},
				},
			},
			wantPhase: idpv1alpha1.GitHubPhaseReady,
			wantConditions: []*metav1.Condition{
				{
					Type:    "ClaimsValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: "spec.claims are valid",
				},
				{
					Type:    "ClientCredentialsSecretValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: fmt.Sprintf("clientID and clientSecret have been read from spec.client.SecretName (%q)", happySecretName),
				},
				{
					Type:    "GitHubConnectionValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: `probed connection to "api.github.com:443" for spec.githubAPI.host ("github.com"): host is reachable and TLS verification succeeds`,
				},
				{
					Type:    "HostValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: `spec.githubAPI.host ("github.com") is valid`,
				},
				{
					Type:    "OrganizationsPolicyValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`,
				},
				{
					Type:    "TLSConfigurationValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: "spec.githubAPI.tls is valid: no TLS configuration provided: using default root CA bundle from container image",
				},
			},
		},
		{
			name: "Invalid Client Secret",
			secrets: []*corev1.Secret{
				{
					Type: "secrets.pinniped.dev/github-client",
					ObjectMeta: metav1.ObjectMeta{
						Name: invalidSecretName,
					},
				},
			},
			idps: []*idpv1alpha1.GitHubIdentityProvider{
				{
					Spec: idpv1alpha1.GitHubIdentityProviderSpec{
						GitHubAPI: idpv1alpha1.GitHubAPIConfig{
							Host: ptr.To("api.github.com"),
						},
						AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
							Organizations: idpv1alpha1.GitHubOrganizationsSpec{
								Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
							},
						},
						Client: idpv1alpha1.GitHubClientSpec{
							SecretName: invalidSecretName,
						},
					},
				},
			},
			wantPhase: idpv1alpha1.GitHubPhaseError,
			wantConditions: []*metav1.Condition{
				{
					Type:    "ClaimsValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: "spec.claims are valid",
				},
				{
					Type:   "ClientCredentialsSecretValid",
					Status: metav1.ConditionFalse,
					Reason: "SecretNotFound",
					Message: fmt.Sprintf(`missing key "clientID": secret from spec.client.SecretName (%q) must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`,
						invalidSecretName,
						supervisorNamespace),
				},
				{
					Type:    "GitHubConnectionValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: `probed connection to "api.github.com:443" for spec.githubAPI.host ("api.github.com"): host is reachable and TLS verification succeeds`,
				},
				{
					Type:    "HostValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: `spec.githubAPI.host ("api.github.com") is valid`,
				},
				{
					Type:    "OrganizationsPolicyValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`,
				},
				{
					Type:    "TLSConfigurationValid",
					Status:  metav1.ConditionTrue,
					Reason:  "Success",
					Message: `spec.githubAPI.tls is valid: no TLS configuration provided: using default root CA bundle from container image`,
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()

			var secretName string
			for _, secret := range tt.secrets {
				secret.GenerateName = generateGitHubNamePrefix

				created, err := secretsClient.Create(ctx, secret, metav1.CreateOptions{})
				require.NoError(t, err)
				t.Cleanup(func() {
					err := secretsClient.Delete(ctx, created.Name, metav1.DeleteOptions{})
					require.NoError(t, err)
				})
				if secretName == "" {
					secretName = created.Name
				}
			}

			for _, idp := range tt.idps {
				idp.Name = ""
				idp.GenerateName = generateGitHubNamePrefix
				idp.Spec.Client.SecretName = secretName

				created, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{})
				require.NoError(t, err)

				t.Cleanup(func() {
					err := gitHubIDPClient.Delete(ctx, created.Name, metav1.DeleteOptions{})
					require.NoError(t, err)
				})
				testlib.WaitForGitHubIDPPhase(ctx, t, gitHubIDPClient, created.Name, tt.wantPhase)
				testlib.WaitForGitHubIdentityProviderStatusConditions(ctx, t, gitHubIDPClient, created.Name, tt.wantConditions)
			}
		})
	}
}

func TestGitHubIDPInWrongNamespace_Parallel(t *testing.T) {
	// The GitHubIdentityProvider must be in the same namespace as the controller
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	t.Cleanup(cancel)

	kubernetesClient := testlib.NewKubernetesClientset(t)

	namespaceClient := kubernetesClient.CoreV1().Namespaces()
	otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
		},
	}, metav1.CreateOptions{})
	require.NoError(t, err)

	t.Cleanup(func() {
		require.NoError(t, namespaceClient.Delete(ctx, otherNamespace.Name, metav1.DeleteOptions{}))
	})

	gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(otherNamespace.Name)

	idp := &idpv1alpha1.GitHubIdentityProvider{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
			Namespace:    otherNamespace.Name,
		},
		Spec: idpv1alpha1.GitHubIdentityProviderSpec{
			GitHubAPI: idpv1alpha1.GitHubAPIConfig{
				Host: ptr.To("github.com"),
			},
			AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
				Organizations: idpv1alpha1.GitHubOrganizationsSpec{
					Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
				},
			},
			Client: idpv1alpha1.GitHubClientSpec{
				SecretName: "does-not-matter",
			},
		},
	}

	createdIDP, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{})
	require.NoError(t, err)

	t.Cleanup(func() {
		err := gitHubIDPClient.Delete(ctx, createdIDP.Name, metav1.DeleteOptions{})
		require.NoError(t, err)
	})

	// We require that there's never an error
	// ... and that the status phase is never anything but Pending
	// ... and that there are no status conditions
	require.Never(t, func() bool {
		idp, err := gitHubIDPClient.Get(ctx, createdIDP.Name, metav1.GetOptions{})
		return err != nil && idp.Status.Phase != idpv1alpha1.GitHubPhasePending && len(idp.Status.Conditions) > 0
	}, 2*time.Minute, 10*time.Second)
}

func TestGitHubIDPSecretInOtherNamespace_Parallel(t *testing.T) {
	// The GitHubIdentityProvider must be in the same namespace as the controller
	supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	t.Cleanup(cancel)

	kubernetesClient := testlib.NewKubernetesClientset(t)
	gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(supervisorNamespace)

	namespaceClient := kubernetesClient.CoreV1().Namespaces()
	otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
		},
	}, metav1.CreateOptions{})
	require.NoError(t, err)

	t.Cleanup(func() {
		require.NoError(t, namespaceClient.Delete(ctx, otherNamespace.Name, metav1.DeleteOptions{}))
	})

	secretsClient := kubernetesClient.CoreV1().Secrets(otherNamespace.Name)

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
			Namespace:    otherNamespace.Name,
		},
		Type: "secrets.pinniped.dev/github-client",
		Data: map[string][]byte{
			"clientID":     []byte("foo"),
			"clientSecret": []byte("bar"),
		},
	}

	// This secret will be cleaned up when its namespace is deleted
	createdSecret, err := secretsClient.Create(ctx, secret, metav1.CreateOptions{})
	require.NoError(t, err)

	idp := &idpv1alpha1.GitHubIdentityProvider{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
			Namespace:    supervisorNamespace,
		},
		Spec: idpv1alpha1.GitHubIdentityProviderSpec{
			GitHubAPI: idpv1alpha1.GitHubAPIConfig{
				Host: ptr.To("github.com"),
			},
			AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
				Organizations: idpv1alpha1.GitHubOrganizationsSpec{
					Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
				},
			},
			Client: idpv1alpha1.GitHubClientSpec{
				SecretName: createdSecret.Name,
			},
		},
	}

	created, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{})
	require.NoError(t, err)

	t.Cleanup(func() {
		err := gitHubIDPClient.Delete(ctx, created.Name, metav1.DeleteOptions{})
		require.NoError(t, err)
	})
	testlib.WaitForGitHubIDPPhase(ctx, t, gitHubIDPClient, created.Name, idpv1alpha1.GitHubPhaseError)

	testlib.WaitForGitHubIdentityProviderStatusConditions(ctx, t, gitHubIDPClient, created.Name, []*metav1.Condition{
		{
			Type:    "ClaimsValid",
			Status:  metav1.ConditionTrue,
			Reason:  "Success",
			Message: "spec.claims are valid",
		},
		{
			Type:   "ClientCredentialsSecretValid",
			Status: metav1.ConditionFalse,
			Reason: "SecretNotFound",
			Message: fmt.Sprintf(`secret %q not found: secret from spec.client.SecretName (%q) must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`,
				idp.Spec.Client.SecretName,
				idp.Spec.Client.SecretName,
				supervisorNamespace),
		},
		{
			Type:    "GitHubConnectionValid",
			Status:  metav1.ConditionTrue,
			Reason:  "Success",
			Message: `probed connection to "api.github.com:443" for spec.githubAPI.host ("github.com"): host is reachable and TLS verification succeeds`,
		},
		{
			Type:    "HostValid",
			Status:  metav1.ConditionTrue,
			Reason:  "Success",
			Message: `spec.githubAPI.host ("github.com") is valid`,
		},
		{
			Type:    "OrganizationsPolicyValid",
			Status:  metav1.ConditionTrue,
			Reason:  "Success",
			Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`,
		},
		{
			Type:    "TLSConfigurationValid",
			Status:  metav1.ConditionTrue,
			Reason:  "Success",
			Message: "spec.githubAPI.tls is valid: no TLS configuration provided: using default root CA bundle from container image",
		},
	})
}

func TestGitHubIDPTooManyOrganizationsStaticValidationOnCreate_Parallel(t *testing.T) {
	adminClient := testlib.NewKubernetesClientset(t)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
	t.Cleanup(cancel)

	namespaceClient := adminClient.CoreV1().Namespaces()

	ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
		},
	}, metav1.CreateOptions{})
	require.NoError(t, err)

	t.Cleanup(func() {
		require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{}))
	})

	gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name)

	input := &idpv1alpha1.GitHubIdentityProvider{
		ObjectMeta: metav1.ObjectMeta{
			GenerateName: generateGitHubNamePrefix,
		},
		Spec: idpv1alpha1.GitHubIdentityProviderSpec{
			AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
				Organizations: idpv1alpha1.GitHubOrganizationsSpec{
					Allowed: func() []string {
						orgs := make([]string, 100)
						for i := range 100 {
							orgs[i] = fmt.Sprintf("org-%d", i)
						}
						return orgs
					}(),
					Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
				},
			},
			Client: idpv1alpha1.GitHubClientSpec{
				SecretName: "name-of-a-secret",
			},
		},
	}

	_, err = gitHubIDPClient.Create(ctx, input, metav1.CreateOptions{})

	wantErr := "spec.allowAuthentication.organizations.allowed: Invalid value: 100: spec.allowAuthentication.organizations.allowed in body should have at most 64 items"
	if testutil.KubeServerMinorVersionAtLeastInclusive(t, adminClient.Discovery(), 24) {
		wantErr = "spec.allowAuthentication.organizations.allowed: Too many: 100: must have at most 64 items"
	}

	require.ErrorContains(t, err, wantErr)
}
