package clusterupgradeoperation

import (
	"context"
	"encoding/json"
	"errors"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/client/fake"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"

	o11yv1 "github.com/tidbcloud/observability/api/regionalserver/resourceworker/v1"

	"github.com/tidbcloud/observability/pkg/resourceworker/controllers/clusterupgradeoperation/providers"
)

func TestReconcile_NotFound(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
	registry := providers.NewCloudProviderRegistryForTesting(nil)

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKey{Namespace: "default", Name: "non-existent"},
	})
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, res)
}

func TestReconcile_PendingToRunning(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	scheduleTime := time.Now().Add(-1 * time.Hour) // already passed
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			Schedule:       metav1.NewTime(scheduleTime),
			Steps: []o11yv1.UpgradeStep{
				{Processor: o11yv1.ProcessorUpgradeControlPlane},
			},
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase:     o11yv1.ClusterUpgradeOperationPhasePending,
			NextSteps: []o11yv1.UpgradeStep{{Processor: o11yv1.ProcessorUpgradeControlPlane}},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	registry := providers.NewCloudProviderRegistryForTesting(map[o11yv1.CloudProvider]providers.CloudProviderInterface{
		o11yv1.CloudProviderEKS: &MockCloudProvider{},
	})

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter, "should requeue after interval")

	// Verify phase transitioned to Running
	latest := &o11yv1.ClusterUpgradeOperation{}
	require.NoError(t, fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), latest))
	assert.Equal(t, o11yv1.ClusterUpgradeOperationPhaseRunning, latest.Status.Phase)
}

func TestReconcile_Paused(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			Schedule:       metav1.NewTime(time.Now().Add(-1 * time.Hour)),
			Paused:         true,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhasePending,
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	registry := providers.NewCloudProviderRegistryForTesting(nil)

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter, "paused should requeue after interval")
}

func TestReconcile_ScheduleNotReached(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	scheduleTime := time.Now().Add(1 * time.Hour) // in the future
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			Schedule:       metav1.NewTime(scheduleTime),
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhasePending,
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	registry := providers.NewCloudProviderRegistryForTesting(nil)

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter, "schedule not reached should requeue")
}

func TestReconcileFinished_Completed(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{Name: "op-1", Namespace: "default"},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase:   o11yv1.ClusterUpgradeOperationPhaseCompleted,
			EndTime: &metav1.Time{Time: time.Now()},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:        fakeClient,
		Scheme:        scheme,
		eventRecorder: record.NewFakeRecorder(10),
	}

	res, err := r.reconcileFinished(context.Background(), op)
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, res)
}

func TestReconcileFinished_TTLExpired_DeletesCR(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	endTime := time.Now().Add(-2 * time.Hour)
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{Name: "op-1", Namespace: "default"},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			TTLSeconds:     &metav1.Duration{Duration: time.Hour},
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase:   o11yv1.ClusterUpgradeOperationPhaseCompleted,
			EndTime: &metav1.Time{Time: endTime},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:        fakeClient,
		Scheme:        scheme,
		eventRecorder: record.NewFakeRecorder(10),
	}

	res, err := r.reconcileFinished(context.Background(), op)
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, res)

	// CR should be deleted
	latest := &o11yv1.ClusterUpgradeOperation{}
	err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), latest)
	assert.True(t, apierrors.IsNotFound(err))
}

func TestReconcile_Running_ProcessesControlPlaneStep(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	opts := o11yv1.ControlPlaneUpgradeOptions{TargetVersion: "1.28"}
	optsRaw, err := json.Marshal(opts)
	require.NoError(t, err)

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhaseRunning,
			CurrentStep: &o11yv1.UpgradeStep{
				Processor: o11yv1.ProcessorUpgradeControlPlane,
				Options:   runtime.RawExtension{Raw: optsRaw},
				BeginTime: &metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
			},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	mockProvider := &MockCloudProvider{ControlPlaneVersion: "1.28.0"}
	registry := providers.NewCloudProviderRegistryForTesting(map[o11yv1.CloudProvider]providers.CloudProviderInterface{
		o11yv1.CloudProviderEKS: mockProvider,
	})

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter)
	require.NoError(t, fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), op))
	assert.Nil(t, op.Status.CurrentStep)
}

func TestReconcile_Deletion_RemovesFinalizer(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	now := metav1.Now()
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:              "op-1",
			Namespace:         "default",
			Finalizers:        []string{o11yv1.ClusterUpgradeOperationFinalizer},
			DeletionTimestamp: &now,
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhaseRunning,
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:        fakeClient,
		Scheme:        scheme,
		eventRecorder: record.NewFakeRecorder(10),
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, res)

	// After removing finalizer, the object is deleted by the API server
	latest := &o11yv1.ClusterUpgradeOperation{}
	err = fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), latest)
	assert.True(t, apierrors.IsNotFound(err))
}

func TestReconcile_AddsFinalizerWhenMissing(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	scheduleTime := time.Now().Add(1 * time.Hour)
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "op-1",
			Namespace: "default",
			// No finalizer
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			Schedule:       metav1.NewTime(scheduleTime),
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhasePending,
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: providers.NewCloudProviderRegistryForTesting(nil),
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter)

	latest := &o11yv1.ClusterUpgradeOperation{}
	require.NoError(t, fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), latest))
	assert.True(t, controllerutil.ContainsFinalizer(latest, o11yv1.ClusterUpgradeOperationFinalizer))
}

func TestLogAndEmitEvent(t *testing.T) {
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{Name: "op-1", Namespace: "default"},
	}
	recorder := record.NewFakeRecorder(4)
	logger := log.Log

	t.Run("with error", func(t *testing.T) {
		LogAndEmitEvent(logger, recorder, op, "Warning", "test error message", errors.New("some error"))
		ev := <-recorder.Events
		assert.Contains(t, ev, "Warning")
		assert.Contains(t, ev, "Error")
		assert.Contains(t, ev, "test error message")
		assert.Contains(t, ev, "some error")
	})

	t.Run("without error", func(t *testing.T) {
		LogAndEmitEvent(logger, recorder, op, "Normal", "test info message", nil)
		ev := <-recorder.Events
		assert.Contains(t, ev, "Normal")
		assert.Contains(t, ev, "Info")
		assert.Contains(t, ev, "test info message")
	})
}

func TestReconcile_Running_ProcessesClusterAddonsStep(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	opts := o11yv1.ClusterAddonsUpgradeOptions{
		Addons: []o11yv1.AddonUpgrade{{Name: "vpc-cni", Version: "1.2.0"}},
	}
	optsRaw, err := json.Marshal(opts)
	require.NoError(t, err)

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhaseRunning,
			CurrentStep: &o11yv1.UpgradeStep{
				Processor: o11yv1.ProcessorUpgradeClusterAddons,
				Options:   runtime.RawExtension{Raw: optsRaw},
				BeginTime: &metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
			},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	mockProvider := &MockCloudProvider{}
	registry := providers.NewCloudProviderRegistryForTesting(map[o11yv1.CloudProvider]providers.CloudProviderInterface{
		o11yv1.CloudProviderEKS: mockProvider,
	})

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter)
}

func TestReconcile_Running_ClusterAddons_GetStatusErr(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	opts := o11yv1.ClusterAddonsUpgradeOptions{
		Addons: []o11yv1.AddonUpgrade{{Name: "vpc-cni", Version: "1.2.0"}},
	}
	optsRaw, err := json.Marshal(opts)
	require.NoError(t, err)

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhaseRunning,
			CurrentStep: &o11yv1.UpgradeStep{
				Processor: o11yv1.ProcessorUpgradeClusterAddons,
				Options:   runtime.RawExtension{Raw: optsRaw},
				BeginTime: &metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
			},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	mockProvider := &MockCloudProvider{
		GetClusterAddonsStatusErr: errors.New("addon status error"),
	}
	registry := providers.NewCloudProviderRegistryForTesting(map[o11yv1.CloudProvider]providers.CloudProviderInterface{
		o11yv1.CloudProviderEKS: mockProvider,
	})

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to get cluster addons status")
	assert.Equal(t, ctrl.Result{}, res)
}

func TestReconcile_Running_ProcessesNodeGroupStep(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	opts := o11yv1.NodeGroupUpgradeOptions{
		NodeGroupName: "ng-1",
		Strategy:      o11yv1.NodeGroupUpgradeStrategyInPlace,
	}
	optsRaw, err := json.Marshal(opts)
	require.NoError(t, err)

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			TargetVersion:  "1.28",
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhaseRunning,
			CurrentStep: &o11yv1.UpgradeStep{
				Processor: o11yv1.ProcessorUpgradeNodeGroup,
				Options:   runtime.RawExtension{Raw: optsRaw},
				BeginTime: &metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
			},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	mockProvider := &MockCloudProvider{
		NodeGroups: []providers.NodeGroup{
			{ID: "ng-1", Name: "ng-1", Version: "1.28.0", Status: providers.NodeGroupStatusActive},
		},
	}
	registry := providers.NewCloudProviderRegistryForTesting(map[o11yv1.CloudProvider]providers.CloudProviderInterface{
		o11yv1.CloudProviderEKS: mockProvider,
	})

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter)
}

func TestReconcile_Finished_TTLRequeue(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	deleteAfter := time.Now().Add(1 * time.Hour)
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			TTLSeconds:     &metav1.Duration{Duration: 2 * time.Hour},
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase:   o11yv1.ClusterUpgradeOperationPhaseCompleted,
			EndTime: &metav1.Time{Time: time.Now().Add(-1 * time.Hour)},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: providers.NewCloudProviderRegistryForTesting(nil),
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.True(t, res.RequeueAfter > 0)
	assert.True(t, res.RequeueAfter <= time.Until(deleteAfter)+time.Second)
}

func TestReconcile_Running_CurrentStepNil_PhaseCompleted(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase:       o11yv1.ClusterUpgradeOperationPhaseCompleted,
			CurrentStep: nil,
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: providers.NewCloudProviderRegistryForTesting(nil),
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, res)
}

func TestReconcile_Running_CloudProviderGetErr_Fails(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	opts := o11yv1.ControlPlaneUpgradeOptions{TargetVersion: "1.28"}
	optsRaw, err := json.Marshal(opts)
	require.NoError(t, err)

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhaseRunning,
			CurrentStep: &o11yv1.UpgradeStep{
				Processor: o11yv1.ProcessorUpgradeControlPlane,
				Options:   runtime.RawExtension{Raw: optsRaw},
				BeginTime: &metav1.Time{Time: time.Now()},
			},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	registry := providers.NewCloudProviderRegistryForTesting(map[o11yv1.CloudProvider]providers.CloudProviderInterface{})

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, res)

	latest := &o11yv1.ClusterUpgradeOperation{}
	require.NoError(t, fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), latest))
	assert.Equal(t, o11yv1.ClusterUpgradeOperationPhaseFailed, latest.Status.Phase)
}

func TestReconcile_Running_UnknownProcessor_Fails(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhaseRunning,
			CurrentStep: &o11yv1.UpgradeStep{
				Processor: "unknown-processor",
				BeginTime: &metav1.Time{Time: time.Now()},
			},
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: providers.NewCloudProviderRegistryForTesting(nil),
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.Equal(t, ctrl.Result{}, res)

	latest := &o11yv1.ClusterUpgradeOperation{}
	require.NoError(t, fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), latest))
	assert.Equal(t, o11yv1.ClusterUpgradeOperationPhaseFailed, latest.Status.Phase)
}

func TestReconcile_Paused_SkipsProcessing(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	scheduleTime := time.Now().Add(-1 * time.Hour)
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			Schedule:       metav1.NewTime(scheduleTime),
			Paused:         true,
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhasePending,
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: providers.NewCloudProviderRegistryForTesting(nil),
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter)
}

func TestReconcile_Pending_ScheduleNotReached_Waits(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	scheduleTime := time.Now().Add(1 * time.Hour)
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			Schedule:       metav1.NewTime(scheduleTime),
			Steps:          []o11yv1.UpgradeStep{{Processor: o11yv1.ProcessorUpgradeControlPlane}},
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: o11yv1.ClusterUpgradeOperationPhasePending,
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: providers.NewCloudProviderRegistryForTesting(nil),
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.True(t, res.RequeueAfter > 0)
	assert.True(t, res.RequeueAfter <= time.Until(scheduleTime)+time.Second)
}

func TestReconcile_InitPhaseToPending(t *testing.T) {
	scheme := runtime.NewScheme()
	require.NoError(t, o11yv1.AddToScheme(scheme))

	scheduleTime := time.Now().Add(-1 * time.Hour)
	op := &o11yv1.ClusterUpgradeOperation{
		ObjectMeta: metav1.ObjectMeta{
			Name:       "op-1",
			Namespace:  "default",
			Finalizers: []string{o11yv1.ClusterUpgradeOperationFinalizer},
		},
		Spec: o11yv1.ClusterUpgradeOperationSpec{
			K8sClusterName: "cluster-1",
			CloudProvider:  o11yv1.CloudProviderEKS,
			Schedule:       metav1.NewTime(scheduleTime),
			Steps:          []o11yv1.UpgradeStep{{Processor: o11yv1.ProcessorUpgradeControlPlane}},
		},
		Status: o11yv1.ClusterUpgradeOperationStatus{
			Phase: "",
		},
	}

	fakeClient := fake.NewClientBuilder().
		WithScheme(scheme).
		WithObjects(op).
		WithStatusSubresource(op).
		Build()

	registry := providers.NewCloudProviderRegistryForTesting(map[o11yv1.CloudProvider]providers.CloudProviderInterface{
		o11yv1.CloudProviderEKS: &MockCloudProvider{},
	})

	r := &ClusterUpgradeOperationReconciler{
		Client:                fakeClient,
		Scheme:                scheme,
		eventRecorder:         record.NewFakeRecorder(10),
		cloudProviderRegistry: registry,
	}

	res, err := r.Reconcile(context.Background(), ctrl.Request{
		NamespacedName: client.ObjectKeyFromObject(op),
	})
	assert.NoError(t, err)
	assert.NotZero(t, res.RequeueAfter)

	latest := &o11yv1.ClusterUpgradeOperation{}
	require.NoError(t, fakeClient.Get(context.Background(), client.ObjectKeyFromObject(op), latest))
	assert.Equal(t, o11yv1.ClusterUpgradeOperationPhaseRunning, latest.Status.Phase)
}
