/*
Copyright © 2022 - 2025 SUSE LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controllers

import (
	"encoding/json"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	elementalv1 "github.com/rancher/elemental-operator/api/v1beta1"
	"github.com/rancher/elemental-operator/pkg/test"
	fleetv1 "github.com/rancher/fleet/pkg/apis/fleet.cattle.io/v1alpha1"
	upgradev1 "github.com/rancher/system-upgrade-controller/pkg/apis/upgrade.cattle.io/v1"
	"github.com/rancher/wrangler/v2/pkg/genericcondition"
	"github.com/rancher/wrangler/v2/pkg/name"
	"gopkg.in/yaml.v3"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/intstr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

var _ = Describe("reconcile managed os image", func() {
	var r *ManagedOSImageReconciler
	var managedOSImage *elementalv1.ManagedOSImage
	var bundle *fleetv1.Bundle

	BeforeEach(func() {
		r = &ManagedOSImageReconciler{
			Client: cl,
			Scheme: cl.Scheme(),
		}

		managedOSImage = &elementalv1.ManagedOSImage{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "default",
			},
		}

		bundle = &fleetv1.Bundle{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name.SafeConcatName("mos", managedOSImage.Name),
				Namespace: managedOSImage.Namespace,
			},
		}
	})

	AfterEach(func() {
		Expect(test.CleanupAndWait(ctx, cl, managedOSImage, bundle)).To(Succeed())
	})

	It("should reconcile managed os image object", func() {
		Expect(cl.Create(ctx, managedOSImage)).To(Succeed())

		_, err := r.Reconcile(ctx, reconcile.Request{
			NamespacedName: types.NamespacedName{
				Namespace: managedOSImage.Namespace,
				Name:      managedOSImage.Name,
			},
		})
		Expect(err).ToNot(HaveOccurred())

		Expect(cl.Get(ctx, client.ObjectKey{
			Name:      managedOSImage.Name,
			Namespace: managedOSImage.Namespace,
		}, managedOSImage)).To(Succeed())

		bundle := &fleetv1.Bundle{}
		Expect(cl.Get(ctx, client.ObjectKey{
			Name:      name.SafeConcatName("mos", managedOSImage.Name),
			Namespace: managedOSImage.Namespace,
		}, bundle)).To(Succeed())
	})
})

var _ = Describe("newFleetBundleResources", func() {
	var r *ManagedOSImageReconciler
	var managedOSImage *elementalv1.ManagedOSImage
	var managedOSVersion *elementalv1.ManagedOSVersion

	BeforeEach(func() {
		r = &ManagedOSImageReconciler{
			Client: cl,
			Scheme: cl.Scheme(),
		}

		managedOSImage = &elementalv1.ManagedOSImage{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "default",
			},
		}

		managedOSVersion = &elementalv1.ManagedOSVersion{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test",
				Namespace: "default",
			},
			Spec: elementalv1.ManagedOSVersionSpec{
				Type: "container",
				UpgradeContainer: &upgradev1.ContainerSpec{
					Image: "foo/bar:image",
				},
			},
		}
	})

	AfterEach(func() {
		Expect(test.CleanupAndWait(ctx, cl, managedOSImage, managedOSVersion)).To(Succeed())
	})

	It("should create fleet bundle resources", func() {
		bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
		Expect(err).ToNot(HaveOccurred())

		Expect(bundleResources).To(HaveLen(5))

		Expect(bundleResources[0].Name).To(Equal("ClusterRole--os-upgrader-test-name-d5689b3c1cd3.yaml"))
		Expect(bundleResources[1].Name).To(Equal("ClusterRoleBinding--os-upgrader-test-name-cc7ce4275b54.yaml"))
		Expect(bundleResources[2].Name).To(Equal("ServiceAccount-cattle-system-os-upgrader-test-name-08929531f5c0.yaml"))
		Expect(bundleResources[3].Name).To(Equal("Secret-cattle-system-os-upgrader-test-name-52e9d8e041f4.yaml"))
		Expect(bundleResources[4].Name).To(Equal("Plan-cattle-system-os-upgrader-test-name-24d63a562894.yaml"))
	})

	It("should create fleet bundle when managedOSVersion exists", func() {
		Expect(r.Create(ctx, managedOSVersion)).To(Succeed())

		managedOSImage.Spec.ManagedOSVersionName = "test"
		bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
		Expect(err).ToNot(HaveOccurred())

		Expect(bundleResources).To(HaveLen(5))
		plan := &upgradev1.Plan{}
		Expect(yaml.Unmarshal([]byte(bundleResources[4].Content), plan)).To(Succeed())
		Expect(plan.Spec.Upgrade.Image).To(Equal("foo/bar:image"))
	})

	It("return error of managedOSVersion doesn't exist", func() {
		managedOSImage.Spec.ManagedOSVersionName = "test"
		bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
		Expect(err).To(HaveOccurred())
		Expect(bundleResources).To(BeNil())
	})

	It("should convert ManagedOSImage name to DNS Label standard", func() {
		managedOSImage.Name = "test.name"
		bundleResources, err := r.newFleetBundleResources(ctx, managedOSImage)
		Expect(err).ToNot(HaveOccurred())

		Expect(bundleResources[0].Name).To(Equal("ClusterRole--os-upgrader-test-name-d5689b3c1cd3.yaml"))
		Expect(bundleResources[1].Name).To(Equal("ClusterRoleBinding--os-upgrader-test-name-cc7ce4275b54.yaml"))
		Expect(bundleResources[2].Name).To(Equal("ServiceAccount-cattle-system-os-upgrader-test-name-08929531f5c0.yaml"))
		Expect(bundleResources[3].Name).To(Equal("Secret-cattle-system-os-upgrader-test-name-52e9d8e041f4.yaml"))
		Expect(bundleResources[4].Name).To(Equal("Plan-cattle-system-os-upgrader-test-name-24d63a562894.yaml"))
	})
})

var _ = Describe("createFleetBundle", func() {
	var r *ManagedOSImageReconciler
	var managedOSImage *elementalv1.ManagedOSImage
	var bundle *fleetv1.Bundle

	BeforeEach(func() {
		r = &ManagedOSImageReconciler{
			Client: cl,
			Scheme: cl.Scheme(),
		}

		managedOSImage = &elementalv1.ManagedOSImage{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "default",
				UID:       "test",
			},
			Spec: elementalv1.ManagedOSImageSpec{
				ClusterRolloutStrategy: &fleetv1.RolloutStrategy{
					MaxUnavailable: &intstr.IntOrString{
						IntVal: 3,
					},
				},
				Targets: []fleetv1.BundleTarget{
					{
						Name: "test",
					},
				},
			},
		}

		bundle = &fleetv1.Bundle{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name.SafeConcatName("mos", managedOSImage.Name),
				Namespace: managedOSImage.Namespace,
			},
		}
	})

	AfterEach(func() {
		Expect(test.CleanupAndWait(ctx, cl, managedOSImage, bundle)).To(Succeed())
	})

	It("should create fleet bundle resources", func() {
		err := r.createFleetBundle(ctx, managedOSImage, []fleetv1.BundleResource{
			{
				Name: "test",
			},
		})
		Expect(err).ToNot(HaveOccurred())

		Expect(cl.Get(ctx, client.ObjectKey{
			Name:      name.SafeConcatName("mos", managedOSImage.Name),
			Namespace: managedOSImage.Namespace,
		}, bundle)).To(Succeed())

		Expect(bundle.Spec.Resources).To(HaveLen(1))
		Expect(bundle.Spec.Resources[0].Name).To(Equal("test"))
		Expect(bundle.Spec.RolloutStrategy).ToNot(BeNil())
		Expect(bundle.Spec.RolloutStrategy.MaxUnavailable.IntVal).To(Equal(int32(3)))
		Expect(bundle.Spec.Targets).To(HaveLen(1))
		Expect(bundle.Spec.Targets[0].Name).To(Equal("test"))
	})

	It("should change target when namespace is fleet-local", func() {
		managedOSImage.Namespace = fleetLocalNamespace
		err := r.createFleetBundle(ctx, managedOSImage, []fleetv1.BundleResource{
			{
				Name: "test",
			},
		})
		Expect(err).ToNot(HaveOccurred())

		Expect(cl.Get(ctx, client.ObjectKey{
			Name:      r.formatBundleName(*managedOSImage),
			Namespace: managedOSImage.Namespace,
		}, bundle)).To(Succeed())

		Expect(bundle.Spec.Resources).To(HaveLen(1))
		Expect(bundle.Spec.Resources[0].Name).To(Equal("test"))
		Expect(bundle.Spec.RolloutStrategy).ToNot(BeNil())
		Expect(bundle.Spec.RolloutStrategy.MaxUnavailable.IntVal).To(Equal(int32(3)))
		Expect(bundle.Spec.Targets).To(HaveLen(1))
		Expect(bundle.Spec.Targets[0].ClusterName).To(Equal("local"))
	})

	It("update fleet bundle if already exists", func() {
		Expect(r.createFleetBundle(ctx, managedOSImage, []fleetv1.BundleResource{})).Should(Succeed())
		meta.SetStatusCondition(&managedOSImage.Status.Conditions, metav1.Condition{
			Type:   elementalv1.FleetBundleCreation,
			Reason: elementalv1.FleetBundleCreateSuccessReason,
			Status: metav1.ConditionTrue,
		})

		wantResource := []fleetv1.BundleResource{{Name: "test", Content: "test-content"}}
		Expect(r.updateFleetBundle(ctx, managedOSImage, wantResource)).Should(Succeed())

		Expect(cl.Get(ctx, client.ObjectKey{
			Name:      r.formatBundleName(*managedOSImage),
			Namespace: managedOSImage.Namespace,
		}, bundle)).To(Succeed())
		Expect(bundle.Spec.Resources).To(Equal(wantResource))
	})
})

var _ = Describe("updateManagedOSImageStatus", func() {
	var r *ManagedOSImageReconciler
	var managedOSImage *elementalv1.ManagedOSImage
	var bundle *fleetv1.Bundle

	BeforeEach(func() {
		r = &ManagedOSImageReconciler{
			Client: cl,
			Scheme: cl.Scheme(),
		}

		managedOSImage = &elementalv1.ManagedOSImage{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "test-name",
				Namespace: "default",
			},
			Spec: elementalv1.ManagedOSImageSpec{
				ClusterRolloutStrategy: &fleetv1.RolloutStrategy{
					MaxUnavailable: &intstr.IntOrString{
						IntVal: 3,
					},
				},
				Targets: []fleetv1.BundleTarget{
					{
						Name: "test",
					},
				},
			},
		}

		bundle = &fleetv1.Bundle{
			ObjectMeta: metav1.ObjectMeta{
				Name:      name.SafeConcatName("mos", managedOSImage.Name),
				Namespace: managedOSImage.Namespace,
			},
		}

	})

	AfterEach(func() {
		Expect(test.CleanupAndWait(ctx, cl, managedOSImage, bundle)).To(Succeed())
	})

	It("should update manage os image status", func() {
		Expect(r.Create(ctx, bundle)).To(Succeed())

		patchBase := client.MergeFrom(bundle.DeepCopy())

		bundle.Status = fleetv1.BundleStatus{
			Conditions: []genericcondition.GenericCondition{
				{
					Type:    "test",
					Status:  corev1.ConditionTrue,
					Reason:  "test",
					Message: "test",
				},
			},
		}
		Expect(r.Status().Patch(ctx, bundle, patchBase)).To(Succeed())

		err := r.updateManagedOSImageStatus(ctx, managedOSImage)
		Expect(err).ToNot(HaveOccurred())

		Expect(managedOSImage.Status.Conditions).To(HaveLen(1))
		Expect(managedOSImage.Status.Conditions[0].Type).To(Equal("test"))
		Expect(managedOSImage.Status.Conditions[0].Status).To(Equal(metav1.ConditionTrue))
		Expect(managedOSImage.Status.Conditions[0].Reason).To(Equal("test"))
	})

	It("should update manage os image status when condition is missing fields", func() {
		Expect(r.Create(ctx, bundle)).To(Succeed())

		patchBase := client.MergeFrom(bundle.DeepCopy())

		bundle.Status = fleetv1.BundleStatus{
			Conditions: []genericcondition.GenericCondition{
				{
					Message: "test",
				},
			},
		}
		Expect(r.Status().Patch(ctx, bundle, patchBase)).To(Succeed())

		err := r.updateManagedOSImageStatus(ctx, managedOSImage)
		Expect(err).ToNot(HaveOccurred())

		Expect(managedOSImage.Status.Conditions).To(HaveLen(1))
		Expect(managedOSImage.Status.Conditions[0].Type).To(Equal("UnknownType"))
		Expect(managedOSImage.Status.Conditions[0].Status).To(Equal(metav1.ConditionUnknown))
		Expect(managedOSImage.Status.Conditions[0].Reason).To(Equal("UnknownReason"))
	})
})

var _ = Describe("metadataEnv", func() {
	It("should container env variables when passed as strings", func() {
		envMap := map[string]runtime.RawExtension{
			"upgradeImage": {Raw: []byte(`"registry.com/repository/image:v1.0"`)},
			"robin":        {Raw: []byte(`"batman"`)},
		}
		env := metadataEnv(envMap)
		Expect(env).To(HaveLen(2))
		for _, e := range env {
			if e.Name == "METADATA_UPGRADEIMAGE" {
				Expect(e.Value).To(Equal("registry.com/repository/image:v1.0"))
			} else {
				Expect(e.Name).To(Equal("METADATA_ROBIN"))
				Expect(e.Value).To(Equal("batman"))
			}
		}

	})

	It("should container env variables when passed as jsondata", func() {
		jsonStruct := struct{ Foo string }{Foo: "foostruct"}
		jsonData, err := json.Marshal(jsonStruct)
		Expect(err).ToNot(HaveOccurred())

		envMap := map[string]runtime.RawExtension{
			"jsondata": {Raw: jsonData},
		}
		env := metadataEnv(envMap)
		Expect(env).To(HaveLen(1))
		Expect(env[0].Name).To(Equal("METADATA_JSONDATA"))
		Expect(env[0].Value).To(Equal("{\"Foo\":\"foostruct\"}"))
	})
})

var _ = Describe("getImageVersion", func() {
	It("should format images with no registry and no version", func() {
		osImage := &elementalv1.ManagedOSImage{
			Spec: elementalv1.ManagedOSImageSpec{
				OSImage: "a-test-image",
			},
		}
		image, version, err := getImageVersion(osImage, nil)
		Expect(err).ShouldNot(HaveOccurred())
		Expect(version).Should(Equal("latest"))
		Expect(image).Should(Equal(osImage.Spec.OSImage))
	})
	It("should format images with no version", func() {
		osImage := &elementalv1.ManagedOSImage{
			Spec: elementalv1.ManagedOSImageSpec{
				OSImage: "a-test-registry/a-test-image",
			},
		}
		image, version, err := getImageVersion(osImage, nil)
		Expect(err).ShouldNot(HaveOccurred())
		Expect(version).Should(Equal("latest"))
		Expect(image).Should(Equal(osImage.Spec.OSImage))
	})
	It("should format images with fully qualified name", func() {
		osImage := &elementalv1.ManagedOSImage{
			Spec: elementalv1.ManagedOSImageSpec{
				OSImage: "a-test-registry/a-test-image:my-version",
			},
		}
		image, version, err := getImageVersion(osImage, nil)
		Expect(err).ShouldNot(HaveOccurred())
		Expect(version).Should(Equal("my-version"))
		Expect(image).Should(Equal("a-test-registry/a-test-image"))
	})
	It("should format images with non standard registry ports", func() {
		osImage := &elementalv1.ManagedOSImage{
			Spec: elementalv1.ManagedOSImageSpec{
				OSImage: "a-test-registry:30000/a-test-image:my-version",
			},
		}
		image, version, err := getImageVersion(osImage, nil)
		Expect(err).ShouldNot(HaveOccurred())
		Expect(version).Should(Equal("my-version"))
		Expect(image).Should(Equal("a-test-registry:30000/a-test-image"))
	})
	It("should format images with non standard registry ports and no version", func() {
		osImage := &elementalv1.ManagedOSImage{
			Spec: elementalv1.ManagedOSImageSpec{
				OSImage: "a-test-registry:30000/a-test-image",
			},
		}
		image, version, err := getImageVersion(osImage, nil)
		Expect(err).ShouldNot(HaveOccurred())
		Expect(version).Should(Equal("latest"))
		Expect(image).Should(Equal(osImage.Spec.OSImage))
	})
})

var _ = Describe("Plan naming", func() {
	It("should replace invalid characters with -", func() {
		input := "-my.invalid!name@"
		wanted := "my-invalid-name"
		Expect(toDNSLabel(input)).To(Equal(wanted))
	})
	It("should not replace valid strings", func() {
		wanted := "my-valid-name"
		Expect(toDNSLabel(wanted)).To(Equal(wanted))
	})
})
