//go:build e2e
// +build e2e

package e2e

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	kvmv1 "github.com/tidbcloud/k8s-version-manager/api/v1"
	"github.com/tidbcloud/k8s-version-manager/test/utils"
)

var _ = Describe("Manager", Ordered, func() {
	var controllerPodName string

	BeforeAll(func() {
		By("ensuring mock-server is ready")
		verifyMockServerReady := func(g Gomega) {
			cmd := exec.Command("kubectl", "get", "pods", "-l", "app=mock-server", "-n", namespace, "-o", "jsonpath={.items[0].status.phase}")
			output, err := utils.Run(cmd)
			g.Expect(err).NotTo(HaveOccurred())
			g.Expect(output).To(Equal("Running"))
		}
		Eventually(verifyMockServerReady, 2*time.Minute).Should(Succeed())
	})

	AfterAll(func() {
		if suiteFailed {
			_, _ = fmt.Fprintf(GinkgoWriter, "Skipping AfterAll cleanup to preserve environment for debugging\n")
			return
		}
		By("cleaning up the curl pod for metrics")
		cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace, "--ignore-not-found")
		_, _ = utils.Run(cmd)
	})

	// After each test, check for failures and collect logs, events,
	// and pod descriptions for debugging.
	AfterEach(func() {
		specReport := CurrentSpecReport()
		if specReport.Failed() {
			suiteFailed = true
			if controllerPodName == "" {
				cmd := exec.Command("kubectl", "get", "pods", "-l", "control-plane=controller-manager", "-n", namespace, "-o", "jsonpath={.items[0].metadata.name}")
				output, err := utils.Run(cmd)
				if err == nil {
					controllerPodName = strings.TrimSpace(output)
				}
			}
			By("Fetching controller manager pod logs")
			cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
			controllerLogs, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
			}

			By("Fetching Kubernetes events")
			cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
			eventsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
			}

			By("Fetching curl-metrics logs")
			cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
			metricsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
			}

			By("Fetching controller manager pod description")
			cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
			podDescription, err := utils.Run(cmd)
			if err == nil {
				fmt.Println("Pod description:\n", podDescription)
			} else {
				fmt.Println("Failed to describe controller pod")
			}
		}
	})

	SetDefaultEventuallyTimeout(2 * time.Minute)
	SetDefaultEventuallyPollingInterval(time.Second)

	Context("Manager", func() {
		It("should run successfully", func() {
			By("validating that the controller-manager pod is running as expected")
			verifyControllerUp := func(g Gomega) {
				// Get the name of the controller-manager pod
				cmd := exec.Command("kubectl", "get",
					"pods", "-l", "control-plane=controller-manager",
					"-o", "go-template={{ range .items }}"+
						"{{ if not .metadata.deletionTimestamp }}"+
						"{{ .metadata.name }}"+
						"{{ \"\\n\" }}{{ end }}{{ end }}",
					"-n", namespace,
				)

				podOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
				podNames := utils.GetNonEmptyLines(podOutput)
				g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
				controllerPodName = podNames[0]
				g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))

				// Validate the pod's status
				cmd = exec.Command("kubectl", "get",
					"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
					"-n", namespace,
				)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
			}
			Eventually(verifyControllerUp).Should(Succeed())
		})

		It("should ensure the metrics endpoint is serving metrics", func() {
			Skip("Skipping metrics test as it is flaky in KIND with custom config")
			By("creating a ClusterRoleBinding for the service account to allow access to metrics")
			cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
				"--clusterrole=k8s-version-manager-metrics-reader",
				fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
			)
			_, err := utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")

			By("validating that the metrics service is available")
			cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")

			By("getting the service account token")
			token, err := serviceAccountToken()
			Expect(err).NotTo(HaveOccurred())
			Expect(token).NotTo(BeEmpty())

			By("ensuring the controller pod is ready")
			verifyControllerPodReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace,
					"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("True"), "Controller pod not ready")
			}
			Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying that the controller manager is serving the metrics server")
			verifyMetricsServerStarted := func(g Gomega) {
				cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(ContainSubstring("Serving metrics server"),
					"Metrics server not yet started")
			}
			Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed())

			// +kubebuilder:scaffold:e2e-metrics-webhooks-readiness

			By("creating the curl-metrics pod to access the metrics endpoint")
			cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
				"--namespace", namespace,
				"--image=curlimages/curl:latest",
				"--overrides",
				fmt.Sprintf(`{
					"spec": {
						"containers": [{
							"name": "curl",
							"image": "curlimages/curl:latest",
							"command": ["/bin/sh", "-c"],
							"args": ["curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics"],
							"securityContext": {
								"readOnlyRootFilesystem": true,
								"allowPrivilegeEscalation": false,
								"capabilities": {
									"drop": ["ALL"]
								},
								"runAsNonRoot": true,
								"runAsUser": 1000,
								"seccompProfile": {
									"type": "RuntimeDefault"
								}
							}
						}],
						"serviceAccountName": "%s"
					}
				}`, token, metricsServiceName, namespace, serviceAccountName))
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")

			By("waiting for the curl-metrics pod to complete.")
			verifyCurlUp := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
					"-o", "jsonpath={.status.phase}",
					"-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
			}
			Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())

			By("getting the metrics by checking curl-metrics logs")
			verifyMetricsAvailable := func(g Gomega) {
				metricsOutput, err := getMetricsOutput()
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
				g.Expect(metricsOutput).NotTo(BeEmpty())
				g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
			}
			Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
		})

		It("should successfully execute a multi-step UpgradeOperation", func() {
			dataPlaneID := "dp-1"
			targetVersion := "1.34"
			upgradeOpName := fmt.Sprintf("test-upgrade-%d", time.Now().UnixNano())

			By("ensuring the cluster starts at an older version")
			initialInfo := &kvmv1.K8sInformation{
				TargetVersion: "1.33",
				ControlPlane: &kvmv1.K8sControlPlane{
					Name:    dataPlaneID,
					Version: "1.33",
				},
				NodePools: []kvmv1.K8sNodePool{
					{Kind: kvmv1.K8sNodePoolKindAdmin, Name: "admin", Version: "1.33", Ready: true},
					{Kind: kvmv1.K8sNodePoolKindWorker, Name: "worker-1", Version: "1.33", Ready: true},
					{Kind: kvmv1.K8sNodePoolKindWorker, Name: "worker-old", Version: "1.33", Ready: true},
				},
			}
			setMockStatus(dataPlaneID, initialInfo)

			By("creating an UpgradeOperation with dataplane-infra and cleanup-nodepools steps")
			req := kvmv1.CreateUpgradeOperationRequest{
				Spec: kvmv1.UpgradeOperationSpec{
					DataPlaneID:   dataPlaneID,
					TargetVersion: targetVersion,
					Schedule:      metav1.Now(),
					Steps: []kvmv1.UpgradeStep{
						{Target: "dataplane-infra", Version: targetVersion},
						{Target: "cleanup-nodepools", Version: targetVersion, Options: map[string]string{"versions-to-delete": "1.33"}},
					},
				},
			}
			createUpgradeOperationViaAPI(controllerPodName, dataPlaneID, upgradeOpName, req)

			By("waiting for the upgrade operation to start (Phase=Running)")
			verifyRunning := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.phase}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Running"))
			}
			Eventually(verifyRunning).Should(Succeed())

			By("verifying it's at the dataplane-infra step")
			verifyStep := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.currentStep.target}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("dataplane-infra"))
			}
			Eventually(verifyStep).Should(Succeed())

			By("simulating dataplane-infra completion by updating mock status")
			// Set mock status to show that infra is upgraded
			mockInfo := &kvmv1.K8sInformation{
				TargetVersion: targetVersion,
				ControlPlane: &kvmv1.K8sControlPlane{
					Name:    dataPlaneID,
					Version: targetVersion,
				},
				NodePools: []kvmv1.K8sNodePool{
					{Kind: kvmv1.K8sNodePoolKindAdmin, Name: "admin", Version: targetVersion, Ready: true},
					{Kind: kvmv1.K8sNodePoolKindWorker, Name: "worker-1", Version: targetVersion, Ready: true},
					{Kind: kvmv1.K8sNodePoolKindWorker, Name: "worker-old", Version: "1.33", Ready: true},
				},
			}
			setMockStatus(dataPlaneID, mockInfo)

			By("waiting for it to transition to cleanup-nodepools step")
			verifyCleanupStep := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.phase}")
				phase, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				if phase == string(kvmv1.UpgradePhaseCompleted) {
					return
				}

				cmd = exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.currentStep.target}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("cleanup-nodepools"))
			}
			Eventually(verifyCleanupStep, 1*time.Minute).Should(Succeed())

			By("simulating cleanup-nodepools completion by updating mock status")
			// Remove the old worker pool
			mockInfo.NodePools = []kvmv1.K8sNodePool{
				{Kind: kvmv1.K8sNodePoolKindAdmin, Name: "admin", Version: targetVersion, Ready: true},
				{Kind: kvmv1.K8sNodePoolKindWorker, Name: "worker-1", Version: targetVersion, Ready: true},
			}
			setMockStatus(dataPlaneID, mockInfo)

			By("waiting for the upgrade operation to complete (Phase=Completed)")
			verifyCompleted := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.phase}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Completed"))
			}
			Eventually(verifyCompleted, 2*time.Minute).Should(Succeed())
		})

		It("should fail the upgrade if retry duration is exhausted", func() {
			dataPlaneID := "dp-fail"
			targetVersion := "1.34"
			upgradeOpName := fmt.Sprintf("fail-upgrade-%d", time.Now().UnixNano())

			By("setting mock to return error for GetK8sInformation")
			setMockError(dataPlaneID, true)

			By("creating an UpgradeOperation with short retry duration")
			req := kvmv1.CreateUpgradeOperationRequest{
				Spec: kvmv1.UpgradeOperationSpec{
					DataPlaneID:   dataPlaneID,
					TargetVersion: targetVersion,
					Schedule:      metav1.Now(),
					Steps: []kvmv1.UpgradeStep{
						{Target: "dataplane-infra", Version: targetVersion, Options: map[string]string{"retry-duration": "10s"}},
					},
				},
			}
			createUpgradeOperationViaAPI(controllerPodName, dataPlaneID, upgradeOpName, req)

			By("waiting for the upgrade operation to fail (Phase=Failed)")
			verifyFailed := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.phase}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Failed"))
			}
			// It should take at least 10s + some reconcile cycles
			Eventually(verifyFailed, 1*time.Minute).Should(Succeed())
		})

		It("should successfully execute a CSNG UpgradeOperation", func() {
			dataPlaneID := "dp-csng-1"
			targetVersion := "1.34"
			upgradeOpName := fmt.Sprintf("csng-upgrade-%d", time.Now().UnixNano())

			By("setting up mock shared pools")
			initialPools := []SharedPool{
				{
					ID:      "pool-1",
					Version: "1.33",
					Phase:   "AVAILABLE",
					ComponentGroups: []Component{
						{
							ID:            "comp-pd",
							ComponentType: "PD",
							Version:       "1.33",
							Phase:         "AVAILABLE",
							NodeQuota: NodeQuota{
								NodeSpec: NodeSpec{NodeVersion: "1.33"},
							},
						},
						{
							ID:            "comp-tikv",
							ComponentType: "TIKV",
							Version:       "1.33",
							Phase:         "AVAILABLE",
							NodeQuota: NodeQuota{
								NodeSpec: NodeSpec{NodeVersion: "1.33"},
							},
						},
					},
				},
			}
			setMockSharedPools(initialPools)

			By("setting mock K8s info")
			initialInfo := &kvmv1.K8sInformation{
				TargetVersion: "1.34", // Assume infra is ready
				ControlPlane:  &kvmv1.K8sControlPlane{Name: dataPlaneID, Version: "1.34"},
				NodePools:     []kvmv1.K8sNodePool{},
			}
			setMockStatus(dataPlaneID, initialInfo)

			By("creating an UpgradeOperation for PD and TiKV")
			req := kvmv1.CreateUpgradeOperationRequest{
				Spec: kvmv1.UpgradeOperationSpec{
					DataPlaneID:   dataPlaneID,
					TargetVersion: targetVersion,
					Schedule:      metav1.Now(),
					Steps: []kvmv1.UpgradeStep{
						{Target: "PD", Version: targetVersion},
						{Target: "TIKV", Version: targetVersion},
					},
				},
			}
			createUpgradeOperationViaAPI(controllerPodName, dataPlaneID, upgradeOpName, req)

			By("waiting for the upgrade operation to start")
			verifyRunning := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.phase}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Running"))
			}
			Eventually(verifyRunning).Should(Succeed())

			By("waiting for step target to be PD and component phase to be MODIFYING")
			verifyPDStep := func(g Gomega) {
				// Verify target
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.currentStep.target}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("PD"))

				// Verify component status in resourcePools
				cmd = exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace,
					"-o", "jsonpath={.status.resourcePools[?(@.id=='pool-1')].components[?(@.id=='comp-pd')].phase}")
				output, err = utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("MODIFYING"))
			}
			Eventually(verifyPDStep).Should(Succeed())

			By("simulating PD upgrade completion")
			// Update PD to AVAILABLE and correct version, Keep TiKV unchanged
			step1Pools := []SharedPool{
				{
					ID:      "pool-1",
					Version: "1.33",
					Phase:   "AVAILABLE",
					ComponentGroups: []Component{
						{
							ID:            "comp-pd",
							ComponentType: "PD",
							Version:       "1.33",
							Phase:         "AVAILABLE",
							NodeQuota: NodeQuota{
								NodeSpec: NodeSpec{NodeVersion: targetVersion},
							},
						},
						{
							ID:            "comp-tikv",
							ComponentType: "TIKV",
							Version:       "1.33",
							Phase:         "AVAILABLE",
							NodeQuota: NodeQuota{
								NodeSpec: NodeSpec{NodeVersion: "1.33"},
							},
						},
					},
				},
			}
			setMockSharedPools(step1Pools)

			By("waiting for step target to be TIKV and component phase to be MODIFYING")
			verifyTiKVStep := func(g Gomega) {
				// Verify target
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.currentStep.target}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("TIKV"))

				// Verify component status in resourcePools
				cmd = exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace,
					"-o", "jsonpath={.status.resourcePools[?(@.id=='pool-1')].components[?(@.id=='comp-tikv')].phase}")
				output, err = utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("MODIFYING"))
			}
			Eventually(verifyTiKVStep).Should(Succeed())

			By("simulating TiKV upgrade completion")
			// Update TiKV to AVAILABLE and correct version, PD remains completed.
			step2Pools := []SharedPool{
				{
					ID:      "pool-1",
					Version: "1.33",
					Phase:   "AVAILABLE",
					ComponentGroups: []Component{
						{
							ID:            "comp-pd",
							ComponentType: "PD",
							Version:       "1.33",
							Phase:         "AVAILABLE",
							NodeQuota: NodeQuota{
								NodeSpec: NodeSpec{NodeVersion: targetVersion},
							},
						},
						{
							ID:            "comp-tikv",
							ComponentType: "TIKV",
							Version:       "1.33",
							Phase:         "AVAILABLE",
							NodeQuota: NodeQuota{
								NodeSpec: NodeSpec{NodeVersion: targetVersion},
							},
						},
					},
				},
			}
			setMockSharedPools(step2Pools)

			By("waiting for the upgrade operation to complete")
			verifyCompleted := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "upgradeoperation", upgradeOpName, "-n", namespace, "-o", "jsonpath={.status.phase}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Completed"))
			}
			Eventually(verifyCompleted, 2*time.Minute).Should(Succeed())
		})
	})
})

// serviceAccountToken returns a token for the specified service account in the given namespace.
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
// and parsing the resulting token from the API response.
func serviceAccountToken() (string, error) {
	const tokenRequestRawString = `{
		"apiVersion": "authentication.k8s.io/v1",
		"kind": "TokenRequest"
	}`

	// Temporary file to store the token request
	secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
	tokenRequestFile := filepath.Join("/tmp", secretName)
	err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
	if err != nil {
		return "", err
	}

	var out string
	verifyTokenCreation := func(g Gomega) {
		// Execute kubectl command to create the token
		cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
			"/api/v1/namespaces/%s/serviceaccounts/%s/token",
			namespace,
			serviceAccountName,
		), "-f", tokenRequestFile)

		output, err := cmd.CombinedOutput()
		g.Expect(err).NotTo(HaveOccurred())

		// Parse the JSON output to extract the token
		var token tokenRequest
		err = json.Unmarshal(output, &token)
		g.Expect(err).NotTo(HaveOccurred())

		out = token.Status.Token
	}
	Eventually(verifyTokenCreation).Should(Succeed())

	return out, err
}

// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
func getMetricsOutput() (string, error) {
	By("getting the curl-metrics logs")
	cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
	return utils.Run(cmd)
}

// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
// containing only the token field that we need to extract.
type tokenRequest struct {
	Status struct {
		Token string `json:"token"`
	} `json:"status"`
}

func setMockStatus(dataPlaneID string, k8sInfo *kvmv1.K8sInformation) {
	By(fmt.Sprintf("setting mock status for %s", dataPlaneID))

	payload := struct {
		DataPlaneID string               `json:"dataPlaneId"`
		K8sInfo     kvmv1.K8sInformation `json:"k8sInfo"`
	}{
		DataPlaneID: dataPlaneID,
		K8sInfo:     *k8sInfo,
	}

	jsonPayload, err := json.Marshal(payload)
	ExpectWithOffset(1, err).NotTo(HaveOccurred())

	escapedJSON := strings.ReplaceAll(string(jsonPayload), `"`, `\"`)
	cmdStr := fmt.Sprintf(`curl -s -X POST http://localhost:8080/mock/set-k8sinfo -H 'Content-Type: application/json' -d "%s"`, escapedJSON)
	cmd := exec.Command("kubectl", "exec", "-n", namespace, "deployment/mock-server", "--", "/bin/sh", "-c", cmdStr)
	_, err = utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to set mock status via kubectl exec")
}

func setMockError(dataPlaneID string, shouldFail bool) {
	By(fmt.Sprintf("setting mock error for %s: %v", dataPlaneID, shouldFail))

	cmdStr := fmt.Sprintf("curl -s -X POST 'http://localhost:8080/mock/set-fail?dataPlaneId=%s&fail=%v'", dataPlaneID, shouldFail)
	cmd := exec.Command("kubectl", "exec", "-n", namespace, "deployment/mock-server", "--", "/bin/sh", "-c", cmdStr)
	_, err := utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to set mock error via kubectl exec")
}

// CSNG Mock Helpers

type SharedPool struct {
	ID              string      `json:"id"`
	Version         string      `json:"version"`
	Phase           string      `json:"phase"`
	ComponentGroups []Component `json:"componentGroups"`
}

type Component struct {
	ID            string    `json:"id"`
	ComponentType string    `json:"componentType"`
	Version       string    `json:"version"`
	Phase         string    `json:"phase"`
	NodeQuota     NodeQuota `json:"nodeQuota"`
}

type NodeQuota struct {
	NodeSpec NodeSpec `json:"nodeSpec"`
}

type NodeSpec struct {
	NodeVersion string `json:"nodeVersion"`
}

func setMockSharedPools(pools []SharedPool) {
	By("setting mock shared pools")
	payload := struct {
		SharedPools []SharedPool `json:"sharedPools"`
	}{
		SharedPools: pools,
	}

	jsonPayload, err := json.Marshal(payload)
	ExpectWithOffset(1, err).NotTo(HaveOccurred())

	escapedJSON := strings.ReplaceAll(string(jsonPayload), `"`, `\"`)
	cmdStr := fmt.Sprintf(`curl -s -X POST http://localhost:8080/mock/set-shared-pools -H 'Content-Type: application/json' -d "%s"`, escapedJSON)
	cmd := exec.Command("kubectl", "exec", "-n", namespace, "deployment/mock-server", "--", "/bin/sh", "-c", cmdStr)
	_, err = utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to set mock shared pools via kubectl exec")
}

func createUpgradeOperationViaAPI(controllerPodName, dataPlaneID, upgradeOpName string, request kvmv1.CreateUpgradeOperationRequest) {
	By(fmt.Sprintf("creating UpgradeOperation %s via HTTP API", upgradeOpName))

	cmd := exec.Command("kubectl", "get", "pods", controllerPodName, "-n", namespace, "-o", "jsonpath={.status.podIP}")
	output, err := utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to get controller-manager IP. Output: %s", output)
	controllerIP := strings.TrimSpace(output)

	jsonPayload, err := json.Marshal(request)
	ExpectWithOffset(1, err).NotTo(HaveOccurred())

	escapedJSON := string(jsonPayload)
	escapedJSON = strings.ReplaceAll(escapedJSON, "'", "'\"'\"'") // safe bash single quoting
	cmdStr := fmt.Sprintf(`echo '%s' > /tmp/req.json && curl -s -f -X PUT http://%s:8080/api/v1/dataplanes/%s/upgradeoperations/%s -H 'Content-Type: application/json' -d @/tmp/req.json`, escapedJSON, controllerIP, dataPlaneID, upgradeOpName)

	verifyAPICall := func(g Gomega) {
		cmd := exec.Command("kubectl", "exec", "-n", namespace, "deployment/mock-server", "--", "/bin/sh", "-c", cmdStr)
		out, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred(), "Failed to create UpgradeOperation via HTTP API. Output: %s", out)
	}
	EventuallyWithOffset(1, verifyAPICall, 2*time.Minute, 5*time.Second).Should(Succeed())
}
