// Copyright 2025 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//	http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Copyright 2025 Microsoft
// Licensed under the Apache License, Version 2.0.
package e2e

import (
	"context"
	"fmt"
	"time"

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

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"

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

	hcpsdk20240610preview "github.com/Azure/ARO-HCP/test/sdk/resourcemanager/redhatopenshifthcp/armredhatopenshifthcp"
	"github.com/Azure/ARO-HCP/test/util/framework"
	"github.com/Azure/ARO-HCP/test/util/labels"
	"github.com/Azure/ARO-HCP/test/util/verifiers"
)

var _ = Describe("Customer", func() {
	BeforeEach(func() {
		// do nothing.  per test initialization usually ages better than shared.
	})

	It("should be able to create a cluster with an external auth config and get the external auth config",
		labels.RequireNothing,
		labels.Critical,
		labels.Positive,
		labels.AroRpApiCompatible,
		labels.MIContainers(1),
		func(ctx context.Context) {
			const (
				customerClusterName       = "ea-cluster"
				customerNodePoolName      = "ea-np-1"
				customerExternalAuthName  = "external-auth"
				externalAuthSubjectPrefix = "prefix-" // TODO: ARO-21008 preventing us setting NoPrefix
			)
			tc := framework.NewTestContext()

			if tc.UsePooledIdentities() {
				err := tc.AssignIdentityContainers(ctx, 1, framework.IdentityContainerAssignmentRetryInterval)
				Expect(err).NotTo(HaveOccurred(), "failed to assign pooled identity containers")
			}

			By("creating a resource group")
			resourceGroup, err := tc.NewResourceGroup(ctx, "external-auth-cluster", tc.Location())
			Expect(err).NotTo(HaveOccurred(), "failed to create resource group for external auth cluster")

			// creating cluster parameters
			clusterParams := framework.NewDefaultClusterParams20240610()
			clusterParams.ClusterName = customerClusterName
			managedResourceGroupName := framework.SuffixName(*resourceGroup.Name, "-managed", 64)
			clusterParams.ManagedResourceGroupName = managedResourceGroupName

			By("creating customer resources (infrastructure and managed identities) for cluster")
			clusterParams, err = tc.CreateClusterCustomerResources20240610(ctx,
				resourceGroup,
				clusterParams,
				map[string]interface{}{},
				TestArtifactsFS,
				framework.RBACScopeResourceGroup,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create customer resources for external auth cluster")

			By("creating the HCP cluster")
			err = tc.CreateHCPClusterFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				clusterParams,
				framework.ClusterCreationTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create HCP cluster for external auth test")

			By("getting credentials")
			adminRESTConfig, err := tc.GetAdminRESTConfigForHCPCluster20240610(
				ctx,
				tc.Get20240610ClientFactoryOrDie(ctx).NewHcpOpenShiftClustersClient(),
				*resourceGroup.Name,
				customerClusterName,
				framework.GetAdminRESTConfigTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to get admin REST config for cluster %s", customerClusterName)

			By("ensuring the cluster is viable")
			err = verifiers.VerifyHCPCluster(ctx, adminRESTConfig)
			Expect(err).NotTo(HaveOccurred(), "failed to verify HCP cluster viability")

			By("creating the node pool")
			nodePoolParams := framework.NewDefaultNodePoolParams20240610()
			nodePoolParams.ClusterName = customerClusterName
			nodePoolParams.NodePoolName = customerNodePoolName

			err = tc.CreateNodePoolFromParam20240610(ctx,
				GinkgoLogr,
				*resourceGroup.Name,
				managedResourceGroupName,
				customerClusterName,
				nodePoolParams,
				framework.NodePoolCreationTimeout,
			)
			Expect(err).NotTo(HaveOccurred(), "failed to create node pool %s", customerNodePoolName)

			By("creating an app registration with a client secret")
			app, sp, err := tc.NewAppRegistrationWithServicePrincipal(ctx)
			Expect(err).NotTo(HaveOccurred(), "failed to create app registration with service principal")

			graphClient, err := tc.GetGraphClient(ctx)
			Expect(err).NotTo(HaveOccurred(), "failed to get Microsoft Graph client")

			baseTime := time.Now()
			// Start time shifted 5 minutes into the past to handle clock skew between test runner and Graph API
			pass, err := graphClient.AddPassword(ctx, app.ID, "external-auth-pass", baseTime.Add(-5*time.Minute), baseTime.Add(24*time.Hour))
			Expect(err).NotTo(HaveOccurred(), "failed to add password to app registration")

			By("creating an external auth config with a prefix")
			extAuth := hcpsdk20240610preview.ExternalAuth{
				Properties: &hcpsdk20240610preview.ExternalAuthProperties{
					Issuer: &hcpsdk20240610preview.TokenIssuerProfile{
						URL:       to.Ptr(fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0", tc.TenantID())),
						Audiences: []*string{to.Ptr(app.AppID)},
					},
					Claim: &hcpsdk20240610preview.ExternalAuthClaimProfile{
						Mappings: &hcpsdk20240610preview.TokenClaimMappingsProfile{
							Username: &hcpsdk20240610preview.UsernameClaimProfile{
								Claim:        to.Ptr("sub"),                                                 // objectID of SP
								PrefixPolicy: to.Ptr(hcpsdk20240610preview.UsernameClaimPrefixPolicyPrefix), // TODO: ARO-21008 preventing us setting NoPrefix
								Prefix:       to.Ptr(externalAuthSubjectPrefix),
							},
							Groups: &hcpsdk20240610preview.GroupClaimProfile{
								Claim: to.Ptr("groups"),
							},
						},
					},
					Clients: []*hcpsdk20240610preview.ExternalAuthClientProfile{
						{
							ClientID: to.Ptr(app.AppID),
							Component: &hcpsdk20240610preview.ExternalAuthClientComponentProfile{
								Name:                to.Ptr("console"),
								AuthClientNamespace: to.Ptr("openshift-console"),
							},
							Type: to.Ptr(hcpsdk20240610preview.ExternalAuthClientTypeConfidential),
						},
						{
							ClientID: to.Ptr(app.AppID),
							Component: &hcpsdk20240610preview.ExternalAuthClientComponentProfile{
								Name:                to.Ptr("cli"),
								AuthClientNamespace: to.Ptr("openshift-console"),
							},
							Type: to.Ptr(hcpsdk20240610preview.ExternalAuthClientTypePublic),
						},
					},
				},
			}
			_, err = framework.CreateOrUpdateExternalAuthAndWait20240610(ctx, tc.Get20240610ClientFactoryOrDie(ctx).NewExternalAuthsClient(), *resourceGroup.Name, customerClusterName, customerExternalAuthName, extAuth, framework.ExternalAuthCreationTimeout)
			Expect(err).NotTo(HaveOccurred(), "failed to create external auth config %s", customerExternalAuthName)

			By("verifying ExternalAuth is in a Succeeded state")
			eaResult, err := framework.GetExternalAuth20240610(ctx, tc.Get20240610ClientFactoryOrDie(ctx).NewExternalAuthsClient(), *resourceGroup.Name, customerClusterName, customerExternalAuthName)
			Expect(err).NotTo(HaveOccurred(), "failed to get external auth config %s", customerExternalAuthName)
			Expect(*eaResult.Properties.ProvisioningState).To(Equal(hcpsdk20240610preview.ExternalAuthProvisioningStateSucceeded), "external auth %s provisioning state should be Succeeded", customerExternalAuthName)

			By("creating a cluster role binding for the entra application")
			err = framework.CreateClusterRoleBinding(ctx, externalAuthSubjectPrefix+sp.ID, adminRESTConfig)
			Expect(err).NotTo(HaveOccurred(), "failed to create cluster role binding for entra application")

			By("creating a rest config using OIDC authentication")
			Expect(tc.TenantID()).NotTo(BeEmpty(), "tenant ID must not be empty for OIDC authentication")
			cred, err := azidentity.NewClientSecretCredential(tc.TenantID(), app.AppID, pass.SecretText, nil)
			Expect(err).NotTo(HaveOccurred(), "failed to create client secret credential for OIDC authentication")

			// MSGraph is eventually consistent, wait up to 2 minutes for the token to be valid
			var accessToken azcore.AccessToken
			var lastTokenErr string
			Eventually(func() error {
				var err error
				accessToken, err = cred.GetToken(ctx, policy.TokenRequestOptions{
					Scopes: []string{fmt.Sprintf("%s/.default", app.AppID)},
				})

				if err != nil {
					if msg := err.Error(); msg != lastTokenErr {
						GinkgoWriter.Printf("GetToken failed: %v\n", err)
						lastTokenErr = msg
					}
				}
				return err
			}, 2*time.Minute, 10*time.Second).Should(Succeed())

			config := &rest.Config{
				Host:        adminRESTConfig.Host,
				BearerToken: accessToken.Token,
				// Copy TLS settings from admin config (handles self-signed certs in dev)
				TLSClientConfig: rest.TLSClientConfig{
					Insecure: adminRESTConfig.Insecure,
					CAData:   adminRESTConfig.CAData,
					CAFile:   adminRESTConfig.CAFile,
				},
			}
			client, err := kubernetes.NewForConfig(config)
			Expect(err).NotTo(HaveOccurred(), "failed to create Kubernetes client with OIDC token")

			// TODO (bvesel): ARO-21634
			// The kube-apiserver restarts on external auth config creation, so we need to wait
			// for it to completely restart. There doesn't appear to be a way to track this in the data plane
			By("confirming we can list namespaces using entra OIDC token")
			Eventually(func() error {
				_, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
				return err
			}, 5*time.Minute, 10*time.Second).Should(Succeed())

			By("creating the console OAuth client secret for external auth")
			adminClient, err := kubernetes.NewForConfig(adminRESTConfig)
			Expect(err).NotTo(HaveOccurred(), "failed to create admin Kubernetes client")
			consoleOAuthSecretName := fmt.Sprintf("%s-console-openshift-console", customerExternalAuthName)
			consoleOAuthSecret := &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      consoleOAuthSecretName,
					Namespace: "openshift-config",
				},
				StringData: map[string]string{
					"clientSecret": pass.SecretText,
				},
			}
			_, err = adminClient.CoreV1().Secrets("openshift-config").Create(ctx, consoleOAuthSecret, metav1.CreateOptions{})
			Expect(err).NotTo(HaveOccurred(), "failed to create console OAuth client secret %s", consoleOAuthSecretName)

			By("verifying cluster operators are available after external auth config creation")
			err = verifiers.VerifyAllClusterOperatorsAvailable().Verify(ctx, adminRESTConfig)
			Expect(err).NotTo(HaveOccurred(), "failed to verify cluster operators are available after external auth config creation")
		})
})
