package actionsgithubcom

import (
	"context"
	"crypto/tls"
	"encoding/base64"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"time"

	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/go-logr/logr"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	"github.com/actions/actions-runner-controller/apis/actions.github.com/v1alpha1"
	"github.com/actions/actions-runner-controller/build"
	"github.com/actions/actions-runner-controller/github/actions"
	"github.com/actions/actions-runner-controller/github/actions/fake"
	"github.com/actions/actions-runner-controller/github/actions/testserver"
)

const (
	autoscalingRunnerSetTestTimeout     = time.Second * 20
	autoscalingRunnerSetTestInterval    = time.Millisecond * 250
	autoscalingRunnerSetTestGitHubToken = "gh_token"
)

var _ = Describe("Test AutoScalingRunnerSet controller", Ordered, func() {
	var ctx context.Context
	var mgr ctrl.Manager
	var controller *AutoscalingRunnerSetReconciler
	var autoscalingNS *corev1.Namespace
	var autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet
	var configSecret *corev1.Secret

	var originalBuildVersion string
	buildVersion := "0.1.0"

	BeforeAll(func() {
		originalBuildVersion = build.Version
		build.Version = buildVersion
	})

	AfterAll(func() {
		build.Version = originalBuildVersion
	})

	BeforeEach(func() {
		ctx = context.Background()
		autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
		configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)

		controller = &AutoscalingRunnerSetReconciler{
			Client:                             mgr.GetClient(),
			Scheme:                             mgr.GetScheme(),
			Log:                                logf.Log,
			ControllerNamespace:                autoscalingNS.Name,
			DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
			ResourceBuilder: ResourceBuilder{
				SecretResolver: &SecretResolver{
					k8sClient:   k8sClient,
					multiClient: fake.NewMultiClient(),
				},
			},
		}
		err := controller.SetupWithManager(mgr)
		Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

		min := 1
		max := 10
		autoscalingRunnerSet = &v1alpha1.AutoscalingRunnerSet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-asrs",
				Namespace: autoscalingNS.Name,
				Labels: map[string]string{
					LabelKeyKubernetesVersion: buildVersion,
				},
			},
			Spec: v1alpha1.AutoscalingRunnerSetSpec{
				GitHubConfigUrl:    "https://github.com/owner/repo",
				GitHubConfigSecret: configSecret.Name,
				MaxRunners:         &max,
				MinRunners:         &min,
				RunnerGroup:        "testgroup",
				Template: corev1.PodTemplateSpec{
					Spec: corev1.PodSpec{
						Containers: []corev1.Container{
							{
								Name:  "runner",
								Image: "ghcr.io/actions/runner",
							},
						},
					},
				},
			},
		}

		err = k8sClient.Create(ctx, autoscalingRunnerSet)
		Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

		startManagers(GinkgoT(), mgr)
	})

	Context("When creating a new AutoScalingRunnerSet", func() {
		It("It should create/add all required resources for a new AutoScalingRunnerSet (finalizer, runnerscaleset, ephemeralrunnerset, listener)", func() {
			// Check if finalizer is added
			created := new(v1alpha1.AutoscalingRunnerSet)
			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, created)
					if err != nil {
						return "", err
					}
					if len(created.Finalizers) == 0 {
						return "", nil
					}
					return created.Finalizers[0], nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer")

			// Check if runner scale set is created on service
			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, created)
					if err != nil {
						return "", err
					}

					if _, ok := created.Annotations[runnerScaleSetIdAnnotationKey]; !ok {
						return "", nil
					}

					if _, ok := created.Annotations[AnnotationKeyGitHubRunnerGroupName]; !ok {
						return "", nil
					}

					return fmt.Sprintf("%s_%s", created.Annotations[runnerScaleSetIdAnnotationKey], created.Annotations[AnnotationKeyGitHubRunnerGroupName]), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("1_testgroup"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's annotation")

			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, created)
					if err != nil {
						return "", err
					}

					if _, ok := created.Labels[LabelKeyGitHubOrganization]; !ok {
						return "", nil
					}

					if _, ok := created.Labels[LabelKeyGitHubRepository]; !ok {
						return "", nil
					}

					return fmt.Sprintf("%s/%s", created.Labels[LabelKeyGitHubOrganization], created.Labels[LabelKeyGitHubRepository]), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("owner/repo"), "RunnerScaleSet should be created/fetched and update the AutoScalingRunnerSet's label")

			// Check if ephemeral runner set is created
			Eventually(
				func() (int, error) {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					if err != nil {
						return 0, err
					}

					return len(runnerSetList.Items), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created")

			// Check if listener is created
			Eventually(
				func() error {
					return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingListener))
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created")

			// Check if status is updated
			runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
			err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
			Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
			Expect(len(runnerSetList.Items)).To(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created")
		})
	})

	Context("When deleting a new AutoScalingRunnerSet", func() {
		It("It should cleanup all resources for a deleting AutoScalingRunnerSet before removing it", func() {
			// Wait till the listener is created
			Eventually(
				func() error {
					return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingListener))
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created")

			// Delete the AutoScalingRunnerSet
			err := k8sClient.Delete(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to delete AutoScalingRunnerSet")

			// Check if the listener is deleted
			Eventually(
				func() error {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingListener))
					if err != nil && errors.IsNotFound(err) {
						return nil
					}

					return fmt.Errorf("listener is not deleted")
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be deleted")

			// Check if all the EphemeralRunnerSet is deleted
			Eventually(
				func() error {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					if err != nil {
						return err
					}

					if len(runnerSetList.Items) != 0 {
						return fmt.Errorf("EphemeralRunnerSet is not deleted, count=%v", len(runnerSetList.Items))
					}

					return nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(Succeed(), "All EphemeralRunnerSet should be deleted")

			// Check if the AutoScalingRunnerSet is deleted
			Eventually(
				func() error {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingRunnerSet))
					if err != nil && errors.IsNotFound(err) {
						return nil
					}

					return fmt.Errorf("AutoScalingRunnerSet is not deleted")
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(Succeed(), "AutoScalingRunnerSet should be deleted")
		})
	})

	Context("When updating a new AutoScalingRunnerSet", func() {
		It("It should re-create EphemeralRunnerSet and Listener as needed when updating AutoScalingRunnerSet", func() {
			// Wait till the listener is created
			listener := new(v1alpha1.AutoscalingListener)
			Eventually(
				func() error {
					return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created")

			runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
			err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
			Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
			Expect(len(runnerSetList.Items)).To(Equal(1), "There should be 1 EphemeralRunnerSet")
			runnerSet := runnerSetList.Items[0]

			// Update the AutoScalingRunnerSet.Spec.Template
			// This should trigger re-creation of EphemeralRunnerSet and Listener
			patched := autoscalingRunnerSet.DeepCopy()
			patched.Spec.Template.Spec.PriorityClassName = "test-priority-class"
			if patched.Annotations == nil {
				patched.Annotations = make(map[string]string)
			}
			patched.Annotations[annotationKeyValuesHash] = "test-hash"
			err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
			Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
			autoscalingRunnerSet = patched.DeepCopy()

			// We should create a new EphemeralRunnerSet and delete the old one, eventually, we will have only one EphemeralRunnerSet
			Eventually(
				func() (string, error) {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					if err != nil {
						return "", err
					}

					if len(runnerSetList.Items) != 1 {
						return "", fmt.Errorf("We should have only 1 EphemeralRunnerSet, but got %v", len(runnerSetList.Items))
					}

					return runnerSetList.Items[0].Annotations[annotationKeyRunnerSpecHash], nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(runnerSet.Annotations[annotationKeyRunnerSpecHash]), "New EphemeralRunnerSet should be created")

			// We should create a new listener
			Eventually(
				func() (string, error) {
					listener := new(v1alpha1.AutoscalingListener)
					err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
					if err != nil {
						return "", err
					}

					return listener.Spec.EphemeralRunnerSetName, nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(runnerSet.Name), "New Listener should be created")

			// Only update the Spec for the AutoScalingListener
			// This should trigger re-creation of the Listener only
			runnerSetList = new(v1alpha1.EphemeralRunnerSetList)
			err = k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
			Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
			Expect(len(runnerSetList.Items)).To(Equal(1), "There should be 1 EphemeralRunnerSet")
			runnerSet = runnerSetList.Items[0]

			listener = new(v1alpha1.AutoscalingListener)
			err = k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
			Expect(err).NotTo(HaveOccurred(), "failed to get Listener")

			patched = autoscalingRunnerSet.DeepCopy()
			min := 10
			patched.Spec.MinRunners = &min
			err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
			Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")

			// We should not re-create a new EphemeralRunnerSet
			Consistently(
				func() (string, error) {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					if err != nil {
						return "", err
					}

					if len(runnerSetList.Items) != 1 {
						return "", fmt.Errorf("We should have only 1 EphemeralRunnerSet, but got %v", len(runnerSetList.Items))
					}

					return string(runnerSetList.Items[0].UID), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created")

			// We should only re-create a new listener
			Eventually(
				func() (string, error) {
					listener := new(v1alpha1.AutoscalingListener)
					err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
					if err != nil {
						return "", err
					}

					return string(listener.UID), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created")

			// Only update the values hash for the autoscaling runner set
			// This should trigger re-creation of the Listener only
			runnerSetList = new(v1alpha1.EphemeralRunnerSetList)
			err = k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
			Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
			Expect(len(runnerSetList.Items)).To(Equal(1), "There should be 1 EphemeralRunnerSet")
			runnerSet = runnerSetList.Items[0]

			listener = new(v1alpha1.AutoscalingListener)
			err = k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
			Expect(err).NotTo(HaveOccurred(), "failed to get Listener")

			patched = autoscalingRunnerSet.DeepCopy()
			patched.Annotations[annotationKeyValuesHash] = "hash-changes"
			err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
			Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")

			// We should not re-create a new EphemeralRunnerSet
			Consistently(
				func() (string, error) {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					if err != nil {
						return "", err
					}

					if len(runnerSetList.Items) != 1 {
						return "", fmt.Errorf("We should have only 1 EphemeralRunnerSet, but got %v", len(runnerSetList.Items))
					}

					return string(runnerSetList.Items[0].UID), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(BeEquivalentTo(string(runnerSet.UID)), "New EphemeralRunnerSet should not be created")

			// We should only re-create a new listener
			Eventually(
				func() (string, error) {
					listener := new(v1alpha1.AutoscalingListener)
					err := k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
					if err != nil {
						return "", err
					}

					return string(listener.UID), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).ShouldNot(BeEquivalentTo(string(listener.UID)), "New Listener should be created")
		})

		It("It should update RunnerScaleSet's runner group on service when it changes", func() {
			updated := new(v1alpha1.AutoscalingRunnerSet)
			// Wait till the listener is created
			Eventually(
				func() error {
					return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, new(v1alpha1.AutoscalingListener))
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(Succeed(), "Listener should be created")

			patched := autoscalingRunnerSet.DeepCopy()
			patched.Spec.RunnerGroup = "testgroup2"
			err := k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
			Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")

			// Check if AutoScalingRunnerSet has the new runner group in its annotation
			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, updated)
					if err != nil {
						return "", err
					}

					if _, ok := updated.Annotations[AnnotationKeyGitHubRunnerGroupName]; !ok {
						return "", nil
					}

					return updated.Annotations[AnnotationKeyGitHubRunnerGroupName], nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval).Should(BeEquivalentTo("testgroup2"), "AutoScalingRunnerSet should have the new runner group in its annotation")

			// delete the annotation and it should be re-added
			patched = autoscalingRunnerSet.DeepCopy()
			delete(patched.Annotations, AnnotationKeyGitHubRunnerGroupName)
			err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
			Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")

			// Check if AutoScalingRunnerSet still has the runner group in its annotation
			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, updated)
					if err != nil {
						return "", err
					}

					if _, ok := updated.Annotations[AnnotationKeyGitHubRunnerGroupName]; !ok {
						return "", nil
					}

					return updated.Annotations[AnnotationKeyGitHubRunnerGroupName], nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo("testgroup2"), "AutoScalingRunnerSet should have the runner group in its annotation")
		})
	})

	Context("When updating an AutoscalingRunnerSet with running or pending jobs", func() {
		It("It should wait for running and pending jobs to finish before applying the update. Update Strategy is set to eventual.", func() {
			// Switch update strategy to eventual (drain jobs )
			controller.UpdateStrategy = UpdateStrategyEventual
			// Wait till the listener is created
			listener := new(v1alpha1.AutoscalingListener)
			Eventually(
				func() error {
					return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(Succeed(), "Listener should be created")

			// Wait till the ephemeral runner set is created
			Eventually(
				func() (int, error) {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					if err != nil {
						return 0, err
					}

					return len(runnerSetList.Items), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo(1), "Only one EphemeralRunnerSet should be created")

			runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
			err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
			Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")

			// Emulate running and pending jobs
			runnerSet := runnerSetList.Items[0]
			activeRunnerSet := runnerSet.DeepCopy()
			activeRunnerSet.Status.CurrentReplicas = 6
			activeRunnerSet.Status.FailedEphemeralRunners = 1
			activeRunnerSet.Status.RunningEphemeralRunners = 2
			activeRunnerSet.Status.PendingEphemeralRunners = 3

			desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{
				CurrentRunners:          activeRunnerSet.Status.CurrentReplicas,
				State:                   "",
				PendingEphemeralRunners: activeRunnerSet.Status.PendingEphemeralRunners,
				RunningEphemeralRunners: activeRunnerSet.Status.RunningEphemeralRunners,
				FailedEphemeralRunners:  activeRunnerSet.Status.FailedEphemeralRunners,
			}

			err = k8sClient.Status().Patch(ctx, activeRunnerSet, client.MergeFrom(&runnerSet))
			Expect(err).NotTo(HaveOccurred(), "Failed to patch runner set status")

			Eventually(
				func() (v1alpha1.AutoscalingRunnerSetStatus, error) {
					updated := new(v1alpha1.AutoscalingRunnerSet)
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, updated)
					if err != nil {
						return v1alpha1.AutoscalingRunnerSetStatus{}, fmt.Errorf("failed to get AutoScalingRunnerSet: %w", err)
					}
					return updated.Status, nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo(desiredStatus), "AutoScalingRunnerSet status should be updated")

			// Patch the AutoScalingRunnerSet image which should trigger
			// the recreation of the Listener and EphemeralRunnerSet
			patched := autoscalingRunnerSet.DeepCopy()
			if patched.Annotations == nil {
				patched.Annotations = make(map[string]string)
			}
			patched.Annotations[annotationKeyValuesHash] = "testgroup2"
			patched.Spec.Template.Spec = corev1.PodSpec{
				Containers: []corev1.Container{
					{
						Name:  "runner",
						Image: "ghcr.io/actions/abcd:1.1.1",
					},
				},
			}
			err = k8sClient.Patch(ctx, patched, client.MergeFrom(autoscalingRunnerSet))
			Expect(err).NotTo(HaveOccurred(), "failed to patch AutoScalingRunnerSet")
			autoscalingRunnerSet = patched.DeepCopy()

			// The EphemeralRunnerSet should not be recreated
			Consistently(
				func() (string, error) {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					Expect(err).NotTo(HaveOccurred(), "failed to fetch AutoScalingRunnerSet")
					return runnerSetList.Items[0].Name, nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(Equal(activeRunnerSet.Name), "The EphemeralRunnerSet should not be recreated")

			// The listener should not be recreated
			Consistently(
				func() error {
					return k8sClient.Get(ctx, client.ObjectKey{Name: scaleSetListenerName(autoscalingRunnerSet), Namespace: autoscalingRunnerSet.Namespace}, listener)
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).ShouldNot(Succeed(), "Listener should not be recreated")
		})
	})

	It("Should update Status on EphemeralRunnerSet status Update", func() {
		ars := new(v1alpha1.AutoscalingRunnerSet)
		Eventually(
			func() (bool, error) {
				err := k8sClient.Get(
					ctx,
					client.ObjectKey{
						Name:      autoscalingRunnerSet.Name,
						Namespace: autoscalingRunnerSet.Namespace,
					},
					ars,
				)
				if err != nil {
					return false, err
				}
				return true, nil
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue(), "AutoscalingRunnerSet should be created")

		runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
		Eventually(func() (int, error) {
			err := k8sClient.List(ctx, runnerSetList, client.InNamespace(ars.Namespace))
			if err != nil {
				return 0, err
			}
			return len(runnerSetList.Items), nil
		},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeEquivalentTo(1), "Failed to fetch runner set list")

		runnerSet := runnerSetList.Items[0]
		statusUpdate := runnerSet.DeepCopy()
		statusUpdate.Status.CurrentReplicas = 6
		statusUpdate.Status.FailedEphemeralRunners = 1
		statusUpdate.Status.RunningEphemeralRunners = 2
		statusUpdate.Status.PendingEphemeralRunners = 3

		desiredStatus := v1alpha1.AutoscalingRunnerSetStatus{
			CurrentRunners:          statusUpdate.Status.CurrentReplicas,
			State:                   "",
			PendingEphemeralRunners: statusUpdate.Status.PendingEphemeralRunners,
			RunningEphemeralRunners: statusUpdate.Status.RunningEphemeralRunners,
			FailedEphemeralRunners:  statusUpdate.Status.FailedEphemeralRunners,
		}

		err := k8sClient.Status().Patch(ctx, statusUpdate, client.MergeFrom(&runnerSet))
		Expect(err).NotTo(HaveOccurred(), "Failed to patch runner set status")

		Eventually(
			func() (v1alpha1.AutoscalingRunnerSetStatus, error) {
				updated := new(v1alpha1.AutoscalingRunnerSet)
				err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, updated)
				if err != nil {
					return v1alpha1.AutoscalingRunnerSetStatus{}, fmt.Errorf("failed to get AutoScalingRunnerSet: %w", err)
				}
				return updated.Status, nil
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeEquivalentTo(desiredStatus), "AutoScalingRunnerSet status should be updated")
	})
})

var _ = Describe("Test AutoScalingController updates", Ordered, func() {
	var originalBuildVersion string
	buildVersion := "0.1.0"

	BeforeAll(func() {
		originalBuildVersion = build.Version
		build.Version = buildVersion
	})

	AfterAll(func() {
		build.Version = originalBuildVersion
	})

	Context("Creating autoscaling runner set with RunnerScaleSetName set", func() {
		var ctx context.Context
		var mgr ctrl.Manager
		var autoscalingNS *corev1.Namespace
		var autoscalingRunnerSet *v1alpha1.AutoscalingRunnerSet
		var configSecret *corev1.Secret

		BeforeEach(func() {
			originalBuildVersion = build.Version
			ctx = context.Background()
			autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
			configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)

			multiClient := fake.NewMultiClient(
				fake.WithDefaultClient(
					fake.NewFakeClient(
						fake.WithUpdateRunnerScaleSet(
							&actions.RunnerScaleSet{
								Id:                 1,
								Name:               "testset_update",
								RunnerGroupId:      1,
								RunnerGroupName:    "testgroup",
								Labels:             []actions.Label{{Type: "test", Name: "test"}},
								RunnerSetting:      actions.RunnerSetting{},
								CreatedOn:          time.Now(),
								RunnerJitConfigUrl: "test.test.test",
								Statistics:         nil,
							},
							nil,
						),
					),
					nil,
				),
			)

			controller := &AutoscalingRunnerSetReconciler{
				Client:                             mgr.GetClient(),
				Scheme:                             mgr.GetScheme(),
				Log:                                logf.Log,
				ControllerNamespace:                autoscalingNS.Name,
				DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
				ResourceBuilder: ResourceBuilder{
					SecretResolver: &SecretResolver{
						k8sClient:   k8sClient,
						multiClient: multiClient,
					},
				},
			}
			err := controller.SetupWithManager(mgr)
			Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

			startManagers(GinkgoT(), mgr)
		})

		It("It should be create AutoScalingRunnerSet and has annotation for the RunnerScaleSetName", func() {
			min := 1
			max := 10
			autoscalingRunnerSet = &v1alpha1.AutoscalingRunnerSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-asrs",
					Namespace: autoscalingNS.Name,
					Labels: map[string]string{
						LabelKeyKubernetesVersion: buildVersion,
					},
				},
				Spec: v1alpha1.AutoscalingRunnerSetSpec{
					GitHubConfigUrl:    "https://github.com/owner/repo",
					GitHubConfigSecret: configSecret.Name,
					MaxRunners:         &max,
					MinRunners:         &min,
					RunnerScaleSetName: "testset",
					RunnerGroup:        "testgroup",
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "runner",
									Image: "ghcr.io/actions/runner",
								},
							},
						},
					},
				},
			}

			err := k8sClient.Create(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

			// Wait for the AutoScalingRunnerSet to be created with right annotation
			ars := new(v1alpha1.AutoscalingRunnerSet)
			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, ars)
					if err != nil {
						return "", err
					}

					if val, ok := ars.Annotations[AnnotationKeyGitHubRunnerScaleSetName]; ok {
						return val, nil
					}

					return "", nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo(autoscalingRunnerSet.Spec.RunnerScaleSetName), "AutoScalingRunnerSet should have annotation for the RunnerScaleSetName")

			update := autoscalingRunnerSet.DeepCopy()
			update.Spec.RunnerScaleSetName = "testset_update"

			err = k8sClient.Patch(ctx, update, client.MergeFrom(autoscalingRunnerSet))
			Expect(err).NotTo(HaveOccurred(), "failed to update AutoScalingRunnerSet")

			// Wait for the AutoScalingRunnerSet to be updated with right annotation
			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, ars)
					if err != nil {
						return "", err
					}

					if val, ok := ars.Annotations[AnnotationKeyGitHubRunnerScaleSetName]; ok {
						return val, nil
					}

					return "", nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo(update.Spec.RunnerScaleSetName), "AutoScalingRunnerSet should have a updated annotation for the RunnerScaleSetName")
		})
	})
})

var _ = Describe("Test AutoscalingController creation failures", Ordered, func() {
	var originalBuildVersion string
	buildVersion := "0.1.0"

	BeforeAll(func() {
		originalBuildVersion = build.Version
		build.Version = buildVersion
	})

	AfterAll(func() {
		build.Version = originalBuildVersion
	})
	Context("When autoscaling runner set creation fails on the client", func() {
		var ctx context.Context
		var mgr ctrl.Manager
		var autoscalingNS *corev1.Namespace

		BeforeEach(func() {
			ctx = context.Background()
			autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)

			controller := &AutoscalingRunnerSetReconciler{
				Client:                             mgr.GetClient(),
				Scheme:                             mgr.GetScheme(),
				Log:                                logf.Log,
				ControllerNamespace:                autoscalingNS.Name,
				DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
				ResourceBuilder: ResourceBuilder{
					SecretResolver: &SecretResolver{
						k8sClient:   k8sClient,
						multiClient: fake.NewMultiClient(),
					},
				},
			}
			err := controller.SetupWithManager(mgr)
			Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

			startManagers(GinkgoT(), mgr)
		})

		It("It should be able to clean up if annotation related to scale set id does not exist", func() {
			min := 1
			max := 10
			autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-asrs",
					Namespace: autoscalingNS.Name,
					Labels: map[string]string{
						LabelKeyKubernetesVersion: buildVersion,
					},
				},
				Spec: v1alpha1.AutoscalingRunnerSetSpec{
					GitHubConfigUrl: "https://github.com/owner/repo",
					MaxRunners:      &max,
					MinRunners:      &min,
					RunnerGroup:     "testgroup",
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "runner",
									Image: "ghcr.io/actions/runner",
								},
							},
						},
					},
				},
			}

			err := k8sClient.Create(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

			// wait for the finalizer to be added
			ars := new(v1alpha1.AutoscalingRunnerSet)
			Eventually(
				func() (string, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, ars)
					if err != nil {
						return "", err
					}
					if len(ars.Finalizers) == 0 {
						return "", nil
					}
					return ars.Finalizers[0], nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer")

			ars.Annotations = make(map[string]string)
			err = k8sClient.Update(ctx, ars)
			Expect(err).NotTo(HaveOccurred(), "Update autoscaling runner set without annotation should be successful")

			Eventually(
				func() (bool, error) {
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, ars)
					if err != nil {
						return false, err
					}
					return len(ars.ObjectMeta.Annotations) == 0, nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo(true), "Autoscaling runner set should be updated with empty annotations")

			err = k8sClient.Delete(ctx, ars)
			Expect(err).NotTo(HaveOccurred(), "Delete autoscaling runner set should be successful")

			Eventually(
				func() (bool, error) {
					updated := new(v1alpha1.AutoscalingRunnerSet)
					err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, updated)
					if err == nil {
						return false, nil
					}
					if !errors.IsNotFound(err) {
						return false, err
					}

					return !controllerutil.ContainsFinalizer(updated, autoscalingRunnerSetFinalizerName), nil
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(BeEquivalentTo(true), "Finalizer and resource should eventually be deleted")
		})
	})
})

var _ = Describe("Test client optional configuration", Ordered, func() {
	var originalBuildVersion string
	buildVersion := "0.1.0"

	BeforeAll(func() {
		originalBuildVersion = build.Version
		build.Version = buildVersion
	})

	AfterAll(func() {
		build.Version = originalBuildVersion
	})
	Context("When specifying a proxy", func() {
		var ctx context.Context
		var mgr ctrl.Manager
		var autoscalingNS *corev1.Namespace
		var configSecret *corev1.Secret
		var controller *AutoscalingRunnerSetReconciler

		BeforeEach(func() {
			ctx = context.Background()
			autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
			configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)
			multiClient := actions.NewMultiClient(logr.Discard())
			controller = &AutoscalingRunnerSetReconciler{
				Client:                             mgr.GetClient(),
				Scheme:                             mgr.GetScheme(),
				Log:                                logf.Log,
				ControllerNamespace:                autoscalingNS.Name,
				DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
				ResourceBuilder: ResourceBuilder{
					SecretResolver: &SecretResolver{
						k8sClient:   k8sClient,
						multiClient: multiClient,
					},
				},
			}

			err := controller.SetupWithManager(mgr)
			Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

			startManagers(GinkgoT(), mgr)
		})

		It("should be able to make requests to a server using a proxy", func() {
			serverSuccessfullyCalled := false
			proxy := testserver.New(GinkgoT(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				serverSuccessfullyCalled = true
				w.WriteHeader(http.StatusOK)
			}))

			min := 1
			max := 10
			autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-asrs",
					Namespace: autoscalingNS.Name,
					Labels: map[string]string{
						LabelKeyKubernetesVersion: buildVersion,
					},
				},
				Spec: v1alpha1.AutoscalingRunnerSetSpec{
					GitHubConfigUrl:    "http://example.com/org/repo",
					GitHubConfigSecret: configSecret.Name,
					MaxRunners:         &max,
					MinRunners:         &min,
					RunnerGroup:        "testgroup",
					Proxy: &v1alpha1.ProxyConfig{
						HTTP: &v1alpha1.ProxyServerConfig{
							Url: proxy.URL,
						},
					},
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "runner",
									Image: "ghcr.io/actions/runner",
								},
							},
						},
					},
				},
			}

			err := k8sClient.Create(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

			// wait for server to be called
			Eventually(
				func() (bool, error) {
					return serverSuccessfullyCalled, nil
				},
				autoscalingRunnerSetTestTimeout,
				1*time.Nanosecond,
			).Should(BeTrue(), "server was not called")
		})

		It("should be able to make requests to a server using a proxy with user info", func() {
			serverSuccessfullyCalled := false
			proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				header := r.Header.Get("Proxy-Authorization")
				Expect(header).NotTo(BeEmpty())

				header = strings.TrimPrefix(header, "Basic ")
				decoded, err := base64.StdEncoding.DecodeString(header)
				Expect(err).NotTo(HaveOccurred())
				Expect(string(decoded)).To(Equal("test:password"))

				serverSuccessfullyCalled = true
				w.WriteHeader(http.StatusOK)
			}))
			GinkgoT().Cleanup(func() {
				proxy.Close()
			})

			secretCredentials := &corev1.Secret{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "proxy-credentials",
					Namespace: autoscalingNS.Name,
				},
				Data: map[string][]byte{
					"username": []byte("test"),
					"password": []byte("password"),
				},
			}

			err := k8sClient.Create(ctx, secretCredentials)
			Expect(err).NotTo(HaveOccurred(), "failed to create secret credentials")

			min := 1
			max := 10
			autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-asrs",
					Namespace: autoscalingNS.Name,
					Labels: map[string]string{
						LabelKeyKubernetesVersion: buildVersion,
					},
				},
				Spec: v1alpha1.AutoscalingRunnerSetSpec{
					GitHubConfigUrl:    "http://example.com/org/repo",
					GitHubConfigSecret: configSecret.Name,
					MaxRunners:         &max,
					MinRunners:         &min,
					RunnerGroup:        "testgroup",
					Proxy: &v1alpha1.ProxyConfig{
						HTTP: &v1alpha1.ProxyServerConfig{
							Url:                 proxy.URL,
							CredentialSecretRef: "proxy-credentials",
						},
					},
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "runner",
									Image: "ghcr.io/actions/runner",
								},
							},
						},
					},
				},
			}

			err = k8sClient.Create(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

			// wait for server to be called
			Eventually(
				func() (bool, error) {
					return serverSuccessfullyCalled, nil
				},
				autoscalingRunnerSetTestTimeout,
				1*time.Nanosecond,
			).Should(BeTrue(), "server was not called")
		})
	})

	Context("When specifying a configmap for root CAs", func() {
		var ctx context.Context
		var mgr ctrl.Manager
		var autoscalingNS *corev1.Namespace
		var configSecret *corev1.Secret
		var rootCAConfigMap *corev1.ConfigMap
		var controller *AutoscalingRunnerSetReconciler

		BeforeEach(func() {
			ctx = context.Background()
			autoscalingNS, mgr = createNamespace(GinkgoT(), k8sClient)
			configSecret = createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)

			cert, err := os.ReadFile(filepath.Join(
				"../../",
				"github",
				"actions",
				"testdata",
				"rootCA.crt",
			))
			Expect(err).NotTo(HaveOccurred(), "failed to read root CA cert")
			rootCAConfigMap = &corev1.ConfigMap{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "root-ca-configmap",
					Namespace: autoscalingNS.Name,
				},
				Data: map[string]string{
					"rootCA.crt": string(cert),
				},
			}
			err = k8sClient.Create(ctx, rootCAConfigMap)
			Expect(err).NotTo(HaveOccurred(), "failed to create configmap with root CAs")

			controller = &AutoscalingRunnerSetReconciler{
				Client:                             mgr.GetClient(),
				Scheme:                             mgr.GetScheme(),
				Log:                                logf.Log,
				ControllerNamespace:                autoscalingNS.Name,
				DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
				ResourceBuilder: ResourceBuilder{
					SecretResolver: &SecretResolver{
						k8sClient:   k8sClient,
						multiClient: fake.NewMultiClient(),
					},
				},
			}
			err = controller.SetupWithManager(mgr)
			Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

			startManagers(GinkgoT(), mgr)
		})

		It("should be able to make requests to a server using root CAs", func() {
			controller.SecretResolver = &SecretResolver{
				k8sClient:   k8sClient,
				multiClient: actions.NewMultiClient(logr.Discard()),
			}

			certsFolder := filepath.Join(
				"../../",
				"github",
				"actions",
				"testdata",
			)
			certPath := filepath.Join(certsFolder, "server.crt")
			keyPath := filepath.Join(certsFolder, "server.key")

			serverSuccessfullyCalled := false
			server := testserver.NewUnstarted(GinkgoT(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				serverSuccessfullyCalled = true
				w.WriteHeader(http.StatusOK)
			}))
			cert, err := tls.LoadX509KeyPair(certPath, keyPath)
			Expect(err).NotTo(HaveOccurred(), "failed to load server cert")

			server.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
			server.StartTLS()

			min := 1
			max := 10
			autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-asrs",
					Namespace: autoscalingNS.Name,
					Labels: map[string]string{
						LabelKeyKubernetesVersion: buildVersion,
					},
				},
				Spec: v1alpha1.AutoscalingRunnerSetSpec{
					GitHubConfigUrl:    server.ConfigURLForOrg("my-org"),
					GitHubConfigSecret: configSecret.Name,
					GitHubServerTLS: &v1alpha1.TLSConfig{
						CertificateFrom: &v1alpha1.TLSCertificateSource{
							ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
								LocalObjectReference: corev1.LocalObjectReference{
									Name: rootCAConfigMap.Name,
								},
								Key: "rootCA.crt",
							},
						},
					},
					MaxRunners:  &max,
					MinRunners:  &min,
					RunnerGroup: "testgroup",
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "runner",
									Image: "ghcr.io/actions/runner",
								},
							},
						},
					},
				},
			}

			err = k8sClient.Create(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

			// wait for server to be called
			Eventually(
				func() (bool, error) {
					return serverSuccessfullyCalled, nil
				},
				autoscalingRunnerSetTestTimeout,
				1*time.Nanosecond,
			).Should(BeTrue(), "server was not called")
		})

		It("it creates a listener referencing the right configmap for TLS", func() {
			min := 1
			max := 10
			autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-asrs",
					Namespace: autoscalingNS.Name,
					Labels: map[string]string{
						LabelKeyKubernetesVersion: buildVersion,
					},
				},
				Spec: v1alpha1.AutoscalingRunnerSetSpec{
					GitHubConfigUrl:    "https://github.com/owner/repo",
					GitHubConfigSecret: configSecret.Name,
					GitHubServerTLS: &v1alpha1.TLSConfig{
						CertificateFrom: &v1alpha1.TLSCertificateSource{
							ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
								LocalObjectReference: corev1.LocalObjectReference{
									Name: rootCAConfigMap.Name,
								},
								Key: "rootCA.crt",
							},
						},
					},
					MaxRunners:  &max,
					MinRunners:  &min,
					RunnerGroup: "testgroup",
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "runner",
									Image: "ghcr.io/actions/runner",
								},
							},
						},
					},
				},
			}

			err := k8sClient.Create(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

			Eventually(
				func(g Gomega) {
					listener := new(v1alpha1.AutoscalingListener)
					err := k8sClient.Get(
						ctx,
						client.ObjectKey{
							Name:      scaleSetListenerName(autoscalingRunnerSet),
							Namespace: autoscalingRunnerSet.Namespace,
						},
						listener,
					)
					g.Expect(err).NotTo(HaveOccurred(), "failed to get listener")

					g.Expect(listener.Spec.GitHubServerTLS).NotTo(BeNil(), "listener does not have TLS config")
					g.Expect(listener.Spec.GitHubServerTLS).To(BeEquivalentTo(autoscalingRunnerSet.Spec.GitHubServerTLS), "listener does not have TLS config")
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(Succeed(), "tls config is incorrect")
		})

		It("it creates an ephemeral runner set referencing the right configmap for TLS", func() {
			min := 1
			max := 10
			autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "test-asrs",
					Namespace: autoscalingNS.Name,
					Labels: map[string]string{
						LabelKeyKubernetesVersion: buildVersion,
					},
				},
				Spec: v1alpha1.AutoscalingRunnerSetSpec{
					GitHubConfigUrl:    "https://github.com/owner/repo",
					GitHubConfigSecret: configSecret.Name,
					GitHubServerTLS: &v1alpha1.TLSConfig{
						CertificateFrom: &v1alpha1.TLSCertificateSource{
							ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
								LocalObjectReference: corev1.LocalObjectReference{
									Name: rootCAConfigMap.Name,
								},
								Key: "rootCA.crt",
							},
						},
					},
					MaxRunners:  &max,
					MinRunners:  &min,
					RunnerGroup: "testgroup",
					Template: corev1.PodTemplateSpec{
						Spec: corev1.PodSpec{
							Containers: []corev1.Container{
								{
									Name:  "runner",
									Image: "ghcr.io/actions/runner",
								},
							},
						},
					},
				},
			}

			err := k8sClient.Create(ctx, autoscalingRunnerSet)
			Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

			Eventually(
				func(g Gomega) {
					runnerSetList := new(v1alpha1.EphemeralRunnerSetList)
					err := k8sClient.List(ctx, runnerSetList, client.InNamespace(autoscalingRunnerSet.Namespace))
					g.Expect(err).NotTo(HaveOccurred(), "failed to list EphemeralRunnerSet")
					g.Expect(runnerSetList.Items).To(HaveLen(1), "expected 1 EphemeralRunnerSet to be created")

					runnerSet := &runnerSetList.Items[0]

					g.Expect(runnerSet.Spec.EphemeralRunnerSpec.GitHubServerTLS).NotTo(BeNil(), "expected EphemeralRunnerSpec.GitHubServerTLS to be set")
					g.Expect(runnerSet.Spec.EphemeralRunnerSpec.GitHubServerTLS).To(BeEquivalentTo(autoscalingRunnerSet.Spec.GitHubServerTLS), "EphemeralRunnerSpec does not have TLS config")
				},
				autoscalingRunnerSetTestTimeout,
				autoscalingRunnerSetTestInterval,
			).Should(Succeed())
		})
	})
})

var _ = Describe("Test external permissions cleanup", Ordered, func() {
	var originalBuildVersion string
	buildVersion := "0.1.0"

	BeforeAll(func() {
		originalBuildVersion = build.Version
		build.Version = buildVersion
	})

	AfterAll(func() {
		build.Version = originalBuildVersion
	})

	It("Should clean up kubernetes mode permissions", func() {
		ctx := context.Background()
		autoscalingNS, mgr := createNamespace(GinkgoT(), k8sClient)

		configSecret := createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)

		controller := &AutoscalingRunnerSetReconciler{
			Client:                             mgr.GetClient(),
			Scheme:                             mgr.GetScheme(),
			Log:                                logf.Log,
			ControllerNamespace:                autoscalingNS.Name,
			DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
			ResourceBuilder: ResourceBuilder{
				SecretResolver: &SecretResolver{
					k8sClient:   k8sClient,
					multiClient: fake.NewMultiClient(),
				},
			},
		}
		err := controller.SetupWithManager(mgr)
		Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

		startManagers(GinkgoT(), mgr)

		min := 1
		max := 10
		autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-asrs",
				Namespace: autoscalingNS.Name,
				Labels: map[string]string{
					"app.kubernetes.io/name":  "gha-runner-scale-set",
					LabelKeyKubernetesVersion: buildVersion,
				},
				Annotations: map[string]string{
					AnnotationKeyKubernetesModeRoleBindingName:    "kube-mode-role-binding",
					AnnotationKeyKubernetesModeRoleName:           "kube-mode-role",
					AnnotationKeyKubernetesModeServiceAccountName: "kube-mode-service-account",
				},
			},
			Spec: v1alpha1.AutoscalingRunnerSetSpec{
				GitHubConfigUrl:    "https://github.com/owner/repo",
				GitHubConfigSecret: configSecret.Name,
				MaxRunners:         &max,
				MinRunners:         &min,
				RunnerGroup:        "testgroup",
				Template: corev1.PodTemplateSpec{
					Spec: corev1.PodSpec{
						Containers: []corev1.Container{
							{
								Name:  "runner",
								Image: "ghcr.io/actions/runner",
							},
						},
					},
				},
			},
		}

		role := &rbacv1.Role{
			ObjectMeta: metav1.ObjectMeta{
				Name:       autoscalingRunnerSet.Annotations[AnnotationKeyKubernetesModeRoleName],
				Namespace:  autoscalingRunnerSet.Namespace,
				Finalizers: []string{AutoscalingRunnerSetCleanupFinalizerName},
			},
		}

		err = k8sClient.Create(ctx, role)
		Expect(err).NotTo(HaveOccurred(), "failed to create kubernetes mode role")

		serviceAccount := &corev1.ServiceAccount{
			ObjectMeta: metav1.ObjectMeta{
				Name:       autoscalingRunnerSet.Annotations[AnnotationKeyKubernetesModeServiceAccountName],
				Namespace:  autoscalingRunnerSet.Namespace,
				Finalizers: []string{AutoscalingRunnerSetCleanupFinalizerName},
			},
		}

		err = k8sClient.Create(ctx, serviceAccount)
		Expect(err).NotTo(HaveOccurred(), "failed to create kubernetes mode service account")

		roleBinding := &rbacv1.RoleBinding{
			ObjectMeta: metav1.ObjectMeta{
				Name:       autoscalingRunnerSet.Annotations[AnnotationKeyKubernetesModeRoleBindingName],
				Namespace:  autoscalingRunnerSet.Namespace,
				Finalizers: []string{AutoscalingRunnerSetCleanupFinalizerName},
			},
			Subjects: []rbacv1.Subject{
				{
					Kind:      "ServiceAccount",
					Name:      serviceAccount.Name,
					Namespace: serviceAccount.Namespace,
				},
			},
			RoleRef: rbacv1.RoleRef{
				APIGroup: rbacv1.GroupName,
				// Kind is the type of resource being referenced
				Kind: "Role",
				Name: role.Name,
			},
		}

		err = k8sClient.Create(ctx, roleBinding)
		Expect(err).NotTo(HaveOccurred(), "failed to create kubernetes mode role binding")

		err = k8sClient.Create(ctx, autoscalingRunnerSet)
		Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

		Eventually(
			func() (string, error) {
				created := new(v1alpha1.AutoscalingRunnerSet)
				err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, created)
				if err != nil {
					return "", err
				}
				if len(created.Finalizers) == 0 {
					return "", nil
				}
				return created.Finalizers[0], nil
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer")

		err = k8sClient.Delete(ctx, autoscalingRunnerSet)
		Expect(err).NotTo(HaveOccurred(), "failed to delete autoscaling runner set")

		err = k8sClient.Delete(ctx, roleBinding)
		Expect(err).NotTo(HaveOccurred(), "failed to delete kubernetes mode role binding")

		err = k8sClient.Delete(ctx, role)
		Expect(err).NotTo(HaveOccurred(), "failed to delete kubernetes mode role")

		err = k8sClient.Delete(ctx, serviceAccount)
		Expect(err).NotTo(HaveOccurred(), "failed to delete kubernetes mode service account")

		Eventually(
			func() bool {
				r := new(rbacv1.RoleBinding)
				err := k8sClient.Get(ctx, types.NamespacedName{
					Name:      roleBinding.Name,
					Namespace: roleBinding.Namespace,
				}, r)

				return errors.IsNotFound(err)
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue(), "Expected role binding to be cleaned up")

		Eventually(
			func() bool {
				r := new(rbacv1.Role)
				err := k8sClient.Get(ctx, types.NamespacedName{
					Name:      role.Name,
					Namespace: role.Namespace,
				}, r)

				return errors.IsNotFound(err)
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue(), "Expected role to be cleaned up")
	})

	It("Should clean up manager permissions and no-permission service account", func() {
		ctx := context.Background()
		autoscalingNS, mgr := createNamespace(GinkgoT(), k8sClient)

		controller := &AutoscalingRunnerSetReconciler{
			Client:                             mgr.GetClient(),
			Scheme:                             mgr.GetScheme(),
			Log:                                logf.Log,
			ControllerNamespace:                autoscalingNS.Name,
			DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
			ResourceBuilder: ResourceBuilder{
				SecretResolver: &SecretResolver{
					k8sClient:   k8sClient,
					multiClient: fake.NewMultiClient(),
				},
			},
		}
		err := controller.SetupWithManager(mgr)
		Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

		startManagers(GinkgoT(), mgr)

		min := 1
		max := 10
		autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-asrs",
				Namespace: autoscalingNS.Name,
				Labels: map[string]string{
					"app.kubernetes.io/name":  "gha-runner-scale-set",
					LabelKeyKubernetesVersion: buildVersion,
				},
				Annotations: map[string]string{
					AnnotationKeyManagerRoleName:                "manager-role",
					AnnotationKeyManagerRoleBindingName:         "manager-role-binding",
					AnnotationKeyGitHubSecretName:               "gh-secret-name",
					AnnotationKeyNoPermissionServiceAccountName: "no-permission-sa",
				},
			},
			Spec: v1alpha1.AutoscalingRunnerSetSpec{
				GitHubConfigUrl: "https://github.com/owner/repo",
				MaxRunners:      &max,
				MinRunners:      &min,
				RunnerGroup:     "testgroup",
				Template: corev1.PodTemplateSpec{
					Spec: corev1.PodSpec{
						Containers: []corev1.Container{
							{
								Name:  "runner",
								Image: "ghcr.io/actions/runner",
							},
						},
					},
				},
			},
		}

		secret := &corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:       autoscalingRunnerSet.Annotations[AnnotationKeyGitHubSecretName],
				Namespace:  autoscalingRunnerSet.Namespace,
				Finalizers: []string{AutoscalingRunnerSetCleanupFinalizerName},
			},
			Data: map[string][]byte{
				"github_token": []byte(defaultGitHubToken),
			},
		}

		err = k8sClient.Create(context.Background(), secret)
		Expect(err).NotTo(HaveOccurred(), "failed to create github secret")

		autoscalingRunnerSet.Spec.GitHubConfigSecret = secret.Name

		role := &rbacv1.Role{
			ObjectMeta: metav1.ObjectMeta{
				Name:       autoscalingRunnerSet.Annotations[AnnotationKeyManagerRoleName],
				Namespace:  autoscalingRunnerSet.Namespace,
				Finalizers: []string{AutoscalingRunnerSetCleanupFinalizerName},
			},
		}

		err = k8sClient.Create(ctx, role)
		Expect(err).NotTo(HaveOccurred(), "failed to create manager role")

		roleBinding := &rbacv1.RoleBinding{
			ObjectMeta: metav1.ObjectMeta{
				Name:       autoscalingRunnerSet.Annotations[AnnotationKeyManagerRoleBindingName],
				Namespace:  autoscalingRunnerSet.Namespace,
				Finalizers: []string{AutoscalingRunnerSetCleanupFinalizerName},
			},
			RoleRef: rbacv1.RoleRef{
				APIGroup: rbacv1.GroupName,
				Kind:     "Role",
				Name:     role.Name,
			},
		}

		err = k8sClient.Create(ctx, roleBinding)
		Expect(err).NotTo(HaveOccurred(), "failed to create manager role binding")

		noPermissionServiceAccount := &corev1.ServiceAccount{
			ObjectMeta: metav1.ObjectMeta{
				Name:       autoscalingRunnerSet.Annotations[AnnotationKeyNoPermissionServiceAccountName],
				Namespace:  autoscalingRunnerSet.Namespace,
				Finalizers: []string{AutoscalingRunnerSetCleanupFinalizerName},
			},
		}

		err = k8sClient.Create(ctx, noPermissionServiceAccount)
		Expect(err).NotTo(HaveOccurred(), "failed to create no permission service account")

		err = k8sClient.Create(ctx, autoscalingRunnerSet)
		Expect(err).NotTo(HaveOccurred(), "failed to create AutoScalingRunnerSet")

		Eventually(
			func() (string, error) {
				created := new(v1alpha1.AutoscalingRunnerSet)
				err := k8sClient.Get(ctx, client.ObjectKey{Name: autoscalingRunnerSet.Name, Namespace: autoscalingRunnerSet.Namespace}, created)
				if err != nil {
					return "", err
				}
				if len(created.Finalizers) == 0 {
					return "", nil
				}
				return created.Finalizers[0], nil
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeEquivalentTo(autoscalingRunnerSetFinalizerName), "AutoScalingRunnerSet should have a finalizer")

		err = k8sClient.Delete(ctx, autoscalingRunnerSet)
		Expect(err).NotTo(HaveOccurred(), "failed to delete autoscaling runner set")

		err = k8sClient.Delete(ctx, noPermissionServiceAccount)
		Expect(err).NotTo(HaveOccurred(), "failed to delete no permission service account")

		err = k8sClient.Delete(ctx, secret)
		Expect(err).NotTo(HaveOccurred(), "failed to delete GitHub secret")

		err = k8sClient.Delete(ctx, roleBinding)
		Expect(err).NotTo(HaveOccurred(), "failed to delete manager role binding")

		err = k8sClient.Delete(ctx, role)
		Expect(err).NotTo(HaveOccurred(), "failed to delete manager role")

		Eventually(
			func() bool {
				r := new(corev1.ServiceAccount)
				err := k8sClient.Get(
					ctx,
					types.NamespacedName{
						Name:      noPermissionServiceAccount.Name,
						Namespace: noPermissionServiceAccount.Namespace,
					},
					r,
				)
				return errors.IsNotFound(err)
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue(), "Expected no permission service account to be cleaned up")

		Eventually(
			func() bool {
				r := new(corev1.Secret)
				err := k8sClient.Get(ctx, types.NamespacedName{
					Name:      secret.Name,
					Namespace: secret.Namespace,
				}, r)

				return errors.IsNotFound(err)
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue(), "Expected role binding to be cleaned up")

		Eventually(
			func() bool {
				r := new(rbacv1.RoleBinding)
				err := k8sClient.Get(ctx, types.NamespacedName{
					Name:      roleBinding.Name,
					Namespace: roleBinding.Namespace,
				}, r)

				return errors.IsNotFound(err)
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue(), "Expected role binding to be cleaned up")

		Eventually(
			func() bool {
				r := new(rbacv1.Role)
				err := k8sClient.Get(
					ctx,
					types.NamespacedName{
						Name:      role.Name,
						Namespace: role.Namespace,
					},
					r,
				)

				return errors.IsNotFound(err)
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue(), "Expected role to be cleaned up")
	})
})

var _ = Describe("Test resource version and build version mismatch", func() {
	It("Should delete and recreate the autoscaling runner set to match the build version", func() {
		ctx := context.Background()
		autoscalingNS, mgr := createNamespace(GinkgoT(), k8sClient)

		configSecret := createDefaultSecret(GinkgoT(), k8sClient, autoscalingNS.Name)

		controller := &AutoscalingRunnerSetReconciler{
			Client:                             mgr.GetClient(),
			Scheme:                             mgr.GetScheme(),
			Log:                                logf.Log,
			ControllerNamespace:                autoscalingNS.Name,
			DefaultRunnerScaleSetListenerImage: "ghcr.io/actions/arc",
			ResourceBuilder: ResourceBuilder{
				SecretResolver: &SecretResolver{
					k8sClient:   k8sClient,
					multiClient: fake.NewMultiClient(),
				},
			},
		}
		err := controller.SetupWithManager(mgr)
		Expect(err).NotTo(HaveOccurred(), "failed to setup controller")

		originalVersion := build.Version
		defer func() {
			build.Version = originalVersion
		}()
		build.Version = "0.2.0"

		min := 1
		max := 10
		autoscalingRunnerSet := &v1alpha1.AutoscalingRunnerSet{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-asrs",
				Namespace: autoscalingNS.Name,
				Labels: map[string]string{
					"app.kubernetes.io/name":    "gha-runner-scale-set",
					"app.kubernetes.io/version": "0.1.0",
				},
				Annotations: map[string]string{
					AnnotationKeyKubernetesModeRoleBindingName:    "kube-mode-role-binding",
					AnnotationKeyKubernetesModeRoleName:           "kube-mode-role",
					AnnotationKeyKubernetesModeServiceAccountName: "kube-mode-service-account",
				},
			},
			Spec: v1alpha1.AutoscalingRunnerSetSpec{
				GitHubConfigUrl:    "https://github.com/owner/repo",
				GitHubConfigSecret: configSecret.Name,
				MaxRunners:         &max,
				MinRunners:         &min,
				RunnerGroup:        "testgroup",
				Template: corev1.PodTemplateSpec{
					Spec: corev1.PodSpec{
						Containers: []corev1.Container{
							{
								Name:  "runner",
								Image: "ghcr.io/actions/runner",
							},
						},
					},
				},
			},
		}

		// create autoscaling runner set before starting a manager
		err = k8sClient.Create(ctx, autoscalingRunnerSet)
		Expect(err).NotTo(HaveOccurred())

		startManagers(GinkgoT(), mgr)

		Eventually(
			func() bool {
				ars := new(v1alpha1.AutoscalingRunnerSet)
				err := k8sClient.Get(ctx, types.NamespacedName{Namespace: autoscalingRunnerSet.Namespace, Name: autoscalingRunnerSet.Name}, ars)
				return errors.IsNotFound(err)
			},
			autoscalingRunnerSetTestTimeout,
			autoscalingRunnerSetTestInterval,
		).Should(BeTrue())
	})
})
