/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

package controllersE2Etest

import (
	"context"
	"fmt"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	metal3v1alpha1 "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
	hwmgmtv1alpha1 "github.com/openshift-kni/oran-o2ims/api/hardwaremanagement/v1alpha1"
	provisioningv1alpha1 "github.com/openshift-kni/oran-o2ims/api/provisioning/v1alpha1"
	"github.com/openshift-kni/oran-o2ims/internal/constants"
	provisioningcontrollers "github.com/openshift-kni/oran-o2ims/internal/controllers"
	ctlrutils "github.com/openshift-kni/oran-o2ims/internal/controllers/utils"
	hwmgrcontrollers "github.com/openshift-kni/oran-o2ims/internal/hardwaremanager/controller"
	testutils "github.com/openshift-kni/oran-o2ims/test/utils"
	hivev1 "github.com/openshift/hive/apis/hive/v1"
)

var _ = Describe("SNO End-to-end ProvisioningRequestReconcile with hardware manager", Ordered, Label("sno-provisioning"), func() {
	// This test suite runs with both O2IMS and hardware manager controllers active
	// Hardware manager runs alongside Provisioning manager for all tests
	const timeout = time.Minute * 3
	const interval = time.Second * 3

	var (
		testCtx                context.Context
		ProvRequestCR          *provisioningv1alpha1.ProvisioningRequest
		reconciledPR           *provisioningv1alpha1.ProvisioningRequest
		ctIncomplete           *provisioningv1alpha1.ClusterTemplate
		ctComplete             *provisioningv1alpha1.ClusterTemplate
		tName                  = "clustertemplate-a"
		tVersion1              = "v1.0.0"
		tVersion2              = "v2.0.0"
		ctNamespace            = "clustertemplate-a-v4-16"
		ciDefaultsCmIncomplete = "clusterinstance-defaults-v1"
		ciDefaultsCmComplete   = "clusterinstance-defaults-v2"
		crName                 = ""
		ptDefaultsCm           = "policytemplate-defaults-v1"
		allocatedNode          *hwmgmtv1alpha1.AllocatedNode
		nar                    *hwmgmtv1alpha1.NodeAllocationRequest
	)

	testCtx = context.Background()

	hwTemplateCRs := []client.Object{
		// BMH Pool namespace
		&corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name: testutils.BmhPoolName,
			},
		},
		// HardwareProfile
		&hwmgmtv1alpha1.HardwareProfile{
			ObjectMeta: metav1.ObjectMeta{
				Name:      testutils.TestHwProfileName,
				Namespace: constants.DefaultNamespace,
			},
			Spec: hwmgmtv1alpha1.HardwareProfileSpec{
				// Basic hardware profile spec - minimal firmware config to satisfy CRD validation
				BiosFirmware: hwmgmtv1alpha1.Firmware{
					Version: "test-bios-v1.0",
					URL:     "https://example.com/bios-firmware.bin",
				},
			},
		},
	}

	// Create BareMetalHosts and associated resources in BeforeSuite
	for _, bmhData := range testutils.TestBMHs {
		bmh := testutils.CreateBareMetalHost(testutils.BMHData{
			Name:           bmhData.Name,
			Namespace:      constants.DefaultNamespace,
			MacAddress:     bmhData.MacAddress,
			BmcAddress:     bmhData.BmcAddress,
			ServerType:     testutils.TestServerType,
			Colour:         bmhData.Colour,
			ResourcePoolId: testutils.TestPoolID,
		})
		hwData := testutils.CreateHardwareData(bmhData.Name, bmhData)
		bmcSecret := testutils.CreateBMCSecret(bmhData.Name)

		hwTemplateCRs = append(hwTemplateCRs, bmh, hwData, bmcSecret)
	}

	mainCRs := []client.Object{
		// Cluster Template Namespace
		&corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name: ctNamespace,
			},
		},
		&corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name: "ztp-" + ctNamespace,
			},
		},
		// Configmap for ClusterInstance defaults v1 - missing required values.
		&corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      ciDefaultsCmIncomplete,
				Namespace: ctNamespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstallationTimeoutConfigKey: "60s",
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
clusterImageSetNameRef: "4.15.0"
holdInstallation: false
cpuPartitioningMode: AllNodes
networkType: OVNKubernetes
pullSecretRef:
  name: "pull-secret"
templateRefs:
- name: "ai-cluster-templates-v1"
  namespace: "siteconfig-operator"
nodes:
- role: master
  automatedCleaningMode: disabled
  ironicInspect: ""
  bootMode: UEFI
  nodeNetwork:
    interfaces:
    - name: eno1
      label: boot-interface
    - name: eth0
      label: base-interface
    - name: eth1
      label: data-interface
`,
			},
		},
		// Configmap for ClusterInstance defaults - complete.
		&corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      ciDefaultsCmComplete,
				Namespace: ctNamespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstallationTimeoutConfigKey: "60s",
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
clusterImageSetNameRef: "4.15.0"
holdInstallation: false
cpuPartitioningMode: AllNodes
networkType: OVNKubernetes
pullSecretRef:
  name: "pull-secret"
templateRefs:
- name: "ai-cluster-templates-v1"
  namespace: "siteconfig-operator"
nodes:
- role: master
  automatedCleaningMode: disabled
  ironicInspect: ""
  bootMode: UEFI
  hostName: node1
  nodeNetwork:
    interfaces:
    - name: eno1
      label: boot-interface
    - name: eth0
      label: base-interface
    - name: eth1
      label: data-interface
  templateRefs:
  - name: test
    namespace: test
`,
			},
		},
		// Configmap for policy template defaults
		&corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      ptDefaultsCm,
				Namespace: ctNamespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterConfigurationTimeoutConfigKey: "1m",
				ctlrutils.PolicyTemplateDefaultsConfigmapKey: `
cpu-isolated: "2-31"
cpu-reserved: "0-1"
defaultHugepagesSize: "1G"`,
			},
		},
		// Pull secret for ClusterInstance
		&corev1.Secret{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "pull-secret",
				Namespace: ctNamespace,
			},
			Data: map[string][]byte{
				".dockerconfigjson": []byte(testutils.TestSecretDataStr),
			},
			Type: corev1.SecretTypeDockerConfigJson,
		},
		// ClusterImageSet for e2e tests
		&hivev1.ClusterImageSet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "4.15.0",
			},
			Spec: hivev1.ClusterImageSetSpec{
				ReleaseImage: "quay.io/openshift-release-dev/ocp-release:4.15.0-x86_64",
			},
		},
	}
	// Define the cluster templates.
	ctIncomplete = &provisioningv1alpha1.ClusterTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name:      provisioningcontrollers.GetClusterTemplateRefName(tName, tVersion1),
			Namespace: ctNamespace,
		},
		Spec: provisioningv1alpha1.ClusterTemplateSpec{
			Name:       tName,
			Version:    tVersion1,
			Release:    "4.15.0",
			TemplateID: "aab39bda-ac56-4143-9b10-d1a71517d04f",
			TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
				ClusterInstanceDefaults: ciDefaultsCmIncomplete,
				PolicyTemplateDefaults:  ptDefaultsCm,
				HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
					HardwareProvisioningTimeout: &metav1.Duration{Duration: 10 * time.Minute},
					NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
						{
							Name:           "single-node",
							Role:           "master",
							ResourcePoolId: testutils.TestPoolID,
							HwProfile:      testutils.TestHwProfileName,
							ResourceSelector: map[string]string{
								"resourceselector.clcm.openshift.io/server-colour": "green",
							},
						},
					},
				},
			},
			TemplateParameterSchema: runtime.RawExtension{Raw: []byte(testutils.TestFullTemplateSchema)},
		},
	}
	ctComplete = &provisioningv1alpha1.ClusterTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name:      provisioningcontrollers.GetClusterTemplateRefName(tName, tVersion2),
			Namespace: ctNamespace,
		},
		Spec: provisioningv1alpha1.ClusterTemplateSpec{
			Name:       tName,
			Version:    tVersion2,
			Release:    "4.15.0",
			TemplateID: "bbb39bda-ac56-4143-9b10-d1a71517d04f",
			TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
				ClusterInstanceDefaults: ciDefaultsCmComplete,
				PolicyTemplateDefaults:  ptDefaultsCm,
				HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
					HardwareProvisioningTimeout: &metav1.Duration{Duration: 10 * time.Minute},
					NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
						{
							Name:           "single-node",
							Role:           "master",
							ResourcePoolId: testutils.TestPoolID,
							HwProfile:      testutils.TestHwProfileName,
							ResourceSelector: map[string]string{
								"resourceselector.clcm.openshift.io/server-colour": "blue",
								"resourceselector.clcm.openshift.io/server-type":   testutils.TestServerType,
								"hardwaredata/cpu_arch":                            "x86_64",
								"hardwaredata/storage;sizeBytes>500000000000":      "present",
								"hardwaredata/ramMebibytes;gt":                     "65536",
							},
						},
					},
				},
			},
			TemplateParameterSchema: runtime.RawExtension{Raw: []byte(testutils.TestFullTemplateSchema)},
		},
	}
	ctCRs := []client.Object{ctComplete, ctIncomplete}

	// Define the provisioning request.
	ProvRequestCR = &provisioningv1alpha1.ProvisioningRequest{
		ObjectMeta: metav1.ObjectMeta{
			Finalizers: []string{provisioningv1alpha1.ProvisioningRequestFinalizer},
			// Name to be set up in each test.
		},
		Spec: provisioningv1alpha1.ProvisioningRequestSpec{
			Name:            "test",
			Description:     "description",
			TemplateName:    tName,
			TemplateVersion: tVersion1,
		},
	}

	BeforeAll(func() {
		reconciledPR = &provisioningv1alpha1.ProvisioningRequest{}
		allocatedNode = &hwmgmtv1alpha1.AllocatedNode{}
		nar = &hwmgmtv1alpha1.NodeAllocationRequest{}

		By("Creating namespaces, ConfigMaps, and ClusterImageSet")
		for _, cr := range mainCRs {
			crCopy := cr.DeepCopyObject().(client.Object)
			err := K8SClient.Create(testCtx, crCopy)
			if err != nil && !errors.IsAlreadyExists(err) {
				Expect(err).ToNot(HaveOccurred())
			}
		}

		By("Creating ClusterTemplates")
		for _, cr := range ctCRs {
			crCopy := cr.DeepCopyObject().(client.Object)
			err := K8SClient.Create(testCtx, crCopy)
			if err != nil && !errors.IsAlreadyExists(err) {
				Expect(err).ToNot(HaveOccurred())
			}
		}

		By("Creating BMH pool namespace, HardwareProfile, and 4 BMHs with HardwareData")
		for _, cr := range hwTemplateCRs {
			err := K8SClient.Create(context.Background(), cr)
			Expect(err).ToNot(HaveOccurred())
		}

		By("Updating BMH status with hardware details and setting Available state")
		for _, bmhData := range testutils.TestBMHs {
			bmh := &metal3v1alpha1.BareMetalHost{}
			Expect(K8SClient.Get(context.Background(), types.NamespacedName{
				Name:      bmhData.Name,
				Namespace: constants.DefaultNamespace,
			}, bmh)).To(Succeed())

			// Add interface labels to BMH so the controller can map interface names to labels
			// Label KEY contains the interface label name, VALUE contains the interface name that should match
			if bmh.Labels == nil {
				bmh.Labels = make(map[string]string)
			}
			bmh.Labels[constants.BootInterfaceLabelFullKey] = "eno1"
			bmh.Labels[constants.LabelPrefixInterfaces+"base-interface"] = "eth0"
			bmh.Labels[constants.LabelPrefixInterfaces+"data-interface"] = "eth1"
			Expect(K8SClient.Update(context.Background(), bmh)).To(Succeed())

			// Set the status with hardware details and Available state
			hostname := bmhData.Hostname
			if bmhData.Name == "bmh-2" {
				// bmh-2 will be selected, so use hostname that matches ClusterInstance template
				hostname = "node1"
			}
			bmh.Status = metal3v1alpha1.BareMetalHostStatus{
				Provisioning: metal3v1alpha1.ProvisionStatus{
					State: metal3v1alpha1.StateAvailable,
				},
				HardwareDetails: &metal3v1alpha1.HardwareDetails{
					Hostname: hostname,
					CPU: metal3v1alpha1.CPU{
						Arch: "x86_64",
					},
					RAMMebibytes: int(bmhData.RamMB),
					NIC: []metal3v1alpha1.NIC{
						{
							Name: "eno1",
							MAC:  bmhData.MacAddress,
						},
						{
							Name: "eth0",
							MAC:  fmt.Sprintf("%s10", bmhData.MacAddress[:15]),
						},
						{
							Name: "eth1",
							MAC:  fmt.Sprintf("%s20", bmhData.MacAddress[:15]),
						},
					},
					Storage: []metal3v1alpha1.Storage{
						{
							Name:         "sda",
							SizeBytes:    1000000000000, // 1TB
							Rotational:   false,         // SSD
							Type:         "SSD",
							Model:        "Samsung SSD 980 PRO 1TB",
							SerialNumber: fmt.Sprintf("SN-%s", bmhData.Name),
						},
					},
				},
			}
			Expect(K8SClient.Status().Update(context.Background(), bmh)).To(Succeed())
		}

		By("Waiting for ClusterTemplate reconciliation")
		Eventually(func() bool {
			newct := &provisioningv1alpha1.ClusterTemplate{}
			Expect(K8SClient.Get(context.Background(), client.ObjectKeyFromObject(ctComplete), newct)).To(Succeed())
			return newct.Status.Conditions != nil
		}, timeout, interval).Should(BeTrue())

		Eventually(func() bool {
			newct := &provisioningv1alpha1.ClusterTemplate{}
			Expect(K8SClient.Get(context.Background(), client.ObjectKeyFromObject(ctIncomplete), newct)).To(Succeed())
			return newct.Status.Conditions != nil
		}, timeout, interval).Should(BeTrue())
	})

	AfterAll(func() {
		By("Deleting created resources")
		// Delete BMH-related resources first
		for _, bmhData := range testutils.TestBMHs {
			bmh := &metal3v1alpha1.BareMetalHost{}
			if err := K8SClient.Get(testCtx, types.NamespacedName{
				Name: bmhData.Name, Namespace: constants.DefaultNamespace}, bmh); err == nil {
				_ = K8SClient.Delete(testCtx, bmh)
			}
			hwData := &metal3v1alpha1.HardwareData{}
			if err := K8SClient.Get(testCtx, types.NamespacedName{
				Name: bmhData.Name, Namespace: constants.DefaultNamespace}, hwData); err == nil {
				_ = K8SClient.Delete(testCtx, hwData)
			}
			secret := &corev1.Secret{}
			if err := K8SClient.Get(testCtx, types.NamespacedName{
				Name: fmt.Sprintf("%s-bmc-secret", bmhData.Name), Namespace: constants.DefaultNamespace}, secret); err == nil {
				_ = K8SClient.Delete(testCtx, secret)
			}
		}

		// Delete ProvisioningRequests.
		// Strip finalizers first: the PR controller's finalizer waits for cluster namespace
		// deletion, which never completes in envtest (no namespace controller running).
		for _, name := range []string{"cluster-1", "cluster-2"} {
			pr := &provisioningv1alpha1.ProvisioningRequest{}
			if err := K8SClient.Get(testCtx, types.NamespacedName{Name: name}, pr); err == nil {
				pr.Finalizers = nil
				_ = K8SClient.Update(testCtx, pr)
				_ = K8SClient.Delete(testCtx, pr)
			}
		}

		// Clean up NARs and AllocatedNodes
		crNames := []string{"cluster-1", "cluster-2"}
		for _, crName := range crNames {
			nar := &hwmgmtv1alpha1.NodeAllocationRequest{}
			if err := K8SClient.Get(testCtx, types.NamespacedName{Name: crName, Namespace: constants.DefaultNamespace}, nar); err == nil {
				nar.Finalizers = nil
				_ = K8SClient.Update(testCtx, nar)
				_ = K8SClient.Delete(testCtx, nar)
			}
			anList := testNonCachingListAllocatedNodesForNAR(testCtx, crName)
			for i := range anList.Items {
				anList.Items[i].Finalizers = nil
				_ = K8SClient.Update(testCtx, &anList.Items[i])
				_ = K8SClient.Delete(testCtx, &anList.Items[i])
			}
		}

		// Delete ClusterTemplates, HardwareProfiles, ClusterImageSet
		for _, ct := range []*provisioningv1alpha1.ClusterTemplate{ctComplete, ctIncomplete} {
			if ct != nil {
				obj := &provisioningv1alpha1.ClusterTemplate{}
				if err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(ct), obj); err == nil {
					_ = K8SClient.Delete(testCtx, obj)
				}
			}
		}
		hwp := &hwmgmtv1alpha1.HardwareProfile{}
		if err := K8SClient.Get(testCtx, types.NamespacedName{
			Name: testutils.TestHwProfileName, Namespace: constants.DefaultNamespace}, hwp); err == nil {
			_ = K8SClient.Delete(testCtx, hwp)
		}

		cis := &hivev1.ClusterImageSet{}
		if err := K8SClient.Get(testCtx, types.NamespacedName{Name: "4.15.0"}, cis); err == nil {
			_ = K8SClient.Delete(testCtx, cis)
		}

		// Delete ConfigMaps and Secrets created in test namespaces.
		for _, name := range []string{ciDefaultsCmIncomplete, ciDefaultsCmComplete, ptDefaultsCm} {
			cm := &corev1.ConfigMap{}
			if err := K8SClient.Get(testCtx, types.NamespacedName{Name: name, Namespace: ctNamespace}, cm); err == nil {
				_ = K8SClient.Delete(testCtx, cm)
			}
		}
		secret := &corev1.Secret{}
		if err := K8SClient.Get(testCtx, types.NamespacedName{Name: "pull-secret", Namespace: ctNamespace}, secret); err == nil {
			_ = K8SClient.Delete(testCtx, secret)
		}
	})

	It("Verify status conditions if ClusterInstance rendering fails", func() {
		crName = "cluster-1"
		By("Creating ProvisioningRequest with incomplete ClusterTemplate")
		oranCT := &provisioningv1alpha1.ClusterTemplate{}
		err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(ctIncomplete), oranCT)
		Expect(err).ToNot(HaveOccurred())

		// Create the ProvisioningRequest.
		ProvRequestCR.Name = crName
		ProvRequestCR.Spec.TemplateParameters = runtime.RawExtension{
			Raw: []byte(testutils.TestFullTemplateParameters),
		}
		copyProvRequestCR := ProvRequestCR.DeepCopy()
		err = K8SClient.Create(testCtx, copyProvRequestCR)
		Expect(err).ToNot(HaveOccurred())

		By("Waiting for validation and render failure conditions")
		Eventually(func() bool {
			err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(ProvRequestCR), reconciledPR)
			Expect(err).ToNot(HaveOccurred())
			return len(reconciledPR.Status.Conditions) == 2
		}, timeout, interval).Should(BeTrue())

		conditions := reconciledPR.Status.Conditions
		Expect(len(conditions)).To(Equal(2))
		testutils.VerifyStatusCondition(conditions[0], metav1.Condition{
			Type:   string(provisioningv1alpha1.PRconditionTypes.Validated),
			Status: metav1.ConditionTrue,
			Reason: string(provisioningv1alpha1.CRconditionReasons.Completed),
		})
		testutils.VerifyStatusCondition(conditions[1], metav1.Condition{
			Type:    string(provisioningv1alpha1.PRconditionTypes.ClusterInstanceRendered),
			Status:  metav1.ConditionFalse,
			Reason:  string(provisioningv1alpha1.CRconditionReasons.Failed),
			Message: "ClusterInstance.siteconfig.open-cluster-management.io \"cluster-1\" is invalid: spec.nodes[0].templateRefs: Required value",
		})

		// Verify provisioningState is failed when the clusterInstance rendering fails.
		testutils.VerifyProvisioningStatus(reconciledPR.Status.ProvisioningStatus,
			provisioningv1alpha1.StateFailed, "Failed to render and validate ClusterInstance", nil)
	})

	It("Starts hardware provisioning", func() {
		crName = "cluster-2"
		By("Creating ProvisioningRequest with complete ClusterTemplate")
		oranCT := &provisioningv1alpha1.ClusterTemplate{}
		err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(ctComplete), oranCT)
		Expect(err).ToNot(HaveOccurred())

		// Create the ProvisioningRequest.
		ProvRequestCR.Name = crName
		templateParms := strings.Replace(
			testutils.TestFullTemplateParameters, "\"clusterName\": \"cluster-1\"", "\"clusterName\": \"cluster-2\"", 1)
		templateParms = strings.Replace(
			templateParms, "\"nodeClusterName\": \"exampleCluster\"", "\"nodeClusterName\": \"cluster-2\"", 1)
		ProvRequestCR.Spec.TemplateParameters = runtime.RawExtension{Raw: []byte(templateParms)}
		copyProvRequestCR := ProvRequestCR.DeepCopy()
		copyProvRequestCR.Spec.TemplateVersion = tVersion2
		err = K8SClient.Create(testCtx, copyProvRequestCR)
		Expect(err).ToNot(HaveOccurred())

		By("Waiting for hardware provisioning to start")
		reconciledPR := &provisioningv1alpha1.ProvisioningRequest{}
		Eventually(func() bool {
			err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(ProvRequestCR), reconciledPR)
			Expect(err).ToNot(HaveOccurred())
			// Check that we have at least the basic conditions and hardware provisioning has started
			if len(reconciledPR.Status.Conditions) < 4 {
				return false
			}
			// Look for HardwareProvisioned condition with status False (in progress)
			for _, cond := range reconciledPR.Status.Conditions {
				if cond.Type == string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned) &&
					cond.Status == metav1.ConditionFalse {
					return true
				}
			}
			return false
		}, timeout, interval).Should(BeTrue())

		// Verify initial state - hardware provisioning in progress.
		conditions := reconciledPR.Status.Conditions
		testutils.VerifyStatusCondition(conditions[0], metav1.Condition{
			Type:   string(provisioningv1alpha1.PRconditionTypes.Validated),
			Status: metav1.ConditionTrue,
			Reason: string(provisioningv1alpha1.CRconditionReasons.Completed),
		})
		testutils.VerifyStatusCondition(conditions[1], metav1.Condition{
			Type:   string(provisioningv1alpha1.PRconditionTypes.ClusterInstanceRendered),
			Status: metav1.ConditionTrue,
			Reason: string(provisioningv1alpha1.CRconditionReasons.Completed),
		})
		// Find and verify HardwareProvisioned condition
		var hwProvCondition *metav1.Condition
		for i := range conditions {
			if conditions[i].Type == string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned) {
				hwProvCondition = &conditions[i]
				break
			}
		}
		Expect(hwProvCondition).ToNot(BeNil())
		testutils.VerifyStatusCondition(*hwProvCondition, metav1.Condition{
			Type:    string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned),
			Status:  metav1.ConditionFalse,
			Reason:  string(provisioningv1alpha1.CRconditionReasons.InProgress),
			Message: "Hardware provisioning is in progress",
		})
		// Verify the provisioningState moves to progressing.
		testutils.VerifyProvisioningStatus(reconciledPR.Status.ProvisioningStatus,
			provisioningv1alpha1.StateProgressing, "Hardware provisioning is in progress", nil)
	})

	It("Creates NodeAllocationRequest and AllocatedNode", func() {
		By("Waiting for NodeAllocationRequest to be created")
		Eventually(func() bool {
			// Check if the NodeAllocationRequest has been created
			err := K8SClient.Get(testCtx, types.NamespacedName{
				Name:      crName,
				Namespace: constants.DefaultNamespace,
			}, nar)
			Expect(err).ToNot(HaveOccurred())
			return true
		}, timeout, interval).Should(BeTrue())

		// Verify NodeAllocationRequest contains expected node groups (only 1 single-node group).
		Expect(len(nar.Spec.NodeGroup)).To(Equal(1))

		// Verify the single node group.
		singleNodeGroup := nar.Spec.NodeGroup[0]
		Expect(singleNodeGroup.NodeGroupData.Name).To(Equal("single-node"))
		Expect(singleNodeGroup.Size).To(Equal(1))
		Expect(singleNodeGroup.NodeGroupData.HwProfile).To(Equal(testutils.TestHwProfileName))

		By("Waiting for AllocatedNode to be created by hardware manager")
		var allocatedNodes *hwmgmtv1alpha1.AllocatedNodeList
		Eventually(func() int {
			allocatedNodes = testNonCachingListAllocatedNodesForNAR(testCtx, crName)
			return len(allocatedNodes.Items)
		}, timeout, interval).Should(Equal(1))

		// Save the single allocated node for later use
		allocatedNode = &allocatedNodes.Items[0]
	})

	It("Updates the BMHs with the right labels", func() {
		By("Verifying bmh-2 was selected over bmh-4 despite identical criteria")
		Expect(allocatedNode.Spec.HwMgrNodeId).To(Equal("bmh-2"))

		// Get the BMH.
		bmh := &metal3v1alpha1.BareMetalHost{}
		err := K8SClient.Get(testCtx, types.NamespacedName{
			Name: allocatedNode.Spec.HwMgrNodeId, Namespace: allocatedNode.Spec.HwMgrNodeNs}, bmh)
		Expect(err).ToNot(HaveOccurred())

		// Check that the allocation labels are present.
		Expect(bmh.Labels).To(HaveKey("clcm.openshift.io/allocated"))
		Expect(bmh.Labels["clcm.openshift.io/allocated"]).To(Equal("true"))

		// Verify the AllocatedNode references the correct NodeAllocationRequest
		Expect(allocatedNode.Spec.NodeAllocationRequest).To(Equal(crName))

		By("Verifying non-selected BMHs do not have allocation labels")
		nonSelectedBMHs := []string{"bmh-1", "bmh-3", "bmh-4"}
		for _, bmhName := range nonSelectedBMHs {
			nonSelectedBMH := &metal3v1alpha1.BareMetalHost{}
			Expect(K8SClient.Get(testCtx, types.NamespacedName{
				Name:      bmhName,
				Namespace: constants.DefaultNamespace,
			}, nonSelectedBMH)).To(Succeed())

			// Check that allocation labels are NOT present.
			Expect(nonSelectedBMH.Labels).ToNot(HaveKey("clcm.openshift.io/allocated"),
				fmt.Sprintf("BMH %s should not have allocated label since it was not selected", bmhName))
			Expect(nonSelectedBMH.Labels).ToNot(HaveKey("clcm.openshift.io/allocated-node"),
				fmt.Sprintf("BMH %s should not have allocated-node label since it was not selected", bmhName))
		}
	})

	It("NodeAllocationRequest and AllocatedNode complete", func() {
		By("Verifying FirmwareUpdateNeeded annotation on selected BMH")
		bmh := &metal3v1alpha1.BareMetalHost{}
		err := K8SClient.Get(testCtx, types.NamespacedName{
			Name: allocatedNode.Spec.HwMgrNodeId, Namespace: allocatedNode.Spec.HwMgrNodeNs}, bmh)
		Expect(err).ToNot(HaveOccurred())

		// Check that the BMH has the FirmwareUpdateNeededAnnotation set.
		Expect(bmh.Annotations[hwmgrcontrollers.FirmwareUpdateNeededAnnotation]).To(Equal("true"))

		By("Transitioning BMH to Preparing state")
		bmh.Status.Provisioning.State = metal3v1alpha1.StatePreparing
		bmh.Status.OperationalStatus = metal3v1alpha1.OperationalStatusOK
		Expect(K8SClient.Status().Update(testCtx, bmh)).To(Succeed())

		By("Waiting for config-in-progress annotation on AllocatedNode")
		Eventually(func() bool {
			err := K8SClient.Get(testCtx, types.NamespacedName{Name: allocatedNode.Name, Namespace: constants.DefaultNamespace}, allocatedNode)
			Expect(err).ToNot(HaveOccurred())
			return allocatedNode.Annotations[hwmgrcontrollers.ConfigAnnotation] != ""
		}, timeout, interval).Should(BeTrue())

		By("Updating HostFirmwareComponents status to match HardwareProfile")
		hfc := &metal3v1alpha1.HostFirmwareComponents{}
		err = K8SClient.Get(testCtx, types.NamespacedName{
			Name: allocatedNode.Spec.HwMgrNodeId, Namespace: allocatedNode.Spec.HwMgrNodeNs}, hfc)
		Expect(err).ToNot(HaveOccurred())

		hfc.Status = metal3v1alpha1.HostFirmwareComponentsStatus{
			Components: []metal3v1alpha1.FirmwareComponentStatus{
				{
					Component:      "bios",
					CurrentVersion: "test-bios-v1.0", // Match the HardwareProfile requirement
				},
			},
			Conditions: []metav1.Condition{
				{
					Type:               string(metal3v1alpha1.HostFirmwareComponentsValid),
					Status:             metav1.ConditionTrue,
					Reason:             "Valid",
					Message:            "All firmware components are valid",
					LastTransitionTime: metav1.Now(),
					ObservedGeneration: 1,
				},
				{
					Type:               string(metal3v1alpha1.HostFirmwareComponentsChangeDetected),
					Status:             metav1.ConditionFalse,
					Reason:             "NoChangesNeeded",
					Message:            "No firmware changes required",
					LastTransitionTime: metav1.Now(),
					ObservedGeneration: 1,
				},
			},
		}
		// Update the status of the HostFirmwareComponents
		Expect(K8SClient.Status().Update(testCtx, hfc)).To(Succeed())

		By("Creating PreprovisioningImage and transitioning BMH to Available")
		ppi := &metal3v1alpha1.PreprovisioningImage{
			ObjectMeta: metav1.ObjectMeta{
				Name:      allocatedNode.Spec.HwMgrNodeId,
				Namespace: allocatedNode.Spec.HwMgrNodeNs,
			},
			Status: metal3v1alpha1.PreprovisioningImageStatus{
				NetworkData: metal3v1alpha1.SecretStatus{
					Name:    "", // Empty to indicate network data is cleared
					Version: "", // Empty to indicate network data is cleared
				},
			},
		}
		Expect(K8SClient.Create(testCtx, ppi)).To(Succeed())
		Expect(K8SClient.Status().Update(testCtx, ppi)).To(Succeed())
		// Update the BMH status to Available.
		// Get the updated BMH.
		err = K8SClient.Get(testCtx, types.NamespacedName{
			Name: allocatedNode.Spec.HwMgrNodeId, Namespace: allocatedNode.Spec.HwMgrNodeNs}, bmh)
		Expect(err).ToNot(HaveOccurred())
		bmh.Status.Provisioning.State = metal3v1alpha1.StateAvailable
		bmh.Status.OperationalStatus = metal3v1alpha1.OperationalStatusOK
		Expect(K8SClient.Status().Update(testCtx, bmh)).To(Succeed())
		// Remove the FirmwareUpdateNeededAnnotation.
		delete(bmh.Annotations, hwmgrcontrollers.FirmwareUpdateNeededAnnotation)
		Expect(K8SClient.Update(testCtx, bmh)).To(Succeed())

		By("Waiting for NAR and AllocatedNode to reach Provisioned=True")
		Eventually(func() bool {
			err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(nar), nar)
			Expect(err).ToNot(HaveOccurred())
			err = K8SClient.Get(testCtx, types.NamespacedName{Name: allocatedNode.Name, Namespace: constants.DefaultNamespace}, allocatedNode)
			Expect(err).ToNot(HaveOccurred())

			return nar.Status.Conditions[0].Status == metav1.ConditionTrue && allocatedNode.Status.Conditions[0].Status == metav1.ConditionTrue
		}, timeout, interval).Should(BeTrue())
		conditions := nar.Status.Conditions
		testutils.VerifyStatusCondition(conditions[0], metav1.Condition{
			Type:   string(hwmgmtv1alpha1.Provisioned),
			Status: metav1.ConditionTrue,
			Reason: string(hwmgmtv1alpha1.Completed),
		})
	})

	It("Completes hardware provisioning", func() {
		By("Waiting for HardwareProvisioned condition to become True")
		Eventually(func() bool {
			err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(ProvRequestCR), reconciledPR)
			Expect(err).ToNot(HaveOccurred())
			// Look for HardwareProvisioned condition with status True (completed).
			for _, cond := range reconciledPR.Status.Conditions {
				if cond.Type == string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned) &&
					cond.Status == metav1.ConditionTrue {
					return true
				}
			}
			return false
		}, timeout, interval).Should(BeTrue())

		conditions := reconciledPR.Status.Conditions
		// Find and verify HardwareProvisioned condition is now completed.
		var hwProvCondition *metav1.Condition
		for i := range conditions {
			if conditions[i].Type == string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned) {
				hwProvCondition = &conditions[i]
				break
			}
		}
		Expect(hwProvCondition).ToNot(BeNil())
		testutils.VerifyStatusCondition(*hwProvCondition, metav1.Condition{
			Type:   string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned),
			Status: metav1.ConditionTrue,
			Reason: string(provisioningv1alpha1.CRconditionReasons.Completed),
		})

	})

	It("Skips re-rendering the ClusterInstance when configuration changes occur during active provisioning", func() {
		By("Waiting for PR to move past hardware provisioning to cluster installation")
		Eventually(func() bool {
			err := K8SClient.Get(testCtx, client.ObjectKeyFromObject(ProvRequestCR), reconciledPR)
			Expect(err).ToNot(HaveOccurred())
			// Check if ProvisioningDetails moved from hardware provisioning message.
			return !strings.Contains(reconciledPR.Status.ProvisioningStatus.ProvisioningDetails, "Hardware provisioning")
		}, timeout, interval).Should(BeTrue())

		By("Changing template version to trigger re-render attempt")
		Expect(K8SClient.Get(testCtx, client.ObjectKeyFromObject(ProvRequestCR), reconciledPR)).To(Succeed())
		reconciledPR.Spec.TemplateVersion = tVersion1
		Expect(K8SClient.Update(testCtx, reconciledPR)).To(Succeed())

		By("Verifying ClusterInstance is not re-rendered during active provisioning")
		Eventually(func() bool {
			Expect(K8SClient.Get(testCtx, client.ObjectKeyFromObject(ProvRequestCR), reconciledPR)).To(Succeed())

			conditions := reconciledPR.Status.Conditions
			testutils.VerifyStatusCondition(conditions[1], metav1.Condition{
				Type:    string(provisioningv1alpha1.PRconditionTypes.ClusterInstanceRendered),
				Status:  metav1.ConditionTrue,
				Reason:  string(provisioningv1alpha1.CRconditionReasons.Completed),
				Message: "ClusterInstance rendered and passed dry-run validation",
			})

			// Find and verify HardwareProvisioned condition after template version change.
			hwProvConditionAfterChange := &metav1.Condition{}
			found := false
			for i := range conditions {
				if conditions[i].Type == string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned) {
					hwProvConditionAfterChange = &conditions[i]
					found = true
					break
				}
			}
			Expect(found).To(BeTrue(), "HardwareProvisioned condition should exist")
			testutils.VerifyStatusCondition(*hwProvConditionAfterChange, metav1.Condition{
				Type:   string(provisioningv1alpha1.PRconditionTypes.HardwareProvisioned),
				Status: metav1.ConditionTrue,
				Reason: string(provisioningv1alpha1.CRconditionReasons.Completed),
			})

			// Verify the provisioningState is progressing.
			testutils.VerifyProvisioningStatus(reconciledPR.Status.ProvisioningStatus,
				provisioningv1alpha1.StateProgressing, fmt.Sprintf("Waiting for ClusterInstance (%s) to be processed", crName), nil)

			return true
		}, timeout, interval).Should(BeTrue())
	})
})
