/*
SPDX-FileCopyrightText: Red Hat

SPDX-License-Identifier: Apache-2.0
*/

/*
Assisted-by: Cursor/claude-4-sonnet
*/

/*
Test Cases Summary for ClusterTemplate Controller

This file contains comprehensive test cases for the ClusterTemplate controller and its validation functions.
The tests are organized into the following test suites:

1. ClusterTemplateReconciler Tests:
   - Validates that a valid ClusterTemplate with all required ConfigMaps and hardware management defaults does not requeue
   - Validates that an invalid ClusterTemplate (missing ConfigMaps) requeues with appropriate error conditions

2. enqueueClusterTemplatesForConfigmap Tests:
   - Tests enqueueing of ClusterTemplates that reference a specific clusterinstance defaults ConfigMap
   - Tests that ClusterTemplates not referencing the ConfigMap are not enqueued

3. validatePolicyTemplateParamsSchema Tests:
   - Tests validation of policy template parameter schema structure
   - Covers missing properties, invalid property structures, incorrect type definitions
   - Tests validation of nested property structures and type constraints

4. validateClusterTemplateCR Tests:
   - Tests complete ClusterTemplate validation including ConfigMaps and hardware management defaults
   - Tests status condition setting for both valid and invalid templates
   - Tests validation of timeout configurations in ConfigMaps
   - Tests hardware management timeout validation

5. validateConfigmapReference Tests:
   - Tests ConfigMap existence and structure validation
   - Tests ClusterInstance CRD schema compliance
   - Tests template data key presence and YAML validity
   - Tests interface label validation in network configurations
   - Tests timeout value parsing and validation
   - Tests ConfigMap immutability requirements and patching

6. Validate Cluster Instance Name Tests:
   - Tests cluster template name validation rules
   - Tests handling of templates with same names in different namespaces
   - Tests metadata name correctness validation

7. Validate Cluster Instance TemplateID Tests:
   - Tests automatic templateID generation for empty values
   - Tests UUID format validation for provided templateIDs
   - Tests both valid and invalid UUID formats

8. validateTemplateParameterSchema Tests (Go test function):
   - Tests JSON schema validation for template parameters
   - Tests required field validation and type checking
   - Tests policy template parameter schema structure
   - Tests error message formatting for various validation failures

9. validateUpgradeDefaults Tests:
    - Tests single upgrade type enforcement (rejects both clusterVersion and imageBasedGroupUpgrade)
    - Tests IBGU upgrade defaults: seedImageRef version matching, malformed data handling
    - Tests ClusterVersion upgrade defaults: desiredUpdate.version matching, clusterUpgradeTimeout
      format validation, intermediateVersion semver and minor version validation
    - Tests schema type consistency between upgrade defaults and upgrade parameters schema
    - Tests schema validation of upgrade defaults against the upgradeParameters schema
    - Tests error handling for missing or malformed upgrade defaults

Each test suite covers both positive and negative test cases to ensure comprehensive validation
of the ClusterTemplate controller functionality.
*/

package controllers

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	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"
	ctlrutils "github.com/openshift-kni/oran-o2ims/internal/controllers/utils"
	typederrors "github.com/openshift-kni/oran-o2ims/internal/typed-errors"
	"github.com/openshift-kni/oran-o2ims/test/fakeclient"
	testutils "github.com/openshift-kni/oran-o2ims/test/utils"
	hivev1 "github.com/openshift/hive/apis/hive/v1"

	corev1 "k8s.io/api/core/v1"
	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"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

var _ = Describe("ClusterTemplateReconciler", func() {
	var (
		ctx          context.Context
		c            client.Client
		reconciler   *ClusterTemplateReconciler
		tName        = "cluster-template-a"
		tVersion     = "v1.0.0"
		ctNamespace  = "cluster-template-a"
		ciDefaultsCm = "clusterinstance-defaults-v1"
		ptDefaultsCm = "policytemplate-defaults-v1"
	)

	BeforeEach(func() {
		ctx = context.Background()
		clusterInstanceCRD, err := ctlrutils.BuildTestClusterInstanceCRD(ctlrutils.TestClusterInstanceSpecOk)
		Expect(err).ToNot(HaveOccurred())
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:       tName,
				Version:    tVersion,
				Release:    "4.15.0",
				TemplateID: "57b39bda-ac56-4143-9b10-d1a71517d04f",
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						HardwareProvisioningTimeout: &metav1.Duration{Duration: 90 * time.Minute},
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "master", Role: "master", HwProfile: "profile-spr-single-processor-64G"},
							{Name: "worker", Role: "worker", HwProfile: "profile-spr-single-processor-128G"},
						},
					},
				},
				TemplateParameterSchema: runtime.RawExtension{Raw: []byte(testutils.TestFullTemplateSchema)},
			},
		}

		c = fakeclient.GetFakeClientFromObjects([]client.Object{ct, clusterInstanceCRD}...)
		reconciler = &ClusterTemplateReconciler{
			Client: c,
			Logger: logger,
		}
	})

	It("should not requeue a valid ClusterTemplate", func() {
		// Create valid ConfigMaps and ClusterTemplate
		cms := []*corev1.ConfigMap{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      ciDefaultsCm,
					Namespace: ctNamespace,
				},
				Data: map[string]string{
					ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
baseDomain: value
clusterImageSetNameRef: "4.15.0"`,
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      ptDefaultsCm,
					Namespace: ctNamespace,
				},
				Data: map[string]string{
					ctlrutils.PolicyTemplateDefaultsConfigmapKey: `
clustertemplate-a-policy-v1-cpu-isolated: "2-31"
clustertemplate-a-policy-v1-cpu-reserved: "0-1"
clustertemplate-a-policy-v1-defaultHugepagesSize: "1G"`,
				},
			},
		}
		for _, cm := range cms {
			Expect(c.Create(ctx, cm)).To(Succeed())
		}

		// Create the ClusterImageSet required for validation
		clusterImageSet := &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",
			},
		}
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		// Create HardwareProfile CRs referenced by hwMgmtDefaults
		for _, name := range []string{"profile-spr-single-processor-64G", "profile-spr-single-processor-128G"} {
			hwProfile := &hwmgmtv1alpha1.HardwareProfile{
				ObjectMeta: metav1.ObjectMeta{
					Name:      name,
					Namespace: ctlrutils.GetEnvOrDefault(constants.DefaultNamespaceEnvName, constants.DefaultNamespace),
				},
			}
			Expect(c.Create(ctx, hwProfile)).To(Succeed())
		}

		req := reconcile.Request{
			NamespacedName: types.NamespacedName{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
		}

		result, err := reconciler.Reconcile(ctx, req)
		Expect(err).ToNot(HaveOccurred())
		// Expect to not requeue on valid cluster template
		Expect(result.Requeue).To(BeFalse())

		// Check the status condition
		updatedCT := &provisioningv1alpha1.ClusterTemplate{}
		Expect(c.Get(ctx, req.NamespacedName, updatedCT)).To(Succeed())
		conditions := updatedCT.Status.Conditions
		Expect(conditions).To(HaveLen(1))
		Expect(conditions[0].Type).To(Equal(string(provisioningv1alpha1.CTconditionTypes.Validated)))
		Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue))
		Expect(conditions[0].Reason).To(Equal(string(provisioningv1alpha1.CTconditionReasons.Completed)))
		Expect(conditions[0].Message).To(Equal("The cluster template validation succeeded"))
	})

	It("should requeue an invalid ClusterTemplate", func() {
		req := reconcile.Request{
			NamespacedName: types.NamespacedName{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
		}

		result, err := reconciler.Reconcile(ctx, req)
		Expect(err).ToNot(HaveOccurred())
		// Expect to requeue invalid cluster template
		Expect(result).To(Equal(requeueWithLongInterval()))

		// Check the status condition
		updatedCT := &provisioningv1alpha1.ClusterTemplate{}
		Expect(c.Get(ctx, req.NamespacedName, updatedCT)).To(Succeed())
		conditions := updatedCT.Status.Conditions
		Expect(conditions).To(HaveLen(1))
		Expect(conditions[0].Type).To(Equal(string(provisioningv1alpha1.CTconditionTypes.Validated)))
		Expect(conditions[0].Status).To(Equal(metav1.ConditionFalse))
		Expect(conditions[0].Reason).To(Equal(string(provisioningv1alpha1.CTconditionReasons.Failed)))
		Expect(conditions[0].Message).To(ContainSubstring(fmt.Sprintf(
			"the ConfigMap '%s' is not found in the namespace '%s'", ciDefaultsCm, ctNamespace)))
		Expect(conditions[0].Message).To(ContainSubstring(fmt.Sprintf(
			"the ConfigMap '%s' is not found in the namespace '%s'", ptDefaultsCm, ctNamespace)))
	})
})

var _ = Describe("enqueueClusterTemplatesForConfigmap", func() {
	var (
		c            client.Client
		ctx          context.Context
		r            *ClusterTemplateReconciler
		cm           *corev1.ConfigMap
		ciDefaultsCm = "clusterinstance-defaults-v1"
		cts          []*provisioningv1alpha1.ClusterTemplate
	)

	BeforeEach(func() {
		ctx = context.Background()
		cts = []*provisioningv1alpha1.ClusterTemplate{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "cluster-template-a.v1",
					Namespace: "cluster-template-a",
				},
				Spec: provisioningv1alpha1.ClusterTemplateSpec{
					Name:             "cluster-template-a",
					Version:          "v1",
					TemplateDefaults: provisioningv1alpha1.TemplateDefaults{},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "cluster-template-a.v2",
					Namespace: "cluster-template-a",
				},
				Spec: provisioningv1alpha1.ClusterTemplateSpec{
					Name:             "cluster-template-a",
					Version:          "v2",
					TemplateDefaults: provisioningv1alpha1.TemplateDefaults{},
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      "cluster-template-b.v1",
					Namespace: "cluster-template-b",
				},
				Spec: provisioningv1alpha1.ClusterTemplateSpec{
					Name:             "cluster-template-b",
					Version:          "v1",
					TemplateDefaults: provisioningv1alpha1.TemplateDefaults{},
				},
			},
		}

		cm = &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      ciDefaultsCm,
				Namespace: "cluster-template-a",
			},
		}

		clusterInstanceCRD, err := ctlrutils.BuildTestClusterInstanceCRD(ctlrutils.TestClusterInstanceSpecOk)
		Expect(err).ToNot(HaveOccurred())

		objs := []client.Object{cm, clusterInstanceCRD}
		for _, ct := range cts {
			objs = append(objs, ct)
		}
		c = fakeclient.GetFakeClientFromObjects(objs...)

		r = &ClusterTemplateReconciler{
			Client: c,
			Logger: logger,
		}
	})

	It("should enqueue ClusterTemplates referencing the clusterinstance defaults ConfigMap", func() {
		cts[0].Spec.TemplateDefaults.ClusterInstanceDefaults = ciDefaultsCm
		cts[1].Spec.TemplateDefaults.ClusterInstanceDefaults = ciDefaultsCm
		cts[2].Spec.TemplateDefaults.ClusterInstanceDefaults = ciDefaultsCm

		Expect(c.Update(ctx, cts[0])).To(Succeed())
		Expect(c.Update(ctx, cts[1])).To(Succeed())
		Expect(c.Update(ctx, cts[2])).To(Succeed())

		// Call the function
		reqs := r.enqueueClusterTemplatesForConfigmap(ctx, cm)

		// Verify the result
		Expect(reqs).To(HaveLen(2))
		Expect(reqs).To(ContainElements(
			reconcile.Request{NamespacedName: types.NamespacedName{Name: "cluster-template-a.v1", Namespace: "cluster-template-a"}},
			reconcile.Request{NamespacedName: types.NamespacedName{Name: "cluster-template-a.v2", Namespace: "cluster-template-a"}},
		))
	})

	It("should not enqueue ClusterTemplates not referencing the clusterinstance defaults ConfigMap", func() {
		cts[0].Spec.TemplateDefaults.ClusterInstanceDefaults = "clusterinstance-defaults-v2"
		cts[1].Spec.TemplateDefaults.ClusterInstanceDefaults = "clusterinstance-defaults-v2"
		cts[2].Spec.TemplateDefaults.ClusterInstanceDefaults = ciDefaultsCm

		Expect(c.Update(ctx, cts[0])).To(Succeed())
		Expect(c.Update(ctx, cts[1])).To(Succeed())
		Expect(c.Update(ctx, cts[2])).To(Succeed())

		// Call the function
		reqs := r.enqueueClusterTemplatesForConfigmap(ctx, cm)

		// Verify the result
		Expect(reqs).To(HaveLen(0))
	})
})

var _ = Describe("validatePolicyTemplateParamsSchema", func() {

	It("Returns error for missing properties", func() {
		var policyTemplateSchema map[string]any
		jsonString := `{
			"type": "object"
		}`

		err := json.Unmarshal([]byte(jsonString), &policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
		err = validatePolicyTemplateParamsSchema(policyTemplateSchema)
		Expect(err).To(HaveOccurred())
		Expect(err).To(MatchError(
			"unexpected policyTemplateParameters structure, no properties present"))
	})

	It("Returns nil for properties not being a map", func() {
		var policyTemplateSchema map[string]any
		jsonString := `{
			"type": "object",
			"properties": "string"
		}`

		err := json.Unmarshal([]byte(jsonString), &policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
		err = validatePolicyTemplateParamsSchema(policyTemplateSchema)
		Expect(err).To(HaveOccurred())
		Expect(err).To(MatchError(
			"unexpected policyTemplateParameters properties structure"))
	})

	It("Returns error for property not being a map", func() {
		var policyTemplateSchema map[string]any
		jsonString := `{
			"type": "object",
			"properties": {
			  "cpu-isolated": "string",
			  "sriov-network-vlan-1": {
				"type": "string"
			  }
			}
		}`
		err := json.Unmarshal([]byte(jsonString), &policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
		err = validatePolicyTemplateParamsSchema(policyTemplateSchema)
		Expect(err).To(HaveOccurred())
		Expect(err).To(MatchError(
			"unexpected policyTemplateParameters structure for the cpu-isolated property"))
	})

	It("Returns error for key different from \"type\"", func() {
		var policyTemplateSchema map[string]any
		jsonString := `{
			"type": "object",
			"properties": {
			  "cpu-isolated": {
				"type": "string"
			  },
			  "sriov-network-vlan-1": {
				"var-type": "integer"
			  }
			}
		}`

		err := json.Unmarshal([]byte(jsonString), &policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
		err = validatePolicyTemplateParamsSchema(policyTemplateSchema)
		Expect(err).To(HaveOccurred())
		Expect(err).To(MatchError(
			"unexpected policyTemplateParameters structure: expected subproperty \"type\" missing"))
	})

	It("Returns error for type property being an object", func() {
		var policyTemplateSchema map[string]any
		jsonString := `{
			"type": "object",
			"properties": {
			  "cpu-isolated": {
				"type": {
					"key": "value"
				}
			  },
			  "sriov-network-vlan-1": {
				"type": "string"
			  }
			}
		}`

		err := json.Unmarshal([]byte(jsonString), &policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
		err = validatePolicyTemplateParamsSchema(policyTemplateSchema)
		Expect(err).To(HaveOccurred())
		Expect(err).To(MatchError(
			"unexpected policyTemplateParameters structure: expected the subproperty \"type\" to be string"))
	})

	It("Returns error for non string type", func() {
		var policyTemplateSchema map[string]any
		jsonString := `{
			"type": "object",
			"properties": {
			  "cpu-isolated": {
				"type": "string"
			  },
			  "sriov-network-vlan-1": {
				"type": "integer"
			  }
			}
		}`

		err := json.Unmarshal([]byte(jsonString), &policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
		err = validatePolicyTemplateParamsSchema(policyTemplateSchema)
		Expect(err).To(HaveOccurred())
		Expect(err).To(MatchError(
			"expected type string for the sriov-network-vlan-1 property"))
	})

	It("Returns nil for expected structure", func() {
		var policyTemplateSchema map[string]any
		jsonString := `{
			"type": "object",
			"properties": {
			  "cpu-isolated": {
				"type": "string"
			  },
			  "sriov-network-vlan-1": {
				"type": "string"
			  }
			}
		}`

		err := json.Unmarshal([]byte(jsonString), &policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
		err = validatePolicyTemplateParamsSchema(policyTemplateSchema)
		Expect(err).ToNot(HaveOccurred())
	})

})

var _ = Describe("validateClusterTemplateCR", func() {
	var (
		c            client.Client
		ctx          context.Context
		cms          []*corev1.ConfigMap
		tName        = "cluster-template-a"
		tVersion     = "v1.0.0"
		ctNamespace  = "cluster-template-a"
		ciDefaultsCm = "clusterinstance-ci-defaults"
		ptDefaultsCm = "policytemplate-ci-defaults"
		t            *clusterTemplateReconcilerTask
	)

	BeforeEach(func() {
		ctx = context.Background()
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:    tName,
				Version: tVersion,
				Release: "4.15.0",
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						HardwareProvisioningTimeout: &metav1.Duration{Duration: 90 * time.Minute},
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "master", Role: "master", ResourcePoolId: "xyz", HwProfile: "profile-spr-single-processor-64G"},
							{Name: "worker", Role: "worker", ResourcePoolId: "xyz", HwProfile: "profile-spr-single-processor-128G"},
						},
					},
				},
				TemplateParameterSchema: runtime.RawExtension{Raw: []byte(testutils.TestFullTemplateSchema)},
			},
		}

		// Valid ConfigMaps
		cms = []*corev1.ConfigMap{
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      ciDefaultsCm,
					Namespace: ctNamespace,
				},
				Data: map[string]string{
					ctlrutils.ClusterInstallationTimeoutConfigKey: "80m",
					ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
baseDomain: value
clusterImageSetNameRef: "4.15.0"`,
				},
			},
			{
				ObjectMeta: metav1.ObjectMeta{
					Name:      ptDefaultsCm,
					Namespace: ctNamespace,
				},
				Data: map[string]string{
					ctlrutils.ClusterConfigurationTimeoutConfigKey: "40m",
					ctlrutils.PolicyTemplateDefaultsConfigmapKey: `
clustertemplate-a-policy-v1-cpu-isolated: "2-31"
clustertemplate-a-policy-v1-cpu-reserved: "0-1"
clustertemplate-a-policy-v1-defaultHugepagesSize: "1G"`,
				},
			},
		}

		clusterInstanceCRD, err := ctlrutils.BuildTestClusterInstanceCRD(ctlrutils.TestClusterInstanceSpecOk)
		Expect(err).ToNot(HaveOccurred())

		c = fakeclient.GetFakeClientFromObjects([]client.Object{ct, clusterInstanceCRD}...)

		t = &clusterTemplateReconcilerTask{
			client: c,
			logger: logger,
			object: ct,
		}
	})

	It("should validate a valid ClusterTemplate and set status condition to true", func() {
		for _, cm := range cms {
			Expect(c.Create(ctx, cm)).To(Succeed())
		}

		// Create the ClusterImageSet required for validation
		clusterImageSet := &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",
			},
		}
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		// Create HardwareProfile CRs referenced by hwMgmtDefaults
		for _, name := range []string{"profile-spr-single-processor-64G", "profile-spr-single-processor-128G"} {
			hwProfile := &hwmgmtv1alpha1.HardwareProfile{
				ObjectMeta: metav1.ObjectMeta{
					Name:      name,
					Namespace: ctlrutils.GetEnvOrDefault(constants.DefaultNamespaceEnvName, constants.DefaultNamespace),
				},
			}
			Expect(c.Create(ctx, hwProfile)).To(Succeed())
		}

		valid, err := t.validateClusterTemplateCR(ctx)
		Expect(err).ToNot(HaveOccurred())
		Expect(valid).To(BeTrue())

		// Check the status condition
		conditions := t.object.Status.Conditions
		Expect(conditions).To(HaveLen(1))
		Expect(conditions[0].Type).To(Equal(string(provisioningv1alpha1.CTconditionTypes.Validated)))
		Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue))
		Expect(conditions[0].Reason).To(Equal(string(provisioningv1alpha1.CTconditionReasons.Completed)))
		Expect(conditions[0].Message).To(Equal("The cluster template validation succeeded"))
	})

	It("should return false and set status condition to false if reference ConfigMap is missing", func() {
		// No ConfigMap created
		valid, err := t.validateClusterTemplateCR(ctx)
		Expect(err).ToNot(HaveOccurred())
		Expect(valid).To(BeFalse())

		// Check the status condition
		conditions := t.object.Status.Conditions
		Expect(conditions).To(HaveLen(1))
		Expect(conditions[0].Type).To(Equal(string(provisioningv1alpha1.CTconditionTypes.Validated)))
		Expect(conditions[0].Status).To(Equal(metav1.ConditionFalse))
		Expect(conditions[0].Reason).To(Equal(string(provisioningv1alpha1.CTconditionReasons.Failed)))
		Expect(conditions[0].Message).To(ContainSubstring(fmt.Sprintf(
			"the ConfigMap '%s' is not found in the namespace '%s'", ciDefaultsCm, ctNamespace)))
		Expect(conditions[0].Message).To(ContainSubstring(fmt.Sprintf(
			"the ConfigMap '%s' is not found in the namespace '%s'", ptDefaultsCm, ctNamespace)))
	})

	It("should return false and set status condition to false if timeouts in ConfigMaps are invalid", func() {
		cms[0].Data[ctlrutils.ClusterInstallationTimeoutConfigKey] = "invalidCiTimeout"
		cms[1].Data[ctlrutils.ClusterConfigurationTimeoutConfigKey] = "invalidPtTimeout"
		for _, cm := range cms {
			Expect(c.Create(ctx, cm)).To(Succeed())
		}

		// Create the ClusterImageSet required for validation
		clusterImageSet := &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",
			},
		}
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		valid, err := t.validateClusterTemplateCR(ctx)
		Expect(err).ToNot(HaveOccurred())
		Expect(valid).To(BeFalse())

		// Check the status condition
		conditions := t.object.Status.Conditions
		Expect(conditions).To(HaveLen(1))
		Expect(conditions[0].Type).To(Equal(string(provisioningv1alpha1.CTconditionTypes.Validated)))
		Expect(conditions[0].Status).To(Equal(metav1.ConditionFalse))
		Expect(conditions[0].Reason).To(Equal(string(provisioningv1alpha1.CTconditionReasons.Failed)))
		Expect(conditions[0].Message).To(ContainSubstring(fmt.Sprintf(
			"the value of key %s from ConfigMap %s is not a valid duration string", ctlrutils.ClusterConfigurationTimeoutConfigKey, ptDefaultsCm)))
		Expect(conditions[0].Message).To(ContainSubstring(fmt.Sprintf(
			"the value of key %s from ConfigMap %s is not a valid duration string", ctlrutils.ClusterInstallationTimeoutConfigKey, ciDefaultsCm)))
	})

	Context("skip clusterimageset validation annotation", func() {
		DescribeTable("should correctly handle annotation values",
			func(annotationValue string, expectValidation bool) {
				// Create a test ClusterTemplate
				testCT := &provisioningv1alpha1.ClusterTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Name:      "test-template.v1.0.0",
						Namespace: "test-namespace",
						Annotations: map[string]string{
							ctlrutils.SkipClusterImageSetValidationAnnotation: annotationValue,
						},
					},
					Spec: provisioningv1alpha1.ClusterTemplateSpec{
						Name:    "test-template",
						Version: "v1.0.0",
						Release: "4.17.0",
						TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
							ClusterInstanceDefaults: "test-ci-defaults",
							PolicyTemplateDefaults:  "test-pt-defaults",
						},
					},
				}

				// Capture log output
				var logBuffer bytes.Buffer
				testLogger := slog.New(slog.NewTextHandler(&logBuffer, &slog.HandlerOptions{Level: slog.LevelInfo}))

				task := &clusterTemplateReconcilerTask{
					client: c,
					logger: testLogger,
					object: testCT,
				}

				// call validateClusterTemplateCR to validate the ClusterImageSet
				_, _ = task.validateClusterTemplateCR(ctx) // Ignore result since other validations will fail

				// Verify the expected behavior based on the expectValidation flag
				logOutput := logBuffer.String()
				if expectValidation {
					Expect(logOutput).To(ContainSubstring("Validating ClusterImageSet"))
					Expect(logOutput).ToNot(ContainSubstring("Skipping ClusterImageSet validation"))
				} else {
					Expect(logOutput).To(ContainSubstring("Skipping ClusterImageSet validation"))
					Expect(logOutput).ToNot(ContainSubstring("Validating ClusterImageSet"))
				}
			},
			Entry("annotation='true' - should skip", "true", false),
			Entry("annotation='TRUE' - should skip", "TRUE", false),
			Entry("annotation='True' - should skip", "True", false),
			Entry("annotation='TrUe' - should skip", "TrUe", false),
			Entry("annotation='false' - should validate", "false", true),
			Entry("annotation='' - should validate", "", true),
			Entry("annotation='yes' - should validate", "yes", true),
			Entry("annotation='1' - should validate", "1", true),
			Entry("annotation='skip' - should validate", "skip", true),
		)
	})
})

var _ = Describe("validateConfigmapReference", func() {
	var (
		c             client.Client
		ctx           context.Context
		namespace     = "default"
		configmapName = "test-configmap"
	)

	BeforeEach(func() {
		ctx = context.Background()
		clusterInstanceCRD, err := ctlrutils.BuildTestClusterInstanceCRD(ctlrutils.TestClusterInstanceSpecOk)
		Expect(err).ToNot(HaveOccurred())
		c = fakeclient.GetFakeClientFromObjects([]client.Object{clusterInstanceCRD}...)
	})

	It("should validate a valid configmap", func() {
		// Create a valid ConfigMap
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstallationTimeoutConfigKey: "40m",
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
baseDomain: example.sno.com`,
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())
		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).ToNot(HaveOccurred())
	})

	It("should return validation error message for a missing configmap", func() {
		// No ConfigMap created
		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(Equal(fmt.Sprintf(
			"failed to get ConfigmapReference: the ConfigMap '%s' is not found in the namespace '%s'", configmapName, namespace)))
	})

	It("should return validation error message for a configmap that does not match the ClusterInstance CRD", func() {
		// Create a valid ConfigMap but with a wrong schema.
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstallationTimeoutConfigKey: "40m",
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
baDomain: example.sno.com`,
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())
		// Cluster Instance schema error.
		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(ContainSubstring("failed to validate the default ConfigMap: the ConfigMap does not match the ClusterInstance schema"))
	})

	It("should return validation error message for missing template data key in configmap", func() {
		// Create a ConfigMap without the expected key
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				"wrong-key": "value",
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())

		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(Equal(fmt.Sprintf(
			"the ConfigMap '%s' does not contain a field named '%s'", configmapName, ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey)))
	})

	It("should return validation error message for invalid YAML in configmap template data", func() {
		// Create a ConfigMap with invalid data YAML
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `invalid-yaml`,
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())

		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(ContainSubstring("the value of key"))
	})

	It("should return validation error message for missing interface label in configmap template data", func() {
		// Create a ConfigMap with invalid data YAML
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
nodes:
- hostname: "node1"
  nodeNetwork:
    interfaces:
    - name: "eno1"
`,
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())

		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(ContainSubstring("'label' is missing for interface"))
	})

	It("should return validation error message for an empty interface label in configmap template data", func() {
		// Create a ConfigMap with invalid data YAML
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
nodes:
- hostname: "node1"
  nodeNetwork:
    interfaces:
    - name: "eno1"
      label: ""
`,
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())

		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(ContainSubstring("'label' is empty for interface"))
	})

	It("should return validation error message for invalid timeout value in configmap", func() {
		// Create a ConfigMap with non-integer string
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstallationTimeoutConfigKey: "invalid-timeout",
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
baseDomain: value`,
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())

		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(ContainSubstring("is not a valid duration string"))
	})

	It("should return validation error message if configmap is mutable", func() {
		// Create a mutable ConfigMap
		mutable := false
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
baseDomain: value`,
			},
			Immutable: &mutable,
		}
		Expect(c.Create(ctx, cm)).To(Succeed())

		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(Equal(fmt.Sprintf(
			"It is not allowed to set Immutable to false in the ConfigMap %s", configmapName)))
	})

	It("should patch a validated configmap to immutable if not set", func() {
		// Create a ConfigMap without immutable field
		cm := &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      configmapName,
				Namespace: namespace,
			},
			Data: map[string]string{
				ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey: `
baseDomain: value`,
			},
		}
		Expect(c.Create(ctx, cm)).To(Succeed())

		err := validateConfigmapReference[map[string]any](
			ctx, c, configmapName, namespace,
			ctlrutils.ClusterInstanceTemplateDefaultsConfigmapKey,
			ctlrutils.ClusterInstallationTimeoutConfigKey)
		Expect(err).ToNot(HaveOccurred())

		// Verify that the configmap is patched to be immutable
		updatedCM := &corev1.ConfigMap{}
		Expect(c.Get(ctx, client.ObjectKey{Name: configmapName, Namespace: namespace}, updatedCM)).To(Succeed())
		Expect(updatedCM.Immutable).ToNot(BeNil())
		Expect(*updatedCM.Immutable).To(BeTrue())
	})
})

var _ = Describe("Validate Cluster Instance Name", func() {
	var (
		c            client.Client
		ctx          context.Context
		tName        = "cluster-template-a"
		tVersion     = "v1.0.0"
		ctNamespace  = "cluster-template-a"
		ciDefaultsCm = "clusterinstance-ci-defaults"
		ptDefaultsCm = "policytemplate-ci-defaults"
	)

	BeforeEach(func() {
		ctx = context.Background()
		c = fakeclient.GetFakeClientFromObjects()
	})

	It("should validate a cluster template name", func() {
		// Create a valid cluster template
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:    tName,
				Version: tVersion,
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "controller", Role: "master", HwProfile: "profile-64G"},
						},
					},
				},
			},
		}
		Expect(c.Create(ctx, ct)).To(Succeed())
		err := validateName(
			c, "myClusterInstance", "v11", "myClusterInstance.v11", "namespace1")
		Expect(err).ToNot(HaveOccurred())
	})

	It("should validate a cluster template name, if a cluster template with a different name exists", func() {
		// Create a valid cluster template
		ct1 := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:    tName,
				Version: tVersion,
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "controller", Role: "master", HwProfile: "profile-64G"},
						},
					},
				},
			},
		}
		ct2 := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: "namespace1",
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:    tName,
				Version: tVersion,
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "controller", Role: "master", HwProfile: "profile-64G"},
						},
					},
				},
			},
		}
		Expect(c.Create(ctx, ct1)).To(Succeed())
		Expect(c.Create(ctx, ct2)).To(Succeed())
		err := validateName(
			c, "myClusterInstance", "v11", "myClusterInstance.v11", "namespace1")
		Expect(err).ToNot(HaveOccurred())
	})

	It("should fail cluster template validation if metadata name is incorrect", func() {
		err := validateName(
			c, "cluster-template-a", "v1", "cluster-template-a.v1", "cluster-template-a")
		Expect(err).ToNot(HaveOccurred())
	})

	It("should fail cluster template validation if a cluster template with a same name"+
		" but in a different namespace exists", func() {
		// Create a valid cluster template
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:    tName,
				Version: tVersion,
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "controller", Role: "master", HwProfile: "profile-64G"},
						},
					},
				},
			},
		}
		Expect(c.Create(ctx, ct)).To(Succeed())
		err := validateName(
			c, "cluster-template-a", "v1.0.0", "cluster-template-a.v1.0.0", "cluster-template-b")
		Expect(err).To(HaveOccurred())
	})
})

// new
var _ = Describe("Validate Cluster Instance TemplateID", func() {
	var (
		c            client.Client
		ctx          context.Context
		tName        = "cluster-template-a"
		tVersion     = "v1.0.0"
		ctNamespace  = "cluster-template-a"
		ciDefaultsCm = "clusterinstance-ci-defaults"
		ptDefaultsCm = "policytemplate-ci-defaults"
	)

	BeforeEach(func() {
		ctx = context.Background()
		c = fakeclient.GetFakeClientFromObjects()
	})

	It("Generate templateID", func() {
		// Create a valid cluster template
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:       tName,
				Version:    tVersion,
				TemplateID: "",
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "controller", Role: "master", HwProfile: "profile-64G"},
						},
					},
				},
			},
		}
		Expect(c.Create(ctx, ct)).To(Succeed())
		err := generateTemplateID(ctx, c, ct)
		Expect(err).ToNot(HaveOccurred())
		ct1 := &provisioningv1alpha1.ClusterTemplate{}
		err = c.Get(ctx, client.ObjectKeyFromObject(ct), ct1)
		Expect(err).ToNot(HaveOccurred())
		Expect(ct1.Spec.TemplateID).ToNot(Equal(""))
	})
	It("should validate templateID if is not empty, bad UUID", func() {
		// Create a valid cluster template
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:       tName,
				Version:    tVersion,
				TemplateID: "kjwchbjkdbckj",
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "controller", Role: "master", HwProfile: "profile-64G"},
						},
					},
				},
			},
		}
		Expect(c.Create(ctx, ct)).To(Succeed())
		err := validateTemplateID(ct)
		Expect(err).To(HaveOccurred())
	})
	It("should validate templateID if is not empty, good UUID", func() {
		// Create a valid cluster template
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: ctNamespace,
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:       tName,
				Version:    tVersion,
				TemplateID: "71ba1920-77f8-4842-a474-010b1af1d40b",
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: ciDefaultsCm,
					PolicyTemplateDefaults:  ptDefaultsCm,
					HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
						NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
							{Name: "controller", Role: "master", HwProfile: "profile-64G"},
						},
					},
				},
			},
		}
		Expect(c.Create(ctx, ct)).To(Succeed())
		err := validateTemplateID(ct)
		Expect(err).ToNot(HaveOccurred())
	})
})

var (
	tName    = "cluster-template-a"
	tVersion = "v1.0.0"
)

func Test_validateTemplateParameterSchema(t *testing.T) {
	type args struct {
		object *provisioningv1alpha1.ClusterTemplate
	}
	tests := []struct {
		name    string
		args    args
		wantErr bool
		errText string
	}{
		{
			name: "ok",
			args: args{
				object: &provisioningv1alpha1.ClusterTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Name: GetClusterTemplateRefName(tName, tVersion),
					},
					Spec: provisioningv1alpha1.ClusterTemplateSpec{
						TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
							HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
								NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
									{Name: "controller", Role: "master", HwProfile: "profile-64G"},
								},
							},
						},
						TemplateParameterSchema: runtime.RawExtension{Raw: []byte(`{
		"properties": {
			"nodeClusterName": {"type": "string"},
			"oCloudSiteId": {"type": "string"},
			"clusterInstanceParameters": {"type": "object"},
			"policyTemplateParameters": {"type": "object", "properties": {}}
		},
		"type": "object",
		"required": [
	"nodeClusterName",
	"oCloudSiteId",
	"policyTemplateParameters",
	"clusterInstanceParameters"
	]
	}`)},
					},
				},
			},
			wantErr: false,
			errText: "",
		},
		{
			name: "bad schema",
			args: args{
				object: &provisioningv1alpha1.ClusterTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Name: GetClusterTemplateRefName(tName, tVersion),
					},
					Spec: provisioningv1alpha1.ClusterTemplateSpec{
						TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
							HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
								NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
									{Name: "controller", Role: "master", HwProfile: "profile-64G"},
								},
							},
						},
						TemplateParameterSchema: runtime.RawExtension{Raw: []byte(`{
		"properties": {
			"nodeClusterName": {"type": "string"},
			"oCloudSiteId": {"type": "string"},
			"clusterInstanceParameters": {"type": "object"},
			"policyTemplateParameters": {"type": "object", "properties": {"a": {}}}
		},
		"type": "object",
		"required": [
	"nodeClusterName",
	"oCloudSiteId",
	"policyTemplateParameters",
	"clusterInstanceParameters"
	]
	}`)},
					},
				},
			},
			wantErr: true,
			errText: "Error validating the policyTemplateParameters schema: unexpected policyTemplateParameters structure: expected subproperty \"type\" missing",
		},
		{
			name: "bad type",
			args: args{
				object: &provisioningv1alpha1.ClusterTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Name: GetClusterTemplateRefName(tName, tVersion),
					},
					Spec: provisioningv1alpha1.ClusterTemplateSpec{
						TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
							HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
								NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
									{Name: "controller", Role: "master", HwProfile: "profile-64G"},
								},
							},
						},
						TemplateParameterSchema: runtime.RawExtension{Raw: []byte(`{
		"properties": {
			"nodeClusterName": {"type": "string"},
			"oCloudSiteId": {"type": "string"},
			"clusterInstanceParameters": {"type": "string"},
			"policyTemplateParameters": {"type": "object", "properties": {"a": {"type": "string"}}}
		},
		"type": "object",
		"required": [
	"nodeClusterName",
	"oCloudSiteId",
	"policyTemplateParameters",
	"clusterInstanceParameters"
	]
	}`)},
					},
				},
			},
			wantErr: true,
			errText: "failed to validate ClusterTemplate: cluster-template-a.v1.0.0. The following entries are present but have a unexpected type: clusterInstanceParameters (expected = object actual= string).",
		},
		{
			name: "missing parameter and bad type",
			args: args{
				object: &provisioningv1alpha1.ClusterTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Name: GetClusterTemplateRefName(tName, tVersion),
					},
					Spec: provisioningv1alpha1.ClusterTemplateSpec{
						TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
							HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
								NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
									{Name: "controller", Role: "master", HwProfile: "profile-64G"},
								},
							},
						},
						TemplateParameterSchema: runtime.RawExtension{Raw: []byte(`{
		"properties": {
			"oCloudSiteId": {"type": "string"},
			"clusterInstanceParameters": {"type": "string"},
			"policyTemplateParameters": {"type": "object", "properties": {"a": {"type": "string"}}}
		},
		"type": "object",
		"required": [
	"nodeClusterName",
	"oCloudSiteId",
	"policyTemplateParameters",
	"clusterInstanceParameters"
	]
	}`)},
					},
				},
			},
			wantErr: true,
			errText: "failed to validate ClusterTemplate: cluster-template-a.v1.0.0. The following mandatory fields are missing: nodeClusterName. The following entries are present but have a unexpected type: clusterInstanceParameters (expected = object actual= string).",
		},
		{
			name: "missing parameter and bad type, and missing required entry",
			args: args{
				object: &provisioningv1alpha1.ClusterTemplate{
					ObjectMeta: metav1.ObjectMeta{
						Name: GetClusterTemplateRefName(tName, tVersion),
					},
					Spec: provisioningv1alpha1.ClusterTemplateSpec{
						TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
							HwMgmtDefaults: provisioningv1alpha1.HwMgmtDefaults{
								NodeGroupData: []hwmgmtv1alpha1.NodeGroupData{
									{Name: "controller", Role: "master", HwProfile: "profile-64G"},
								},
							},
						},
						TemplateParameterSchema: runtime.RawExtension{Raw: []byte(`{
		"properties": {
			"oCloudSiteId": {"type": "string"},
			"clusterInstanceParameters": {"type": "string"},
			"policyTemplateParameters": {"type": "object", "properties": {"a": {"type": "string"}}}
		},
		"type": "object",
		"required": [
	"nodeClusterName",
	"policyTemplateParameters",
	"clusterInstanceParameters"
	]
	}`)},
					},
				},
			},
			wantErr: true,
			errText: "failed to validate ClusterTemplate: cluster-template-a.v1.0.0. The following mandatory fields are missing: nodeClusterName. The following entries are present but have a unexpected type: clusterInstanceParameters (expected = object actual= string).",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var err error
			if err = validateTemplateParameterSchema(tt.args.object); (err != nil) != tt.wantErr {
				t.Errorf("validateTemplateParameterSchema() error = %v, wantErr %v", err, tt.wantErr)
			}
			if err != nil && err.Error() != tt.errText {
				t.Errorf("validateTemplateParameterSchema() errorText = %s, wantErrorText %s", err.Error(), tt.errText)
			}
		})
	}
}

var _ = Describe("validateUpgradeDefaults", func() {
	var (
		t        *clusterTemplateReconcilerTask
		tName    = "cluster-template-test"
		tVersion = "v1.0.0"
	)

	BeforeEach(func() {
		ct := &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      GetClusterTemplateRefName(tName, tVersion),
				Namespace: "default",
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:             tName,
				Version:          tVersion,
				Release:          "4.17.0",
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{},
			},
		}

		t = &clusterTemplateReconcilerTask{
			logger: logger,
			object: ct,
		}
	})

	It("should return error when upgrade defaults data is not a map", func() {
		t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
			Raw: []byte(`[]`),
		}
		err := t.validateUpgradeDefaults()
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("upgradeDefaults is not a map"))
	})

	It("should return error when upgrade defaults is malformed JSON", func() {
		t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
			Raw: []byte(`{invalid json`),
		}
		err := t.validateUpgradeDefaults()
		Expect(err).To(HaveOccurred())
	})

	It("should pass when upgradeDefaults is an empty object", func() {
		t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
			Raw: []byte(`{}`),
		}
		err := t.validateUpgradeDefaults()
		Expect(err).ToNot(HaveOccurred())
	})

	It("should reject when both clusterVersion and imageBasedGroupUpgrade keys are present", func() {
		t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
			Raw: []byte(`{"clusterVersion":{},"imageBasedGroupUpgrade":{}}`),
		}
		err := t.validateUpgradeDefaults()
		Expect(err).To(HaveOccurred())
		Expect(typederrors.IsInputError(err)).To(BeTrue())
		Expect(err.Error()).To(ContainSubstring("only one upgrade type is allowed"))
	})

	Context("IBGU upgrade defaults", func() {
		BeforeEach(func() {
			t.object.Spec.TemplateParameterSchema = runtime.RawExtension{
				Raw: []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"imageBasedGroupUpgrade":{"type":"object","properties":{"ibuSpec":{"type":"object","properties":{"seedImageRef":{"type":"object","properties":{"image":{"type":"string"},"version":{"type":"string"}}},"oadpContent":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"namespace":{"type":"string"}}}}}},"plan":{"type":"array","items":{"type":"object","properties":{"actions":{"type":"array","items":{"type":"string"}}}}}}}}}}}`),
			}
		})

		It("should validate valid IBGU upgrade defaults successfully", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"imageBasedGroupUpgrade":{"ibuSpec":{"seedImageRef":{"image":"quay.io/openshift-release-dev/ocp-release","version":"4.17.0"},"oadpContent":[{"name":"oadp-backup","namespace":"openshift-adp"}]},"plan":[{"actions":["Prep"]},{"actions":["Upgrade"]}]}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).ToNot(HaveOccurred())
		})

		It("should return error when release version does not match seedImageRef version", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"imageBasedGroupUpgrade":{"ibuSpec":{"seedImageRef":{"image":"quay.io/ocp","version":"4.18.0"}},"plan":[{"actions":["Prep"]},{"actions":["Upgrade"]}]}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("The ClusterTemplate spec.release (4.17.0) does not match the seedImageRef version (4.18.0) from the upgrade defaults"))
		})

		It("should pass when seedImageRef version is empty", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"imageBasedGroupUpgrade":{"ibuSpec":{"seedImageRef":{"image":"quay.io/ocp"}},"plan":[{"actions":["Prep"]}]}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).ToNot(HaveOccurred())
		})

		It("should return error when seedImageRef version is set but release is empty", func() {
			t.object.Spec.Release = ""
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"imageBasedGroupUpgrade":{"ibuSpec":{"seedImageRef":{"image":"quay.io/openshift-release-dev/ocp-release","version":"4.17.0"},"oadpContent":[{"name":"oadp-backup","namespace":"openshift-adp"}]},"plan":[{"actions":["Prep"]},{"actions":["Upgrade"]}]}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("The ClusterTemplate spec.release () does not match the seedImageRef version (4.17.0) from the upgrade defaults"))
		})
	})

	Context("ClusterVersion upgrade defaults", func() {
		BeforeEach(func() {
			t.object.Spec.TemplateParameterSchema = runtime.RawExtension{
				Raw: []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"clusterVersion":{"type":"object","properties":{"desiredUpdate":{"type":"object","properties":{"version":{"type":"string"}}}}},"clusterUpgradeTimeout":{"type":"string"},"intermediateVersion":{"type":"string"}}}}}`),
			}
		})

		It("should validate valid clusterVersion defaults", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{"desiredUpdate":{"version":"4.17.0"}},"clusterUpgradeTimeout":"2h30m","intermediateVersion":"4.16.3"}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).ToNot(HaveOccurred())
		})

		It("should reject when desiredUpdate.version does not match spec.release", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{"desiredUpdate":{"version":"4.18.0"}}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("the clusterVersion desiredUpdate.version (4.18.0) does not match the ClusterTemplate spec.release (4.17.0)"))
		})

		It("should pass when desiredUpdate.version is empty", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{"desiredUpdate":{}}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).ToNot(HaveOccurred())
		})

		It("should pass when desiredUpdate is absent", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).ToNot(HaveOccurred())
		})

		It("should reject invalid clusterUpgradeTimeout", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{},"clusterUpgradeTimeout":"notaduration"}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("invalid clusterUpgradeTimeout"))
		})

		It("should reject invalid intermediateVersion", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{},"intermediateVersion":"not-semver"}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("is not valid semver"))
		})

		It("should reject intermediateVersion when major version does not match release major", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{},"intermediateVersion":"3.16.0"}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("major version (3) must equal ClusterTemplate's spec.release major version (4)"))
		})

		It("should reject intermediateVersion when minor+1 does not match release minor", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{},"intermediateVersion":"4.15.0"}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("intermediateVersion 4.15.0 must be exactly one minor version below ClusterTemplate's spec.release version 4.17.0"))
		})

		It("should reject when clusterVersion value is not an object", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":"invalid"}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring("upgradeDefaults does not conform to the upgradeParameters schema"))
		})
	})

	Context("schema type consistency", func() {
		It("should reject when defaults have clusterVersion but schema has imageBasedGroupUpgrade", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{}}`),
			}
			t.object.Spec.TemplateParameterSchema = runtime.RawExtension{
				Raw: []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"imageBasedGroupUpgrade":{"type":"object"}}}}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring(`upgradeDefaults defines "clusterVersion", but upgradeParameters schema does not include a matching "clusterVersion" definition`))
		})

		It("should reject when defaults have imageBasedGroupUpgrade but schema has clusterVersion", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"imageBasedGroupUpgrade":{"ibuSpec":{"seedImageRef":{"image":"quay.io/ocp","version":"4.17.0"}},"plan":[{"actions":["Prep"]}]}}`),
			}
			t.object.Spec.TemplateParameterSchema = runtime.RawExtension{
				Raw: []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"clusterVersion":{"type":"object"}}}}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(typederrors.IsInputError(err)).To(BeTrue())
			Expect(err.Error()).To(ContainSubstring(`upgradeDefaults defines "imageBasedGroupUpgrade", but upgradeParameters schema does not include a matching "imageBasedGroupUpgrade" definition`))
		})
	})

	Context("schema validation of defaults", func() {
		It("should reject when defaults do not conform to schema", func() {
			t.object.Spec.TemplateDefaults.UpgradeDefaults = runtime.RawExtension{
				Raw: []byte(`{"clusterVersion":{},"extraField":"unexpected"}`),
			}
			t.object.Spec.TemplateParameterSchema = runtime.RawExtension{
				Raw: []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"clusterVersion":{"type":"object"}},"additionalProperties":false}}}`),
			}
			err := t.validateUpgradeDefaults()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("upgradeDefaults does not conform to the upgradeParameters schema"))
		})
	})
})

var _ = Describe("validateUpgradeParametersSchema", func() {
	It("should pass when upgradeParameters is not in schema and no upgradeDefaults", func() {
		schema := []byte(`{"type":"object","properties":{"otherParam":{"type":"string"}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).ToNot(HaveOccurred())
	})

	It("should return error when upgradeParameters is not in schema but upgradeDefaults is set", func() {
		schema := []byte(`{"type":"object","properties":{"otherParam":{"type":"string"}}}`)
		err := validateUpgradeParametersSchema(schema, true)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(Equal(`templateParameterSchema must define "upgradeParameters" when upgradeDefaults is set`))
	})

	It("should return error when upgradeParameters is not type object", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"string"}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(Equal(`"upgradeParameters" must have type "object"`))
	})

	It("should pass when upgradeParameters has imageBasedGroupUpgrade of type object", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"imageBasedGroupUpgrade":{"type":"object"}}}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).ToNot(HaveOccurred())
	})

	It("should pass when upgradeParameters has clusterVersion of type object", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"clusterVersion":{"type":"object"}}}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).ToNot(HaveOccurred())
	})

	It("should reject when upgradeParameters has both clusterVersion and imageBasedGroupUpgrade", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"clusterVersion":{"type":"object"},"imageBasedGroupUpgrade":{"type":"object"}}}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("choose exactly one upgrade type"))
	})

	It("should reject when imageBasedGroupUpgrade is not type object", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"imageBasedGroupUpgrade":{"type":"string"}}}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring(`upgradeParameters.imageBasedGroupUpgrade must have type "object"`))
	})

	It("should reject when clusterVersion is not type object", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"clusterVersion":{"type":"string"}}}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring(`upgradeParameters.clusterVersion must have type "object"`))
	})

	It("should return error when upgradeParameters has no properties section", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object"}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(Equal(`"upgradeParameters" schema must have a properties section`))
	})

	It("should pass when upgradeParameters has no upgrade type and no upgradeDefaults", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"otherKey":{"type":"string"}}}}}`)
		err := validateUpgradeParametersSchema(schema, false)
		Expect(err).ToNot(HaveOccurred())
	})

	It("should reject when upgradeDefaults is set but no upgrade type property defined", func() {
		schema := []byte(`{"type":"object","properties":{"upgradeParameters":{"type":"object","properties":{"otherKey":{"type":"string"}}}}}`)
		err := validateUpgradeParametersSchema(schema, true)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("must define either"))
	})
})

var _ = Describe("validateClusterImageSetMatchesRelease", func() {
	var (
		ctx             context.Context
		c               client.Client
		task            *clusterTemplateReconcilerTask
		clusterTemplate *provisioningv1alpha1.ClusterTemplate
		configMap       *corev1.ConfigMap
		clusterImageSet *hivev1.ClusterImageSet
		scheme          *runtime.Scheme
	)

	BeforeEach(func() {
		ctx = context.Background()

		// Initialize scheme with required types
		scheme = runtime.NewScheme()
		err := corev1.AddToScheme(scheme)
		Expect(err).ToNot(HaveOccurred())
		err = provisioningv1alpha1.AddToScheme(scheme)
		Expect(err).ToNot(HaveOccurred())
		err = hivev1.AddToScheme(scheme)
		Expect(err).ToNot(HaveOccurred())

		c = fake.NewClientBuilder().WithScheme(scheme).Build()

		clusterTemplate = &provisioningv1alpha1.ClusterTemplate{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-cluster-template",
				Namespace: "test-namespace",
			},
			Spec: provisioningv1alpha1.ClusterTemplateSpec{
				Name:    "test-cluster-template",
				Version: "v1.0.0",
				Release: "4.17.0",
				TemplateDefaults: provisioningv1alpha1.TemplateDefaults{
					ClusterInstanceDefaults: "test-configmap",
				},
			},
		}

		configMap = &corev1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-configmap",
				Namespace: "test-namespace",
			},
			Data: map[string]string{
				"clusterinstance-defaults": `
clusterImageSetNameRef: "4.17.0"
baseDomain: example.com
clusterType: SNO
`,
			},
		}

		clusterImageSet = &hivev1.ClusterImageSet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "4.17.0",
			},
			Spec: hivev1.ClusterImageSetSpec{
				ReleaseImage: "quay.io/openshift-release-dev/ocp-release:4.17.0-x86_64",
			},
		}

		task = &clusterTemplateReconcilerTask{
			client: c,
			logger: logger,
			object: clusterTemplate,
		}
	})

	It("should succeed when ClusterImageSet version matches ClusterTemplate release", func() {
		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).ToNot(HaveOccurred())
	})

	It("should fail when ClusterImageSet version does not match ClusterTemplate release", func() {
		// Create ConfigMap and ClusterImageSet with mismatched versions
		clusterTemplate.Spec.Release = "4.16.0" // Different from ClusterImageSet version

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("does not match ClusterTemplate release version"))
		Expect(err.Error()).To(ContainSubstring("4.17.0"))
		Expect(err.Error()).To(ContainSubstring("4.16.0"))
	})

	It("should fail when ClusterInstanceDefaults ConfigMap does not exist", func() {
		// Don't create the ConfigMap
		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("failed to get ClusterInstanceDefaults ConfigMap"))
	})

	It("should fail when clusterinstance-defaults key is missing from ConfigMap", func() {
		configMap.Data = map[string]string{
			"wrong-key": "some-data",
		}
		Expect(c.Create(ctx, configMap)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("failed to extract clusterinstance-defaults"))
	})

	It("should fail when clusterImageSetNameRef is missing from cluster instance data", func() {
		configMap.Data = map[string]string{
			"clusterinstance-defaults": `
baseDomain: example.com
clusterType: SNO
# clusterImageSetNameRef is missing
`,
		}
		Expect(c.Create(ctx, configMap)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("clusterImageSetNameRef not found"))
	})

	It("should fail when clusterImageSetNameRef is not a string", func() {
		configMap.Data = map[string]string{
			"clusterinstance-defaults": `
clusterImageSetNameRef: 123  # Not a string
baseDomain: example.com
`,
		}
		Expect(c.Create(ctx, configMap)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("clusterImageSetNameRef in ClusterInstanceDefaults ConfigMap"))
		Expect(err.Error()).To(ContainSubstring("is not a string"))
	})

	It("should fail when ClusterImageSet does not exist", func() {
		Expect(c.Create(ctx, configMap)).To(Succeed())
		// Don't create the ClusterImageSet

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("failed to get ClusterImageSet"))
	})

	It("should fail when ClusterImageSet has no releaseImage in spec", func() {
		clusterImageSet.Spec.ReleaseImage = ""

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("could not extract version from ClusterImageSet"))
	})

	It("should fail when ClusterImageSet releaseImage contains no extractable version", func() {
		clusterImageSet.Spec.ReleaseImage = "registry.example.com/openshift:latest" // No version pattern

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("could not extract version from ClusterImageSet"))
	})

	It("should fail when extracted version is invalid semver", func() {
		clusterImageSet.Spec.ReleaseImage = "registry.example.com/openshift:4.17.0-rc.invalid.identifier" // Invalid semver pre-release

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("failed to parse ClusterImageSet version"))
	})

	It("should fail when release image has partial version (missing patch)", func() {
		clusterImageSet.Spec.ReleaseImage = "registry.example.com/openshift:4.17" // Missing patch version

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("could not extract version from ClusterImageSet"))
	})

	It("should fail when ClusterTemplate release version is invalid semver", func() {
		clusterTemplate.Spec.Release = "invalid-version"

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("failed to parse ClusterTemplate release version"))
	})

	It("should handle pre-release versions correctly", func() {
		clusterTemplate.Spec.Release = "4.17.0-rc1"

		// Update the ConfigMap to reference the matching pre-release ClusterImageSet
		configMap.Data = map[string]string{
			"clusterinstance-defaults": `
clusterImageSetNameRef: "4.17.0-rc1"
baseDomain: example.com
clusterType: SNO
`,
		}

		// Update ClusterImageSet name and release image to match
		clusterImageSet.ObjectMeta.Name = "4.17.0-rc1"
		clusterImageSet.Spec.ReleaseImage = "quay.io/openshift-release-dev/ocp-release:4.17.0-rc1-x86_64"

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).ToNot(HaveOccurred())
	})

	It("should handle different architectures in release image", func() {
		clusterImageSet.Spec.ReleaseImage = "quay.io/openshift-release-dev/ocp-release:4.17.0-aarch64"

		Expect(c.Create(ctx, configMap)).To(Succeed())
		Expect(c.Create(ctx, clusterImageSet)).To(Succeed())

		err := task.validateClusterImageSetMatchesRelease(ctx)
		Expect(err).ToNot(HaveOccurred())
	})

})

var _ = Describe("extractVersionFromReleaseImage", func() {
	It("should extract version from standard OpenShift release image", func() {
		testCases := []struct {
			description     string
			releaseImage    string
			expectedVersion string
		}{
			{
				description:     "standard OCP release image",
				releaseImage:    "quay.io/openshift-release-dev/ocp-release:4.17.0-x86_64",
				expectedVersion: "4.17.0",
			},
			{
				description:     "release image without architecture",
				releaseImage:    "quay.io/openshift-release-dev/ocp-release:4.16.1",
				expectedVersion: "4.16.1",
			},
			{
				description:     "release image with pre-release tag",
				releaseImage:    "registry.redhat.io/ubi8/ubi:4.15.2-rc1",
				expectedVersion: "4.15.2-rc1",
			},
			{
				description:     "simple registry with version",
				releaseImage:    "my-registry/openshift:4.14.10",
				expectedVersion: "4.14.10",
			},
			{
				description:     "version with build metadata",
				releaseImage:    "quay.io/openshift:4.18.0-build.123",
				expectedVersion: "4.18.0",
			},
		}

		for _, tc := range testCases {
			By(tc.description)
			version := extractVersionFromReleaseImage(tc.releaseImage)
			Expect(version).To(Equal(tc.expectedVersion))
		}
	})

	It("should return empty string for digest references", func() {
		version := extractVersionFromReleaseImage(
			"quay.io/openshift-release-dev/ocp-release@sha256:5b87a665045cdfe0a1b271024be936a0c46de17b25a112d6a136c5af89d861c4")
		Expect(version).To(Equal(""))
	})

	It("should return empty string for invalid release images", func() {
		testCases := []struct {
			description  string
			releaseImage string
		}{
			{
				description:  "image without tag",
				releaseImage: "quay.io/openshift-release-dev/ocp-release",
			},
			{
				description:  "image with non-version tag",
				releaseImage: "quay.io/openshift-release-dev/ocp-release:latest",
			},
			{
				description:  "image with partial version",
				releaseImage: "quay.io/openshift-release-dev/ocp-release:4.17",
			},
			{
				description:  "digest reference",
				releaseImage: "quay.io/openshift-release-dev/ocp-release@sha256:5b87a665045cdfe0a1b271024be936a0c46de17b25a112d6a136c5af89d861c4",
			},
			{
				description:  "empty string",
				releaseImage: "",
			},
			{
				description:  "invalid format",
				releaseImage: "not-a-valid-image",
			},
		}

		for _, tc := range testCases {
			By(tc.description)
			version := extractVersionFromReleaseImage(tc.releaseImage)
			Expect(version).To(Equal(""))
		}
	})
})

var _ = Describe("extractVersionFromClusterImageSet", func() {
	It("should extract version from releaseTag label (ACM 2.17+ with digest reference)", func() {
		cis := &hivev1.ClusterImageSet{
			ObjectMeta: metav1.ObjectMeta{
				Name:   "img4.20.4-x86-64-appsub",
				Labels: map[string]string{"releaseTag": "4.20.4-x86_64"},
			},
			Spec: hivev1.ClusterImageSetSpec{
				ReleaseImage: "quay.io/openshift-release-dev/ocp-release@sha256:5b87a665045cdfe0a1b271024be936a0c46de17b25a112d6a136c5af89d861c4",
			},
		}
		Expect(extractVersionFromClusterImageSet(cis)).To(Equal("4.20.4"))
	})

	It("should fall back to tagged release image when releaseTag label is absent", func() {
		cis := &hivev1.ClusterImageSet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "img4.17.0-x86-64-appsub",
			},
			Spec: hivev1.ClusterImageSetSpec{
				ReleaseImage: "quay.io/openshift-release-dev/ocp-release:4.17.0-x86_64",
			},
		}
		Expect(extractVersionFromClusterImageSet(cis)).To(Equal("4.17.0"))
	})

	It("should return empty when neither releaseTag nor tagged image is available", func() {
		cis := &hivev1.ClusterImageSet{
			ObjectMeta: metav1.ObjectMeta{
				Name: "img4.20.4-x86-64-appsub",
			},
			Spec: hivev1.ClusterImageSetSpec{
				ReleaseImage: "quay.io/openshift-release-dev/ocp-release@sha256:5b87a665045cdfe0a1b271024be936a0c46de17b25a112d6a136c5af89d861c4",
			},
		}
		Expect(extractVersionFromClusterImageSet(cis)).To(Equal(""))
	})

	It("should prefer releaseTag label over tagged release image", func() {
		cis := &hivev1.ClusterImageSet{
			ObjectMeta: metav1.ObjectMeta{
				Name:   "img4.20.4-x86-64-appsub",
				Labels: map[string]string{"releaseTag": "4.20.5-x86_64"},
			},
			Spec: hivev1.ClusterImageSetSpec{
				ReleaseImage: "quay.io/openshift-release-dev/ocp-release:4.20.4-x86_64",
			},
		}
		Expect(extractVersionFromClusterImageSet(cis)).To(Equal("4.20.5"))
	})
})

var _ = Describe("validateVersionsMatch", func() {
	It("should succeed when versions match exactly", func() {
		testCases := []struct {
			description     string
			imageVersion    string
			templateVersion string
		}{
			{
				description:     "exact semantic versions",
				imageVersion:    "4.17.0",
				templateVersion: "4.17.0",
			},
			{
				description:     "versions with v prefix",
				imageVersion:    "v4.16.1",
				templateVersion: "v4.16.1",
			},
			{
				description:     "mixed prefix formats",
				imageVersion:    "4.15.2",
				templateVersion: "v4.15.2",
			},
			{
				description:     "pre-release versions",
				imageVersion:    "4.18.0-rc1",
				templateVersion: "4.18.0-rc1",
			},
		}

		for _, tc := range testCases {
			By(tc.description)
			err := validateVersionsMatch(tc.imageVersion, tc.templateVersion)
			Expect(err).ToNot(HaveOccurred())
		}
	})

	It("should fail when versions do not match", func() {
		testCases := []struct {
			description     string
			imageVersion    string
			templateVersion string
		}{
			{
				description:     "different patch versions",
				imageVersion:    "4.17.0",
				templateVersion: "4.17.1",
			},
			{
				description:     "different minor versions",
				imageVersion:    "4.16.0",
				templateVersion: "4.17.0",
			},
			{
				description:     "different major versions",
				imageVersion:    "3.17.0",
				templateVersion: "4.17.0",
			},
			{
				description:     "pre-release vs release",
				imageVersion:    "4.17.0-rc1",
				templateVersion: "4.17.0",
			},
		}

		for _, tc := range testCases {
			By(tc.description)
			err := validateVersionsMatch(tc.imageVersion, tc.templateVersion)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("versions do not match exactly"))
		}
	})

	It("should fail with invalid semver versions", func() {
		testCases := []struct {
			description     string
			imageVersion    string
			templateVersion string
			expectedError   string
		}{
			{
				description:     "invalid image version",
				imageVersion:    "invalid-version",
				templateVersion: "4.17.0",
				expectedError:   "failed to parse ClusterImageSet version",
			},
			{
				description:     "invalid template version",
				imageVersion:    "4.17.0",
				templateVersion: "not-semver",
				expectedError:   "failed to parse ClusterTemplate release version",
			},
			{
				description:     "both versions invalid",
				imageVersion:    "bad-version",
				templateVersion: "also-bad",
				expectedError:   "failed to parse ClusterImageSet version",
			},
			{
				description:     "partial version",
				imageVersion:    "4.17",
				templateVersion: "4.17.0",
				expectedError:   "failed to parse ClusterImageSet version",
			},
		}

		for _, tc := range testCases {
			By(tc.description)
			err := validateVersionsMatch(tc.imageVersion, tc.templateVersion)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring(tc.expectedError))
		}
	})
})
